|
本帖最后由 正点原子 于 2023-5-15 14:40 编辑
1)实验平台:正点原子 DFZU2EG_4EV MPSoC开发板
2)购买链接:https://item.taobao.com/item.htm?&id=692368045899
3)全套实验源码+手册+视频下载地址:http://www.openedv.com/thread-340252-1-1.html
4)正点原子官方B站:https://space.bilibili.com/394620890
5)正点原子FPGA交流群:994244016
第三十三章基于lwip的TCP服务器性能测试实验
上一章的lwip Echo Server实验让我们对lwip有一个基本的了解,而Echo Server是基于TCP协议的。TCP协议是为了在不可靠的互联网络上提供可靠的端到端字节流而专门设计的一个传输协议。本章我们将了解lwip tcp协议的使用并测试lwip tcp服务的性能。本章分为以下几个部分:
33.1简介
33.2实验任务
33.3硬件设计
33.4软件设计
33.5下载验证
33.1简介
1)TCP 协议简介
TCP(Transmission Control Protocol,传输控制协议)是一种面向连接的、可靠的、基于字节流的传输层通信协议。在开放系统互连OSI参考模型中,它完成第四层传输层所指定的功能,UDP(User Datagram Protocol,用户数据报协议)是同一层内另一个重要的传输协议。在因特网协议族(Internet protocol suite)中,TCP层是位于IP层之上,应用层之下的中间层。不同主机的应用层之间经常需要可靠的、像管道一样的连接,但是IP层不提供这样的流机制,而是提供不可靠的包交换。
应用层向TCP层发送用于网间传输的、用8位字节表示的数据流,然后TCP把数据流分区成适当长度的报文段(通常受该计算机连接的网络的数据链路层的最大传输单元(MTU)的限制)。之后TCP把报文段传给IP 层,由它来通过网络将报文段传送给接收端的TCP层。TCP为了保证报文传输的可靠,就给每个包一个序号,同时序号也保证了传送到接收端的数据包能被按序接收。然后接收端对已成功收到的字节发回一个相应的确认(ACK);如果发送端在合理的往返时延(RTT)内未收到确认,那么对应的数据(假设丢失了)将会被重传。
在数据正确性与合法性上,TCP用一个校验和函数来检验数据是否有错误,在发送和接收时都要计算校验和。在保证可靠性上,采用超时重传和捎带确认机制。在流量控制上,采用滑动窗口协议,协议中规定,对于窗口内未经确认的分组需要重传。在拥塞控制上,采用TCP拥塞控制算法。
作为与TCP协议处于同一层的UDP协议,我们简单的介绍下两者的区别:
1、TCP协议面向连接,是流传输协议,通过连接发送数据,而UDP协议传输不需要连接,是数据报协议;
2.TCP为可靠传输协议,而UDP为不可靠传输协议。即TCP协议可以保证数据的完整和有序,而UDP不能保证;
3.UDP由于不需要连接,故传输速度比TCP快,且占用资源比TCP少;
4.应用场合:TCP协议常用在对数据文件完整性较高的一些场景中,如文件传输等。UDP常用于对通讯速度有较高要求或者传输数据较少时,比如对速度要求较高的视频直播和传输数据较少的QQ等。
2)TCP首部
图33.1.1体现了以太网协议体系分层设计的思想。发送端在应用层将需要传输的数据传给传输层,传输层对应用层的数据进行处理、封装,如果使用TCP协议,则对数据处理后加上TCP标识头进行封装,如果使用UDP协议,则添加UDP标识头进行封装,封装后的数据传递给网络层。网络层对传输层下发的数据处理后添加IP标识头进而传递给数据链路层。数据链路层将网络层下发的数据打包成以太网数据包递交给物理层传输。接收端接收到以太网数据包后进行反向操作从而得到发送端应用层发送的数据。
图33.1.1 以太网包数据格式
下图是TCP报文段标识头,在没有选项的情况下,它通常是20个字节。
图33.1.2 TCP报文段标识头
源端口号和目的端口号用于寻找发送端和接收端的应用进程,这个和UDP报文相同,这两个值加上IP首部中的源IP地址和目的IP地址唯一确定一个TCP连接。
序列号字段用来标识从TCP发送端向TCP接收端发送的数据字节流,它表示在这个报文段中的第一个数据字节。当建立一个新的连接时,握手报文中的SYN标志置1,这个握手报文中的序号字段为随机选择的初始序号ISN(Initial Sequence Number),当连接建立好以后发送方要发送的第一个字节序号为ISN+1。
确认号字段只有在ACK为1的时候才有用,确认号中包含发送确认的一方所期望收到的下一个序号,确认号是在上一次成功接收到的数据字节序列号上加一,例如上次接收成功接收到对方发过来的数据序号为X,那么返回的确认号就应该为X+1。
首部长度中给出了首部的长度,以4字节为单位,这个字段有4bit,因此TCP最多有60字节的首部((2^4-1)*4 = 60字节)。如果没有任何的选项字段,正常的首部长度是20字节。
TCP首部中还有6个标志比特,这6个标志位的说明如下表所示。
表 33.1.1 TCP 首部标志位说明
窗口尺寸也就窗口大小,其中填写相应的值以通知对方自己期望接收的字节数,窗口大小字段是TCP流量控制的关键字段,窗口大小是一个16bit的字段,因此窗口大小最大为65535字节。
16位的校验和覆盖了整个TCP报文段:TCP首部和TCP数据。校验和首先在数据发送方通过特殊的算法计算得出,在传递到接收方之后,还需要再重新计算。如果某个数据报在传输过程中被第三方篡改或者由于线路噪音等原因受到损坏,发送和接收方的校验计算和将不会相符,由此UDP协议可以检测是否出错。
紧急指针只有在URG置1时有效,紧急指针是一个正的偏移量,和序号字段中的值相加表示紧急数据最后一个字节的序号。
3)LWIP中RAW API编程接口中与TCP相关的函数
LWIP提供了很多关于TCP的RAW API编程函数,我们可以使用这些函数来完成有关TCP的实验,我们在表 33.1.2列出了一部分函数。
表 33.1.2 TCP RAW API函数
TCP连接由协议控制块(PCB)识别。有两种方法可以建立连接
1.被动连接(服务端)
整个过程可分为以下几个步骤:
a)调用tcp_new函数来创建一个pcb
b)(可选)调用tcp_arg函数以将特定于应用程序的值与pcb相关联
c)调用tcp_bind函数指定本地IP地址和端口
d)调用tcp_listen函数或tcp_listen_with_backlog函数
e)调用tcp_accept函数以指定新连接到达时要调用的函数
2.主动连接(客户端)
整个过程可分为以下几个步骤:
a)调用tcp_new函数来创建一个pcb
b)(可选)调用tcp_arg函数以将特定于应用程序的值与pcb相关联
c)(可选)调用tcp_bind函数以指定本地IP地址和端口
d)调用tcp_connect函数
33.2实验任务
本章的实验任务是使用VITIS软件自带的Lwip TCP perf Server模版了解TCP服务器的性能。
33.3硬件设计
本章的硬件设计与前一章《基于lwip的echo server实验》相同,此处不再赘述。将《基于lwip的echo server实验》的Vivado工程另存为“lwip_tcp_server_perf”,然后导出硬件平台文件到vitis文件夹,打开Vitis,进入软件设计。
33.4软件设计
在菜单栏中选择“File->New->Application Project”, 新建一个Vitis应用工程。
在弹出的下图所示界面中,输入工程名“lwip_tcp_server_perf”,其它选项保持默认,点击“Next”。
图 33.4.1 配置工程
然后添加应用平台文件,添加完成后,接下来依次点击“Next>”,直到弹出选择模板界面,选择工程模版“lwIP TCP Perf Server”,然后点击“Finish”按钮,如图 33.4.2所示。
LwIP TCP Perf Server应用程序用于创建TCP服务器并使用轻量级IP堆栈(lwIP)测试下行链路性能。该应用程序将MPSOC开发板MAC地址设置为00:0a:35:00:01:02,默认使用DHCP获取动态IP地址,如果DHCP失败,则使用默认设置的静态IPv4地址192.168.1.10。应用程序在MPSOC开发板上创建TCP服务器并侦听TCP客户端的连接,一旦远程客户端与此服务器连接,TCP服务器将开始从客户端接收数据。客户端连接的详细信息和数据传输统计信息将由服务器在串行控制台上显示。
图 33.4.2 选择“lwIP TCP Perf Server”模版
展开lwip_tcp_server_perf应用工程目录下的src目录,可以看到很多平台相关的文件(主要是platform开头的文件)。为了方便分析,我们将src文件夹中与本实验不相关的平台文件删除,删除后的src文件夹内容如下图所示:
图 33.4.3 删除后的src文件夹内容
该实验的main.c文件与《lwip echo server》实验的main.c基本相同,无实质性变化,由于在《lwip echo server》实验已讲解了main.c的内容,此处我们就不赘述了。本章我们重点关注的是lwip raw接口下tcp的使用,创建tcp服务器和测试性能主要在tcp_perf_server.c文件中实现。在讲解tcp_perf_server.c文件之前我们先来看一下当使用lwip作为TCP服务器时,会话的建立过程。
由于raw TCP实现主要通过回调执行,因此其操作往往与各个消息的接收和处理密切相关。因此,熟悉底层TCP协议是有帮助的。对于没有lwIP使用经验的人来说,有时并不清楚什么时候需要调用什么。下表显示了远程客户端和本地lwIP tcp服务器之间交互的顺序图。
表 33.4.1 lwIP会话建立(远程客户端/本地lwIP服务器)
现在对照上表我们来分析tcp_perf_server.c文件的代码。
首先我们来看start_application函数,该函数在main函数中调用,后面的函数基本上都是通过该函数回调使用。start_application函数代码如下:
204 void start_application(void)
205 {
206 err_t err;
207 struct tcp_pcb *pcb, *lpcb;
208
209 //创建服务器PCB
210 pcb = tcp_new_ip_type(IPADDR_TYPE_ANY);
211 if (!pcb) {
212 xil_printf("TCP server: Error creating PCB. Out of Memory\r\n");
213 return;
214 }
215
216 //绑定端口号
217 err = tcp_bind(pcb, IP_ADDR_ANY, TCP_CONN_PORT);
218 if (err != ERR_OK) {
219 xil_printf("TCP server: Unable to bind to port %d: "
220 "err = %d\r\n" , TCP_CONN_PORT, err);
221 tcp_close(pcb);
222 return;
223 }
224
225 //将连接队列限制设置为1,以便一次为一个客户端提供服务
226 lpcb = tcp_listen_with_backlog(pcb, 1);
227 if (!lpcb) {
228 xil_printf("TCP server: Out of memory while tcp_listen\r\n");
229 tcp_close(pcb);
230 return;
231 }
232
233 //此处不需要回调函数的任何参数
234 tcp_arg(lpcb, NULL);
235
236 //指定用于传入连接的回调
237 tcp_accept(lpcb, tcp_server_accept);
238
239 return;
240 }
可以看到函数的调用与表 33.4.1中的“lwIP服务器操作”列的函数顺序一致。其中tcp_new_ip_type函数与我们在表 33.1.2中列的tcp_new函数有点区别,tcp_new_ip_type函数不仅具有tcp_new函数的功能,而且可以指定侦听的IP地址的类型,是用IPv4(IPADDR_TYPE_V4)、IPv6(IPADDR_TYPE_V6)还是两者都可以(IPADDR_TYPE_ANY),IPADDR_TYPE_ANY只有在lwip开启了IPv6功能才可以使用,本实验因为我们没有使能IPv6功能,所以tcp_new_ip_type函数的IPADDR_TYPE_ANY不起作用,此处调用该函数主要是考虑兼容性,如果只用IPv4,可以调用tcp_new函数。
tcp_bind函数用于绑定本地端口号和IP地址,IP_ADDR_ANY表示任意本地地址,TCP_CONN_PORT在tcp_perf_server.h头文件中宏定义为5001,该端口是我们后面使用的iperf工具的默认连接端口。
tcp_listen_with_backlog函数的第二个参数为8位二进制的最大值0xff。由于本实验实现的功能是测试TCP的性能,所以此处将连接队列限制设置为1,以便一次为一个客户端提供服务。
tcp_accept函数用于设置接受(accept)回调函数tcp_server_accept。
从表 33.4.1可以看到,执行start_application函数后,客户端就可以发起连接,然后底层堆栈与客户端进行三次握手,握手成功后,调用接受回调,也就是调用tcp_server_accept函数,该函数代码如下:
170 //用于服务器accept的回调函数
171 static err_t tcp_server_accept(void *arg, struct tcp_pcb *newpcb, err_t err)
172 {
173 if ((err != ERR_OK) || (newpcb == NULL)) {
174 return ERR_VAL;
175 }
176 //保存连接的客户端PCB
177 c_pcb = newpcb;
178
179 //保存最终报告的开始时间
180 server.start_time = get_time_ms();
181 server.end_time = 0; /* ms */
182 //更新连接的客户端ID
183 server.client_id++;
184 server.total_bytes = 0;
185
186 //初始化临时报告参数
187 server.i_report.report_interval_time =
188 INTERIM_REPORT_INTERVAL * 1000; /* ms */
189 server.i_report.last_report_time = 0;
190 server.i_report.start_time = 0;
191 server.i_report.total_bytes = 0;
192
193 //打印连接状态信息
194 print_tcp_conn_stats();
195
196 //设置tcp rx连接的回调
197 tcp_arg(c_pcb, NULL);
198 tcp_recv(c_pcb, tcp_recv_perf_traffic);
199 tcp_err(c_pcb, tcp_server_err);
200
201 return ERR_OK;
202 }
该函数的主要作用是设置recv接收回调和错误中止回调。代码第180-191行用于初始化测试TCP性能的相关数据,INTERIM_REPORT_INTERVAL参数在tcp_perf_server.h中宏定义如下:
/* seconds between periodic bandwidth reports */
#define INTERIM_REPORT_INTERVAL 5
该值代表每隔多长时间打印一次报告,也就是临时报告,此处的5代表5秒,当然也可以修改成其它值。
从表 33.4.1可以看到,设置好tcp_server_accept的最后三个函数后,连接就正式建立了,这时候就可以互通有无了。此时如果客户端发起数据传输,lwip堆栈调用recv回调,也就是tcp_recv(c_pcb, tcp_recv_perf_traffic)函数中的tcp_recv_perf_traffic函数,该函数代码如下:
129 //用于服务器recv的回调函数
130 static err_t tcp_recv_perf_traffic(void *arg, struct tcp_pcb *tpcb,
131 struct pbuf *p, err_t err)
132 {
133 if (p == NULL) {
134 u64_t now = get_time_ms();
135 u64_t diff_ms = now - server.start_time;
136 tcp_server_close(tpcb);
137 tcp_conn_report(diff_ms, TCP_DONE_SERVER);
138 xil_printf("TCP test passed Successfully\n\r");
139 return ERR_OK;
140 }
141
142 //记录最终报告的总字节数
143 server.total_bytes += p->tot_len;
144
145 if (server.i_report.report_interval_time) {
146 u64_t now = get_time_ms();
147 //记录临时报告的总字节数
148 server.i_report.total_bytes += p->tot_len;
149 if (server.i_report.start_time) {
150 u64_t diff_ms = now - server.i_report.start_time;
151
152 if (diff_ms >= server.i_report.report_interval_time) {
153 tcp_conn_report(diff_ms, INTER_REPORT);
154 //重置临时报告计数器
155 server.i_report.start_time = 0;
156 server.i_report.total_bytes = 0;
157 }
158 } else {
159 //保存临时报告的开始时间
160 server.i_report.start_time = now;
161 }
162 }
163
164 tcp_recved(tpcb, p->tot_len);
165
166 pbuf_free(p);
167 return ERR_OK;
168 }
由于本实验的功能是使用lwip测试TCP服务器的链路性能,测试的方法是接收客户端发来的数据流,对该数据流以每隔INTERIM_REPORT_INTERVAL时间统计一次客户端在该时间段内发送的数据总字节数,从而统计该段时间内的平均带宽,并打印该段时间内的临时报告。除此之外,还统计整个测试时间内的总字节数,从而统计整个测试时间内的平均带宽,并打印最终的测试报告。所以该函数的主要作用就是对接收到的数据进行处理。代码第153行调用的tcp_conn_report函数内容实现如下:
67 //tcp服务器会话的report函数
68 static void tcp_conn_report(u64_t diff,
69 enum report_type report_type)
70 {
71 u64_t total_len;
72 double duration, bandwidth = 0;
73 char data[16], perf[16], time[64];
74
75 if (report_type == INTER_REPORT) {
76 total_len = server.i_report.total_bytes;
77 } else {
78 server.i_report.last_report_time = 0;
79 total_len = server.total_bytes;
80 }
81
82 //将持续时间从毫秒转换为秒,带宽转换为比特/秒
83 duration = diff / 1000.0; /* secs */
84 if (duration)
85 bandwidth = (total_len / duration) * 8.0;
86
87 stats_buffer(data, total_len, BYTES);
88 stats_buffer(perf, bandwidth, SPEED);
89
90 //在32位平台上,xil_printf无法打印u64_t的值,因此在字符串中转换这些值并显示结果
91 sprintf(time, "%4.1f-%4.1f sec",
92 (double)server.i_report.last_report_time,
93 (double)(server.i_report.last_report_time + duration));
94 xil_printf("[%3d] %s %sBytes %sbits/sec\n\r", server.client_id,
95 time, data, perf);
96
97 if (report_type == INTER_REPORT)
98 server.i_report.last_report_time += duration;
99 }
该函数的主要作用是打印测试报告信息。代码第87行调用的stats_buffer函数的主要作用是进行数据换算,包括单位换算,如将bit数换算成合适的单位,如Mbit,其实现如下:
41 static void stats_buffer(char* outString,
42 double data, enum measure_t type)
43 {
44 int conv = KCONV_UNIT;
45 const char *format;
46 double unit = 1024.0;
47
48 if (type == SPEED)
49 unit = 1000.0;
50
51 while (data >= unit && conv <= KCONV_GIGA) {
52 data /= unit;
53 conv++;
54 }
55
56 if (data < 9.995) { // 9.995四舍五入到10.0
57 format = "%4.2f %c"; // #.##
58 } else if (data < 99.95) { // 99.95四舍五入到100
59 format = "%4.1f %c"; // ##.#
60 } else {
61 format = "%4.0f %c"; // ####
62 }
63 sprintf(outString, format, data, kLabel[conv]);
64 }
注意代码第46-49行的unit数值的差异,当测试速度时,单位是1000,而不是统一的用1024,这在下载验证时会体现出来。
从表 33.4.1可以看到,调用接收回调后,如果客户端需要请求数据的话就需要调用tcp_write()函数响应数据请求。本实验因客户端无数据请求,因此无需调用tcp_write()函数。tcp_recved()函数在接收回调函数tcp_recv_perf_traffic中实现,见代码第164行。
如果客户端结束连接,就根据代码第199行的tcp_err(c_pcb, tcp_server_err)函数,调用错误/中止回调函数tcp_server_err,结束连接。tcp_server_err函数及其相关函数实现如下:
101 //tcp服务器关闭会话
102 static void tcp_server_close(struct tcp_pcb *pcb)
103 {
104 err_t err;
105
106 if (pcb != NULL) {
107 tcp_recv(pcb, NULL);
108 tcp_err(pcb, NULL);
109 err = tcp_close(pcb);
110 if (err != ERR_OK) {
111 /* Free memory with abort */
112 tcp_abort(pcb);
113 }
114 }
115 }
116
117 //错误回调,tcp会话中止
118 static void tcp_server_err(void *arg, err_t err)
119 {
120 LWIP_UNUSED_ARG(err);
121 u64_t now = get_time_ms();
122 u64_t diff_ms = now - server.start_time;
123 tcp_server_close(c_pcb);
124 c_pcb = NULL;
125 tcp_conn_report(diff_ms, TCP_ABORTED_REMOTE);
126 xil_printf("TCP connection aborted\n\r");
127 }
可以看到tcp_server_err函数对接收到的客户端的数据做最后的处理,并调用tcp_server_close函数关闭会话。
至此,tcp_perf_server.c文件的主要代码内容就讲完了,接下来进行下载验证,看一下tcp服务器的链路性能。
33.5下载验证
首先我们将下载器与MPSOC开发板上的JTAG接口连接,下载器另外一端与电脑连接。然后使用USB连接线将USB UART接口与电脑连接,用于串口通信。使用网线一端连接MPSOC开发板的以太网接口,另一端与电脑或路由器连接。最后连接开发板的电源,给开发板上电。
在Vitis软件的下方的Vitis Serial Terminal窗口中点击右上角的加号连接串口。然后下载本次实验的elf文件。下载完成后,可以看到串口打印的结果。
图 33.5.1 显示打印结果
从串口打印的倒数第二行可以看到,TCP服务器侦听的端口号为5001。最后一行是本实验的关键命令,用于测试TCP链路性能。此时我们需要一个测试的工具iperf。iperf是一种用于主动测量IP网络上可达到的最大带宽的工具。它支持调整与时序、协议和缓冲区相关的各种参数。对于每个测试,它报告测量的吞吐量/比特率、损耗和其它参数。利用 Iperf这一特性,可以用来测试一些网络设备如路由器、防火墙、交换机等的性能。
由于iperf是第三方工具,需要我们手动安装,可以在开发板随附的资料“6_软件资料/1_软件”中找到,名为iperf.exe即是,直接将iperf.exe复制到C:\Windows目录即可。
iperf常用参数列在下表:
我们打开电脑的CMD(按win+r键后输入cmd),输入“iperf -c 192.168.1.10 -i 5 -t 30 -w 2M”,如下图所示:
图 33.5.2 进行iperf测试
该命令指示iperf以客户端模式启动,连接到服务器192.168.1.10,指定TCP窗口大小为2Mbyte(参数-w 2M), 测试30秒(-t 30),因为300秒太长,不方便截图,所以改为30秒,每隔5秒打印一次输出(-i 5)。
回车后,打印连接信息,并启动测试,如下图所示:
图 33.5.3 iperf测试结果
可以看到,打印出来的测试信息分为四列,分别是ID、Interval、Transfer和Bandwidth。ID用于标识测试的连接,Interval是测试时间段,由于每隔5秒打印一次输出(-i 5),所以Transfer表示该时间段内传输的数据总量,Bandwidth为该时间段内的平均带宽。
由于只测试了30秒,可以看到该段时间内带宽稳定在949Mbits/sec,对于千兆网来说,是一个合理值。另外Vitis的串口终端也打印出了信息,如下图所示:
图 33.5.4 Vitis打印iperf测试结果 |
|