EMS 知识库
首页 / knowledge/代码解析/midware/ethernt/lan

socket.c

源文件路径:midware/ethernt/lan/socket.c 所属模块:midware/ethernt/lan(W5500 以太网协议栈 — Socket API 实现层) 代码行数:719

1. 文件职责

本文件是 WIZnet W5500 官方 socket 库在本工程中的实际实现体(文件头标注 V1.0.3,2014/05/01),把 W5500 的 8 个硬件 socket 封装成一套类 Berkeley 接口,向上层网络应用屏蔽 Sn_* 寄存器操作、TX/RX 环形缓冲区搬运和 socket 状态机细节。

它处在数据流的最底层:所有 TCP/UDP 报文的收发最终都落到本文件的 send/recv/sendto/recvfrom 上。在 EMS 中它的调用者有四类:midware/ethernt/ethernet.c(云端 TCP 客户端 socket 2)、midware/interface/modbus/modbus.c(Modbus TCP 从站 socket 3)、midware/ethernt/lan/dhcp.c 与 dns.c(UDP)、net/ems.c 与 net/server.c(直接调用 send/recv 收发云端报文)。

与官方原版相比,本文件被 Alpha 改动过两处以适配 uCOS-II:recv() 里加入了基于 OSTimeGet() 的 2000 tick 阻塞超时(socket.c:319、socket.c:354),sendto() 里加入了 OSTimeDlyHMSM(0,0,0,1) 让出 1 ms(socket.c:431)。因此本文件不能当作纯裸机驱动看待——它是与 RTOS 时基耦合的。文件本身不含业务逻辑,不含任务,靠上层的网络任务调用驱动。

2. 关键数据结构与宏

本文件没有自定义结构体/枚举,类型全部来自 socket.h 与 wizchip_conf.h;核心内容是一组文件级静态状态变量与四个形参校验宏。

名称类型说明
SOCK_ANY_PORT_NUM宏0xC000,socket() 未指定端口时的自增起始端口(第 54 行,定义末尾多了一个分号,见第 7 节)
SEND_TIME_OUT宏(500),值 500,在本文件中定义后从未被使用(第 62 行)
CHECK_SOCKNUM()宏(do-while 语句块)校验 sn > _WIZCHIP_SOCK_NUM_ 则返回 SOCKERR_SOCKNUM(第 68-71 行)
CHECK_SOCKMODE(mode)宏(do-while)校验 getSn_MR(sn) & 0x0F 是否等于 mode,不符返回 SOCKERR_SOCKMODE(第 73-76 行)
CHECK_SOCKINIT()宏(do-while)校验 getSn_SR(sn) == SOCK_INIT,否则返回 SOCKERR_SOCKINIT(第 78-81 行)
CHECK_SOCKDATA()宏(do-while)校验 len == 0 则返回 SOCKERR_DATALEN(第 83-86 行)
sock_any_portstatic uint16_t自动端口分配游标,初值 SOCK_ANY_PORT_NUM,回绕点 0xFFF0(第 56 行)
sock_io_modestatic uint16_t每个 socket 一位的"非阻塞 IO"位图;bit n = socket n 处于非阻塞模式(第 57 行)
sock_is_sending全局 uint16_t(非 static)每个 socket 一位的"发送中"位图,send() 用它做重入保护(第 58 行)
sock_remained_size[_WIZCHIP_SOCK_NUM_]static uint16_t[8]非 TCP 模式下"整包尚未取完的字节数"(第 59 行)
sock_pack_info[_WIZCHIP_SOCK_NUM_]static uint8_t[8]非 TCP 模式下的包分片标志,取 PACK_FIRST/PACK_REMAINED/PACK_COMPLETED(第 60 行)
sock_next_rd[_WIZCHIP_SOCK_NUM_]static uint16_t[8]仅 _WIZCHIP_ == 5200 时定义,用于规避 W5200 ARP errata。本工程 _WIZCHIP_ == 5500,该数组不参与编译(第 64-66 行)
sockint_kind枚举(socket.h:333)socket 中断位掩码:SIK_CONNECTED(1<<0)/SIK_DISCONNECTED/SIK_RECEIVED/SIK_TIMEOUT/SIK_SENT(1<<4)/SIK_ALL(0x1F)
ctlsock_type枚举(socket.h:347)ctlsocket() 操作码:CS_SET_IOMODE…CS_GET_INTMASK
sockopt_type枚举(socket.h:364)setsockopt()/getsockopt() 选项码:SO_FLAG…SO_PACKINFO
PACK_FIRST / PACK_REMAINED / PACK_COMPLETED宏(socket.h:129-131)0x80 / 0x01 / 0x00
SF_IO_NONBLOCK宏(socket.h:124)0x01,本文件用 (flag & SF_IO_NONBLOCK) << sn 把标志位搬到对应 socket 位上,依赖其值为 1
SF_TCP_NODELAY / SF_IGMP_VER2 / SF_MULTI_ENABLE / SF_UNI_BLOCK宏(socket.h:112-121)分别映射 Sn_MR_ND(0x20)/Sn_MR_MC(0x20)/Sn_MR_MULTI(0x80)/Sn_MR_UCASTB(0x10),在 socket() 的参数校验中使用
SOCK_OK/SOCK_BUSY/SOCK_FATAL/SOCKERR_*/SOCKFATAL_PACKLEN宏(socket.h:88-107)返回码。SOCK_BUSY 与 SOCK_ERROR 同为 0,与 SOCK_OK 为 1 互补,调用者用 == SOCK_OK / < 0 判断
_WIZCHIP_宏(wizchip_conf.h:59)本工程为 5500,决定大量 #if 分支的走向
_WIZCHIP_SOCK_NUM_宏(wizchip_conf.h:138)本工程为 8,即数组长度与合法 socket 号上界

