1 背景Lwip是适用于类似嵌入式等资源有限的系统要实现lwip在嵌入式系统上的开发遍历和功能迭代一般需要搭配rtos一起实现可以避免繁杂的事件回调轻松实现任务分离和提高对上层协议的适配度。在使用lwip开发tcp client过程中一个比较常见的功能就是要求client周期性的向server请求数据要求client实时接收server的数据。但是在过程中发现client的发送部分并不能自动检测到与server的连接状态导致无法在server主动断开连接后做出重连动作。2 Socket收发流程在LwIP中使用Socket接口也叫 BSD Socket API来开发TCP Client是最方便、也最接近PC编程习惯的方式。作为TCP client收发数据的核心流程是建立Socket → 连接Server → 收发数据2.1 建立socket使用socket创建一个TCP类型的socketint sock -1; sock socket (AF_INET, SOCK_STREAM, 0) : if (sock 0) { printf(Socket error\n); //创建失败的处理 vTaskDelay(10); continue; }函数原型intlwip_socket(intdomain,inttype,intprotocol);socket()第一个参数domain其中AF_INET表示IPv4协议族在lwip中一般都是AF_INETsocket()第二个参数typeSOCK_STREAM表示套接字类型为流套接字对应于TCP协议SOCK_DGRAM表示数据报套接字对应于DUP协议SOCK_RAW表示原始套接字能够处理IP层协议。socket()第三个参数protocol通常为0让系统根据type自动选择。返回分配socket成功返回socket编号否则为 -1。2.2 连接Server配置好服务器的IP地址和端口号然后调用 connect() 函数连接server。struct sockaddr_in server_addr; server_addr.sin_family AF_INET; server_addr.sin_port htons((DEST_PORT); // 端口号需要转换为网络字节序 server_addr.sin_addr.s_addr ipaddr.addr; // 服务器的IP地址 if (connect(sock, (struct sockaddr *)server_addr, sizeof(server_addr)) 0) { // 连接失败处理 printf(Connect failed!\r\n); }函数原型intlwip_connect(ints, const struct sockaddr *name, socklen_tnamelen);connect ()第一个参数ssocket编号对应于socket()返回的值connect ()第二个参数name包含server的IP和port以及连接server的协议族为ipv4。socket()第三个参数namelen是sockaddr_in的大小。返回连接server成功返回0否则为 -1。2.3 发送数据在连接成功后在一个循环中按需求发送数据包如果client本地socket发送失败则退出循环重新连接server。while (1) { if(write(sock,send_buf,sizeof(send_buf)) 0) break; vTaskDelay(1000); }2..4 接收数据在连接成功后在一个循环中阻塞接收数据包如果client接收出错则退出循环重新连接server。while (1) { ulreturn xSemaphoreTake(Binary_Sem, 0); recv_len recv(sock, recbuf, RECV_DATAMAX, 0); if ((recv_len 0) (!ulreturn)) { printf(%s\r\n, recbuf); } else { printf(Receive error.\r\n); break; } }3 socket同时收发我尝试了几种方法也就是踩过的几个坑都展示出来1使用两个独立的socket连接tcp分别做发送/接收功能问题是接收部分能精准的识别到client和server断开连接发送部分不能识别到不能进行重连2使用一个socket做tcp连接发送轮询单独开接收任务问题是发送部分不能识别到不能进行重连接收也会失效3使用一个socket做tcp连接接收轮询单独开发送任务能识别到重新连接但是链接后会重复发送两包相同数据使用了接收对断开连接敏感的特性后发现是因为断一次后socket连接tcp时又单独开了发送任务。4使用一个socket做连接tcp分时段轮询tcp发送接收使用fdassert做500us的延时判断接收发送通过消息队列进行发送发送可被其他调用消息队列改大造成hardfault5使用一个socket做连接tcp分时段轮询tcp发送接收使用fdassert做500us的延时判断接收发送通过消息队列进行发送发送可被其他调用使用消息队列结构体做独立的发送部分建立新的消息队列总结来说TCP发送write和接收recv两个部分对TCP连接断开检测的灵敏度完全不同。接收任务调用 recv()这是被动等待的函数。当 Server 断开连接时recv() 会立即返回 0表示连接被对方正常关闭当你的代码检测到 recv_len 0 后立即跳出内层循环关闭 socket进入外层循环重连这意味着接收任务能在断开发生后的第一时间感知并马上执行重连逻辑。发送任务调用 send()这是主动推送的函数。当 Server 断开连接时比如拔网线或突然断电存在一个致命的盲区TCP 协议栈不知道连接已经断开仍然认为连接是正常的。因此 send() 可能返回成功数据进了发送缓冲区但并未真正到达对端或者 send() 长时间阻塞/等待直到 TCP 的重传机制超时这个时间可能长达几十秒甚至几分钟在此期间send() 可能持续返回正值虽然数据根本没发出去导致内层循环永远不退出。3.1 方法一是上表中的第3条使用一个socket做tcp连接接收轮询单独开发送任务。关键步骤是在socket connect成功之后创建独立的线程共享同一个socket用于client定期发送消息发送线程中如果发送失败通过信号量告知接收线程在接收时判断信号量和接收状态如果接收或者发送感知到失败则会断开重连server。附上原码/******************************************************************************* * copyright: jxxf * filename: client.c * brief: This file provides the client socket example. * author: jxxf * version: V1.0.0/2024-7-10 * 1.Initial version * log: *******************************************************************************/ /* Includes ------------------------------------------------------------------*/ #include client.h #include lwip/opt.h #include lwip/sys.h #include lwip/api.h #include lwip/sockets.h /* Private typedef -----------------------------------------------------------*/ /* Private define ------------------------------------------------------------*/ #define RECV_DATAMAX (1024) /* Private macro -------------------------------------------------------------*/ /* Private variables ---------------------------------------------------------*/ static SemaphoreHandle_t Binary_Sem NULL; char recbuf[RECV_DATAMAX]; /* Private function prototypes -----------------------------------------------*/ static void client(void *thread_param); static void client_send(void* sock); /* Private functions ---------------------------------------------------------*/ /** * brief Connect to server port and receive data. * param thread_param:pass a parameter pointer contains information. * retval None */ static void client(void *thread_param) { int recv_len 0; BaseType_t ulreturn 0; int sock -1; struct sockaddr_in client_addr; ip4_addr_t ipaddr; printf(Dest IP Addr:%d.%d.%d.%d \t Port No:%d\n\n, \ DEST_IP_ADDR0, DEST_IP_ADDR1, DEST_IP_ADDR2, DEST_IP_ADDR3, DEST_PORT); printf(Set your computers upper computer to TCP Server. In the User/arch/sys_arch.h file, change the destination IP address to the IP address on your computer\n\n); printf(Modify the corresponding Macro:DEST_IP_ADDR0,DEST_IP_ADDR1,DEST_IP_ADDR2,DEST_IP_ADDR3,DEST_PORT\n\n); IP4_ADDR(ipaddr, DEST_IP_ADDR0, DEST_IP_ADDR1, DEST_IP_ADDR2, DEST_IP_ADDR3); while (1) { sock socket(AF_INET, SOCK_STREAM, 0); if (sock 0) { printf(Socket error\n); vTaskDelay(10); continue; } client_addr.sin_family AF_INET; client_addr.sin_port htons(DEST_PORT); client_addr.sin_addr.s_addr ipaddr.addr; memset((client_addr.sin_zero), 0, sizeof(client_addr.sin_zero)); if (connect(sock, (struct sockaddr *)client_addr, sizeof(struct sockaddr)) -1) { printf(Connect failed!\n); closesocket(sock); vTaskDelay(2000); continue; } printf(Connect to server successful!\n); sys_thread_t Client_Send sys_thread_new(client_send, client_send, sock, 512, 4); while (1) { ulreturn xSemaphoreTake(Binary_Sem, 0); recv_len recv(sock, recbuf, RECV_DATAMAX, 0); if ((recv_len 0) (!ulreturn)) { printf(%s\r\n, recbuf); } else if (recv_len 0) { printf(Server closed the connection.\r\n); break; } else { printf(Receive error.\r\n); break; } } closesocket(sock); vTaskDelete(Client_Send); } } /** * brief Send data to server periodically. * param thread_param:pass a parameter pointer contains information. * retval None */ static void client_send(void* sock) { int send_sock *(int*)sock; uint8_t send_buf[] STM32F4xx Eval TCP Client test...\n; while (1) { if (write(send_sock, send_buf, sizeof(send_buf)) 0) while (pdFALSE xSemaphoreGive(Binary_Sem)); vTaskDelay(1000); } } /** * brief Create a TCP client task. * param None * retval None */ void client_init(void) { Binary_Sem xSemaphoreCreateBinary(); sys_thread_new(client, client, NULL, 512, 4); }3.2 方法二是上表中的第5条使用一个socket做连接tcp分时段轮询tcp发送接收使用fdassert做500us的延时判断接收。关键步骤是在socket connect成功之后做一个多路IO复用fd_set扫描选定socket是否有接收事件发生int ret select(sock 1, readfds, NULL, NULL, tv);如果有事件发生判断是否为这个socket的。如果都符合进入接收流程。发送通过消息队列进行发送可以通过其他任务异步发送消息消息队列结构体创建为指针形式需要注意堆得分配和释放时机附上原码/******************************************************************************* * copyright: jxxf * filename: client.c * brief: This file provides the client socket example. * author: jxxf * version: V1.0.0/2026-6-16 * 1.Initial version * log: *******************************************************************************/ /* Includes ------------------------------------------------------------------*/ #include client.h #include lwip/opt.h #include lwip/sys.h #include lwip/api.h #include lwip/sockets.h /* Private typedef -----------------------------------------------------------*/ typedef struct { uint8_t *data; size_t len; } send_msg_t; /* Private define ------------------------------------------------------------*/ #define SEND_QUEUE_LEN 4 #define RECV_DATAMAX 1024 /* Private macro -------------------------------------------------------------*/ /* Private variables ---------------------------------------------------------*/ char recbuf[RECV_DATAMAX]; const char send_buf[] STM32F4xx Eval TCP Client test...\n; //SemaphoreHandle_t BinarySem_Handle; static QueueHandle_t send_queue NULL; /* Private function prototypes -----------------------------------------------*/ static void client(void *thread_param); static void client_generic_send(void *thread_param); static int tcp_send_async(const void *data, size_t len); /* Private functions ---------------------------------------------------------*/ /** * brief Transmit data periodically. * param thread_param:pass a parameter pointer contains information。 * retval None */ static void client_generic_send(void *thread_param) { while (1) { tcp_send_async(send_buf, sizeof(send_buf)); vTaskDelay(pdMS_TO_TICKS(1000)); } } /** * brief Connect to server port and transmit data periodically. * param thread_param:pass a parameter pointer contains information。 * retval None */ static void client(void *thread_param) { int sock -1; send_msg_t send_msg; struct sockaddr_in client_addr; uint32_t last_send 0; ip4_addr_t ipaddr; printf(Dest IP Addr:%d.%d.%d.%d \t Port No:%d\n\n, \ DEST_IP_ADDR0, DEST_IP_ADDR1, DEST_IP_ADDR2, DEST_IP_ADDR3, DEST_PORT); printf(Set your computers upper computer to TCP Server. In the User/arch/sys_arch.h file, change the destination IP address to the IP address on your computer\n\n); printf(Modify the corresponding Macro:DEST_IP_ADDR0,DEST_IP_ADDR1,DEST_IP_ADDR2,DEST_IP_ADDR3,DEST_PORT\n\n); IP4_ADDR(ipaddr, DEST_IP_ADDR0, DEST_IP_ADDR1, DEST_IP_ADDR2, DEST_IP_ADDR3); while (1) { if (sock 0) { sock socket(AF_INET, SOCK_STREAM, 0); if (sock 0) { printf(Socket error\n); vTaskDelay(10); continue; } client_addr.sin_family AF_INET; client_addr.sin_port htons(DEST_PORT); client_addr.sin_addr.s_addr ipaddr.addr; memset((client_addr.sin_zero), 0, sizeof(client_addr.sin_zero)); if (connect(sock, (struct sockaddr *)client_addr, sizeof(struct sockaddr)) -1) { printf(Connect failed!\n); closesocket(sock); vTaskDelay(2000); continue; } printf(Connect to server successful!\n); while (xQueueReceive(send_queue, send_msg, 0) pdPASS) { vPortFree(send_msg.data); } } /* Waiting event: The socket is readable or has sent a message */ fd_set readfds; struct timeval tv {0, 500}; FD_ZERO(readfds); FD_SET(sock, readfds); int ret select(sock 1, readfds, NULL, NULL, tv); /* handle receive */ if (ret 0 FD_ISSET(sock, readfds)) { int len recv(sock, recbuf, sizeof(recbuf) - 1, 0); if (len 0) { recbuf[len] \0; printf(Recv: %s\r\n, recbuf); } else { close(sock); sock -1; printf(Receive error.\r\n); continue; } } /* Handle sent messages (non-blocking queue fetching) */ while (xQueueReceive(send_queue, send_msg, 0) pdPASS) { int sent send(sock, send_msg.data, send_msg.len, 0); vPortFree(send_msg.data); if (sent 0) { close(sock); sock -1; break; } } /* Regular heartbeat,need server perfection mechanism */ if (sys_now() - last_send 30000) { const char *hb PING; send(sock, hb, strlen(hb), 0); last_send sys_now(); } } } /** * brief Client user generic send msg. * param data: point to const msg that want to send. * param len: the length of const msg. * retval if transmit ok return the len, otherwise return -1. */ static int tcp_send_async(const void *data, size_t len) { send_msg_t msg; if (send_queue NULL) return -1; /* Copy the data to the message */ msg.data pvPortMalloc(len); if (msg.data NULL) return -1; memcpy(msg.data, data, len); msg.len len; /* Send to the queue (non-blocking) */ if (xQueueSend(send_queue, msg, 0) ! pdPASS) { vPortFree(msg.data); return -1; } return len; } /** * brief Create a TCP client task. * param None * retval None */ void client_init(void) { send_queue xQueueCreate(SEND_QUEUE_LEN, sizeof(send_msg_t)); sys_thread_new(client, client, NULL, 512, 4); sys_thread_new(client_generic_send, client_generic_send, NULL, 512, 4); }4. 更好的办法实现client和server之间的心跳机制需要双方修改应用层这是最灵活和可控的方式。双方约定一个特定的“心跳数据包”通常是空包或极短的固定指令如 PING / PONG。流程Client启动定时器如每隔xx秒调用 lwip_send() 发送心跳请求。Server 收到后立即调用 lwip_send() 回复心跳响应。如果 Server 在超时时间如连续 3 个周期内未收到心跳则主动调用 lwip_close() 释放连接。反之如果 Client 未收到 Server 的响应也判定连接失效尝试重连。本文章针对在嵌入式环境下使用lwip的工程师本文纯属个人理解若有更好的方法希望大家不吝赐教。