更新BSD_Socket_API使用指南至现状:新增net_recv_to/net_relisten/对账等新增机制,§3.6/§11.2/§12.10
This commit is contained in:
+162
-26
@@ -1,6 +1,6 @@
|
||||
# BSD Socket API 使用指南
|
||||
|
||||
> **恢复说明**:本文档曾在提交 `4e9ac0d`(2026-08-28)中误删,2026-09-13 从 git 历史恢复,并同步当前 API(`net_init` 4 参数 / `net_socket` 2 参数 / 网络代次号 / `NET_NO_OS` 双端构建)。详见文末变更记录。
|
||||
> **恢复说明**:本文档曾在提交 `4e9ac0d`(2026-08-28)中误删,2026-09-13 从 git 历史恢复。API 随代码多轮同步(2026-09-13:`net_init` 4 参数 / `net_socket` 2 参数 / 网络代次号 / `NET_NO_OS` 双端构建;2026-09-23:`net_recv_to` / `net_sock_state` / `net_resync_listen` / `net_relisten`、L3 pending 化、D10 非阻塞重监听、Trap 22 周期对账、Trap 23 排渣隔离)。详见文末变更记录。
|
||||
|
||||
## 目录
|
||||
|
||||
@@ -58,7 +58,7 @@
|
||||
| `net_config.h` | 配置宏定义(最大 Socket 数、超时时间等) |
|
||||
| `net_types.h` | 类型定义(地址结构、Socket 控制块等) |
|
||||
| `net_socket.h` | BSD Socket API 头文件,含 `net_msg_t` 消息类型定义 |
|
||||
| `net_socket.c` | API 核心实现,含消息队列处理、线程安全封装、内部 `_sock()` 版本 |
|
||||
| `net_socket.c` | API 核心实现:消息队列处理与线程安全封装、内部 `_sock()` 版本;另含 L3 pending 化 recv、D10 非阻塞重监听重试(30ms/2s)、Trap 22 周期对账(200ms,去抖+冷却)、Trap 23 排渣隔离(400ms 起/800ms 上限) |
|
||||
| `net_select.h` | select/poll API 头文件 |
|
||||
| `net_select.c` | select/poll 实现(仅 MCU 端编译) |
|
||||
| `net_socket_sim.c`(`simulator/sim/`) | Winsock2 后端(模拟器端,与 `net_socket.c` 互斥编译),详见 §2.3 |
|
||||
@@ -150,10 +150,10 @@ sequenceDiagram
|
||||
|
||||
| 规则 | 说明 |
|
||||
|------|------|
|
||||
| **`netTask` 外调用** | 使用 `net_send()` / `net_recv()` / `net_close()` 公共 API |
|
||||
| **`netTask` 内调用** | 使用 `net_send_sock()` / `net_recv_sock()` / `net_close_sock()` 内部 API |
|
||||
| **创建/绑定/监听** | 在 `main()` 初始化阶段完成(`osKernelStart()` 之前),无需队列 |
|
||||
| **`net_poll()` 和 `net_process_messages()`** | 必须在 `netTask` 主循环中调用 |
|
||||
| **`netTask` 外(任意应用任务)** | 公共 API:`net_send()` / `net_recv()` / `net_recv_to()` / `net_accept()` / `net_connect()` / `net_close()` / `net_listen()`,及自愈 `net_relisten()` / `net_resync_listen()`——请求入队列,netTask 串行执行,调用方阻塞等回执 |
|
||||
| **`netTask` 内** | 内部 API `net_send_sock()` / `net_recv_sock()` / `net_close_sock()` / `net_listen_locked()` |
|
||||
| **`net_socket()` / `net_bind()`** | 直接调用,无需队列;实际在就绪屏障后的服务任务(timeSyncTask / ftpTask / uiTask)中创建监听 |
|
||||
| **`net_poll()` 和 `net_process_messages()`** | 必须在 `netTask` 主循环调用;`net_process_messages()` 每轮先跑 D10 重监听重试 → L3 挂起 recv 完成 → Trap 22 对账(200ms)三个非阻塞 tick,再处理队列 |
|
||||
|
||||
### 3.4 内部 API 说明
|
||||
|
||||
@@ -170,9 +170,27 @@ int net_close_sock(net_sock_t *p_sock);
|
||||
|
||||
- **消息队列由 CubeMX 创建**:在 `MX_FREERTOS_Init()`(`osKernelInitialize()` 之后)中调用 `osMessageQueueNew(8, sizeof(net_msg_t), NULL)` 创建
|
||||
- **队列句柄全局可见**:通过 `extern osMessageQueueId_t netMsgQueueHandle` 引用
|
||||
- **队列满时**:`net_send()` / `net_recv()` 会阻塞等待,直到 `netTask` 处理完消息腾出空间
|
||||
- **`netTask` 不可向自身发消息**:`netTask` 中直接调 `net_send()` 会死锁(等待自己处理消息),必须在 `netTask` 中使用 `net_send_sock()` 等内部版本
|
||||
- **`net_socket()` / `net_bind()` / `net_listen()` 无需线程安全**:这些仅在初始化阶段调用,消息队列仅保护数据收发和连接关闭
|
||||
- **阻塞 recv 不再独占 netTask(L3 pending 化,2026-09-22)**:无数据时公共 `net_recv()` / `net_recv_to()` **登记挂起请求后返回**(每 fd 至多一个,禁止覆盖——覆盖即丢失前调用方通知 = 永久挂死);完成由 `net_pending_tick()` 按主循环节拍检查(①数据 ②到点 ③断连,看门狗 1s)并经任务通知唤醒调用方——等待期间调用方在自己上下文睡通知,netTask 零占用(消除 E-02 头阻塞)。副作用:`buf` 指针须保持有效至返回;close / 网络重建 / 陈旧重同步会**显式取消并通知**(`NET_ERR_CONNRESET` 等),调用方永不挂死
|
||||
- **队列阻塞语义**:只有 `osMessageQueuePut` 失败(8 长度固定队列,极端情形)时公共 API 阻塞;recv 挂起等待**不计入**队列占用(完成前队列中无消息滞留)
|
||||
- **哪些操作要求 netTask 已运行**:`net_listen()` / `net_accept()` / `net_connect()` / `net_send()` / `net_recv()` / `net_close()` / `net_relisten()` / `net_resync_listen()` 全经消息队列 → 只能在就绪屏障后的任务调用;`net_socket()` / `net_bind()` 为直接调用,但同样建议屏障后
|
||||
|
||||
### 3.6 芯片/本层状态对账与重监听自愈(Trap 09 / 22 / 23)
|
||||
|
||||
单连接模型下"监听 Socket = 数据通道",多种芯片行为会造成**芯片侧与本层状态失同步**,纯事件(中断)驱动无法自愈。当前版本有四层自愈,全在 netTask 内、全非阻塞、不冻结主循环:
|
||||
|
||||
| 机制 | 触发 | 动作 | 关键参数 |
|
||||
|---|---|---|---|
|
||||
| **`auto_relisten`** | 数据通道正常 EOF / DISCONNECT | 重开 + 重新 LISTEN,回 LISTENING;应用层无需 close/重 listen | `net_listen()` 置位;`net_close()` 清 |
|
||||
| **D10 非阻塞重监听重试** | 重开失败(Trap 09:芯片 close 后需沉降) | 单次尝试失败即登记待重试,每轮到点重试;超时限放弃并记 `relisten give up` | 重试间隔 30ms / 时限 2s(自首次尝试);同 fd 有在途挂起 recv 时跳过(L3 保险,防毁在途连接) |
|
||||
| **Trap 22 周期对账** | 芯片丢 CONNECT/DISCONNECT 中断(快速复连);或静默回收 Socket(peer FIN,无 DISCONNECT 中断) | 每 200ms 对在用 socket 回读芯片状态,修正三类失同步:① 本层 ESTABLISHED 而芯片已断 → 按断开处理;② 本层 LISTENING 而芯片已握手 → 补 accept;③ 本层 LISTENING 而芯片监听消失 → 重建 | 周期 200ms;**连续 2 次失配**才动手(去抖 ≈400ms)+ 动手后 **1s 冷却**(防"刚 open 未生效"瞬态重建风暴) |
|
||||
| **Trap 23 排渣隔离** | 异常释放(芯片 TIMEOUT,如客户端中止上传):在途数据落进 RX 缓冲,直接复监听 → 下条连接把 ~2048B 残渣当自己的数据读走(上传静默截断) | 复监听前隔离:持续排空 RX 残渣 + 主动 close + 排空中断,等芯片回 CLOSED/TCP_CLOSED 才 open+listen;到期强制放行 | 隔离 400ms 起 / **800ms 上限(必须 < 2s 时限**,否则监听被放弃);仅异常路径生效,正常 FIN 路径零改动 |
|
||||
|
||||
**行为结论**:监听器恢复从"一次性事件"变为"登记 → 到点重试"过程:
|
||||
|
||||
- 正常断连:通常一次重开即成;失败时 ~30ms 量级恢复,最迟 ~1s(2s 后放弃,错误日志可见)。恢复窗内该端口拒绝新连接 → 调用方重连逻辑须容忍 ~1s 中断。
|
||||
- **快速复连不再"监听死亡"(Trap 22)**:对账保证失同步 ≤200ms 内修正(2026-09-23 生产路径:待机后 UI `:7003` 快速复连 10/10;FTP `:21` 100ms/0ms/600ms 间隔各 50/50;零重建风暴)。
|
||||
- **中止复连首上传不再被静默截断(Trap 23)**:RST/FIN 中止后首笔复连上传完整通过(RST 10/10、FIN 5/5,SHA-256 全匹配)。详见 `docs/问题记录/CH395F_Trap_Records.md` Trap 22/23。
|
||||
|
||||
---
|
||||
|
||||
@@ -223,6 +241,8 @@ int main(void)
|
||||
- 更新所有 Socket 的连接状态(CONNECT / DISCONNECT / RECV / TIMEOUT)
|
||||
- 触发事件回调
|
||||
- 处理数据接收
|
||||
- `net_process_messages()` 每轮先跑 3 个**非阻塞**检查再取队列:D10 重监听重试(断连后监听器恢复)→ L3 挂起 recv 完成检查(阻塞 recv 唤醒,E-02 修复)→ Trap 22 芯片/本层对账(200ms 周期,丢中断/静默回收自愈)
|
||||
- **监听 Socket 建立**:由就绪屏障后的服务任务(timeSyncTask / ftpTask / uiTask)经队列 API 建立(`net_socket` → `net_bind` → `net_listen`);断连后 net 层自动重监听(§3.6),服务层只需循环 `net_accept`,无需维护重监听逻辑
|
||||
|
||||
```c
|
||||
int main(void)
|
||||
@@ -377,13 +397,13 @@ void tcp_client(void)
|
||||
return;
|
||||
}
|
||||
|
||||
/* 4. 等待连接建立 */
|
||||
/* 4. 等待连接建立(跨任务只读快照:net_sock_state,
|
||||
描述符无效返回 -1) */
|
||||
while (1)
|
||||
{
|
||||
net_poll();
|
||||
|
||||
net_sock_t *sock_info = net_get_sock(sock);
|
||||
if (sock_info != NULL && sock_info->state == NET_SOCK_STATE_ESTABLISHED) {
|
||||
if (net_sock_state(sock) == NET_SOCK_STATE_ESTABLISHED) {
|
||||
printf("连接成功!\r\n");
|
||||
break;
|
||||
}
|
||||
@@ -849,6 +869,8 @@ int net_accept(int sockfd, struct net_sockaddr *addr, int *addrlen);
|
||||
| `addrlen` | 输入输出:地址长度 |
|
||||
| 返回值 | 新连接描述符(单连接模式下 == 监听 `sockfd`),无连接时返回 -1(可循环重试) |
|
||||
|
||||
> 单连接模式:监听 Socket 在 CONNECT 后**自身即数据通道**(§5.1)。`net_accept` 失败时若状态为 CLOSED 且 `auto_relisten` 开启,内部触发**非阻塞**重监听(失败交重试轮询,§3.6)。中断丢失也不会让监听死亡——对账在 200ms 内修正"本层 LISTENING 而芯片监听消失"等失同步。
|
||||
|
||||
#### net_connect - 连接服务器
|
||||
|
||||
```c
|
||||
@@ -885,10 +907,26 @@ int net_recv(int sockfd, void *buf, int len, int flags);
|
||||
| 参数 | 说明 |
|
||||
|------|------|
|
||||
| `sockfd` | socket 描述符 |
|
||||
| `buf` | 接收缓冲区 |
|
||||
| `buf` | 接收缓冲区(须保持有效至调用返回) |
|
||||
| `len` | 缓冲区大小 |
|
||||
| `flags` | 0=阻塞,`NET_MSG_DONTWAIT`=非阻塞 |
|
||||
| 返回值 | 实际接收字节数;`0`=对端已关闭(net 层随即自动重监听);`-1`=错误(非阻塞且无数据时返回 `NET_ERR_WOULDBLOCK`) |
|
||||
| 返回值 | 实际接收字节数 (>0);`0`=对端关闭(net 层随即自动重监听);`-1`=错误(非阻塞无数据=`NET_ERR_WOULDBLOCK`;阻塞预算内无数据=`NET_ERR_TIMEDOUT`,默认 30s `NET_RECV_TIMEOUT_MS`;close/网络重建/陈旧重同步取消=`NET_ERR_CONNRESET`) |
|
||||
|
||||
> **L3 pending 化(2026-09-22)**:无数据时阻塞接收**不占 netTask**——登记挂起请求(每 fd 至多一个,禁止覆盖:覆盖会丢失前调用方通知 = 永久挂死)后**立即返回**;完成按主循环节拍检查(数据/到点/断连,看门狗 1s)并经任务通知唤醒调用方。等待期间调用方在自己上下文睡通知,netTask 全程零占用(消除 E-02 头阻塞)。
|
||||
|
||||
#### net_recv_to - 接收数据(调用方指定超时,分片阻塞版)
|
||||
|
||||
```c
|
||||
int net_recv_to(int sockfd, void *buf, int len, uint32_t timeout_ms);
|
||||
```
|
||||
|
||||
| 参数 | 说明 |
|
||||
|------|------|
|
||||
| `sockfd` / `buf` / `len` | 同 `net_recv` |
|
||||
| `timeout_ms` | 本片等待预算(ms,**必须 >0**;0 会被解释为"默认 30s",恰是本接口要规避的长阻塞,直接拒 `NET_ERR_INVAL`) |
|
||||
| 返回值 | >0=字节数(允许部分读,TCP 分段);`0`=对端关闭(排空残留后);`-1`=真错误(`net_get_errno` 可查);**-2=本片预算内无数据**(**不是 errno**,不得依赖 errno 判超时——`s_net_errno` 是跨任务共享全局量) |
|
||||
|
||||
调用方累计自身总预算:见到 -2 发起下一片,直到收齐 / EOF / 总预算耗尽。生产用途:lftpd 数据行读、timeSync 帧读(E-02 头阻塞修复,完整例 §11.2)。
|
||||
|
||||
#### net_sendto - 发送 UDP 数据
|
||||
|
||||
@@ -910,6 +948,40 @@ int net_recvfrom(int sockfd, void *buf, int len, int flags,
|
||||
int net_close(int sockfd);
|
||||
```
|
||||
|
||||
#### 自愈与查询 API
|
||||
|
||||
**net_sock_state — 查询 Socket 状态(只读快照)**
|
||||
|
||||
```c
|
||||
int net_sock_state(int sockfd);
|
||||
```
|
||||
|
||||
跨任务只读快照(单字节读);描述符无效/未占用返回 -1;值可能滞后 netTask 最新状态。用于**状态轮询**(客户端等 ESTABLISHED,§5.2),不替代 accept/connect。
|
||||
|
||||
**net_resync_listen — 断连重同步(仅修 MCU 侧)**
|
||||
|
||||
```c
|
||||
int net_resync_listen(int sockfd);
|
||||
```
|
||||
|
||||
适用场景(D14):对端 FIN 后芯片**静默回收** Socket 回监听且**不发 DISCONNECT 中断** → 本层滞留 ESTABLISHED,新连接字节被旧会话吞掉。须**确认连接已死**(如控制通道超时中止后)再调;只把本层状态改回 LISTENING 并取消本 fd 挂起 recv,**不发任何芯片命令**(发 close/重监听会摧毁芯片侧健康监听器 → 连接永拒)。返回 0=已同步(或原本即一致),-1=失败。注:Trap 22 周期对账同样能自愈合此失同步(≤200ms),显式 API 用于"不愿等下一 tick"的场景。
|
||||
|
||||
**net_relisten — 无条件重新监听**
|
||||
|
||||
```c
|
||||
int net_relisten(int sockfd);
|
||||
```
|
||||
|
||||
TCP 监听型 Socket:芯片侧 open+listen(D10 非阻塞重试)。芯片侧已在监听时:首次 open 失败 → close 重建 → 监听短暂中断 ~1s;两种情形都收敛到"监听器确定在位"。用途:代次号变化后显式重建(见 `docs/网络配置说明.md` §4)、应用判定连接已死需确定性监听。正常流无需(auto_relisten + 对账已覆盖)。返回 0 成功 / -1 失败。
|
||||
|
||||
**net_getsockname — 获取 Socket 本地地址(IP+端口)**
|
||||
|
||||
```c
|
||||
int net_getsockname(int sockfd, struct net_sockaddr *addr, int *addrlen);
|
||||
```
|
||||
|
||||
Socket 已创建或已绑定时可用;port 为 0(动态分配)时返回已分配的动态端口(49152 起)。
|
||||
|
||||
### 10.2 消息队列 API
|
||||
|
||||
#### net_process_messages - 处理消息队列
|
||||
@@ -918,7 +990,13 @@ int net_close(int sockfd);
|
||||
void net_process_messages(void);
|
||||
```
|
||||
|
||||
处理其他任务通过 `net_send()` / `net_recv()` / `net_close()` 发送到消息队列的请求。必须在 `netTask` 主循环中调用。
|
||||
处理其他任务投递的请求(send/recv/connect/accept/close/**resync-listen / relisten**)。每次调用先跑 3 个**非阻塞** tick,再取队列消息(0 超时出队):
|
||||
|
||||
1. **D10 重监听重试**:断连后监听器恢复的到点重试(30ms 间隔 / 2s 时限,§3.6)
|
||||
2. **L3 挂起 recv 完成检查**:按 ①数据 ②到点 ③断连 检查每个挂起请求并通知调用方(看门狗 1s)
|
||||
3. **Trap 22 芯片/本层对账**:200ms 周期修正三类失同步(§3.6)
|
||||
|
||||
> `net_msg_t.timeout_ms`:L3 分片预算(`net_recv_to()`),0=默认 `NET_RECV_TIMEOUT_MS`(30s)。消息类型含 `NET_MSG_RESYNC_LISTEN`(仅 MCU 侧状态)与 `NET_MSG_RELISTEN`(芯片侧重监听)。
|
||||
|
||||
#### net_send_sock - 发送数据(内部版本)
|
||||
|
||||
@@ -1053,9 +1131,7 @@ void tcp_client_reconnect(void)
|
||||
/* 检查连接状态 */
|
||||
if (sock >= 0)
|
||||
{
|
||||
net_sock_t *sock_info = net_get_sock(sock);
|
||||
|
||||
if (sock_info != NULL && sock_info->state == NET_SOCK_STATE_ESTABLISHED)
|
||||
if (net_sock_state(sock) == NET_SOCK_STATE_ESTABLISHED)
|
||||
{
|
||||
/* 已连接,发送数据 */
|
||||
static uint32_t last_send = 0;
|
||||
@@ -1077,8 +1153,7 @@ void tcp_client_reconnect(void)
|
||||
sock = -1;
|
||||
}
|
||||
}
|
||||
else if (sock_info != NULL &&
|
||||
(sock_info->state == NET_SOCK_STATE_CLOSED))
|
||||
else if (net_sock_state(sock) == NET_SOCK_STATE_CLOSED)
|
||||
{
|
||||
/* 连接失败或断开 */
|
||||
printf("连接失败,准备重连...\r\n");
|
||||
@@ -1090,15 +1165,55 @@ void tcp_client_reconnect(void)
|
||||
}
|
||||
```
|
||||
|
||||
### 11.2 分片阻塞读(`net_recv_to`,timeSync 模式)
|
||||
|
||||
长等待 recv 曾独占 netTask(E-02 头阻塞)。L3 pending 化后:单次 `net_recv_to` 单次占用有界(≤ 本片预算)且等待期间 netTask 零占用(挂起模型);调用方累计总预算,收齐协议帧再返回:
|
||||
|
||||
```c
|
||||
#define FRAME_LEN 8 /* "TIME" + uint32 大端 本地时间 */
|
||||
#define TOTAL_BUDGET 30000U /* 总等待预算(ms) */
|
||||
|
||||
static int read_time_frame(int conn_sock, uint8_t buf[FRAME_LEN]) {
|
||||
int got = 0;
|
||||
TickType_t t0 = xTaskGetTickCount();
|
||||
|
||||
while (got < FRAME_LEN) {
|
||||
TickType_t elapsed = (TickType_t)(xTaskGetTickCount() - t0);
|
||||
uint32_t rem = (elapsed >= pdMS_TO_TICKS(TOTAL_BUDGET)) ? 0U
|
||||
: (uint32_t)(pdMS_TO_TICKS(TOTAL_BUDGET) - elapsed);
|
||||
if (rem == 0U) {
|
||||
return -1; /* 总预算耗尽 */
|
||||
}
|
||||
|
||||
int n = net_recv_to(conn_sock, buf + got, FRAME_LEN - got, rem);
|
||||
if (n > 0) {
|
||||
got += n; /* 允许部分读(TCP 分段) */
|
||||
} else if (n == 0) {
|
||||
return (got >= FRAME_LEN) ? 0 : -1; /* EOF:收齐成功,否则失败 */
|
||||
} else if (n == -2) {
|
||||
continue; /* 本片超时:累计预算,下片继续 */
|
||||
} else {
|
||||
return -1; /* 真错误(net_get_errno 可查) */
|
||||
}
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
```
|
||||
|
||||
约束:`buf` 须在**整个读过程**有效(挂起持指针,不拷数据);同一 fd 同一时刻至多一个挂起 recv(覆盖会挂死前调用方);close/网络重建/陈旧重同步会通知(`NET_ERR_CONNRESET` 等)→ 上述 `n<0` 分支统一当失败处理。
|
||||
|
||||
---
|
||||
|
||||
## 12. 常见问题
|
||||
|
||||
### 12.1 net_poll() 必须调用吗?
|
||||
|
||||
**是的**。`net_poll()` 负责轮询 CH395F 的中断状态并更新所有 Socket 的状态。如果不调用,连接状态不会更新,数据也无法接收。
|
||||
**是的**,且 `net_poll()` 与 `net_process_messages()` 缺一不可:
|
||||
|
||||
建议至少每 10ms 调用一次。
|
||||
- `net_poll()`:GPIO 轮询 INT#,**每轮批处理一批**中断,更新各 Socket 状态、触发事件回调(RECV 为电平触发,每轮只处理一批,避免饿死主循环)。
|
||||
- `net_process_messages()`:除串行处理队列请求外,每轮先跑 3 个**非阻塞**检查——D10 重监听重试(断连后监听器恢复)→ L3 挂起 recv 完成检查(阻塞 recv 的唤醒)→ Trap 22 芯片/本层对账(200ms 周期,丢中断/静默回收自愈)。
|
||||
|
||||
建议主循环周期 ≤ 10ms(本项目 1ms)。若 netTask 主循环停摆(断点、任务被挂起),监听器不会恢复、阻塞 recv 不会完成——这是软件侧"监听死机"的最常见原因。
|
||||
|
||||
### 12.2 Socket 数量有限制吗?
|
||||
|
||||
@@ -1112,6 +1227,7 @@ void tcp_client_reconnect(void)
|
||||
- 每次发送的数据长度不能超过 `NET_SEND_BUF_SIZE`(默认 4096 字节)
|
||||
- TCP 需要等待发送缓冲区空闲(`send_ready` 标志)
|
||||
- UDP 单包载荷最大 1472 字节(写入超过此值,CH395F 会自动分片为多个 UDP 包发送)
|
||||
- 接收侧:每 Socket RX 缓冲 2KB(4×512B,即芯片接收窗口)、TX FIFO 1KB(2×512B)、MSS=1024。对端常态把 2KB 窗口填满是**正常**现象,不等于"冻结"(Trap 21 结案,见 §12.10)
|
||||
|
||||
### 12.4 如何处理连接断开?
|
||||
|
||||
@@ -1151,6 +1267,13 @@ if (len < 0 && net_get_errno() == NET_ERR_WOULDBLOCK) {
|
||||
(2026-09-21 台架实证:`ui_net_sink` 曾在 EOF 时无条件 `net_close`,表现为 7003 答完一帧即断连、
|
||||
端口随后"连接被拒"。)
|
||||
|
||||
**芯片静默回收(D14,2026-09-22 台架实证)**:对端发 FIN 后,芯片可能**自行把 Socket 回收回 LISTEN 且不发 DISCONNECT 中断** → 本层滞留 ESTABLISHED,应用继续往死连接上读写,新连接的字节被旧会话吞掉。两种处理:
|
||||
|
||||
1. **等对账自愈(默认,推荐)**:§3.6 对账 ≤200ms 发现"本层 ESTABLISHED 而芯片已回收" → 按断开处理 → 自动重监听;等待期间该端口实际可接新连接(芯片侧已 LISTEN),新连接经对账补 accept 被本层感知。
|
||||
2. **显式重同步(需立即对齐时)**:确认会话已死(如控制通道超时中止)后调 `net_resync_listen(fd)`——只修 MCU 侧状态 + 取消挂起 recv,**不发芯片命令**(发 close/重监听会摧毁芯片侧健康监听器 → 连接永拒)。
|
||||
|
||||
⚠ 切勿用**单次**芯片状态读决定重建:单次读可能落在"刚 open 未生效"瞬态,曾致监听反复重建直至 `relisten give up`(2026-09-23 实测风暴,三服务口全死)——任何重建动作必须去抖(连续确认)+ 冷却(§3.6)。
|
||||
|
||||
**方式三:事件回调**
|
||||
```c
|
||||
void callback(int sockfd, net_event_t event, void *arg)
|
||||
@@ -1225,17 +1348,17 @@ CH395F 底层驱动返回的状态码(`ch395f_get_cmd_status()` 或 `ch395f_po
|
||||
|
||||
### 12.9 线程安全问题
|
||||
|
||||
**问题:`net_send()` / `net_recv()` / `net_close()` 可以在任意任务中调用吗?**
|
||||
**问题:`net_send()` / `net_recv()` / `net_close()`(含 `net_recv_to()`)可以在任意任务中调用吗?**
|
||||
|
||||
可以。这些公共 API 通过消息队列将请求委托给 `netTask` 串行执行,自身阻塞等待结果,是线程安全的。
|
||||
可以。这些公共 API 通过消息队列将请求委托给 `netTask` 串行执行;**注意阻塞 recv 的"阻塞"发生在调用方自己的上下文**(挂起模型:登记请求后调用方睡任务通知,netTask 不等待,§3.6)——因此多个任务可同时持有 recv,netTask 只被数据路径短暂占用。
|
||||
|
||||
**问题:`netTask` 内可以直接调用 `net_send()` / `net_recv()` 吗?**
|
||||
|
||||
不可以。`netTask` 中必须使用 `net_send_sock()` / `net_recv_sock()` 等内部版本,否则会死锁(任务向自身发送消息,永远无人处理)。内部版本直接操作 Socket 控制块,不经过消息队列。
|
||||
不可以。`netTask` 中必须使用 `net_send_sock()` / `net_recv_sock()` 等内部版本,否则会死锁(任务向自身发送消息,永远无人处理)。netTask 上下文的"立即看有无数据"需求用 `NET_MSG_DONTWAIT`(L3:挂起请求对执行任务自身发起时直接拒绝,防自等自通知死锁)。
|
||||
|
||||
**问题:`net_socket()` / `net_bind()` / `net_listen()` 线程安全吗?**
|
||||
**问题:哪些操作要求 netTask 已运行?**
|
||||
|
||||
这些函数仅在系统初始化阶段(`osKernelStart()` 之前)调用,不存在并发访问。消息队列仅保护数据收发和连接关闭操作。
|
||||
`net_listen()` / `net_accept()` / `net_connect()` / `net_send()` / `net_recv()` / `net_close()` / `net_relisten()` / `net_resync_listen()` 全部经消息队列 → 只能在**netTask 已运行的上下文**(就绪屏障后的服务任务)调用;内核启动前的代码禁调任何 FreeRTOS/CMSIS-RTOS API,网络监听建立天然在内核启动后。`net_socket()` / `net_bind()` 为直接调用,但同样建议在屏障后。
|
||||
|
||||
**问题:消息队列由谁创建?在哪里创建?**
|
||||
|
||||
@@ -1252,12 +1375,25 @@ CH395F 底层驱动返回的状态码(`ch395f_get_cmd_status()` 或 `ch395f_po
|
||||
需要回传请用调用方提供的指针,或复用通知值。(2026-09-21 实证:曾按"加 `msg.err` 字段"方式修错误码,
|
||||
结果 `ui_net_sink` 每次空轮询都拿到栈垃圾 → 误判为"真错误" → 连上即 `net_close`。)
|
||||
|
||||
### 12.10 监听器自愈行为与排渣(D10 / Trap 09、Trap 22、Trap 23)
|
||||
|
||||
**默认行为**(无需应用代码):断连 → `auto_relisten` 重开监听;重开失败 → 30ms 节奏重试(2s 时限);芯片丢中断/静默回收 → 200ms 对账自愈;中止复连首上传 → 排渣隔离。全在 netTask 内,应用只需保证 `net_poll()` + `net_process_messages()` 主循环在跑。
|
||||
|
||||
| 场景 | 修复前症状 | 机制 | 生产验证(2026-09-23) |
|
||||
|---|---|---|---|
|
||||
| **Trap 22** 快速复连丢中断 → 监听静默死亡 | 待机后 UI `:7003` 快速复连 2/10 且**永久**拒连;FTP `:21` 100ms 间隔自第 29 轮死亡;设备日志**无报错**(状态静默滞留) | 周期对账 `net_reconcile_tick()`(200ms,三类失同步)+ 去抖(连续 2 次)+ 冷却(1s);`handle_disconnect_event` LISTENING 分支改为"已不可监听即重建" | `:7003` 10/10;`:21` 100ms/0ms/600ms 各 50/50;UI 20/20;FTP 正常 10/10(SHA-256);中止 RST/FIN 5/5 + 5/5;零重建风暴 |
|
||||
| **Trap 23** 中止后首笔复连上传被静默截断 ≤2048B 并误报 226 成功 | RST/FIN 中止上传 → 紧接着的首笔上传"收"到 ~2KB 残留后判结束回 226 → 客户端以为成功 = **静默数据损坏**(第二笔起正常) | 异常释放(TIMEOUT)预重监听隔离:`net_drain_chip_rx()` 分块排空 RX 残渣 + 主动 close + 排空中断 + 等 CLOSED/TCP_CLOSED(400ms 起 / 800ms 上限 < 2s 时限) | RST 10/10、FIN 5/5 全通过;正常路径 30/30 + 20/20,SHA-256 全匹配 |
|
||||
| **Trap 21**(TC-NET-604)"接收冻结"虚警 | 旧判读脚本把"对端填满 2KB RX 窗口"误判为 STALL/WEDGE | **结论:非缺陷**(芯片 RX 缓冲即窗口,对端填满是常态);判据改为脚本层 STEP + 客户端内容校验(RET SHA-256),虚警工具归档(`MDK-ARM/retracted_bench_20260922/`,README 禁重跑) | 真实 FTP 路径 227 轮全通过(8KB×10 … 1MB×2);并发 UI 流 600/600 CRC;设备零 FAIL/ERROR/WARN |
|
||||
|
||||
**排障判据**(无 Wireshark 环境,纯生产路径):`MDK-ARM/ftp_freeze_probe.py`(真实 STOR + RETR SHA-256)、`MDK-ARM/ftp_abort_probe.py`(RST/FIN 中止复连)、`MDK-ARM/tcp_reconnect_probe.py`(快速复连存活甄别)、`MDK-ARM/bench_verdict.py`(脚本层 STEP 为真值;**`probe_classify.py` A/C 判读不可信**,见 retracted 归档 README)。详见 `docs/问题记录/CH395F_Trap_Records.md`。
|
||||
|
||||
---
|
||||
|
||||
## 变更记录
|
||||
|
||||
| 日期 | 变更 |
|
||||
|------|------|
|
||||
| 2026-09-23 | 同步 L3 pending 化、D10 非阻塞重监听、Trap 22 对账、Trap 23 排渣隔离后的现状:新增 `net_recv_to` / `net_sock_state` / `net_resync_listen` / `net_relisten`(§10.1)及 `net_getsockname`;§3 新增 3.6(对账与自愈四机制)、3.3/3.5 改写(recv 挂起模型、队列语义、netTask 运行要求);§4/§5 示例更新(监听由就绪屏障后服务任务建立、`net_sock_state` 轮询);§10.2 改述 `net_process_messages()` 每轮节奏(重监听重试 → 挂起 recv 完成 → 200ms 对账 → 队列),补 `timeout_ms`/新消息类型说明;新增 §11.2 分片阻塞读(`net_recv_to`);§12.1/12.3/12.4/12.9 更新,新增 §12.10(自愈行为数据与排障判据,含 Trap 21 结案) |
|
||||
| 2026-09-21 | **修正公开 API 的错误码回传**:`net_msg_t` 按值拷入队列 → netTask 对消息的写入调用方不可见,改用 `xTaskNotify()` 通知值回传具体错误码(负值),公开 API 恢复 errno 后统一返回 -1(此前被覆盖成 `NET_ERR`,使 `net_get_errno() != NET_ERR_WOULDBLOCK` 的"空轮询"判断恒成立 → `ui_net_sink` 连上即 `net_close`);§12.4 补单连接模型断开处理(EOF 时不得 `net_close` 监听 socket);§12.9 补错误码回传约束 |
|
||||
| 2026-09-15 | 同步配置引擎属主变更(P1-3):`net_init` 调用方说明改为"netTask 只读 RAM 副本"(网口配置由 defaultTask 在就绪屏障前加载);模拟器端启动序列补 `net_cfg_save_run()`(主循环 = 模拟器侧配置引擎属主,执行 UI 保存请求) |
|
||||
| 2026-09-13 | 从 git 历史恢复(`4e9ac0d` 2026-08-28 误删);同步当前代码:`net_init` 4 参数(新增 `mac`)、`net_socket` 2 参数(移除 `protocol`)、新增 `net_get_generation` / `net_generation_inc` / `net_listen_locked`、`NET_NO_OS` 双端构建、netTask 周期 10ms → 1ms |
|
||||
|
||||
Reference in New Issue
Block a user