3. 函数清单

函数名作用域说明
socket全局打开一个 socket:校验协议/标志、close(sn) 复位、写 Sn_MR/Sn_PORT、发 Sn_CR_OPEN,未给端口时从 sock_any_port 自增分配(第 90 行)
close全局发 Sn_CR_CLOSE,清全部中断 setSn_IR(sn,0xFF),清发送/剩余包状态,等到 SOCK_CLOSED 返回(第 160 行)
listen全局仅在 TCP 且 socket 处于 SOCK_INIT 时发 Sn_CR_LISTEN,轮询等到 SOCK_LISTEN(第 176 行)
connect全局仅在 TCP 且已初始化时设置目的 IP/端口并发 Sn_CR_CONNECT;非阻塞模式下立即返回 SOCK_BUSY(第 195 行)
disconnect全局仅在 TCP 下发 Sn_CR_DISCONN,阻塞等 SOCK_CLOSED,期间检查 Sn_IR_TIMEOUT(第 229 行)
send全局TCP 发送:检查发送中标志与 SENDOK/TIMEOUT 中断、等 TX 空闲空间足够、wiz_send_data、发 Sn_CR_SEND(第 251 行)
recv全局TCP 接收:检查 socket 状态与 RX_RSR,并带有本工程新增的 2000 tick 阻塞超时(第 315 行)
sendto全局UDP/MACRAW 发送:设置目的 IP/端口、等 TX 空闲、发 Sn_CR_SEND,然后轮询 Sn_IR_SENDOK/Sn_IR_TIMEOUT(第 372 行)
recvfrom全局UDP/MACRAW/IPRAW 接收:先读 8 字节(UDP)或 2 字节(MACRAW)包头取出对端地址、端口与包长,再搬数据,支持分包续传(第 454 行)
ctlsocket全局socket 控制:读写 IO 模式、读写最大 TX/RX 缓冲、清/读中断、设/读中断掩码(第 574 行)
setsockopt全局设置 socket 选项:SO_TTL/SO_TOS/SO_MSS/SO_DESTIP/SO_DESTPORT/SO_KEEPALIVESEND/SO_KEEPALIVEAUTO(第 617 行)
getsockopt全局读取 socket 选项与运行时信息:SO_FLAG/SO_TTL/SO_TOS/SO_MSS/SO_DESTIP/SO_DESTPORT/SO_KEEPALIVEAUTO/SO_SENDBUF/SO_RECVBUF/SO_STATUS/SO_REMAINSIZE/SO_PACKINFO(第 670 行)

