最近有個網友在詢問關于LWIP的速度,本文就LWIP網速做個簡單測驗,為了對比,本文將使用無系統和有系統兩種環境,
5.1網路測速工具介紹
不過在測速之前,需要介紹下測速的工具,這里有兩個軟體:iPerf與jperf,
iPerf 是一個跨平臺的網路性能測驗工具,它支持Win/Linux/Mac/Android/iOS 等平臺,iPerf 可以測驗TCP 和UDP(我們一般不對UDP 進行測速)帶寬質量,iPerf 可以測量最大TCP 帶寬,可以具有多種引數進行測驗,同時iPerf 還可以報告帶寬,延遲抖動和資料包丟失的情況,我們可以利用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可以參看筆者以前的文章,
移植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服務器實作
實作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);
/* 創建成功則啟動執行緒 */
if (thread != RT_NULL)
{
rt_thread_startup(thread);
}
else
{
ret = RT_ERROR;
}
return ret;
}
#ifdef FINSH_USING_MSH
#include <finsh.h>
MSH_CMD_EXPORT(server, network interface device test);
#endif /* FINSH_USING_MSH */
添加好相應代碼進行編譯,編譯后才能后下載韌體,測驗結果如下:

可以看出,其傳輸速度也在1M左右,相對無系統的環境,其速度相對慢些,
要想提高速度,就配置下LWIP引數,筆者配置的引數如下:

編譯,下載,測驗結果如下:

可以看到其速度還是有所提升的,只是沒有無系統時提升的明顯,至于原因后面的章節將會具體分析,
資源獲取方法
1.長按下面二維碼,關注公眾號[嵌入式實驗樓]
2.在公眾號回復關鍵詞[LWIP]獲取資料

歡迎訪問我的網站
BruceOu的嗶哩嗶哩
BruceOu的主頁
BruceOu的博客
BruceOu的CSDN博客
BruceOu的簡書
BruceOu的知乎
轉載請註明出處,本文鏈接:https://www.uj5u.com/qita/321187.html
標籤:其他
