LWIP的Socket API 与实现关系
由于本人使用的方式原因,只打算了解socket模式,其余两种方式。适合裸机的RAW模式和基础的CONNECT模式不适合面向对象的编程方式,不打算看。
Socket API 与实现关系
1) 总体映射关系
- LwIP 的 BSD-like Socket API 是在 netconn API 之上实现的:
- socket() 会创建一个 netconn,并把 netconn 指针保存在全局 sockets[] 数组对应的
struct lwip_sock
中。 - sockets 数字(文件描述符)就是数组索引 + 偏移(LWIP_SOCKET_OFFSET)。
- socket() 会创建一个 netconn,并把 netconn 指针保存在全局 sockets[] 数组对应的
- 绝大多数 socket 操作(bind/connect/send/recv/setsockopt 等)最终交给 netconn 层处理;netconn 进一步调用 core 层(udp/tcp/ip)完成真正的网络 I/O。
- sockets 提供了兼容 POSIX 的接口(lwip_* 或系统宏映射),但在实现细节上依赖 lwIP 的线程/消息模型(tcpip_thread / netconn mailbox)。
2) 重要结构:struct lwip_sock
- 主要成员:
- struct netconn *conn:对应的 netconn(实际 I/O 对象)。
- union lwip_sock_lastdata lastdata:保存上次未完全消费的数据(TCP 用 pbuf,UDP/RAW 用 netbuf)。
- rcvevent、sendevent、errevent:用于 select/poll 判断事件与计数。
- select_waiting:有多少线程在 select/poll 上等待该 socket。
- (可选)fd_used、fd_free_pending:用于 full-duplex 模式下防止并发释放。
-
基本类型与约定
- u8_t/u16_t/s8_t/s32_t:lwIP 的固定宽度整型(通常映射到 uint8_t/uint16_t/int8_t/int32_t),用于二进制协议字段与位掩码,避免平台差异。
- size_t:用于字节计数(拷贝/写入长度);注意与 pbuf->tot_len(u16_t 或 u32_t,取决实现)之间的转换/截断风险。
- sys_mbox_t/sys_sem_t:OS 抽象类型,封装消息队列/信号量;不同端口具体类型不同,尽量通过 sys_* API 操作,不直接访问内部字段。
- ip_addr_t/pbuf/netbuf/netconn:关键复合类型,含指针(需注意生命周期和所有权:谁负责 free)。
-
netconn 相关宏与标志(语义)
- NETCONN_NOFLAG/COPY/MORE/DONTBLOCK:写入时的行为控制
- MORE:表示后续还有更多分段(影响 TCP Nagle/PSH 组合)。
- DONTBLOCK:告诉 netconn_write_partly 立即返回,不阻塞。
- NETCONN_FLAG_*(conn->flags)
- MBOXCLOSED:接收/accept 队列已关闭,后续阻塞操作应失败/立刻返回。
- NON_BLOCKING:netconn_is_nonblocking() 读取此位决定 recv/send 是否可阻塞。
- IN_NONBLOCKING_CONNECT:用于 connect 的状态机(非阻塞 connect 的后续处理)。
- CHECK_WRITESPACE:若此前写被拒绝,需要 poll 再次检测写入可用性。
- PKTINFO:启用 recv 时附带到达接口/目标地址信息(netbuf->toaddr/toport)。
- NETCONNTYPE_GROUP / DATAGRAM / ISIPV6:用来按类型(TCP/UDP/RAW)做通用处理,避免显式枚举每一项。
- NETCONN_NOFLAG/COPY/MORE/DONTBLOCK:写入时的行为控制
-
事件枚举(netconn_evt)语义要点
- RCVPLUS / RCVMINUS:计数式事件,表示“可安全再做一次潜在阻塞的 recv/accept”。socket 层把这些累加到 sock->rcvevent,再由 select/poll 判定。
- SENDPLUS / SENDMINUS:写可/不可用事件(通常用作标志,不计数)。
- ERROR:异步错误通知(设置 conn->pending_err),应用应通过 netconn_err() 查询。
-
netbuf 结构与 API 细节
- struct netbuf { pbuf *p, *ptr; ip_addr_t addr; u16_t port; flags; toport_chksum; toaddr; }
- p: 链表头,ptr: 当前遍历指针(netbuf_next 使用),netbuf_len 返回 p->tot_len(注意 tot_len 是 pbuf 的累积长度)。
- NETBUF_FLAG_DESTADDR:表示 netbuf 包含目标地址(用于 UDP send/forward 场景)。
- NETBUF_FLAG_CHKSUM:当 CHECKSUM_ON_COPY 被启用时,toport_chksum 存储端口与校验相关信息。
- 常用宏:
- netbuf_copy_partial/netbuf_take:基于 pbuf API 做部分拷贝;对大数据需多次调用 netbuf_next。
- netbuf_fromaddr / netbuf_fromport:接收方读取源地址/端口的便捷宏。
- netbuf_destaddr / netbuf_destport:在带 recvinfo 时用于目的地址(例如 IP_PKTINFO 场景)。
- struct netbuf { pbuf *p, *ptr; ip_addr_t addr; u16_t port; flags; toport_chksum; toaddr; }
-
并发、生命周期与所有权规则
- recvmbox/acceptmbox:netconn 将接收到的数据/连接放入 mbox,应用线程负责从 mbox 中取出并释放(netbuf_delete / pbuf_free)。
- recv_avail(LWIP_SO_RCVBUF):用于实现 FIONREAD / 限制接收缓冲,注意仅对 UDP/RAW 有效(TCP 使用 TCP_WND)。
- op_completed(若没有 per-thread sem):netconn 在 core 上运行时用于同步完成回调,注意在 netconn_thread_init/cleanup 的配置差异。
- callback 与 callback_arg:conn->callback 在 tcpip core 上调用,传入的 callback_arg 用于 socket 层/应用关联;回调必须是非阻塞且尽量短(因为运行在核心线程)。
-
常见易错点与调试策略(针对宏/类型)
- 误用 NETCONN_FLAG_NON_BLOCKING:直接修改 flags 位能改变行为,但不要绕过 netconn_set_nonblocking 宏(便于日后调整)。
- pbuf/tot_len 与 size_t 交互:当数据量大于 pbuf 的 u16_t 限制(某些平台),注意可能发生截断或需要分片读取。
- RCVPLUS 计数语义:收到多包会产生多次 RCVPLUS,socket 层可能把它转成 rcvevent 计数;select/poll 唤醒逻辑依赖该计数,调试时打印 conn->flags 与 sock->rcvevent 帮助定位唤醒丢失。
- NETBUF_FLAG_DESTADDR 与 NETCONN_FLAG_PKTINFO:若期望获取目的地址,需同时在 netconn/pcb 层启用 recvinfo 标志,否则 netbuf 的 toaddr 不会被填充。
3) recv/send 的实现要点
- 接收:
- lwip_recvfrom -> lwip_recvfrom_udp_raw(UDP/RAW) 或 lwip_recv_tcp(TCP)。
- 如果 sock->lastdata 有残留数据,会直接从 lastdata 返回(支持 MSG_PEEK、分段拷贝、部分读取)。
- 当没有残留数据时,lwip_recvfrom 会调用 netconn_recv_*(在 netconn 层)去取数据;netconn 层在 tcpip_thread 中等待或从 mailbox 返回 netbuf/pbuf。
- netbuf/pbuf 中的数据被拷贝到用户缓冲区;如果不是 peek,netbuf/pbuf 将被释放,或保存到 sock->lastdata 以便下次继续读取。
- 发送:
- lwip_sendto -> 构造 netbuf(或 pbuf 链) -> netconn_send(sock->conn, &buf)
- 对于 TCP,lwip_send 使用 netconn_write_partly(可返回写入字节数)。
- 发送调用通常会在调用线程与 tcpip_thread 之间通过消息/回调协调(取决于 CORE_LOCKING 配置)。
4) 阻塞 / 非阻塞 与 fcntl/ioctl
- 非阻塞 I/O:可以用 fcntl(F_SETFL, O_NONBLOCK) 或 ioctlsocket(FIONBIO)。实现上通过 netconn_set_nonblocking 将 netconn 标记为非阻塞。
- recv/send 的 MSG_DONTWAIT 也会被转成 NETCONN_DONTBLOCK 传递给 netconn 层。
- select/poll 可配合非阻塞使用,也可用于阻塞等待事件发生。
5) select / poll 机制(实现细节)
- 等待者管理:
- 全局链表 select_cb_list 保存所有正在等待的 select/poll 的控制块(struct lwip_select_cb)。
- 每个等待者分配一个 semaphore(或使用线程本地 sem),并把控制块加入链表后进入等待。
- 事件产生与通知路径(从网络到应用):
- 数据到达网卡 → lwIP core 线程(tcpip_thread)处理 → 协议层最终把数据入 netconn 的接收队列。
- netconn 在接收或缓冲数据时触发事件(NETCONN_EVT_RCVPLUS / SENDPLUS / ERROR 等)。
- event_callback(socket 模块注册为 netconn 回调)在 tcpip_thread 上被调用:
- 根据 evt 更新对应 socket 的 sock->rcvevent / sendevent / errevent。
- 若 sock->select_waiting>0 并且需要检查等待者,则调用 select_check_waiters。
- select_check_waiters 遍历 select_cb_list,比较每个 select 的 fdset/pollfds 和 sock 的事件:
- 若命中,则对 select 对应的控制块 sem_signalled=1 并 sys_sem_signal() 唤醒等待线程。
- 等待线程醒来后重新调用 lwip_selscan/lwip_pollscan 来读取最终事件并返回给应用。
- 关键变量/机制:
- sock->rcvevent 用于计数“接收到数据”的次数(避免丢失连续到达的唤醒)。
- select_waiting 防止当等待者数多时 socket 被误释放(并用于上/下调等待计数)。
- select_scan(lwip_selscan)会结合 sock->lastdata(是否有残留数据)和 rcvevent 判断是否可读。
6) select 与 poll 的差异与实现共性
- 共性:
- 都使用同样的事件来源(event_callback)和唤醒链表(select_cb_list)。
- 都在被唤醒后调用各自的 scan 函数(lwip_selscan / lwip_pollscan)重新判断实际就绪项。
- 差异:
- select 使用 fd_set 位集合,poll 使用 pollfd 数组(更适合动态 fd 数量)。
- poll 的实现额外提供 revents 存储并对 POLLNVAL 处理;poll 在唤醒时避免多次 copy fd_set。
- 注意:
- FD 在 FD_SET 时要考虑 LWIP_SOCKET_OFFSET 偏移(实现宏 FD_SET/FD_ISSET 已封装),不要直接用小范围的 fd 操作系统宏。
- select/poll 都受限于 MEMP_NUM_NETCONN(即 sockets 数量)。
7) 同步与线程上下文
- 若配置 LWIP_TCPIP_CORE_LOCKING:部分 socket 操作直接在任意线程加 core lock 即可调用内部实现;否则某些操作会通过 tcpip_callback 交给 tcpip_thread,并用 semaphore 等待完成(见 lwip_setsockopt_impl/getsockopt_impl 的分支)。
- select/poll 的唤醒必须在 tcpip core 锁或受保护的上下文中进行(源码中有若干断言与锁宏)。
8) 重要的 socket options 与行为提示
- SO_BROADCAST:若要 sendto 广播 IP(255.255.255.255 或 子网广播),必须先 setsockopt(SOL_SOCKET, SO_BROADCAST)。
- SO_NO_CHECK:可用于 UDP 跳过校验(udp_set_flags)。
- IP_PKTINFO / NETBUF_FLAG_DESTADDR:可用于接收方获取到达接口信息(需打开 NETCONN_FLAG_PKTINFO)。
- IP_ADD_MEMBERSHIP / IPV6_JOIN_GROUP:通过 lwip_setsockoptImpl 对 IGMP/MLD 做组管理,socket close 时会自动 drop(代码中有注册表管理)。
9) 调试建议(常见问题定位)
- 捕获数据但 recv 未返回:
- 检查 sock->lastdata(peek 情况)、sock->rcvevent、netconn_recv 是否成功取到 netbuf。
- 在 tcpip_thread 中断点:netconn 层(netconn_recv_udp_raw_netbuf_flags)、event_callback、select_check_waiters。
- select 未被唤醒:
- 检查 event_callback 是否被调用(netconn->callback 是否存在)。
- 检查 select_cb_list 是否正确加入,以及 select_waiting 是否被增减。
- 内存/句柄泄漏:
- 检查 alloc_socket / free_socket 路径,确认 done_socket 在每个返回点都被调用。
- 检查 lastdata 是否在释放路径中被 free(free_socket_free_elements)。
10) Socket API 使用详解(阻塞/非阻塞、select/poll 实现)
基本 Socket API 使用流程
TCP 服务端示例
int server_fd = lwip_socket(AF_INET, SOCK_STREAM, 0);
struct sockaddr_in addr = {0};
addr.sin_family = AF_INET;
addr.sin_addr.s_addr = INADDR_ANY;
addr.sin_port = htons(8080);lwip_bind(server_fd, (struct sockaddr*)&addr, sizeof(addr));
lwip_listen(server_fd, 5);int client_fd = lwip_accept(server_fd, NULL, NULL);
char buffer[1024];
ssize_t len = lwip_recv(client_fd, buffer, sizeof(buffer), 0);
lwip_send(client_fd, "Hello", 5, 0);
lwip_close(client_fd);
lwip_close(server_fd);
UDP 客户端示例
int udp_fd = lwip_socket(AF_INET, SOCK_DGRAM, 0);
struct sockaddr_in server_addr = {0};
server_addr.sin_family = AF_INET;
inet_pton(AF_INET, "192.168.1.100", &server_addr.sin_addr);
server_addr.sin_port = htons(9999);char data[] = "UDP Message";
lwip_sendto(udp_fd, data, sizeof(data), 0, (struct sockaddr*)&server_addr, sizeof(server_addr));char response[512];
struct sockaddr_in from_addr;
socklen_t from_len = sizeof(from_addr);
ssize_t recv_len = lwip_recvfrom(udp_fd, response, sizeof(response), 0,(struct sockaddr*)&from_addr, &from_len);
lwip_close(udp_fd);
阻塞与非阻塞模式实现
阻塞模式(默认)
- 实现原理:
recv/send
调用时,若无数据/缓冲区满,线程会在netconn->recvmbox
/sendbuffer
上等待- netconn 层通过
sys_arch_mbox_fetch()
无限期等待数据到达 - 数据到达时,tcpip_thread 会将 netbuf/pbuf 放入 mbox 并唤醒等待线程
// 阻塞接收 - 等到有数据才返回
char buffer[1024];
ssize_t len = lwip_recv(fd, buffer, sizeof(buffer), 0); // 会阻塞
if (len > 0) {// 处理接收到的数据
}
非阻塞模式设置与使用
// 方法1:使用 fcntl 设置
int flags = lwip_fcntl(fd, F_GETFL, 0);
lwip_fcntl(fd, F_SETFL, flags | O_NONBLOCK);// 方法2:使用 ioctl 设置
int nonblock = 1;
lwip_ioctl(fd, FIONBIO, &nonblock);// 方法3:在单次调用中指定
ssize_t len = lwip_recv(fd, buffer, sizeof(buffer), MSG_DONTWAIT);
if (len < 0 && errno == EWOULDBLOCK) {// 当前无数据可读,稍后重试
}
- 实现原理:
- 设置
NETCONN_FLAG_NON_BLOCKING
标志位 - netconn 层调用
netconn_recv_*
时传入NETCONN_DONTBLOCK
- 若 mbox 为空,立即返回
ERR_WOULDBLOCK
而不等待
- 设置
select 机制详解
基本 select 使用
fd_set readfds, writefds, exceptfds;
int max_fd = 0;FD_ZERO(&readfds);
FD_ZERO(&writefds);
FD_ZERO(&exceptfds);// 添加需要监听的 socket
FD_SET(sock1, &readfds);
FD_SET(sock2, &readfds);
FD_SET(sock3, &writefds);
max_fd = MAX(sock1, MAX(sock2, sock3));struct timeval timeout = {5, 0}; // 5秒超时
int ready = lwip_select(max_fd + 1, &readfds, &writefds, &exceptfds, &timeout);if (ready > 0) {if (FD_ISSET(sock1, &readfds)) {// sock1 可读handle_read(sock1);}if (FD_ISSET(sock3, &writefds)) {// sock3 可写handle_write(sock3);}
} else if (ready == 0) {// 超时
} else {// 错误处理
}
select 实现原理
数据结构与状态管理:
// 每个 socket 的事件计数器
struct lwip_sock {s16_t rcvevent; // 接收事件计数(累加)u16_t sendevent; // 发送事件标志(0/1)u16_t errevent; // 错误事件标志(0/1)SELWAIT_T select_waiting; // 等待该socket的select数量
};// select 等待控制块
struct lwip_select_cb {fd_set *readset, *writeset, *exceptset;int sem_signalled; // 是否已被唤醒sys_sem_t sem; // 等待信号量struct lwip_select_cb *next, *prev; // 链表节点
};
select 执行流程:
-
初始扫描:
lwip_selscan()
检查所有fd当前状态- 检查
sock->lastdata
(是否有残留数据) - 检查
sock->rcvevent > 0
(是否有接收事件) - 检查
sock->sendevent != 0
(是否可写)
- 检查
-
等待设置(若无就绪事件):
- 创建
lwip_select_cb
并加入全局select_cb_list
- 对所有相关socket增加
select_waiting
计数 - 线程在信号量上等待
- 创建
-
事件触发:
- 网络数据到达 →
event_callback()
更新sock->rcvevent
- 调用
select_check_waiters()
检查等待列表 - 若匹配则设置
sem_signalled=1
并sys_sem_signal()
- 网络数据到达 →
-
唤醒与清理:
- 等待线程被唤醒,重新调用
lwip_selscan()
获取最终结果 - 减少
select_waiting
计数,从等待列表移除
- 等待线程被唤醒,重新调用
关键时序(数据到达唤醒select):
NIC中断 → tcpip_thread处理 → udp_input/tcp_input → pcb->recv_callback
→ netconn接收队列 → API_EVENT(NETCONN_EVT_RCVPLUS) → event_callback
→ sock->rcvevent++ → select_check_waiters → sys_sem_signal → 等待线程唤醒
poll 机制详解
基本 poll 使用
struct pollfd fds[3];
memset(fds, 0, sizeof(fds));// 设置要监听的事件
fds[0].fd = sock1;
fds[0].events = POLLIN; // 监听可读fds[1].fd = sock2;
fds[1].events = POLLIN | POLLOUT; // 监听可读可写fds[2].fd = sock3;
fds[2].events = POLLOUT; // 监听可写int timeout_ms = 3000; // 3秒超时
int ready = lwip_poll(fds, 3, timeout_ms);if (ready > 0) {for (int i = 0; i < 3; i++) {if (fds[i].revents & POLLIN) {// fds[i].fd 可读handle_read(fds[i].fd);}if (fds[i].revents & POLLOUT) {// fds[i].fd 可写handle_write(fds[i].fd);}if (fds[i].revents & POLLERR) {// fds[i].fd 发生错误handle_error(fds[i].fd);}}
}
poll vs select 差异
特性 | select | poll |
---|---|---|
fd集合表示 | fd_set 位图(固定大小) | pollfd 数组(动态大小) |
fd数量限制 | FD_SETSIZE(通常1024) | 仅受内存限制 |
事件表示 | 分离的读/写/异常集合 | 每个fd的events/revents字段 |
超时精度 | struct timeval(微秒) | int(毫秒) |
poll实现要点:
- 使用
lwip_pollscan()
扫描和更新revents
- 同样使用
select_cb_list
和事件回调机制 lwip_poll_should_wake()
检查特定fd的特定事件
超时机制实现
socket 级别超时
// 设置接收超时
struct timeval tv = {5, 0}; // 5秒
lwip_setsockopt(fd, SOL_SOCKET, SO_RCVTIMEO, &tv, sizeof(tv));// 设置发送超时
tv.tv_sec = 3;
lwip_setsockopt(fd, SOL_SOCKET, SO_SNDTIMEO, &tv, sizeof(tv));// 后续recv/send会在指定时间后超时返回
select/poll 超时
- select:
struct timeval *timeout
,NULL表示无限等待 - poll:
int timeout
,-1表示无限等待,0表示立即返回
超时实现:
- 通过
sys_arch_sem_wait(sem, timeout_ms)
实现 - 超时返回
SYS_ARCH_TIMEOUT
,正常返回时间差
高级使用技巧
1. 水平触发 vs 边缘触发
LwIP的select/poll是水平触发:
- 只要条件满足(有数据可读/可写),每次select/poll都会返回
- 需要在处理完数据后再次调用select/poll
2. 组合使用示例
// 非阻塞socket + select的典型模式
int setup_nonblocking_server(int port) {int server_fd = lwip_socket(AF_INET, SOCK_STREAM, 0);// 设置非阻塞int flags = lwip_fcntl(server_fd, F_GETFL, 0);lwip_fcntl(server_fd, F_SETFL, flags | O_NONBLOCK);// 绑定监听struct sockaddr_in addr = {0};addr.sin_family = AF_INET;addr.sin_addr.s_addr = INADDR_ANY;addr.sin_port = htons(port);lwip_bind(server_fd, (struct sockaddr*)&addr, sizeof(addr));lwip_listen(server_fd, 10);return server_fd;
}void event_loop(int server_fd) {fd_set master_set, read_set;int max_fd = server_fd;FD_ZERO(&master_set);FD_SET(server_fd, &master_set);while (1) {read_set = master_set;int ready = lwip_select(max_fd + 1, &read_set, NULL, NULL, NULL);for (int fd = 0; fd <= max_fd && ready > 0; fd++) {if (FD_ISSET(fd, &read_set)) {ready--;if (fd == server_fd) {// 新连接到达int client_fd = lwip_accept(server_fd, NULL, NULL);if (client_fd >= 0) {// 设置客户端为非阻塞int flags = lwip_fcntl(client_fd, F_GETFL, 0);lwip_fcntl(client_fd, F_SETFL, flags | O_NONBLOCK);FD_SET(client_fd, &master_set);if (client_fd > max_fd) max_fd = client_fd;}} else {// 客户端数据到达char buffer[1024];ssize_t len = lwip_recv(fd, buffer, sizeof(buffer), 0);if (len <= 0) {// 连接关闭或错误lwip_close(fd);FD_CLR(fd, &master_set);} else {// 处理数据handle_client_data(fd, buffer, len);}}}}}
}
3. 错误处理要点
// 完整的错误处理示例
ssize_t safe_recv(int fd, void *buf, size_t len) {ssize_t result = lwip_recv(fd, buf, len, 0);if (result < 0) {switch (errno) {case EWOULDBLOCK:case EAGAIN:// 非阻塞模式下无数据,稍后重试return 0;case ECONNRESET:// 连接被对端重置printf("Connection reset by peer\n");return -1;case EINTR:// 被信号中断,可重试return safe_recv(fd, buf, len);default:printf("recv error: %s\n", strerror(errno));return -1;}}return result;
}