共 12 个函数,全部为全局作用域,文件内没有 static 函数。注意 socket.h:467 声明了 send_u32(),但本文件没有实现(全工程也无调用点,见第 7 节)。

4. 核心逻辑解析

4.1 文件级状态:三个位图 + 两个数组 + 一个游标

本文件全部状态都是"按 socket 号索引"的:

4.2 socket 生命周期与合法性校验

socket() 的顺序是严格且不可跳过的:

  1. CHECK_SOCKNUM();
  2. 协议白名单——Sn_MR_TCP(0x01)、Sn_MR_UDP(0x02)、Sn_MR_MACRAW(0x04);Sn_MR_IPRAW/Sn_MR_PPPoE 两个 case 被 #if (_WIZCHIP_ < 5200) 裁掉了(第 99-103 行),即 W5500 下不开放;
  3. 标志合法性——if((flag & 0x06) != 0) return SOCKERR_SOCKFLAG; 先排除 bit1/bit2;TCP 下若既没有 SF_TCP_NODELAY 也没有 SF_IO_NONBLOCK 则报错;UDP 下若给了 SF_IGMP_VER2 却没有 SF_MULTI_ENABLE 则报错;
  4. close(sn) 强制复位 → setSn_MR(sn, protocol | (flag & 0xF0))(高 4 位标志写进模式寄存器)→ 写端口 → Sn_CR_OPEN 并 while(getSn_CR(sn)) 等命令被硬件接收;
  5. 刷新本文件的三个状态位,最后 while(getSn_SR(sn) == SOCK_CLOSED) 等状态机离开 CLOSED,返回 sn。

close() 与 disconnect() 的差别是:close() 立即发 Sn_CR_CLOSE、清 Sn_IR 全 0xFF、无条件等到 SOCK_CLOSED;disconnect() 只在 TCP 下发 Sn_CR_DISCONN(正常四次挥手),等待过程中若发现 Sn_IR_TIMEOUT 则退化为 close() 并返回 SOCKERR_TIMEOUT。

listen() 里有一个可疑的判据:等待 SOCK_LISTEN 的循环中写的是 if(getSn_CR(sn) == SOCK_CLOSED)(第 185 行)——把命令寄存器 Sn_CR 与 socket 状态 SOCK_CLOSED 比较,几乎恒为假。正常写法应是 getSn_SR(sn)。这一段容错分支实际上是死代码。

4.3 TCP 发送 send()

流程(第 251-313 行):

  1. 校验 socket 号 / 模式(必须是 TCP)/ len != 0;
  2. Sn_SR 必须是 SOCK_ESTABLISHED 或 SOCK_CLOSE_WAIT,否则 SOCKERR_SOCKSTATUS;
  3. 若 sock_is_sending 对应位已置,按 Sn_IR 处理上一笔(见 4.1);
  4. len 先被裁剪到 getSn_TxMAX(sn);
  5. 循环等 getSn_TX_FSR(sn) >= len:期间一旦发现 socket 掉出 ESTABLISHED/CLOSE_WAIT 就 close(sn) 返回错误;非阻塞模式且空间不足则返回 SOCK_BUSY;
  6. wiz_send_data() 把数据写进 W5500 的 TX 环形缓冲 → 发 Sn_CR_SEND → 等命令寄存器 → 置 sock_is_sending 位 → 返回实际发送长度 len。

注意返回值语义:返回值就是请求发送的字节数,并不代表对端已收到(真正的完成由下一次调用时的 SENDOK 确认)。

4.4 TCP 接收 recv()(本工程改动点)

