feat(NET): N34 阶段 1.5 recv 语义主流化——无限默认+具名非阻塞+模式属性

核心语义(实施方案 §3.5):
- net_recv 默认无限阻塞:删除 NET_RECV_TIMEOUT_MS(30s 隐藏默认预算),
  超时归调用方策略;唤醒源=数据/EOF/close/网络重建(CONNRESET),
  会话死亡由对账① ~400ms 判死兜底,无"对端消失永久挂死"窗口
- 新增 net_recv_nb(NET_MSG_DONTWAIT 路径转正具名)与
  net_setblocking(fd,on)(阻塞模式挂控制块属性,对齐 O_NONBLOCK/
  Python setblocking;直写单字节,net_set_event_cb 先例)
- net_types.h net_sock_t 新增 blocking 属性;net_socket() 默认置 1
  (close memset 清零后显式重置,同 released_abnormal 纪律)
- 原 1s 强制了结看门狗降级为 60s tripwire:只 [ERR] 留痕一次,
  绝不伪造 TIMEDOUT 返回值(旧实现掩盖完成路径缺陷)
- net_recvfrom 删 30s 自旋超时、同判据对齐(零生产调用者,注释警示)
- 文档契约:IWDG required_mask 内任务禁止无限阻塞 recv(BSD 指南
  §3.5/§10.1 + net_socket.h 注释)

调用点审计(§3.5.5):零生产调用点采用无限默认——iec104_serv/
ui_net_sink 转 net_recv_nb(受监控任务逐位等价);lftpd/time_sync
已是显式预算 net_recv_to 不动;32 轮盲收兜底未动(阶段 3 E6 门禁)

模拟器双端镜像 net_recv_nb/net_recv_to/net_setblocking(包装层
Sleep(1) 轮询模拟阻塞);差距清单 N-3/N-6 登记义务已履行

门禁与台架:
- @build 0/0:Code 209580→209780(+200B) ZI +32B;RAM 受管区官方
  脚本同步;ci_local L0 32/32 + L0.5 951/951
- 台架全回归:relisten :7003 50/50、iec104 18/18、对时正常、
  ftp_regression 11/11、Trap23 RST 5/5+FIN 5/5、ftp_integrity
  20/20 字节一致、n30 0/10、@test PASS
- 模拟器 build_sim OK + check_remo_sink 黄金参考 0/25600 位差

