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类型的socket;
int sock = -1;
sock = socket (AF_INET, SOCK_STREAM, 0) :
if (sock < 0)
{
printf("Socket error\n"); //创建失败的处理
vTaskDelay(10);
continue;
}
函数原型:int lwip_socket(int domain, int type, int protocol);
socket()第一个参数domain:其中AF_INET表示IPv4协议族,在lwip中一般都是AF_INET
socket()第二个参数type:SOCK_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");
}
函数原型:int lwip_connect(int s, const struct sockaddr *name, socklen_t namelen);
connect ()第一个参数s:socket编号,对应于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的延时判断接收 |
发送通过消息队列进行发送,发送可被其他调用,消息队列改大造成hardfault; |
|
5 |
使用一个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 computer's 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 computer's 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的工程师,本文纯属个人理解,若有更好的方法,希望大家不吝赐教。
396



被折叠的 条评论
为什么被折叠?