流程(第 315-370 行):

  1. len 裁剪到 getSn_RxMAX(sn);
  2. 循环读 getSn_RX_RSR(sn) 与 Sn_SR:状态为 SOCK_CLOSE_WAIT 时会先把残留数据读完再关;其它非 ESTABLISHED 状态一律 close(sn) 返回 SOCKERR_SOCKSTATUS;
  3. 非阻塞模式且 RX_RSR == 0 → SOCK_BUSY;
  4. 本工程新增:s_u32_timeout_tick = OSTimeGet() 记录起点,每次循环判断 (OSTimeGet() - s_u32_timeout_tick) > 2000 则 break(OS_TICKS_PER_SEC = 1000,即 2000 ms);并处理 OSTimeGet() 回绕(若当前 tick 小于起点则重置起点)。这段是 Alpha 为了解决"阻塞式 recv() 在 TCP 连接被静默断开时把网络任务卡死"而加的;
  5. 取到数据后按 min(len, recvsize) 调 wiz_recv_data(),发 Sn_CR_RECV 释放 W5500 接收缓冲,返回实际长度。

因为第 4 步会以 len = 0 结束该函数,超时路径的返回值为 0,与 SOCK_BUSY(=0) 同值,调用方无法区分"超时"与"非阻塞无数据",这是本文件最容易踩的坑。

4.5 UDP/MACRAW 发送 sendto()

第 372-452 行。与 send() 的差别:

4.6 UDP/MACRAW 接收 recvfrom() 与分包

这是本文件最复杂的一段(第 454-571 行)。

启动条件:若 sock_remained_size[sn] == 0(表示这是新包的开头),先自旋等 Sn_RX_RSR != 0,非阻塞模式下为 0 则返回 SOCK_BUSY,socket 关闭则 SOCKERR_SOCKCLOSED。

随后把 sock_pack_info[sn] 置为 PACK_COMPLETED,按 mr & 0x07 分三种包头解析:

收尾(第 563-570 行):再发一次 Sn_CR_RECV 释放缓冲,sock_remained_size[sn] -= pack_len;若还剩数据就把 PACK_REMAINED 或进 sock_pack_info[sn];返回 pack_len。下一次调用会走到 sock_remained_size[sn] != 0 的分支,跳过包头解析,继续搬剩余部分。

状态机图:

                     RX_RSR != 0
   [空闲]  ─────────────────────────►  读包头(UDP 8B / MACRAW 2B)
   remained_size=0                       │
                                         ▼
                        pack_info = PACK_FIRST
                                         │
                     ┌───────────────────┴────────────────────┐
                     │  len >= remained          len < remained│
                     ▼                                         ▼
              一次取完整包                          只取 len 字节
              pack_info 保持 FIRST                  pack_info |= REMAINED
                     │                                         │
                     └──────────────┬──────────────────────────┘
                                    ▼
                     remained_size -= pack_len; 返回 pack_len
                                    │
                     remained_size != 0? ──是──► 下次调用跳过包头直接续传

4.7 控制与选项 ctlsocket / setsockopt / getsockopt

5. 对外接口

本文件导出(供谁调用)

导出项调用方
socket / close / listen / connect / disconnectmidware/ethernt/ethernet.c:723/:732/:708/:688/:970(云端 TCP 客户端,socket 号 SOCK_TCP_CLIENT=2);midware/interface/modbus/modbus.c:9744/:9647(Modbus TCP 从站,SOCK_TCP_SERVER=3,端口 502);midware/ethernt/lan/dns.c:520、dhcp.c:700(UDP)
sendmidware/ethernt/ethernet.c:593;net/ems.c:6241(云端上报直接发送并打印 ret)
recvnet/server.c:6298、net/server.c:6326(先收 7 字节头再收体,依赖本文件新增的超时行为)
sendto / recvfrommidware/ethernt/lan/dns.c:527/:539、dhcp.c:409/:505/:565/:581/:911(全部为裸机式同步收发,未改 uCOS 超时)
ctlsocket / setsockopt / getsockoptethernet.c:699、modbus.c:9657 等设置保活;其余为通用能力
sock_is_sending全局变量(未加 static),外部模块理论上可读,工程中未见外部引用

本文件调用的外部函数/变量

