在嵌入式网络开发中,LWIP作为轻量级TCP/IP协议栈被广泛应用。很多开发者在使用LWIP的raw API时会遇到一个典型问题:明明发送数据流程看起来没问题,但通信一段时间后却无法继续接收数据。这往往与TCP滑动窗口机制和tcp_recved函数的调用时机密切相关。
TCP协议通过滑动窗口实现流量控制,这个窗口就像快递柜的格子间。当接收方收到数据包时,窗口会相应缩小(相当于格子被占用);只有当应用层处理完数据后,才需要通过tcp_recved通知协议栈释放窗口空间(相当于腾出空格子)。LWIP的特别之处在于,使用raw API时需要开发者手动管理这个过程,而socket/netconn等高级API会自动处理。
我曾在一个智能家居项目中踩过坑:设备作为TCP服务器接收控制指令时,最初把tcp_recved放在发送回调函数中调用。测试时发现,如果客户端连续发送10条指令后停止发送,服务器就无法接收第11条指令。这就是典型的接收窗口耗尽现象——相当于快递柜所有格子都被占满,新的包裹无法投递。
网上很多示例代码(包括某些开发板提供的tcp_echoserver)存在一个典型误区:在tcp_sent发送回调中调用tcp_recved。这种做法在echo服务(收到数据立即原样返回)场景下看似可行,但在实际业务中会引发严重问题。
这种错误用法的本质是混淆了发送窗口和接收窗口的管理。当我们在发送回调中调用tcp_recved时,只有在发送数据时才会释放接收窗口。如果应用长时间只收不发(比如监控设备持续接收控制指令),接收窗口就会逐渐收缩直至归零。这就像只在你寄出包裹时才清理快递柜,显然不符合实际需求。
正确的做法应该遵循"谁消费,谁释放"原则。具体操作要点包括:
这里有个实际项目中的优化技巧:如果处理数据需要较长时间(比如要写入Flash),可以先立即调用tcp_recved再处理数据,避免影响网络吞吐。我在工业传感器项目中测试发现,这种方式能使通信效率提升30%以上。
c复制static err_t tcp_data_recv(void *arg, struct tcp_pcb *tpcb, struct pbuf *p, err_t err)
{
if(p != NULL) {
// 先通知LWIP释放窗口空间
tcp_recved(tpcb, p->tot_len);
// 再处理数据(可能耗时)
process_data(p->payload, p->len);
pbuf_free(p);
}
return ERR_OK;
}
很多新手容易混淆tcp_active_pcbs和监听PCB的关系。就像银行柜台业务:监听PCB相当于取号机(只负责接收新连接请求),而活跃PCB才是真正的业务窗口(处理实际数据传输)。
关键注意事项:
我在代码审查时经常发现这样的错误:
c复制// 错误示例:使用监听PCB发送数据
tcp_write(listen_pcb, data, len, 0);
当需要支持多客户端连接时,必须维护PCB与业务数据的映射关系。推荐的做法是:
c复制struct client_session {
struct tcp_pcb *pcb;
uint8_t mac[6];
uint32_t last_active;
};
static err_t tcp_accept_cb(void *arg, struct tcp_pcb *newpcb, err_t err)
{
struct client_session *session = mem_malloc(sizeof(struct client_session));
session->pcb = newpcb;
get_client_mac(session->mac); // 获取客户端标识
tcp_arg(newpcb, session); // 关键关联操作
// 设置其他回调...
}
TCP连接的关闭是个易错点,特别是TIME_WAIT状态的处理。常见问题包括:
经过多个项目实践,我总结出可靠的关闭流程:
c复制void tcp_connection_close(struct tcp_pcb *tpcb)
{
struct client_session *session = tcp_arg(tpcb);
// 清除回调链
tcp_arg(tpcb, NULL);
tcp_recv(tpcb, NULL);
// 其他回调清理...
// 释放业务数据
if(session) {
mem_free(session);
}
// 实际关闭连接
err_t err = tcp_close(tpcb);
if(err != ERR_OK) {
tcp_abort(tpcb); // 强制终止
}
}
当需要快速重启服务时,TIME_WAIT状态会导致端口绑定失败。除了修改LWIP的SO_REUSEADDR选项外,我有两个实用技巧:
c复制static uint16_t port_base = 5000;
err = tcp_bind(pcb, IP_ADDR_ANY, port_base++);