《嵌入式 - Lwip开发指南》第5章 LWIP测速
Posted Bruceoxl
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了《嵌入式 - Lwip开发指南》第5章 LWIP测速相关的知识,希望对你有一定的参考价值。
最近有个网友在询问关于LWIP的速度,本文就LWIP网速做个简单测试。为了对比,本文将使用无系统和有系统两种环境。
5.1网络测速工具介绍
不过在测速之前,需要介绍下测速的工具,这里有两个软件:iPerf与jperf。
iPerf 是一个跨平台的网络性能测试工具,它支持Win/Linux/Mac/android/ios 等平台,iPerf 可以测试TCP 和UDP(我们一般不对UDP 进行测速)带宽质量,iPerf 可以测量最大TCP 带宽,可以具有多种参数进行测试,同时iPerf 还可以报告带宽,延迟抖动和数据包丢失的情况,我们可以利用iPerf的这些特性来测试一些网络设备如路由器,防火墙,交换机等的性能。
虽然iPerf 很好用,但是它却是命令行格式的软件,对使用测试的人员并不友好,使用者需要记下他繁琐的命令,不过它还有一个图形界面程序叫做JPerf,使用JPerf 程序能简化了复杂命令行参数的构造,而且 它还保存测试结果,并且将测试结果实时图形化出来,更加一目了然,当然,JPerf 也肯定拥有iPerf 的所有功能,本质执行的iperf的功能。因此本文使用JPerf测速。
关于JPerf软件请自行在后文指引下获取,下载JPerf后,解压。
然后单击jperf.bat即可打开软件。
值得注意的是,运行该软件还需要Java环境,请自行配置Java环境。
5.2无系统测速(RAW API)
要想使用JPerf测速,必须先实现TCP服务器或客户端。关于TCP理论这里就不在赘述了,网上的资料很多。这里只讲解如何使用RAW API实现TCP服务器。
5.2.1 TCP相关的RAW API
在开始实现TCP服务器之前,我们首先来看一看LwIP中与TCP相关的RAW API函数有哪些。并简单的了解一下其功能。
- 建立TCP连接的API函数
函数 | 描述 |
---|---|
tcp_new() | 创建TCP的PCB控制块 |
tcp_bind() | 绑定服务器的IP和端口号 |
tcp_listen() | 监听TCP的PCB控制块 |
tcp_accepted() | 通知 LWIP 协议栈一个 TCP 连接被接受了 |
tcp_conect() | 连接远端主机,客户端使用 |
- 发送TCP数据的API函数
函数 | 描述 |
---|---|
tcp_write() | 构造一个报文并放到控制块的发送缓冲队列中 |
tcp_sent() | 控制块 sent 字段注册的回调函数,数据发送成功后被回调 |
tcp_output() | 将发送缓冲队列中的数据发送出去 |
- 接收 TCP 数据
函数 | 描述 |
---|---|
tcp_recv() | 控制块 recv 字段注册的回调函数,当接收到新数据时被调用 |
tcp_recved() | 当程序处理完数据后一定要调用这个函数,通知内核更新接收窗口 |
- 轮询函数
函数 | 描述 |
---|---|
tcp_poll() | 控制块 poll 字段注册的回调函数,该函数周期性调用 |
- 关闭和中止连接
函数 | 描述 |
---|---|
tcp_close() | 关闭一个 TCP 连接 |
tcp_err() | 控制块 err 字段注册的回调函数,遇到错误时被调用 |
tcp_abort() | 中断 TCP 连接 |
在具体实现TCP服务器之前,先配合着下LWIP,关于如何移植LWIP可以参看笔者以前的文章。
笔者这里使用静态IP,并开启TCP模块。
5.2.2 TCP服务器实现流程
前面了解了TCP所涉及到的API函数,也通过STM32CubeMX打开了相关配置,那么使用这些函数怎么实现一个TCP服务器呢?我们先简单说明一下其基本的流程。
1.新建控制块
使用tcp_new()函数建立一个TCP控制块。
2.绑定控制块
对于服务器来说,新建一个控制快后,需要在控制块上绑定本地IP和端口,以方便客户端的连接。
3.控制块侦听
使用tcp_listen函数,对于服务器来说,需要显性调用tcp_listen函数以使控制块进入监听状态,等待客户端的连接请求。
4.建立连接
在tcp_listen函数进入服务器监听状态后,需要马上使用tcp_accept函数来注册一个接收处理函数,因为一旦有客户端连接请求被成功建立后,服务器就会调用这个处理函数。
5.接受并处理数据
一旦连接成功,accept回调函数会调用tcp_recv函数注册一个接收完成的处理函数。对于服务器来说,接收到了客户端的数据或操作要求,就会调用这一回调函数进行处理。这其实是一个复杂的过程:接收到数据后,首先通知更新接受窗口(使用tcp_recved函数),处理并发送数据(使用tcp_write函数),数据发送成功则清除已发送的数据(使用tcp_sent函数),最后关闭连接(使用函数tcp_close)。
整个流程图所示如下:
5.2.3 TCP服务器代码实现
前面分析了TCP服务器的实现流程,接下来就是通过前面介绍的API来实现。
首先是TCP服务器的初始化。其实现代码如下:
/**
* @brief TCP服务器初始化
* @param None
* @retval res
*/
uint8_t tcp_server_init(void)
{
uint8_t res = 0;
err_t err;
struct tcp_pcb *tcppcbnew; //定义一个TCP服务器控制块
struct tcp_pcb *tcppcbconn; //定义一个TCP服务器控制块
/* 为tcp服务器分配一个tcp_pcb结构体 */
tcppcbnew = tcp_new();
if(tcppcbnew) //创建成功
{
//将本地IP与指定的端口号绑定在一起,IP_ADDR_ANY为绑定本地所有的IP地址
err = tcp_bind(tcppcbnew,IP_ADDR_ANY,TCP_SERVER_PORT);
if(err==ERR_OK) //绑定完成
{
tcppcbconn=tcp_listen(tcppcbnew); //设置tcppcb进入监听状态
//初始化LWIP的tcp_accept的回调函数
tcp_accept(tcppcbconn,tcp_server_accept);
}
else
{
res=1;
}
}
else
{
res=1;
}
return res;
}
可以看到tcp_accept()函数注册了一个回调函数,实现代码如下:
/**
* @brief lwIP tcp_accept()的回调函数
* @param arg,newpcb, err
* @retval ret_err
*/
err_t tcp_server_accept(void *arg, struct tcp_pcb *newpcb,err_t err)
{
err_t ret_err;
struct tcp_server_struct *es;
LWIP_UNUSED_ARG(arg);
LWIP_UNUSED_ARG(err);
tcp_setprio(newpcb,TCP_PRIO_MIN);//设置新创建的pcb优先级
es=(struct tcp_server_struct*)mem_malloc(sizeof(struct tcp_server_struct)); //分配内存
if(es!=NULL) //内存分配成功
{
es->state = ES_TCPSERVER_ACCEPTED; //接收连接
es->pcb = newpcb;
es->p = NULL;
tcp_arg(newpcb, es);
tcp_recv(newpcb, tcp_server_recv); //初始化tcp_recv()的回调函数
tcp_err(newpcb, tcp_server_error); //初始化tcp_err()回调函数
tcp_poll(newpcb, tcp_server_poll,1); //初始化tcp_poll回调函数
tcp_sent(newpcb, tcp_server_sent); //初始化发送回调函数
tcp_server_flag |= 1<<5; //标记有客户端连上了
ret_err=ERR_OK;
}
else
{
ret_err=ERR_MEM;
}
return ret_err;
}
这个函数中用于与客户端进行数据交互,函数中有注册了接收发送等函数。本文最重要的就是需要接收函数,代码如下:
/**
* @brief lwIP tcp_recv()函数的回调函数
* @param arg,tpcb, p, err
* @retval ret_err
*/
err_t tcp_server_recv(void *arg, struct tcp_pcb *tpcb, struct pbuf *p, err_t err)
{
err_t ret_err;
uint32_t data_len = 0;
struct pbuf *q;
struct tcp_server_struct *es;
LWIP_ASSERT("arg != NULL",arg != NULL);
es=(struct tcp_server_struct *)arg;
if(p == NULL) //从客户端接收到空数据
{
es->state = ES_TCPSERVER_CLOSING;//需要关闭TCP 连接了
es->p = p;
ret_err = ERR_OK;
}
else if(err != ERR_OK) //从客户端接收到一个非空数据,但是由于某种原因err!=ERR_OK
{
if(p)
{
pbuf_free(p); //释放接收pbuf
}
ret_err = err;
}
else if(es->state == ES_TCPSERVER_ACCEPTED) //处于连接状态
{
if(p != NULL) //当处于连接状态并且接收到的数据不为空时将其打印出来
{
memset(tcp_server_recvbuf, 0, TCP_SERVER_RX_BUFSIZE); //数据接收缓冲区清零
for(q = p; q != NULL; q = q->next) //遍历完整个pbuf链表
{
//判断要拷贝到TCP_SERVER_RX_BUFSIZE中的数据是否大于TCP_SERVER_RX_BUFSIZE的剩余空间,如果大于
//的话就只拷贝TCP_SERVER_RX_BUFSIZE中剩余长度的数据,否则的话就拷贝所有的数据
if(q->len > (TCP_SERVER_RX_BUFSIZE-data_len))
{
memcpy(tcp_server_recvbuf+data_len,q->payload,(TCP_SERVER_RX_BUFSIZE-data_len));//拷贝数据
}
else
{
memcpy(tcp_server_recvbuf+data_len,q->payload,q->len);
}
data_len += q->len;
if(data_len > TCP_SERVER_RX_BUFSIZE)
{
break; //超出TCP客户端接收数组,跳出
}
}
tcp_server_flag |= 1<<6; //标记接收到数据了
tcp_recved(tpcb,p->tot_len);//用于获取接收数据,通知LWIP可以获取更多数据
pbuf_free(p); //释放内存
ret_err=ERR_OK;
}
}
else//服务器关闭了
{
tcp_recved(tpcb,p->tot_len);//用于获取接收数据,通知LWIP可以获取更多数据
es->p = NULL;
pbuf_free(p); //释放内存
ret_err = ERR_OK;
}
return ret_err;
}
可以看到,以上函数都是一层一层的调用,都是使用的回调函数。其他相关函数请自行参看源码。这里就不细讲了。
最后再main()函数初始化TCP服务器即可。
/**
* @brief The application entry point.
* @retval int
*/
int main(void)
{
/* USER CODE BEGIN 1 */
/* USER CODE END 1 */
/* Enable I-Cache---------------------------------------------------------*/
SCB_EnableICache();
/* Enable D-Cache---------------------------------------------------------*/
SCB_EnableDCache();
/* MCU Configuration--------------------------------------------------------*/
/* Reset of all peripherals, Initializes the Flash interface and the Systick. */
HAL_Init();
/* USER CODE BEGIN Init */
/* USER CODE END Init */
/* Configure the system clock */
SystemClock_Config();
/* USER CODE BEGIN SysInit */
/* USER CODE END SysInit */
/* Initialize all configured peripherals */
MX_GPIO_Init();
MX_USART3_UART_Init();
MX_LWIP_Init();
/* USER CODE BEGIN 2 */
tcp_server_init();
/* USER CODE END 2 */
/* Infinite loop */
/* USER CODE BEGIN WHILE */
while (1)
{
/* USER CODE END WHILE */
MX_LWIP_Process(); //LWIP轮询任务
/* USER CODE BEGIN 3 */
}
/* USER CODE END 3 */
}
然后编译工程,下载到板子中。打开jperf软件,配合好相应参数,其结果如下:
从上图可以看出,传输速度大约为1-2M左右,还是有点慢的。
那么要想提高LwIP网络传输速度的方法,就要对LwIP的配置进行合适的调整,主要增加内存的Heap Size、内存池大小、TCP报文段数量、最大TCP报文段、TCP发送缓冲区队列的最大长度等。
关于以上参数的修改可通过STM32CubeMX配置。也就是如下选项:
其对应的文件是lwipopts.h和opt.h,主要的配置参数在opt.h中。
笔者直接在文件中修改的,修改的参数如下:
//内存堆 heap 大小
#define MEM_SIZE (24*1024)
/* memp 结构的 pbuf 数量,如果应用从 ROM 或者静态存储区发送大量数据时这个值应该设置大一点 */
#define MEMP_NUM_PBUF 24
/* 最多同时在 TCP 缓冲队列中的报文段数量 */
#define MEMP_NUM_TCP_SEG 150
/* 内存池大小 */
#define PBUF_POOL_SIZE 64
/* 最大 TCP 报文段, TCP_MSS = (MTU - IP 报头大小 - TCP 报头大小 */
#define TCP_MSS (1500 - 40)
/* TCP 发送缓冲区大小(字节) */
#define TCP_SND_BUF (11*TCP_MSS)
/* TCP 接收窗口大小 */
#define TCP_WND (11*TCP_MSS)
修改后再进行编译,测试结果如下:
对比前文使用的默认参数,可以发现,速度增加明显。大约快了4-5倍。
关于LWIP的性能优化会在后面的章节讲解,本文的重点是测速。
5.3 RT-Thread系统测速(Socket API)
上一节使用RAW API来实现TCP服务器,本节将使用Socket API来实现TCP服务器,关于TCP服务器的实现可参考笔者博文。
实现TCP的服务器代码如下:
#include <rtthread.h>
#include <sys/socket.h>
#include <arpa/inet.h>
#include <netdev.h>
#include <stdio.h>
#include <string.h>
#define SERVER_PORT 8888
#define BUFF_SIZE 4096
static char recvbuff[BUFF_SIZE];
static void net_server_thread_entry(void *parameter)
{
int sfd, cfd, maxfd, i, nready, n;
struct sockaddr_in server_addr, client_addr;
struct netdev *netdev = RT_NULL;
char sendbuff[] = "Hello client!";
socklen_t client_addr_len;
fd_set all_set, read_set;
//FD_SETSIZE里面包含了服务器的fd
int clientfds[FD_SETSIZE - 1];
// 通过名称获取 netdev 网卡对象
netdev = netdev_get_by_name((char*)parameter);
if (netdev == RT_NULL)
{
rt_kprintf("get network interface device(%s) failed.\\n", (char*)parameter);
}
//创建socket
if ((sfd = socket(AF_INET, SOCK_STREAM, 0)) < 0)
{
rt_kprintf("Socket create failed.\\n");
}
server_addr.sin_family = AF_INET;
server_addr.sin_port = htons(SERVER_PORT);
//server_addr.sin_addr.s_addr = htonl(INADDR_ANY);
// 获取网卡对象中 IP 地址信息
server_addr.sin_addr.s_addr = netdev->ip_addr.addr;
//绑定socket
if (bind(sfd, (struct sockaddr *)&server_addr, sizeof(struct sockaddr)) < 0)
{
rt_kprintf("socket bind failed.\\n");
closesocket(sfd);
}
rt_kprintf("socket bind network interface device(%s) success!\\n", netdev->name);
//监听socket
if(listen(sfd, 5) == -1)
{
rt_kprintf("listen error");
}
else
{
rt_kprintf("listening...\\n");
}
client_addr_len = sizeof(client_addr);
//初始化 maxfd 等于 sfd
maxfd = sfd;
//清空fdset
FD_ZERO(&all_set);
//把sfd文件描述符添加到集合中
FD_SET(sfd, &all_set);
//初始化客户端fd的集合
for(i = 0; i < FD_SETSIZE -1 ; i++)
{
//初始化为-1
clientfds[i] = -1;
}
while(1)
{
//每次select返回之后,fd_set集合就会变化,再select时,就不能使用,
//所以我们要保存设置fd_set 和 读取的fd_set
read_set = all_set;
nready = select(maxfd + 1, &read_set, NULL, NULL, NULL);
//没有超时机制,不会返回0
if(nready < 0)
{
rt_kprintf("select error \\r\\n");
}
//判断监听的套接字是否有数据
if(FD_ISSET(sfd, &read_set))
{
//有客户端进行连接了
cfd = accept(sfd, (struct sockaddr *)&client_addr, &client_addr_len);
if(cfd < 0)
{
rt_kprintf("accept socket error\\r\\n");
//继续select
continue;
}
rt_kprintf("new client connect fd = %d\\r\\n", cfd);
//把新的cfd 添加到fd_set集合中
FD_SET(cfd, &all_set);
//更新要select的maxfd
maxfd = (cfd > maxfd)?cfd:maxfd;
//把新的cfd 保存到cfds集合中
for(i = 0; i < FD_SETSIZE -1 ; i++)
{
if(clientfds[i] == -1)
{
clientfds[i] = cfd;
//退出,不需要添加
break;
}
}
//没有其他套接字需要处理:这里防止重复工作,就不去执行其他任务
if(--nready == 0)
{
//继续select
continue;
}
}
//遍历所有的客户端文件描述符
for(i = 0; i < FD_SETSIZE -1 ; i++)
{
if(clientfds[i] == -1)
{
//继续遍历
continue;
}
//判断是否在fd_set集合里面
if(FD_ISSET(clientfds[i], &read_set))
{
n = recv(clientfds[i], recvbuff, sizeof(recvbuff), 0);
//rt_kprintf("clientfd %d: %s \\r\\n",clientfds[i], recvbuff);
if(n <= 0)
{
//从集合里面清除
FD_CLR(clientfds[i], &all_set);
//当前的客户端fd 赋值为-1
clientfds[i] = -1; }
else
{
//写回客户端
n = send(clientfds[i], sendbuff, strlen(sendbuff), 0);
if(n < 0)
{
//从集合里面清除
FD_CLR(clientfds[i], &all_set);
//当前的客户端fd 赋值为-1
clientfds[i] = -1;
}
}
}
}
}
}
static int server(int argc, char **argv)
{
rt_err_t ret = RT_EOK;
if (argc != 2)
{
rt_kprintf("bind_test [netdev_name] --bind network interface device by name.\\n");
return -RT_ERROR;
}
/* 创建 serial 线程 */
rt_thread_t thread = rt_thread_create("server",
net_server_thread_entry,
argv[1],
2048,
5,
10);
/* 创建成功则启动线程 */
《嵌入式 - Lwip开发指南》第1章 LWIP概述
《嵌入式 - Lwip开发指南》第2章 LWIP开发环境简介
《嵌入式 - Lwip开发指南》第2章 LWIP开发环境简介
《嵌入式 - Lwip开发指南》第3章 移植LWIP(无系统)