外部符号来源
getSn_MR / getSn_SR / setSn_MR / setSn_CR / getSn_CR / setSn_IR / getSn_IR / getSn_IMR / setSn_IMR / setSn_PORT / setSn_DIPR / getSn_DIPR / setSn_DPORT / getSn_DPORT / getSn_TTL / setSn_TTL / getSn_TOS / setSn_TOS / getSn_MSSR / setSn_MSSR / getSn_TxMAX / getSn_RxMAX / getSn_TX_FSR / getSn_RX_RSR / getSn_KPALVTR / setSn_KPALVTR / getSn_TX_RDmidware/ethernt/lan/w5500.h / w5500.c(寄存器访问层)
wiz_send_data / wiz_recv_data / wiz_recv_ignoremidware/ethernt/lan/w5500.c:327/:343/:361
SOCK_INIT / SOCK_CLOSED / SOCK_LISTEN / SOCK_ESTABLISHED / SOCK_CLOSE_WAIT / SOCK_UDP / SOCK_MACRAW / Sn_CR_* / Sn_IR_* / Sn_MR_*w5500.h
OSTimeGetuCOS-II(UCOSII/uCOS-II/Source/os_time.c)
OSTimeDlyHMSMuCOS-II(os_time.c),仅在 sendto() 第 431 行使用
#if _WIZCHIP_ == 5200 分支中的 setSUBRw5200.h,本工程 _WIZCHIP_ = 5500,该分支不编译

6. 依赖关系

依赖

socket.c
  └─ socket.h
       └─ wizchip_conf.h        (_WIZCHIP_=5500、_WIZCHIP_SOCK_NUM_=8、wiz_send_data 等声明)
            └─ w5500.h / w5500.c (Sn_* 寄存器宏与实现)
                 └─ uCOS-II 时基    (本文件新增的 OSTimeGet/OSTimeDlyHMSM)

