一、问题描述
最近用stmf429单片机作为TCP服务端遇到一个问题,就是客户端特别频繁的发送消息,过一段时间以后,客户端的请求不再被客户端接收到,而且服务器端监控的掉线回调函数也不会被调用,好像这个连接就凭空的消失了。
二、问题排除
首先,我以为是QT的客户端出现问题了,因为请求过于频繁,所以tcp的发送缓存区满了,但是我在发送消息的时候加了缓存区内容判断,有内容就不发送,还是不管用,然后用了串口助手软件以同样速度向单片机发送消息,过不了多久也就失去连接了,所以排除了客户端的问题。确定是单片机服务器端的问题,然后我又改了服务器端的返回消息那里,也是加发送缓存区的判断,同样是不管用,最后发现,是没有加tcp_recved的原因。
三、 tcp_recved描述
tcp_recved
是 lwIP(Lightweight IP,轻量级 TCP/IP 协议栈)中用于 TCP 连接的数据接收确认操作的重要函数调用。
1.函数作用
tcp_recved
函数的主要作用是告知 lwIP 协议栈,应用程序已经成功处理了指定长度的数据,这样 lwIP 可以更新接收窗口的大小,允许发送方继续发送更多的数据。在 TCP 协议中,接收方会维护一个接收窗口,用于告知发送方自己当前能够接收的数据量。当应用程序处理完一部分数据后,需要调用 tcp_recved
函数来调整这个窗口大小,以保证数据的正常传输。
2.函数原型及参数
void tcp_recved(struct tcp_pcb *tpcb, u16_t len);
- 参数:
tpcb
:指向struct tcp_pcb
类型的指针,该结构体代表一个 TCP 连接的协议控制块,包含了该连接的所有状态信息。len
:表示应用程序已经成功处理的数据长度,单位是字节。
3. 使用场景
在 TCP 服务器或客户端的接收回调函数中,当应用程序接收到数据并完成处理后,需要调用 tcp_recved
函数。例如,在 client_recv_handler
回调函数中,当处理完接收到的 pbuf
(数据包缓冲区)中的数据后,就会调用该函数。
4.实现原理
- 更新接收窗口:
tcp_recved
函数会根据传入的len
参数,更新 TCP 连接的接收窗口大小。接收窗口的大小表示接收方当前还能接收多少字节的数据。当应用程序处理完数据后,接收窗口会相应地增大,允许发送方发送更多的数据。 - 发送确认信息:lwIP 会根据更新后的接收窗口信息,向发送方发送确认信息(ACK),告知发送方可以继续发送的数据范围。
4.示例代码
#include "lwip/tcp.h"
// 接收回调函数
err_t client_recv_handler(void *arg, struct tcp_pcb *tpcb, struct pbuf *p, err_t err)
{
if (p != NULL) {
// 处理接收到的数据
// 这里可以添加具体的数据处理逻辑,如解析数据、存储数据等
// ...
// 告知 lwIP 已经处理完这些数据
tcp_recved(tpcb, p->tot_len);
// 释放 pbuf 占用的内存
pbuf_free(p);
} else if (err == ERR_OK) {
// 连接关闭处理
tcp_close(tpcb);
}
return ERR_OK;
}
5.注意事项
- 及时调用:在处理完接收到的数据后,应及时调用
tcp_recved
函数,否则接收窗口不会更新,发送方可能会认为接收方没有足够的空间来接收新的数据,从而暂停数据发送,导致网络性能下降,我这次就是因为没有调用这个函数,客户端与服务器端频繁通讯出现了连接失效的问题。 - 数据长度准确:传入
tcp_recved
函数的len
参数必须准确表示应用程序已经处理的数据长度,否则可能会导致接收窗口大小计算错误,影响数据的正常传输。 - 内存管理:在调用
tcp_recved
函数后,通常需要释放pbuf
占用的内存,避免内存泄漏。可以使用pbuf_free
函数来释放内存。
四、TCP服务端完整代码
#include "tcp_demo.h"
extern u8* GETDATA_BUFF8;
extern short *GETDATA_BUFF16;
extern u8 nowtype;
extern OS_SEM Gather_SEM;
#define TCP_SERVER_PORT 8088 // 定义TCP服务器监听的端口号,可按需修改
#define MAX_CONNECTIONS 2 // 最大允许同时连接的客户端数量
#define RX_BUFFER_SIZE 1024 // 接收缓冲区大小
// 用于存储客户端连接的结构体数组
struct tcp_pcb *tcp_server_pcbs[MAX_CONNECTIONS];
// 用于保护对tcp_server_pcbs数组操作的互斥信号量
OS_SEM tcp_server_pcbs_sem;
// 处理客户端连接的函数
err_t client_connection_handler(void *arg, struct tcp_pcb *newpcb, err_t err);
// 处理客户端接收数据的函数
err_t client_recv_handler(void *arg, struct tcp_pcb *tpcb, struct pbuf *p, err_t err);
// 处理客户端发送数据的函数(示例中简单回显数据给客户端)
err_t client_send_handler(void *arg, struct tcp_pcb *tpcb, u16_t len);
// 处理客户端断开连接的函数
void client_err_handler(void *arg, err_t err);
// TCP服务器任务函数
void tcp_server_task(void *p_arg);
//TCP客户端任务
#define TCP_PRIO 7
//任务堆栈大小
#define TCP_STK_SIZE 300
//任务控制块
OS_TCB TcpTaskTCB;
//任务堆栈
CPU_STK TCP_TASK_STK[TCP_STK_SIZE];
int close_tcp=0;
void tcp_server_task(void *p_arg)
{
struct tcp_pcb *server_pcb;
err_t err;
OS_ERR os_err;
// 创建一个TCP协议控制块(PCB)用于服务器端
server_pcb = tcp_new();
if (server_pcb == NULL)
{
printf("Error creating TCP PCB\n");
return;
}
// 绑定服务器的IP地址和端口号
err = tcp_bind(server_pcb, IP_ADDR_ANY, TCP_SERVER_PORT);
if (err!= ERR_OK)
{
printf("Error binding TCP socket: %d\n", err);
tcp_close(server_pcb);
return;
}
// 将服务器PCB设置为监听状态,等待客户端连接
server_pcb = tcp_listen(server_pcb);
if (server_pcb == NULL)
{
printf("Error listening for connections\n");
tcp_close(server_pcb);
return;
}
// 设置接受客户端连接的回调函数
tcp_accept(server_pcb, client_connection_handler);
while (1)
{
// 任务可以在这里进行适当的阻塞等待,避免过度占用CPU资源
OSTimeDlyHMSM(0,0,0,100,OS_OPT_TIME_HMSM_STRICT,&os_err); //延时100ms
}
}
err_t client_connection_handler(void *arg, struct tcp_pcb *newpcb, err_t err)
{
OS_ERR os_err;
int client_index;
if (err!= ERR_OK)
{
return err;
}
// 获取互斥信号量,保护对tcp_server_pcbs数组的访问
OSSemPend(&tcp_server_pcbs_sem,0,OS_OPT_PEND_BLOCKING,0,&os_err); //请求信号量
if(tcp_server_pcbs[close_tcp]!=NULL){
tcp_close(tcp_server_pcbs[close_tcp]);
}
tcp_server_pcbs[close_tcp]=newpcb;
if(close_tcp==0){
close_tcp=1;
}else{
close_tcp=0;
}
// 设置接收、发送和错误处理的回调函数
tcp_recv(newpcb, client_recv_handler);
tcp_sent(newpcb, client_send_handler);
tcp_err(newpcb, client_err_handler);
OSSemPost(&tcp_server_pcbs_sem, OS_OPT_POST_1, &os_err);
return ERR_OK;
}
err_t client_recv_handler(void *arg, struct tcp_pcb *tpcb, struct pbuf *p, err_t err)
{
OS_ERR os_err;
if (err == ERR_OK && p!= NULL)
{
// 将接收到的pbuf数据复制到本地缓冲区(这里简单示例,可优化)
/**char rx_buffer[RX_BUFFER_SIZE];
int copied_bytes = 0;
struct pbuf *q = p;
while (q!= NULL)
{
int bytes_to_copy = (q->len < (RX_BUFFER_SIZE - copied_bytes))? q->len : (RX_BUFFER_SIZE - copied_bytes);
memcpy(&rx_buffer[copied_bytes], q->payload, bytes_to_copy);
copied_bytes += bytes_to_copy;
q = q->next;
}**/
// 这里可以对接收到的数据进行处理,比如根据协议解析等,现在简单回显数据给客户端
//tcp_write(tpcb, rx_buffer, copied_bytes, TCP_WRITE_FLAG_COPY);
//tcp_output(tpcb);
OSSemPend(&Gather_SEM,0,OS_OPT_PEND_BLOCKING,0,&os_err); //请求信号量
u16_t free_space = tcp_sndbuf(tpcb);
if (free_space >= 1441) {
// 缓冲区有足够空间,写入数据
tcp_write(tpcb, GETDATA_BUFF8, 1441, TCP_WRITE_FLAG_COPY);
tcp_output(tpcb);
} else {
// 缓冲区空间不足,进行相应处理,如等待或丢弃数据
// 这里简单打印提示信息
printf("Send buffer is not enough!\n");
}
OSSemPost (&Gather_SEM,OS_OPT_POST_1,&os_err); //发送信号量
// 释放接收到的pbuf内存
tcp_recved(tpcb, p->tot_len);
pbuf_free(p);
}
else if (err == ERR_OK && p == NULL)
{
OSSemPend(&tcp_server_pcbs_sem,0,OS_OPT_PEND_BLOCKING,0,&os_err); //请求信号量
// 客户端关闭了连接,正常处理
for (int i = 0; i < MAX_CONNECTIONS; i++)
{
if (tcp_server_pcbs[i] == tpcb)
{
tcp_server_pcbs[i] = NULL;
break;
}
}
OSSemPost(&tcp_server_pcbs_sem, OS_OPT_POST_1, &os_err);
tcp_close(tpcb);
}
else
{
// 出现错误情况,关闭连接
tcp_close(tpcb);
}
return ERR_OK;
}
err_t client_send_handler(void *arg, struct tcp_pcb *tpcb, u16_t len)
{
// 这里可以根据实际发送情况做一些后续处理,当前示例只是简单回显,无需额外操作
return ERR_OK;
}
void client_err_handler(void *arg, err_t err)
{
struct tcp_pcb *tpcb = (struct tcp_pcb *)arg;
OS_ERR os_err;
// 获取互斥信号量,保护对tcp_server_pcbs数组的访问
OSSemPend(&tcp_server_pcbs_sem,0,OS_OPT_PEND_BLOCKING,0,&os_err); //请求信号量
for (int i = 0; i < MAX_CONNECTIONS; i++)
{
if (tcp_server_pcbs[i] == tpcb)
{
tcp_server_pcbs[i] = NULL;
break;
}
}
OSSemPost(&tcp_server_pcbs_sem, OS_OPT_POST_1, &os_err);
tcp_close(tpcb);
}
//创建TCP线程
//返回值:0 TCP创建成功
// 其他 TCP创建失败
u8 tcp_demo_init(void)
{
OS_ERR err;
CPU_SR_ALLOC();
OS_CRITICAL_ENTER();//进入临界区
//创建TCP任务
OSTaskCreate((OS_TCB * )&TcpTaskTCB,
(CPU_CHAR * )"tcp task",
(OS_TASK_PTR )tcp_server_task,
(void * )0,
(OS_PRIO )TCP_PRIO,
(CPU_STK * )&TCP_TASK_STK[0],
(CPU_STK_SIZE)TCP_STK_SIZE/10,
(CPU_STK_SIZE)TCP_STK_SIZE,
(OS_MSG_QTY )0,
(OS_TICK )0,
(void * )0,
(OS_OPT )OS_OPT_TASK_STK_CHK|OS_OPT_TASK_STK_CLR,
(OS_ERR * )&err);
OS_CRITICAL_EXIT(); //退出临界区
return err;
}
1. 全局变量和宏定义
- 端口和连接相关:
TCP_SERVER_PORT
:定义了 TCP 服务器监听的端口号,这里设为 8088。MAX_CONNECTIONS
:规定了服务器最大允许同时连接的客户端数量,为 2。RX_BUFFER_SIZE
:设置了接收缓冲区的大小,为 1024 字节。
- 客户端连接数组和信号量:
tcp_server_pcbs
:是一个用于存储客户端连接的struct tcp_pcb
类型的数组,长度为MAX_CONNECTIONS
。tcp_server_pcbs_sem
:用于保护对tcp_server_pcbs
数组操作的互斥信号量。
- 其他外部变量:
GETDATA_BUFF8
、GETDATA_BUFF16
、nowtype
是外部定义的变量,Gather_SEM
是一个外部信号量。
2. 函数声明
声明了多个处理 TCP 连接、数据接收、发送、错误处理以及服务器任务的函数,具体如下:
client_connection_handler
:处理客户端连接请求。client_recv_handler
:处理客户端发送的数据。client_send_handler
:处理数据发送完成后的操作。client_err_handler
:处理连接错误。tcp_server_task
:TCP 服务器的主任务函数。
3. TCP 服务器任务函数 tcp_server_task
- 创建和绑定 PCB:使用
tcp_new()
创建一个 TCP 协议控制块(PCB),并通过tcp_bind()
将其绑定到指定的 IP 地址(IP_ADDR_ANY
表示监听所有可用的 IP 地址)和端口号。 - 监听连接:调用
tcp_listen()
将服务器 PCB 设置为监听状态,等待客户端连接。 - 设置回调函数:使用
tcp_accept()
设置接受客户端连接的回调函数为client_connection_handler
。 - 任务循环:在一个无限循环中,使用
OSTimeDlyHMSM()
函数进行 100ms 的延时,避免任务过度占用 CPU 资源。
4. 客户端连接处理函数 client_connection_handler
- 错误检查:检查连接是否存在错误,若有则直接返回错误码。
- 管理连接数组:获取互斥信号量
tcp_server_pcbs_sem
,保护对tcp_server_pcbs
数组的访问。如果数组中已有连接,关闭该连接,然后将新的连接添加到数组中。 - 设置回调函数:为新的客户端连接设置接收、发送和错误处理的回调函数,分别为
client_recv_handler
、client_send_handler
和client_err_handler
。 - 释放信号量:操作完成后,释放互斥信号量。
5. 客户端数据接收处理函数 client_recv_handler
- 正常接收数据:当接收到数据(
err == ERR_OK && p != NULL
)时,先获取Gather_SEM
信号量,检查发送缓冲区是否有足够空间。若空间足够,将GETDATA_BUFF8
中的 1441 字节数据写入发送缓冲区并尝试发送;若空间不足,打印提示信息。处理完后释放Gather_SEM
信号量,调用tcp_recved()
告知 lwIP 已处理的数据长度,并释放接收到的pbuf
内存。 - 客户端关闭连接:当客户端关闭连接(
err == ERR_OK && p == NULL
)时,获取tcp_server_pcbs_sem
信号量,从tcp_server_pcbs
数组中移除该连接,然后释放信号量并关闭连接。 - 错误处理:若出现其他错误情况,直接关闭连接。
6. 客户端数据发送处理函数 client_send_handler
该函数目前只是简单返回 ERR_OK
,可根据实际发送情况添加后续处理逻辑。
7. 客户端连接错误处理函数 client_err_handler
获取 tcp_server_pcbs_sem
信号量,从 tcp_server_pcbs
数组中移除出现错误的连接,释放信号量并关闭连接。
8. TCP 任务创建函数 tcp_demo_init
使用 uC/OS-II 的 OSTaskCreate()
函数创建 tcp_server_task
任务,指定任务的优先级、堆栈等参数。如果任务创建成功,返回 0;否则返回相应的错误码。