队友 p15-recv-semantics 实施,Lead 全量审阅 + 独立复跑门禁与回归
This commit is contained in:
2026-10-13 11:23:19 +08:00
parent 8f902e09fd
commit d346e4ee84
17 changed files with 487 additions and 147 deletions
+143 -24
View File
@@ -10,8 +10,10 @@
*
* 设计要点:
* 1. fd 采用与 MCU 端一致的"控制块索引"模型(0 ~ NET_MAX_SOCKETS-1)
* 2. 所有 Socket 创建即置为非阻塞(ioctlsocket FIONBIO),使 net_recv /
* net_accept 的 DONTWAIT 语义与 MCU 端一致(无数据返回 -1 + NET_ERR_WOULDBLOCK)
* 2. Winsock 句柄创建即置非阻塞(ioctlsocket FIONBIO),阻塞语义在包装层
* 按控制块 blocking 属性模拟(N34 阶段 1.5:模式归使用者——blocking=1
* 时 recv 轮询等待(Sleep(1)),=0 或 NET_MSG_DONTWAIT 立即 WOULDBLOCK),
* 使 net_recv / net_recv_nb / net_recv_to / net_accept 的语义与 MCU 端一致
* 3. 代次号在模拟器上恒为 0(无"网络重建"概念),net_get_generation 恒返回 0
* 4. net_get_sock 以 in_use 判活,供上层判断"监听 Socket 是否随客户端一同失效"
* 5. Winsock 句柄存放于并行数组 s_ws[],不侵入共享的 net_sock_t 结构体
@@ -213,6 +215,9 @@ int net_socket(int domain, int type) {
s_socks[idx].type = (uint8_t)type;
s_socks[idx].state = NET_SOCK_STATE_CREATED;
s_socks[idx].ch395_sock = (uint8_t)idx;
/* N34 阶段 1.5:阻塞模式默认 = 阻塞(与 MCU 端 net_socket 一致;
* Winsock 句柄仍恒非阻塞,阻塞语义由包装层按本属性模拟,见文件头要点 2) */
s_socks[idx].blocking = 1;
s_ws[idx] = ws;
s_net_errno = NET_OK;
@@ -329,6 +334,8 @@ int net_accept(int sockfd, struct net_sockaddr *addr, int *addrlen) {
s_socks[nidx].type = (uint8_t)NET_SOCK_STREAM;
s_socks[nidx].state = NET_SOCK_STATE_ESTABLISHED;
s_socks[nidx].ch395_sock = (uint8_t)nidx;
/* N34 阶段 1.5:accept 出的新连接同样默认阻塞模式(与 MCU 端一致) */
s_socks[nidx].blocking = 1;
s_ws[nidx] = cws;
s_net_errno = NET_OK;
@@ -451,32 +458,18 @@ int net_send(int sockfd, const void *buf, int len, int flags) {
}
/*
* 函数功能:接收数据(TCP)
* 入口参数:sockfd - socket 描述符
* buf - 接收缓冲区
* 函数功能:单次非阻塞 recv(内部公共体:立即返回,不等待)
* 入口参数:sockfd - socket 描述符(已校验)
* buf - 接收缓冲区(已校验)
* len - 缓冲区大小
* flags - 通常为 0,可设 NET_MSG_DONTWAIT(Socket 本就非阻塞)
* 出口参数:buf - 填充接收数据
* 返回值:实际接收字节数;0 = 对端关闭;-1 = 无数据(NET_ERR_WOULDBLOCK)或错误
* 限定条件:socket 已建立
* 函数说明:与 MCU 端语义一致——>0 有数据,0 对端关闭,<0 且 errno 区分
* "暂无数据"与"真错误"
* 返回值:>0 字节数;0 = 对端关闭;-1 = 无数据(WOULDBLOCK) 或错误
* 限定条件:Winsock 句柄为非阻塞(FIONBIO)
* 函数说明:net_recv 的非阻塞路径与 net_recv_nb 共用;错误码映射与旧实现一致
*/
int net_recv(int sockfd, void *buf, int len, int flags) {
int r;
static int recv_once(int sockfd, void *buf, int len) {
int r = recv(s_ws[sockfd], (char *)buf, len, 0);
(void)flags;
if (sockfd < 0 || sockfd >= NET_MAX_SOCKETS || s_socks[sockfd].in_use == 0) {
s_net_errno = NET_ERR_BADF;
return -1;
}
if (buf == NULL || len < 0) {
s_net_errno = NET_ERR_INVAL;
return -1;
}
r = recv(s_ws[sockfd], (char *)buf, len, 0);
if (r > 0) {
s_socks[sockfd].recv_bytes += (uint32_t)r;
s_net_errno = NET_OK;
@@ -494,6 +487,110 @@ int net_recv(int sockfd, void *buf, int len, int flags) {
return -1;
}
/*
* 函数功能:阻塞语义模拟(内部公共体:轮询等待数据/EOF/错误)
* 入口参数:sockfd - socket 描述符(已校验)
* buf / len - 接收缓冲与容量(已校验)
* has_budget - 1 = 有预算(net_recv_to);0 = 无限等待(net_recv 阻塞)
* budget_ms - 预算毫秒(has_budget=0 时忽略)
* 出口参数:buf - 填充接收数据
* 返回值:>0 字节数;0 = 对端关闭;-1 = 错误;-2 = 预算耗尽无数据(仅 has_budget)
* 限定条件:Winsock 句柄为非阻塞;调用方为 UI/服务线程(Sleep(1) 粒度可接受)
* 函数说明:与 MCU 端挂起 recv 的对外契约对齐——MCU 端等待发生在调用方任务
* (睡任务通知),模拟器端等价地睡 Sleep(1) 轮询;EOF/错误立即返回
*/
static int recv_wait(int sockfd, void *buf, int len, int has_budget, unsigned long long budget_ms) {
unsigned long long t0 = GetTickCount64();
for (;;) {
int r = recv_once(sockfd, buf, len);
if (r != -1 || s_net_errno != NET_ERR_WOULDBLOCK) {
return r; /* 数据 / EOF / 真错误:立即返回 */
}
if (has_budget && (GetTickCount64() - t0) >= budget_ms) {
s_net_errno = NET_ERR_TIMEDOUT;
return -2; /* 预算耗尽(对齐 MCU net_recv_to 的 -2) */
}
Sleep(1);
}
}
/*
* 函数功能:接收数据(TCP,**默认无限阻塞**——N34 阶段 1.5 与 MCU 端语义对齐)
* 入口参数:sockfd - socket 描述符
* buf - 接收缓冲区
* len - 缓冲区大小
* flags - 通常为 0;NET_MSG_DONTWAIT 单次强制非阻塞
* 出口参数:buf - 填充接收数据
* 返回值:实际接收字节数;0 = 对端关闭;-1 = 无数据(NET_ERR_WOULDBLOCK,仅非阻塞)或错误
* 限定条件:socket 已建立
* 函数说明:与 MCU 端语义一致——blocking 属性(net_setblocking)决定无数据时
* 无限等待还是立即 WOULDBLOCK;DONTWAIT 单次覆盖属性
*/
int net_recv(int sockfd, void *buf, int len, int flags) {
if (sockfd < 0 || sockfd >= NET_MAX_SOCKETS || s_socks[sockfd].in_use == 0) {
s_net_errno = NET_ERR_BADF;
return -1;
}
if (buf == NULL || len < 0) {
s_net_errno = NET_ERR_INVAL;
return -1;
}
/* 非阻塞:DONTWAIT 单次覆盖,或 socket 的 blocking 属性为 0 */
if ((flags & NET_MSG_DONTWAIT) || s_socks[sockfd].blocking == 0U) {
return recv_once(sockfd, buf, len);
}
/* 阻塞:无限等待(无默认预算,N34 阶段 1.5) */
return recv_wait(sockfd, buf, len, 0, 0ULL);
}
/*
* 函数功能:接收数据(非阻塞具名入口,与 MCU 端同名 API 对齐——N34 阶段 1.5)
* 入口参数:sockfd / buf / len - 同 net_recv
* 返回值:>0 字节数;0 = 对端关闭;-1 = 无数据(WOULDBLOCK) 或错误
* 限定条件:socket 已建立
* 函数说明:等价 net_recv(sockfd, buf, len, NET_MSG_DONTWAIT),不改变 blocking 属性
*/
int net_recv_nb(int sockfd, void *buf, int len) {
if (sockfd < 0 || sockfd >= NET_MAX_SOCKETS || s_socks[sockfd].in_use == 0) {
s_net_errno = NET_ERR_BADF;
return -1;
}
if (buf == NULL || len < 0) {
s_net_errno = NET_ERR_INVAL;
return -1;
}
return recv_once(sockfd, buf, len);
}
/*
* 函数功能:接收数据(**显式预算**便捷入口,与 MCU 端同名 API 对齐——N34 阶段 1.5)
* 入口参数:sockfd / buf / len - 同 net_recv
* timeout_ms - 本次等待预算(ms,必须 >0;0 = 无限等待语义
* 属 net_recv 的职责,此处拒 INVAL,与 MCU 端一致)
* 返回值:>0 字节数;0 = 对端关闭;-1 = 真错误;-2 = 预算耗尽无数据
* 限定条件:socket 已建立
* 函数说明:模拟器以 GetTickCount64 + Sleep(1) 轮询兑现预算
*/
int net_recv_to(int sockfd, void *buf, int len, uint32_t timeout_ms) {
if (sockfd < 0 || sockfd >= NET_MAX_SOCKETS || s_socks[sockfd].in_use == 0) {
s_net_errno = NET_ERR_BADF;
return -1;
}
if (buf == NULL || len < 0) {
s_net_errno = NET_ERR_INVAL;
return -1;
}
if (timeout_ms == 0U) {
/* 0 = 无限等待(net_recv 的语义),与本接口"有界"存在理由相反 → 拒绝 */
s_net_errno = NET_ERR_INVAL;
return -1;
}
return recv_wait(sockfd, buf, len, 1, (unsigned long long)timeout_ms);
}
/*
* 函数功能:发送 UDP 数据报(含目标地址)
* 入口参数:sockfd - socket 描述符
@@ -634,6 +731,28 @@ int net_set_event_cb(int sockfd, net_event_cb_t cb, void *arg) {
return 0;
}
/*
* 函数功能:设置 Socket 阻塞/非阻塞**模式属性**(与 MCU 端同名 API 对齐——N34 阶段 1.5)
* 入口参数:sockfd - socket 描述符
* on - 1 = 阻塞(net_socket/net_accept 默认);0 = 非阻塞
* 返回值:0 成功;-1 失败(描述符无效/未占用)
* 限定条件:socket 已创建
* 函数说明:Winsock 句柄恒非阻塞(FIONBIO,UI 主循环安全底线),本属性只驱动
* 包装层的等待策略(net_recv 无限轮询 / 立即 WOULDBLOCK),
* 语义与 MCU 端"模式挂控制块属性"一致
*/
int net_setblocking(int sockfd, int on) {
if (sockfd < 0 || sockfd >= NET_MAX_SOCKETS || s_socks[sockfd].in_use == 0) {
s_net_errno = NET_ERR_BADF;
return -1;
}
s_socks[sockfd].blocking = (on != 0) ? 1U : 0U;
s_net_errno = NET_OK;
return 0;
}
/*
* 函数功能:获取 socket 最后错误码
* 入口参数:无