被依赖(全部通过 #include "socket.h",共 7 个文件)

对应头文件文档见 docs/knowledge/代码解析/midware/ethernt/lan/socket.h.md。

7. 备注与风险点

  1. CHECK_SOCKNUM() 的边界少一(socket.c:70):判据是 sn > _WIZCHIP_SOCK_NUM_,而 _WIZCHIP_SOCK_NUM_ = 8 是数组长度。传 sn == 8 时校验通过,随后 sock_remained_size[8]/sock_pack_info[8] 就是数组越界写。当前调用方只用了 socket 2/3,暂未触发,但这是个真实的越界隐患。
  2. connect() 的 IP 校验算式有误(socket.c:202-205):四次左移的最后一个字节取的是 addr[0] 而不是 addr[3],导致 taddr 只是"IP 前 3 字节 + 首字节"。例如 1.2.3.0 会得到 0x01020301 而非 0x01020300,非法地址不会按预期被 SOCKERR_IPINVALID 拦下。工程中 connect() 只连固定服务器 IP,实际影响有限,但属于 V1.0.3 注释里号称已修的问题("Validation a destination ip in connect() & sendto()")在 connect() 中的残留错误。
  3. sendto() 里的 *((uint32_t*)addr) 未对齐 32 位访问(socket.c:394):addr 是调用方给的 uint8_t*,工程中多为结构体成员或栈数组,不保证 4 字节对齐;Cortex-M4 对单条 LDR 支持非对齐访问,但这是未定义行为级别的写法。同时第 389-392 行算出的 taddr 完全未被使用(既没参与判断也没参与告警),是明显的死代码,编译器会报"变量赋值后未使用"。
  4. sendto() 的 Sn_IR 轮询形同虚设(socket.c:446-449):else { return SOCKERR_TIMEOUT; } 让"中断尚未产生"也被判成超时。因为前面只让出了 1 ms,实测中 UDP 发送在负载稍高时容易误报超时,而调用方(dns.c/dhcp.c)拿到 -13 只会重试,掩盖了这一行为。
  5. recvfrom() 的 IPRAW 分支有拼写错误(socket.c:548):sock_remaiend_size[sn] = ...(多了个 end)。该行位于 #if (_WIZCHIP_ < 5200) 内,_WIZCHIP_ = 5500 时不参与编译,所以现在是"潜伏的编译错误"——一旦换回 W5200 或 W5100 就会编译失败。同类问题还有 send() 里的 #if _WZICHIP_ == 5200(socket.c:269)把 _WIZCHIP_ 拼成了 _WZICHIP_,宏未定义 → 恒为 0 → 该分支永远不编译。
  6. recvfrom() 的 sock_remained_size 可能下溢(socket.c:566):sock_remained_size[sn] -= pack_len; 没有先判 pack_len <= sock_remained_size[sn]。在 len > 剩余长度 时上游已保证 pack_len = 剩余长度,但在"上一次调用被 SOCK_BUSY 提前中断"等边界下,Default 分支会用 pack_len 直接赋值 sock_remained_size[sn] = pack_len,逻辑分支较绕,后续维护时容易踩。
  7. listen() 的容错判据写错寄存器(socket.c:185):if(getSn_CR(sn) == SOCK_CLOSED) 应为 getSn_SR(sn)。结果是 listen() 在端口被占用/对端拒绝等异常路径下会永远自旋在 while(getSn_SR(sn) != SOCK_LISTEN) 里,把一个 uCOS 任务钉死。工程中 modbus.c:9647 会调用 listen(),若 Modbus TCP 端口创建失败,该网络任务将失去调度。
  8. getsockopt() 多处缺少 break(socket.c:684-689、:698-704):SO_MSS 会继续执行 SO_DESTIP 的 getSn_DIPR(sn,(u8*)arg),把 4 字节 IP 覆盖掉调用方那个 u8* 目标;SO_SENDBUF 会继续写 SO_RECVBUF、SO_STATUS;SO_RECVBUF 会继续写 SO_STATUS。若调用方按 u8 语义传参,SO_SENDBUF 这类"应为 u16"的取值会越界写 1 字节。当前工程未见使用这些选项,属埋雷。
  9. ctlsocket() 的 CS_GET_INTMASK 缺 break(socket.c:610-612):读完 getSn_IMR 后直接落入 default: return SOCKERR_ARG;,即"读取中断掩码"这个功能永远返回错误码,尽管副作用已经发生。
  10. #define SOCK_ANY_PORT_NUM 0xC000; 末尾多了分号(socket.c:54):展开后 sock_any_port = 0xC000;;,多一个空语句,当前无害,但若被写进 if (x) sock_any_port = SOCK_ANY_PORT_NUM; 之外的表达式位置会出问题。
  11. SEND_TIME_OUT (500) 定义了却从未使用(socket.c:62)。从命名与数值看,编写者原本想给 TCP 发送加 500 ms 超时,最终没有落地——send() 在 TX 缓冲不足时用的是无限自旋,只靠 Sn_SR 变化退出。
  12. send() 的 TX 等待循环没有超时(socket.c:289-302):非阻塞模式下会返回 SOCK_BUSY,但阻塞模式下(ethernet.c、modbus.c 的 socket 用了 Sn_MR_ND 而未带 SF_IO_NONBLOCK,属阻塞)会在 while(1) 里一直等 TX 空闲,一旦对端不读导致窗口关闭,任务会长时间不释放 CPU。与之对照,recv() 已经被补上了 2000 ms 超时,send() 没有,二者不对称。
  13. send() 与 recv() 的返回值语义不一致:send() 返回的是请求长度(乐观),recv() 返回的是实收长度(悲观);两者都可能在错误时返回与 SOCK_BUSY(0) 同值的 0(send() 的 len 被裁到 0 的情况、recv() 的超时路径),调用方若只判 > 0 会把"无数据"和"超时"混为一谈。
  14. _WIZCHIP_SOCK_NUM_ 与数组实际长度耦合:sock_remained_size/sock_pack_info/sock_next_rd 都以该宏为长度,sock_io_mode/sock_is_sending 又是 uint16_t 位图(上限 16 个 socket)。这套设计与 wizchip_conf.h 中的 socket 数定义强绑定,换芯片型号需同步复核。
  15. socket() 的 sock_io_mode |= ((flag & SF_IO_NONBLOCK) << sn)(socket.c:152)依赖 SF_IO_NONBLOCK == 0x01。若哪天该宏被改成其它位掩码(例如 0x08),这一行会把状态位搬到错误的 socket 上,且编译器不会报警。