当前位置: 首页 > news >正文

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 操作(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_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 场景)。
  • 并发、生命周期与所有权规则

    • 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),并把控制块加入链表后进入等待。
  • 事件产生与通知路径(从网络到应用):
    1. 数据到达网卡 → lwIP core 线程(tcpip_thread)处理 → 协议层最终把数据入 netconn 的接收队列。
    2. netconn 在接收或缓冲数据时触发事件(NETCONN_EVT_RCVPLUS / SENDPLUS / ERROR 等)。
    3. event_callback(socket 模块注册为 netconn 回调)在 tcpip_thread 上被调用:
      • 根据 evt 更新对应 socket 的 sock->rcvevent / sendevent / errevent。
      • 若 sock->select_waiting>0 并且需要检查等待者,则调用 select_check_waiters。
    4. select_check_waiters 遍历 select_cb_list,比较每个 select 的 fdset/pollfds 和 sock 的事件:
      • 若命中,则对 select 对应的控制块 sem_signalled=1 并 sys_sem_signal() 唤醒等待线程。
    5. 等待线程醒来后重新调用 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 执行流程

  1. 初始扫描lwip_selscan() 检查所有fd当前状态

    • 检查 sock->lastdata(是否有残留数据)
    • 检查 sock->rcvevent > 0(是否有接收事件)
    • 检查 sock->sendevent != 0(是否可写)
  2. 等待设置(若无就绪事件):

    • 创建 lwip_select_cb 并加入全局 select_cb_list
    • 对所有相关socket增加 select_waiting 计数
    • 线程在信号量上等待
  3. 事件触发

    • 网络数据到达 → event_callback() 更新 sock->rcvevent
    • 调用 select_check_waiters() 检查等待列表
    • 若匹配则设置 sem_signalled=1sys_sem_signal()
  4. 唤醒与清理

    • 等待线程被唤醒,重新调用 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 差异
特性selectpoll
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 超时
  • selectstruct timeval *timeout,NULL表示无限等待
  • pollint 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;
}
http://www.dtcms.com/a/341575.html

相关文章:

  • c#入门笔记(3)
  • 图像数据增强的高效执行
  • Linux下运行Jmeter压测
  • Kafka如何保证「消息不丢失」,「顺序传输」,「不重复消费」,以及为什么会发生重平衡(reblanace)
  • 攻克PostgreSQL专家认证
  • Git Commit 提交信息标准格式
  • Python打卡Day47 注意力热图可视化
  • 字符设备驱动、块设备驱动和网络设备驱动
  • Gitee仓库 日常操作详细步骤
  • Linux服务器性能优化总结
  • 【数据结构】快速排序算法精髓解析
  • shell脚本——搜索某个目录下带指定前缀的文件
  • 50.Seata-AT模式
  • Cyberduck (FTP和SFTP工具) v9.2.3.43590
  • 189.轮转数组
  • 设计模式的一些笔记
  • list集合可以一边遍历一遍修改元素吗?
  • Rust 入门 包 (二十一)
  • 计算机网络基础复习
  • 【数据分享】295个地级市互联网用户、邮电业务数据(2001-2022)
  • win10安装最新docker 4.44.2版图文教程(2025版)
  • 3.Shell脚本修炼手册之---Shell 变量基础知识
  • Android动画小补充
  • 【Obsidian插件】HiNote
  • 爬虫项目实践之淘宝商品详情数据采集​||电商API接口
  • 结构化 OCR 技术:破解各类检测报告信息提取难题
  • 5.Kotlin作用于函数let、run、with、apply、also
  • SpringCloud微服务架构入门指南
  • Day12--滑动窗口与双指针--2762. 不间断子数组,LCP 68. 美观的花束,2743. 计算没有重复字符的子字符串数量
  • day075-MySQL数据库服务安装部署与基础服务管理命令