简介这是一份面向嵌入式开发者的uIP网络协议栈学习资料包适合需要在资源受限设备上实现TCP/IP通信的硬件工程师与物联网初学者。uIP以轻量高效著称资料围绕TCP/IP协议分层、uIP模块协同、内存与缓冲区管理、TCP连接状态机、事件驱动编程及Wireshark抓包调试等核心内容展开并结合HTTP、FTP等应用层协议与传感网、物联网设备实例帮助读者从原理到实际工程逐步吃透。压缩包共127个文件涵盖c与h源码、PDF教程、txt说明、PNG图解、HTML文档及makefile构建脚本等约4.8MB目录结构便于按主题查阅。目前已吸引171人学习下载适合系统研读uIP源码设计、嵌入式协议栈移植以及与lwIP等栈横向对比的进阶参考。1. 为什么 5 年嵌入式老手还在翻 uIP 源码如果你觉得 uIP 只是给单片机练手的玩具协议栈那可能错过了它最值钱的部分。uIP 的定位是“用最少的 RAM 和 ROM 让设备连上网”整个协议栈核心代码只有几千行但它在设计上的克制程度比很多商业协议栈要高明得多。这个uIP学习资料.rar里打包的 uip.c、uip_arp.c、psock.c 等源码刚好覆盖了 TCP/IP 协议栈从链路层到传输层再到应用层的关键模块适合两类人一类是刚接触嵌入式网络、想搞懂 TCP 状态机怎么在 MCU 上转起来的新手另一类是在用 lwIP 或 FreeRTOSTCP 做产品、但遇到内存优化瓶颈时回来看 uIP 的零拷贝缓冲设计找灵感的老手。文章会用 uIP 官方的代码结构和典型应用场景作为主线配合抓包和调试手段把协议栈拆开看。下面进入正题。2. uIP 的架构与内存模型零拷贝和单一缓冲区是核心2.1 单一全局缓冲区设计uIP 从头到尾只维护一个全局接收缓冲区uip_buf所有网络收发都围绕它展开。这个设计和常规操作系统协议栈的 sk_buff 或 mbuf 机制完全不同——后者用链表管理分散的内存块而 uIP 直接把整包数据放在一个连续数组里驱动层收完数据、协议栈解析、应用层读取全部在这块内存上操作。// uip.c 中的核心缓冲区定义 #ifndef UIP_BUFSIZE #define UIP_BUFSIZE 400 #endif u8_t uip_buf[UIP_BUFSIZE]; // 唯一的收发缓冲区缓冲区大小由UIP_BUFSIZE宏控制默认 400 字节其中前 40 字节左右是头部预留空间。uip_buf既是网卡驱动的 DMA 目标也是构造发送帧的源地址。这种设计的优势在于零拷贝——数据从网卡进来到应用层读取不需要任何一次内存拷贝代价是同一时刻只能处理一个数据包所以 uIP 天生是事件驱动的。提示如果设备需要收发超过UIP_BUFSIZE的包比如 1500 字节的以太网帧要手动把宏调到 1500 以上但 RAM 占用会线性增长选型时要按设备实际网络场景权衡。2.2 ARP 表与 uip_arp.c 的职责在以太网环境下uIP 在 IP 层之下还需要处理 ARP。uip_arp.c负责 ARP 请求/应答的构造和解析同时维护一张 ARP 缓存表。默认表项数量是UIP_ARPTAB_SIZE每个表项由 IP 地址、MAC 地址和时间戳组成。// uip_arp.c 中 ARP 表项结构简化 struct arp_entry { u16_t ipaddr[2]; // IP 地址16 位对齐两段存储 u16_t ethaddr[3]; // MAC 地址三个 16 位字 u8_t time; // ARP 缓存时间计数用于老化 }; #define UIP_ARPTAB_SIZE 8 // 默认支持 8 个 ARP 表项time字段配合周期性调用的uip_arp_timer()做老化超过UIP_ARP_MAXAGE默认 120 秒的表项会被清掉。很多人在做局域网通信时发现 ping 不通排查半天发现是 ARP 表没刷——因为 uIP 不会自动发免费 ARP需要应用层主动触发。2.3 事件驱动的三种触发机制uIP 的协议栈不主动跑线程它必须在被调用时才处理数据。常见的事件触发有三种路径网卡中断收到新包、应用周期调用uip_periodic()处理 TCP 超时重传、以及uip_poll()在空闲时轮询各连接是否有数据要发。核心入口统一在uip_input()和uip_periodic()两个函数里前者处理收包后者驱动 TCP 状态机。// 典型的主循环 周期调用 while(1) { if(netif_packet_ready()) { // 收包路径 uip_len netif_read_packet(uip_buf[UIP_LLH_LEN]); uip_input(); // 协议栈处理 IP/TCP/UDP if(uip_len 0) { netif_send_packet(uip_buf[UIP_LLH_LEN], uip_len); // 回包 } } else { // 周期路径遍历所有 TCP 连接 for(i 0; i UIP_CONNS; i) { uip_periodic(i); if(uip_len 0) { netif_send_packet(uip_buf[UIP_LLH_LEN], uip_len); } } } }这段代码的调用顺序决定了 uIP 的性能边界。uip_input()解析完数据包后如果应用有响应要发会把数据写进uip_appdata指向的缓冲区并更新uip_len主循环检测到uip_len非零就触发网卡发送。uip_periodic()的回传机制也一样uip_len是协议栈和应用层之间的“数据信封”。3. HTTP 与 DNS 模块解析httpd.c 和 resolv.c 的实战用法3.1 httpd.c 的静态文件服务实现httpd.c配合httpd-fsdata.c实现了一个极简 Web Server。httpd-fsdata.c通常由httpd-fs目录下的脚本把静态 HTML 文件转成 C 语言数组每个文件对应一个struct httpd_fsdata_file结构体。uIP 自带的例子是 404.html 和 index.html编译进 ROM 后直接用指针访问。// httpd-fsdata.c 生成的数据结构示意 static const struct httpd_fsdata_file file_404_html { NULL, 404.html, 0, data_404_html, sizeof(data_404_html) - 1 }; static const struct httpd_fsdata_file file_index_html { file_404_html, index.html, 0, data_index_html, sizeof(data_index_html) - 1 }; const struct httpd_fsdata_file *httpd_fs_data file_index_html;这个模块的设计思路是“文件即数组”不涉及文件系统。应用层改造时如果要加一个动态数据页比如sensor.html返回实时温度需要在httpd.c的httpd_fs_open()之后通过 URLCallback 机制处理动态请求常见的做法是在httpd_fs_get_file()里先查静态文件表查不到就交给应用层回调函数按 URL 动态生成内容。3.2 resolv.c 的 DNS 查询流程resolv.c提供了异步 DNS 解析能力核心结构是resolv_table_entry每个表项维护主机名、IP 地址和查询状态。查询时先在本地缓存找找不到就向 DNS 服务器发 UDP 查询包同时设置超时重传计数。// resolv.c 中查询项结构 struct resolv_table_entry { u8_t state; // STATE_UNUSED / STATE_LOADING / STATE_READY u16_t ipaddr[2]; // 解析结果缓存 u16_t time; // 超时计数 char hostname[UIP_RESOLV_ENTRIES][UIP_RESOLV_HOSTNAME_SIZE]; }; void resolv_query(const char *name); // 发起异步查询 u16_t *resolv_lookup(const char *name); // 查询结果NULL 表示还没解析完成使用上有个典型的坑resolv_query()只是发起查询不阻塞等待结果。要在应用层主循环里周期调用uip_resolv_event()等到状态变成STATE_READY后才能取 IP。很多人第一次用 uIP DNS 时直接在主流程里while(!resolv_lookup())结果协议栈永远跑不动因为 DNS 响应的处理本身就依赖主循环去调用uip_input()。提示resolv.conf风格的 DNS 服务器地址在 uIP 中是编译期常量UIP_DNS_SERVER如果要支持 DHCP 动态下发 DNS需要手动在 dhcpc.c 的应答解析里把 Option 6 的字段拷到resolv.c的服务器地址变量。3.3 dhcpc.c 与 webclient.c 的协同使用dhcpc.c实现 DHCP 客户端状态机包含 INIT、SELECTING、REQUESTING、BOUND 四个阶段。auto 模式下设备上电先发 DHCP Discover拿到 Offer 后发 Request最终进入 BOUND 获得 IP、网关和 DNS。结合webclient.c可以做出“上电自动联网、自动上报数据”的 IoT 节点。// dhcpc.c 关键状态机驱动 void dhcpc_appcall(void) { switch(dhcpc_state) { case DHCPC_STATE_INIT: dhcpc_send_discover(); // 广播 UDP 包找 DHCP 服务器 dhcpc_state DHCPC_STATE_SELECTING; break; case DHCPC_STATE_SELECTING: if(uip_newdata()) { // 解析 Offer发送 Request dhcpc_send_request(); } break; case DHCPC_STATE_BOUND: // 租约到期前续租 if(dhcpc_lease_expired()) { dhcpc_send_request(); // 重新 Request } break; } }webclient.c则实现 HTTP 客户端可以 GET 远程 URL。它内部用psock的发送/接收缓冲机制应用层只要准备好请求行和 Host 头就能复用uip_connect()建立的 TCP 连接。实际做产品时常见的做法是先用dhcpc拿到 IP再让resolv解析域名最后走webclient拉取远程配置或上报数据。4. uip-fw.c 与 uip-fw.h 的状态机实现及滑动窗口特性4.1 uip-fw.c 的转发和过滤逻辑uip-fw.c是 uIP 1.0 之后引入的“防火墙 转发”模块它的设计目标是让 uIP 支持的设备能做基本的路由过滤和包转发。uip_fw_forward()负责任何非本机目的地址的 IP 包转发uip_fw_register()用于注册网络接口。// uip-fw.c 转发判断的骨架 #if UIP_CONF_IP_FORWARDING void uip_fw_forward(void) { if(uip_len 0 uip_ipaddr_cmp(uip_buf[UIP_IPH_LEN], uip_buf[UIP_IPH_LEN 16])) { // 目的 IP 非本机查路由表决定转发出口 ... } } #endif在路由模式下uIP 同时管理多个网络接口每个接口有自己的uip_buf和 MAC 地址。转发前要先把 IP 头的 TTL 减一并重新计算校验和这些都在uip-fw.c的uip_fw_forward()内完成。实际项目中常用它做简单的 Mesh 中继节点——一个节点收下传感器数据再转发给相邻节点。4.2 连接状态机CLOSED 到 TIME_WAIT 的流转uIP 的 TCP 状态机和 RFC 793 标准保持一致但实现上做了极致的精简。每个 TCP 连接用一个struct uip_conn管理主要字段包括本地端口、远端 IP/端口、tcpstate状态、以及mss最大报文段长度。tcpstate的枚举定义如下// uip.h 中的 TCP 状态枚举 enum uip_tcp_state { UIP_CLOSED 0, UIP_SYN_RCVD, UIP_SYN_SENT, UIP_ESTABLISHED, UIP_FIN_WAIT_1, UIP_FIN_WAIT_2, UIP_CLOSING, UIP_TIME_WAIT, UIP_LAST_ACK };状态迁移全部在uip.c的uip_process()大函数里完成。收到 SYN 且端口有监听时从 CLOSED 进入 SYN_RCVD回复 SYNACK收到 ACK 后进入 ESTABLISHED。应用层主动关闭时通过uip_abort()直接发 RST 或者走四次挥手。嵌入式设备资源有限uIP 的 TIME_WAIT 默认只维持很短时间由UIP_TIME_WAIT_TIMEOUT控制不像 Linux 要等 2MSL。4.3 滑动窗口与UIP_TCP_MSS的取舍uIP 1.0 之前的版本窗口固定为 1意味着每发一个包就得等 ACK。后续版本通过uip_conn-tcpchunk和uip_conn-len字段模拟简单的窗口滑动// uip.c 发送处理 if(conn-tcpchunk UIP_TCP_MSS) { // 组装数据段逐字节拷贝到 uip_buf for(i 0; i len; i) { uip_buf[UIP_TCPIP_HLEN i] appdata[i]; } ... conn-tcpchunk len; // 记录已发送未确认字节数 }只要接收端 ACK 回来就把tcpchunk清零继续发。UIP_TCP_MSS默认 536 字节这是 IPv4 最小 MTU 下的安全值。如果调整UIP_BUFSIZE到 600 以上可以把 MSS 改为UIP_BUFSIZE - UIP_TCPIP_HLEN提升吞吐。但要注意MSS 增大后uip_buf必须同时变大否则协议栈在组包时会越界。5. psock.c 的应用层编程模型阻塞式 IO 在事件驱动协议栈上的实现5.1 psock 的发送和接收缓冲机制psock.c是 uIP 提供的一个“伪阻塞”套接字层。它的实现原理是在事件驱动的协议栈上层模拟出阻塞式send()/recv()模型内部用struct psock的sendptr、sendlen、readptr等字段维护数据指针和剩余长度配合生成器generator机制把函数调用切分成多次状态恢复。// psock.h 简化定义 struct psock { struct uip_conn *conn; // 所属 TCP 连接 u8_t state; // PSOCK_READ / PSOCK_WRITE 等状态 u16_t sendlen; // 待发送数据剩余长度 u16_t readlen; // 待接收数据长度 const char *sendptr; // 发送数据指针 char *readptr; // 接收缓冲区指针 };5.2 用 PSOCK_BEGIN 写一个 HTTP GET 客户端uIP 的 psock 提供了一组宏PSOCK_BEGIN、PSOCK_SEND、PSOCK_READTO、PSOCK_END。这组宏本质上是协程切换——通过switch 状态变量保存执行位置每次uip_conn有事件进来时从上次断点继续跑而不是从函数头重新执行。// webclient.c 核心伪代码基于 psock 的生成器 static void webclient_handler(struct psock *p) { PSOCK_BEGIN(p); PSOCK_SEND(p, GET /index.html HTTP/1.0\r\n, 27); PSOCK_SEND(p, Host: example.com\r\n\r\n, 23); while(1) { PSOCK_READTO(p, \n); // 阻塞式读到换行符 if(p-readlen 0) break; // 处理一行内容 } PSOCK_END(p); }PSOCK_READTO(p, \n)会在没有数据时让出 CPU等协议栈收到新 TCP 段后再恢复执行。应用层看起来是线性代码但实际上每次uip_appcall()只执行到下一个阻塞点。这种编程模型在 MCU 上非常实用不需要 RTOS也不需要状态机手写拆分。5.3 telnetd.c 的会话管理案例telnetd.c把 psock 的多连接能力发挥到了极致。它维护一个telnetd_state数组每个活动 Telnet 会话对应一个struct uip_conn应用层通过uip_conn-appstate存储会话上下文。telnetd_appcall()在每次 TCP 事件触发时被调用内部直接用 psock 提供行编辑、回显和命令分发。// telnetd.c 会话切换节选 static void telnetd_appcall(void) { struct telnetd_state *s states[uip_conn-appstate]; PSOCK_BEGIN(s-p); PSOCK_SEND(s-p, Hello, uIP Telnet\r\n, 19); PSOCK_READTO(s-p, \n); // 读取命令并回显 PSOCK_SEND(s-p, s-buf, s-len); PSOCK_END(s-p); }这里的uip_conn-appstate是关键这两个字节被 uIP 协议栈保留给应用层做索引telnetd用它把struct psock实例关联到对应连接。如果你的应用要同时服务多个 TCP 连接一定要在连接建立时为每个新连接分配独立的struct psock否则两个连接共用同一个psock数据区会导致串包。6. 抓包验证与 Debug 技巧用 Wireshark 对比 uIP 协议栈行为拿一套资源里的代码移植到自己的板子后不要急着跑通就收工先用抓包把协议栈行为摸一遍。常见做法是在开发板上跑 uIP 的 Web Server 和webclient例子用 PC 上游 Linux 或 Wireshark 抓包重点看三个包的时序ARP 请求/应答、TCP 三次握手、HTTP 请求/响应。# Linux 下用 tcpdump 抓指定 IP 的流量 sudo tcpdump -i eth0 -nn host 192.168.1.100如果 Wireshark 显示 TCP 三次握手正常但 HTTP 响应一直不出现优先查httpd-fsdata.c是否正常注册了文件表——很多时候httpd_fs_data是个空链表导致 404 都不回。另一个高发问题是 RST 包频繁出现常见原因是应用层代码在uip_input()之后没有马上回包就返回导致协议栈认为连接异常。// 排查 TCP 状态机的常用输出手段 #if UIP_DEBUG printf(tcpstate %d, c %d, mss %d\n, conn-tcpstate, conn-tcpchunk, conn-mss); #endiftelnetd.c的调试更有代表性。如果 Telnet 连上后一敲按键就断大概率是PSOCK_READTO读到的数据里包含了\r\n但你的解析逻辑没有处理\r的过滤缓冲区内残留长度溢出导致状态错乱。这时在telnetd_appcall()的数据处理分支加打印对比抓包中的按键 HEX 序列就能定位。Wireshark 里可以给 uIP 设备按 IP 配置过滤器配合Follow TCP Stream功能把 HTTP 或 Telnet 交互记录完整展开。遇到「能发不能收」「能收不能发」的情况先确认uip_input()和uip_periodic()调用频率是否正常——很多移植失败的共同点是uip_periodic()走得太慢TCP 重传定时器迟迟不触发抓包就会看到对方一直在重传 SYN而设备毫无反应。这个排查动作比盲目加打印效率高得多。本文还有配套的精品资源点击获取