Files
STM32F4-Base/docs/BSD_Socket_API_使用指南.md

1213 lines
36 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# BSD Socket API 使用指南
## 目录
1. [概述](#1-概述)
2. [架构说明](#2-架构说明)
3. [线程安全设计](#3-线程安全设计)
4. [快速开始](#4-快速开始)
5. [TCP 编程](#5-tcp-编程)
6. [UDP 编程](#6-udp-编程)
7. [I/O 多路复用](#7-io-多路复用)
8. [事件回调机制](#8-事件回调机制)
9. [字节序处理](#9-字节序处理)
10. [API 参考](#10-api-参考)
11. [完整示例](#11-完整示例)
12. [常见问题](#12-常见问题)
---
## 1. 概述
本项目基于 CH395F 以太网协议栈芯片,提供了一套兼容 Linux BSD Socket API 的网络编程接口。这套接口屏蔽了底层硬件细节,让嵌入式网络编程与标准 Linux 网络编程保持一致。
### 1.1 支持的功能
| 功能 | 说明 |
|------|------|
| TCP Client | 主动连接远程服务器 |
| TCP Server单连接 | 支持 1 个客户端连接 |
| UDP Client | 发送 UDP 数据报 |
| UDP Server | 接收 UDP 数据报 |
| select/poll | I/O 多路复用 |
| 事件回调 | 异步事件通知 |
### 1.2 与 Linux Socket 的差异
| 特性 | Linux Socket | 本实现 |
|------|--------------|--------|
| Socket 数量 | 无限制 | 最多 8 个 |
| 非阻塞模式 | 支持 | 通过 `net_poll()` 实现 |
| 错误码 | errno | `net_get_errno()` |
| 头文件 | `<sys/socket.h>` | `"net_socket.h"` |
---
## 2. 架构说明
### 2.1 分层架构
```
┌──────────────────────────────────────────────────────────────┐
│ 应用层任务 (多个 FreeRTOS Task) │
│ net_send() / net_recv() / net_close() (线程安全封装) │
├──────────────────────────────────────────────────────────────┤
│ 消息队列 (osMessageQueue) │
│ 由 CubeMX 生成,在 MX_FREERTOS_Init() 中创建 │
├──────────────────────────────────────────────────────────────┤
│ netTask (网络任务线程) │
│ net_process_messages() + net_poll() — 串行化执行 │
│ net_send_sock() / net_recv_sock() / net_close_sock() │
├──────────────────────────────────────────────────────────────┤
│ net_socket.c (BSD Socket API 核心) │
│ Socket 状态机 + CH395F 命令封装 │
├──────────────────────────────────────────────────────────────┤
│ ch395f.c/h (底层 SPI 驱动) │
│ 所有 SPI 事务必须由 netTask 发起 │
├──────────────────────────────────────────────────────────────┤
│ CH395F 硬件 (SPI2) │
└──────────────────────────────────────────────────────────────┘
```
### 2.2 文件说明
| 文件 | 说明 |
|------|------|
| `net_config.h` | 配置宏定义(最大 Socket 数、超时时间等) |
| `net_types.h` | 类型定义地址结构、Socket 控制块等) |
| `net_socket.h` | BSD Socket API 头文件,含 `net_msg_t` 消息类型定义 |
| `net_socket.c` | API 核心实现,含消息队列处理、线程安全封装、内部 `_sock()` 版本 |
| `net_select.h` | select/poll API 头文件 |
| `net_select.c` | select/poll 实现 |
### 2.4 Socket 状态机
```
net_socket()
┌──────────── CREATED ────────────┐
│ │ │
│ net_bind() │
│ │ │
│ ▼ │
│ BOUND ─────────────────┤
│ │ │ │
│ net_listen() net_connect() │
│ │ │ │
│ ▼ ▼ │
│ LISTENING CONNECTING │
│ │ │ │
│ net_accept() │ (连接成功) │
│ │ │ │
│ ▼ ▼ │
│ ESTABLISHED ◄──────────────┘
│ │ │
│ net_accept() 返回监听 Socket 自身 │
│ │ │
│ net_send/recv() │
│ │ │
│ net_close() │
│ │ │
│ ▼ │
└────── CLOSED ──────────────────┘
```
> **单连接 Server 自动重监听(`auto_relisten`**`net_listen()` 会为 TCP 监听 Socket 置 `auto_relisten = 1`。连接断开(`net_recv` 收到 EOF / 空闲 `net_accept`net 层**自动**重新 `OPEN + TCP_LISTEN`,状态由 `ESTABLISHED`/`CLOSED` 自动回到 `LISTENING`**应用层无需调用 `net_close()` + `net_listen()`**。这与 Linux BSD Socket“监听 Socket 常驻”语义一致——`net_accept()` 在每条新连接到达时返回,服务端循环不必维护重监听逻辑。
---
## 3. 线程安全设计
### 3.1 设计背景
NET 层(`net_socket.c` + `ch395f.c`)不是线程安全的:
- Socket 控制块(`s_net_socks[]`)无互斥保护
- CH395F SPI 事务不重入STM32 HAL SPI 无内部锁)
- 状态机在多个任务中并发访问会崩溃
引入消息队列将所有的 CH395F 操作串行化到 `netTask` 中执行。
### 3.2 消息队列模式
```
App Task A App Task B netTask
| | |
net_send() net_recv() net_process_messages()
| | + net_poll()
▼ ▼ │
┌─────────┐ ┌─────────┐ │
│send msg │ │recv msg │ │
└────┬────┘ └────┬────┘ │
│ │ │
└─────────┬─────────┘ │
▼ ▼
┌──────────────────┐ ┌──────────────┐
│ 消息队列 │──────────│ 取出消息 │
│ osMessageQueue │ dequeue │ 执行操作 │
└──────────────────┘ │ 写结果 │
▲ │ 通知调用方 │
│ └──────┬───────┘
│ ulTaskNotifyTake() │
│ (阻塞等待结果) │ xTaskNotifyGive()
│ │
┌─────┴──────┐ │
│ 返回结果给 │◄──────────────────────┘
│ 调用方 │
└────────────┘
```
### 3.3 关键规则
| 规则 | 说明 |
|------|------|
| **`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` 主循环中调用 |
### 3.4 内部 API 说明
内部 API`_sock` 后缀)直接操作 Socket 控制块和 CH395F不经过消息队列。用于 `netTask` 中的网络业务逻辑,避免向自身发送消息造成死锁:
```c
/* 在 netTask 中直接操作 Socket绕过消息队列 */
int net_send_sock(net_sock_t *p_sock, const void *buf, int len);
int net_recv_sock(net_sock_t *p_sock, void *buf, int len, int flags);
int net_close_sock(net_sock_t *p_sock);
```
### 3.5 注意事项
- **消息队列由 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()` 无需线程安全**:这些仅在初始化阶段调用,消息队列仅保护数据收发和连接关闭
---
## 4. 快速开始
### 4.1 包含头文件
```c
#include "net_socket.h"
#include "net_select.h"
```
### 4.2 初始化网络
```c
int main(void)
{
/* 初始化网络子系统
* 参数IP地址, 子网掩码, 网关
* 传入 NULL 使用 DHCP 自动获取
*/
int ret = net_init("192.168.1.100", "255.255.255.0", "192.168.1.1");
if (ret != 0) {
printf("网络初始化失败!\r\n");
return -1;
}
/* 其他初始化... */
while (1)
{
net_poll(); /* 必须在主循环中调用 */
/* 其他任务... */
}
}
```
### 4.3 主循环要求
**重要**:本项目使用 FreeRTOS 多任务架构,`net_poll()``net_process_messages()` 均在 `netTask`FreeRTOS 线程)中定期调用,而非主循环。
- **主循环**:系统初始化完成后进入空循环,等待 `osKernelStart()` 启动调度器
- **netTask**:在任务函数中持续运行,每 10ms 调用一次 `net_poll()` + `net_process_messages()`,负责:
- 轮询 CH395F INT# 引脚电平GPIO 轮询,无需 EXTI 中断)
- 通过 `GET_GLOB_INT_STATUS_ALL`2字节版读取所有 8 个 Socket 的中断状态
- 更新所有 Socket 的连接状态CONNECT / DISCONNECT / RECV / TIMEOUT
- 触发事件回调
- 处理数据接收
```c
int main(void)
{
/* 外设初始化... */
/* BSD Socket API 初始化 */
net_init("192.168.1.100", "255.255.255.0", "192.168.1.1");
/* 创建 Socket、绑定、监听... */
osKernelInitialize();
MX_FREERTOS_Init(); /* 创建 netTask 等 FreeRTOS 对象 */
osKernelStart(); /* 启动调度器 */
while (1) { } /* 空循环,控制流已交给调度器 */
}
/* netTask 中freertos.c */
void StartNetTask(void *argument)
{
for (;;)
{
net_poll(); /* CH395F 中断轮询 + Socket 状态更新 */
net_process_messages(); /* 处理其他任务发送的网络操作请求 */
/* 应用层网络业务逻辑... */
osDelay(10); /* 10ms 周期 */
}
}
```
---
## 5. TCP 编程
### 5.1 TCP Server单连接
单连接模式下1 个 Socket 既做监听又做数据通信:
```c
#include "net_socket.h"
void tcp_server_single(void)
{
int listen_sock;
struct net_sockaddr_in addr;
struct net_sockaddr_in client_addr;
int client_addr_len;
char buf[256];
int len;
/* 1. 创建 Socket */
listen_sock = net_socket(AF_INET, SOCK_STREAM, 0);
if (listen_sock < 0) {
printf("创建 Socket 失败\r\n");
return;
}
/* 2. 绑定地址和端口 */
addr.sin_family = AF_INET;
addr.sin_port = net_htons(8080); /* 端口号 8080 */
addr.sin_addr.s_addr = net_inet_addr("0.0.0.0"); /* 监听所有网卡 */
if (net_bind(listen_sock, (struct net_sockaddr *)&addr, sizeof(addr)) != 0) {
printf("绑定失败\r\n");
net_close(listen_sock);
return;
}
/* 3. 开始监听 */
if (net_listen(listen_sock) != 0) {
printf("监听失败\r\n");
net_close(listen_sock);
return;
}
printf("TCP Server 启动,监听端口 8080\r\n");
/* 4. 主循环 */
while (1)
{
net_poll();
/* 尝试接受新连接 */
client_addr_len = sizeof(client_addr);
int client_sock = net_accept(listen_sock,
(struct net_sockaddr *)&client_addr, &client_addr_len);
if (client_sock >= 0)
{
char ip_str[16];
printf("新连接: %s:%d\r\n",
net_inet_ntoa(client_addr.sin_addr.s_addr, ip_str),
net_ntohs(client_addr.sin_port));
/* 接收和发送数据单连接模式client_sock 即 listen_sock 自身) */
while (1)
{
len = net_recv(client_sock, buf, sizeof(buf) - 1, 0);
if (len <= 0) {
/* 对端关闭net 层已在 EOF 时自动重监听,
直接 break外层 net_accept 会接下一个连接 */
printf("连接断开net 层自动重监听\r\n");
break;
}
buf[len] = '\0';
printf("收到: %s\r\n", buf);
/* 回显数据 */
net_send(client_sock, buf, len, 0);
}
/* 单连接模式不要在此 net_close(client_sock):它即监听 Socket
一旦关闭需重新 net_socket+net_bind+net_listen重监听已由 net 层自动完成 */
}
}
}
```
### 5.2 TCP Client
```c
#include "net_socket.h"
void tcp_client(void)
{
int sock;
struct net_sockaddr_in addr;
char buf[256];
int len;
/* 1. 创建 Socket */
sock = net_socket(AF_INET, SOCK_STREAM, 0);
if (sock < 0) {
printf("创建 Socket 失败\r\n");
return;
}
/* 2. 设置服务器地址 */
addr.sin_family = AF_INET;
addr.sin_port = net_htons(8080);
addr.sin_addr.s_addr = net_inet_addr("192.168.1.200");
/* 3. 连接服务器 */
printf("正在连接服务器...\r\n");
if (net_connect(sock, (struct net_sockaddr *)&addr, sizeof(addr)) != 0) {
printf("连接失败\r\n");
net_close(sock);
return;
}
/* 4. 等待连接建立 */
while (1)
{
net_poll();
net_sock_t *sock_info = net_get_sock(sock);
if (sock_info != NULL && sock_info->state == NET_SOCK_STATE_ESTABLISHED) {
printf("连接成功!\r\n");
break;
}
HAL_Delay(10);
}
/* 5. 发送数据 */
const char *msg = "Hello, CH395F!";
net_send(sock, msg, strlen(msg), 0);
printf("发送: %s\r\n", msg);
/* 6. 接收数据 */
len = net_recv(sock, buf, sizeof(buf) - 1, 0);
if (len > 0) {
buf[len] = '\0';
printf("收到: %s\r\n", buf);
}
/* 7. 关闭连接 */
net_close(sock);
}
```
---
## 6. UDP 编程
### 6.1 UDP Client
```c
#include "net_socket.h"
void udp_client(void)
{
int sock;
struct net_sockaddr_in local_addr;
struct net_sockaddr_in dest_addr;
char buf[256];
int len;
/* 1. 创建 UDP Socket */
sock = net_socket(AF_INET, SOCK_DGRAM, 0);
if (sock < 0) {
printf("创建 Socket 失败\r\n");
return;
}
/* 2. 绑定本地端口(可选) */
local_addr.sin_family = AF_INET;
local_addr.sin_port = net_htons(12345);
local_addr.sin_addr.s_addr = net_inet_addr("0.0.0.0");
net_bind(sock, (struct net_sockaddr *)&local_addr, sizeof(local_addr));
/* 3. 设置目标地址 */
dest_addr.sin_family = AF_INET;
dest_addr.sin_port = net_htons(8888);
dest_addr.sin_addr.s_addr = net_inet_addr("192.168.1.200");
/* 4. 发送数据 */
const char *msg = "UDP Hello!";
net_sendto(sock, msg, strlen(msg), 0,
(struct net_sockaddr *)&dest_addr, sizeof(dest_addr));
printf("发送: %s\r\n", msg);
/* 5. 接收数据 */
struct net_sockaddr_in src_addr;
int src_addr_len = sizeof(src_addr);
len = net_recvfrom(sock, buf, sizeof(buf) - 1, 0,
(struct net_sockaddr *)&src_addr, &src_addr_len);
if (len > 0) {
buf[len] = '\0';
char ip_str[16];
printf("收到: %s (from %s:%d)\r\n", buf,
net_inet_ntoa(src_addr.sin_addr.s_addr, ip_str),
net_ntohs(src_addr.sin_port));
}
net_close(sock);
}
```
### 6.2 UDP Server
```c
#include "net_socket.h"
void udp_server(void)
{
int sock;
struct net_sockaddr_in addr;
char buf[256];
/* 1. 创建 UDP Socket */
sock = net_socket(AF_INET, SOCK_DGRAM, 0);
if (sock < 0) {
printf("创建 Socket 失败\r\n");
return;
}
/* 2. 绑定本地端口 */
addr.sin_family = AF_INET;
addr.sin_port = net_htons(8888);
addr.sin_addr.s_addr = net_inet_addr("0.0.0.0");
if (net_bind(sock, (struct net_sockaddr *)&addr, sizeof(addr)) != 0) {
printf("绑定失败\r\n");
net_close(sock);
return;
}
printf("UDP Server 启动,监听端口 8888\r\n");
/* 3. 循环接收数据 */
while (1)
{
net_poll();
struct net_sockaddr_in src_addr;
int src_addr_len = sizeof(src_addr);
int len = net_recvfrom(sock, buf, sizeof(buf) - 1, 0,
(struct net_sockaddr *)&src_addr, &src_addr_len);
if (len > 0) {
buf[len] = '\0';
char ip_str[16];
printf("收到: %s (from %s:%d)\r\n", buf,
net_inet_ntoa(src_addr.sin_addr.s_addr, ip_str),
net_ntohs(src_addr.sin_port));
/* 回显数据 */
net_sendto(sock, buf, len, 0,
(struct net_sockaddr *)&src_addr, src_addr_len);
}
}
}
```
---
## 7. I/O 多路复用
### 7.1 select 使用
`net_select()` 可以同时监控多个 Socket 的读写事件:
```c
#include "net_select.h"
void select_example(void)
{
int sock1, sock2;
net_fd_set readfds;
struct net_timeval timeout;
/* 创建并连接两个 Socket ... */
while (1)
{
NET_FD_ZERO(&readfds);
NET_FD_SET(sock1, &readfds);
NET_FD_SET(sock2, &readfds);
timeout.tv_sec = 0;
timeout.tv_usec = 500000; /* 500ms 超时 */
int ret = net_select(sock2 + 1, &readfds, NULL, NULL, &timeout);
if (ret > 0) {
if (NET_FD_ISSET(sock1, &readfds)) {
char buf[128];
int len = net_recv(sock1, buf, sizeof(buf), 0);
/* 处理数据... */
}
if (NET_FD_ISSET(sock2, &readfds)) {
char buf[128];
int len = net_recv(sock2, buf, sizeof(buf), 0);
/* 处理数据... */
}
} else if (ret == 0) {
/* 超时 */
}
}
}
```
### 7.2 poll 使用
`net_poll_events()` 使用 `pollfd` 结构体数组:
```c
#include "net_select.h"
void poll_example(void)
{
net_pollfd fds[2];
fds[0].fd = sock1;
fds[0].events = NET_POLLIN;
fds[1].fd = sock2;
fds[1].events = NET_POLLIN;
while (1)
{
int ret = net_poll_events(fds, 2, 500); /* 500ms 超时 */
if (ret > 0) {
if (fds[0].revents & NET_POLLIN) {
char buf[128];
int len = net_recv(fds[0].fd, buf, sizeof(buf), 0);
/* 处理数据... */
}
if (fds[1].revents & NET_POLLIN) {
char buf[128];
int len = net_recv(fds[1].fd, buf, sizeof(buf), 0);
/* 处理数据... */
}
}
}
}
```
### 7.3 事件掩码说明
| 事件 | 说明 |
|------|------|
| `NET_POLLIN` | 有数据可读,或有新连接到达(监听 Socket |
| `NET_POLLOUT` | 发送缓冲区有空间,可以写入数据 |
| `NET_POLLERR` | 发生错误 |
| `NET_POLLHUP` | 连接挂起(对端关闭) |
| `NET_POLLNVAL` | 无效的文件描述符 |
---
## 8. 事件回调机制
可以通过注册回调函数,异步获取 Socket 事件通知:
### 8.1 定义回调函数
```c
#include "net_socket.h"
void my_event_callback(int sockfd, net_event_t event, void *arg)
{
switch (event)
{
case NET_EVENT_CONNECTED:
printf("Socket %d: 连接建立\r\n", sockfd);
break;
case NET_EVENT_DISCONNECTED:
printf("Socket %d: 连接断开\r\n", sockfd);
break;
case NET_EVENT_DATA_RECEIVED:
printf("Socket %d: 收到数据\r\n", sockfd);
/* 注意:这里只是通知,需要调用 net_recv() 读取 */
break;
case NET_EVENT_SEND_COMPLETE:
printf("Socket %d: 发送完成\r\n", sockfd);
break;
case NET_EVENT_TIMEOUT:
printf("Socket %d: 操作超时\r\n", sockfd);
break;
case NET_EVENT_ERROR:
printf("Socket %d: 发生错误\r\n", sockfd);
break;
}
}
```
### 8.2 注册回调
```c
void event_callback_example(void)
{
int sock;
/* 创建 Socket ... */
/* 注册事件回调 */
net_set_event_cb(sock, my_event_callback, NULL);
/* 主循环 */
while (1)
{
net_poll();
}
}
```
---
## 9. 字节序处理
网络协议使用**大端序**(网络字节序),而 STM32 使用**小端序**(主机字节序)。发送和接收数据时需要进行字节序转换。
### 9.1 端口号转换
```c
uint16_t port = 8080;
/* 主机序 -> 网络序(发送前转换) */
uint16_t net_port = net_htons(port);
/* 网络序 -> 主机序(接收后转换) */
uint16_t host_port = net_ntohs(net_port);
```
### 9.2 IP 地址转换
```c
/* 字符串转网络序 IP */
uint32_t ip = net_inet_addr("192.168.1.100");
/* 网络序 IP 转字符串 */
char ip_str[16];
net_inet_ntoa(ip, ip_str); /* ip_str = "192.168.1.100" */
```
### 9.3 填充地址结构体
```c
struct net_sockaddr_in addr;
addr.sin_family = AF_INET;
addr.sin_port = net_htons(8080); /* 端口转换 */
addr.sin_addr.s_addr = net_inet_addr("192.168.1.100"); /* IP 转换 */
/* 如果要绑定任意地址 */
addr.sin_addr.s_addr = net_inet_addr("0.0.0.0");
/* 或者直接 */
addr.sin_addr.s_addr = INADDR_ANY;
```
### 9.4 读取地址信息
```c
struct net_sockaddr_in client_addr;
int addr_len = sizeof(client_addr);
int client_sock = net_accept(listen_sock,
(struct net_sockaddr *)&client_addr, &addr_len);
if (client_sock >= 0)
{
/* 转换端口到主机序 */
uint16_t port = net_ntohs(client_addr.sin_port);
/* 转换 IP 到字符串 */
char ip_str[16];
net_inet_ntoa(client_addr.sin_addr.s_addr, ip_str);
printf("客户端: %s:%d\r\n", ip_str, port);
}
```
---
## 10. API 参考
### 10.1 核心 API
#### net_init - 网络初始化
```c
int net_init(const char *ip, const char *mask, const char *gateway);
```
| 参数 | 说明 |
|------|------|
| `ip` | IP 地址字符串NULL 使用 DHCP |
| `mask` | 子网掩码字符串NULL 使用默认 |
| `gateway` | 网关地址字符串NULL 使用默认 |
| 返回值 | 0 成功,-1 失败 |
#### net_poll - 状态轮询
```c
int net_poll(void);
```
| 参数 | 说明 |
|------|------|
| 返回值 | 有事件发生的 socket 数量 |
**注意**:必须在主循环中定期调用。
#### net_socket - 创建 Socket
```c
int net_socket(int domain, int type, int protocol);
```
| 参数 | 说明 |
|------|------|
| `domain` | 地址族,仅支持 `AF_INET` |
| `type` | `SOCK_STREAM`(TCP) 或 `SOCK_DGRAM`(UDP) |
| `protocol` | 通常为 0 |
| 返回值 | socket 描述符 (0~7),失败返回 -1 |
#### net_bind - 绑定地址
```c
int net_bind(int sockfd, const struct net_sockaddr *addr, int addrlen);
```
| 参数 | 说明 |
|------|------|
| `sockfd` | socket 描述符 |
| `addr` | 地址结构体指针 |
| `addrlen` | 地址结构体长度 |
| 返回值 | 0 成功,-1 失败 |
#### net_listen - 开始监听
```c
int net_listen(int sockfd);
```
| 参数 | 说明 |
|------|------|
| `sockfd` | socket 描述符 |
| 返回值 | 0 成功,-1 失败 |
> **自动重监听**`net_listen()` 对 TCP 监听 Socket 置 `auto_relisten = 1`。连接结束后 net 层自动回到 `LISTENING`,应用层无需手动 `net_close()` + `net_listen()`。
#### net_accept - 接受连接
```c
int net_accept(int sockfd, struct net_sockaddr *addr, int *addrlen);
```
| 参数 | 说明 |
|------|------|
| `sockfd` | 监听 socket 描述符 |
| `addr` | 输出:客户端地址 |
| `addrlen` | 输入输出:地址长度 |
| 返回值 | 新连接描述符(单连接模式下 == 监听 `sockfd`),无连接时返回 -1可循环重试 |
#### net_connect - 连接服务器
```c
int net_connect(int sockfd, const struct net_sockaddr *addr, int addrlen);
```
| 参数 | 说明 |
|------|------|
| `sockfd` | socket 描述符 |
| `addr` | 服务器地址 |
| `addrlen` | 地址长度 |
| 返回值 | 0 成功,-1 失败 |
#### net_send - 发送数据
```c
int net_send(int sockfd, const void *buf, int len, int flags);
```
| 参数 | 说明 |
|------|------|
| `sockfd` | socket 描述符 |
| `buf` | 数据缓冲区 |
| `len` | 数据长度 |
| `flags` | 通常为 0 |
| 返回值 | 实际发送字节数,失败返回 -1 |
#### net_recv - 接收数据
```c
int net_recv(int sockfd, void *buf, int len, int flags);
```
| 参数 | 说明 |
|------|------|
| `sockfd` | socket 描述符 |
| `buf` | 接收缓冲区 |
| `len` | 缓冲区大小 |
| `flags` | 0=阻塞,`NET_MSG_DONTWAIT`=非阻塞 |
| 返回值 | 实际接收字节数;`0`=对端已关闭net 层随即自动重监听);`-1`=错误(非阻塞且无数据时返回 `NET_ERR_WOULDBLOCK` |
#### net_sendto - 发送 UDP 数据
```c
int net_sendto(int sockfd, const void *buf, int len, int flags,
const struct net_sockaddr *dest_addr, int addrlen);
```
#### net_recvfrom - 接收 UDP 数据
```c
int net_recvfrom(int sockfd, void *buf, int len, int flags,
struct net_sockaddr *src_addr, int *addrlen);
```
#### net_close - 关闭 Socket
```c
int net_close(int sockfd);
```
### 10.2 消息队列 API
#### net_process_messages - 处理消息队列
```c
void net_process_messages(void);
```
处理其他任务通过 `net_send()` / `net_recv()` / `net_close()` 发送到消息队列的请求。必须在 `netTask` 主循环中调用。
#### net_send_sock - 发送数据(内部版本)
```c
int net_send_sock(net_sock_t *p_sock, const void *buf, int len);
```
直接操作 Socket 控制块进行发送,不经过消息队列。**只能在 `netTask` 中调用**,用于 `netTask` 内的网络业务逻辑。
#### net_recv_sock - 接收数据(内部版本)
```c
int net_recv_sock(net_sock_t *p_sock, void *buf, int len, int flags);
```
直接操作 Socket 控制块进行接收,不经过消息队列。**只能在 `netTask` 中调用**。
#### net_close_sock - 关闭 Socket内部版本
```c
int net_close_sock(net_sock_t *p_sock);
```
直接操作 Socket 控制块关闭连接,不经过消息队列。**只能在 `netTask` 中调用**。
### 10.3 辅助 API
#### net_set_event_cb - 注册事件回调
```c
int net_set_event_cb(int sockfd, net_event_cb_t cb, void *arg);
```
#### net_get_sock - 获取 Socket 控制块
```c
net_sock_t *net_get_sock(int sockfd);
```
#### net_get_errno - 获取错误码
```c
int net_get_errno(void);
```
### 10.4 字节序转换
```c
uint16_t net_htons(uint16_t hostshort); /* 主机序 -> 网络序 */
uint16_t net_ntohs(uint16_t netshort); /* 网络序 -> 主机序 */
uint32_t net_htonl(uint32_t hostlong); /* 主机序 -> 网络序 */
uint32_t net_ntohl(uint32_t netlong); /* 网络序 -> 主机序 */
uint32_t net_inet_addr(const char *cp); /* 字符串 -> 网络序 IP */
char *net_inet_ntoa(uint32_t addr, char *buf); /* 网络序 IP -> 字符串 */
```
### 10.5 select/poll
```c
int net_select(int nfds, net_fd_set *readfds, net_fd_set *writefds,
net_fd_set *exceptfds, net_timeval *timeout);
int net_poll_events(net_pollfd *fds, int nfds, int timeout);
```
### 10.6 fd_set 操作宏
```c
NET_FD_ZERO(fdset) /* 清空集合 */
NET_FD_SET(fd, fdset) /* 添加 fd */
NET_FD_CLR(fd, fdset) /* 移除 fd */
NET_FD_ISSET(fd, fdset) /* 检查 fd 是否在集合中 */
```
---
## 11. 完整示例
### 11.1 TCP Client 自动重连
```c
#include "net_socket.h"
#include <stdio.h>
#include <string.h>
#define SERVER_IP "192.168.1.200"
#define SERVER_PORT 8080
void tcp_client_reconnect(void)
{
int sock = -1;
struct net_sockaddr_in addr;
uint32_t last_try = 0;
uint32_t reconnect_interval = 5000; /* 5 秒重连间隔 */
/* 初始化网络 */
net_init("192.168.1.100", "255.255.255.0", "192.168.1.1");
while (1)
{
net_poll();
/* 尝试重连 */
if (sock < 0 && (HAL_GetTick() - last_try) >= reconnect_interval)
{
last_try = HAL_GetTick();
sock = net_socket(AF_INET, SOCK_STREAM, 0);
if (sock >= 0) {
memset(&addr, 0, sizeof(addr));
addr.sin_family = AF_INET;
addr.sin_port = net_htons(SERVER_PORT);
addr.sin_addr.s_addr = net_inet_addr(SERVER_IP);
if (net_connect(sock, (struct net_sockaddr *)&addr, sizeof(addr)) == 0) {
printf("正在连接服务器...\r\n");
} else {
net_close(sock);
sock = -1;
}
}
}
/* 检查连接状态 */
if (sock >= 0)
{
net_sock_t *sock_info = net_get_sock(sock);
if (sock_info != NULL && sock_info->state == NET_SOCK_STATE_ESTABLISHED)
{
/* 已连接,发送数据 */
static uint32_t last_send = 0;
if ((HAL_GetTick() - last_send) >= 1000) {
last_send = HAL_GetTick();
net_send(sock, "heartbeat", 9, 0);
}
/* 检查接收 */
char buf[128];
int len = net_recv(sock, buf, sizeof(buf) - 1, NET_MSG_DONTWAIT);
if (len > 0) {
buf[len] = '\0';
printf("收到: %s\r\n", buf);
} else if (len == 0) {
/* 连接断开 */
printf("连接断开\r\n");
net_close(sock);
sock = -1;
}
}
else if (sock_info != NULL &&
(sock_info->state == NET_SOCK_STATE_CLOSED))
{
/* 连接失败或断开 */
printf("连接失败,准备重连...\r\n");
net_close(sock);
sock = -1;
}
}
}
}
```
---
## 12. 常见问题
### 12.1 net_poll() 必须调用吗?
**是的**`net_poll()` 负责轮询 CH395F 的中断状态并更新所有 Socket 的状态。如果不调用,连接状态不会更新,数据也无法接收。
建议至少每 10ms 调用一次。
### 12.2 Socket 数量有限制吗?
是的CH395F 最多支持 8 个 Socket索引 0~7。其中
- TCP Server单连接1 个监听 Socket 即数据通道
- TCP Client最多 8 个
- UDP最多 8 个
### 12.3 发送数据有什么限制?
- 每次发送的数据长度不能超过 `NET_SEND_BUF_SIZE`(默认 4096 字节)
- TCP 需要等待发送缓冲区空闲(`send_ready` 标志)
- UDP 单包载荷最大 1472 字节写入超过此值CH395F 会自动分片为多个 UDP 包发送)
### 12.4 如何处理连接断开?
**方式一:阻塞接收**
```c
int len = net_recv(sock, buf, sizeof(buf), 0);
if (len <= 0) {
/* 连接断开或错误 */
net_close(sock);
}
```
**方式二:非阻塞接收**
```c
int len = net_recv(sock, buf, sizeof(buf), NET_MSG_DONTWAIT);
if (len < 0 && net_get_errno() == NET_ERR_WOULDBLOCK) {
/* 没有数据 */
} else if (len <= 0) {
/* 连接断开 */
}
```
**方式三:事件回调**
```c
void callback(int sockfd, net_event_t event, void *arg)
{
if (event == NET_EVENT_DISCONNECTED) {
/* 连接断开 */
}
}
```
### 12.5 select 和 poll 的区别?
| 特性 | select | poll |
|------|--------|------|
| 描述符集合 | fd_set位掩码 | pollfd 数组 |
| 最大 fd 数 | 32 | 无限制 |
| 使用方式 | NET_FD_SET 宏 | 直接操作结构体 |
两者功能相同,推荐使用 `net_select()`,更接近 Linux 编程习惯。
### 12.6 错误码说明
| 错误码 | 宏定义 | 说明 |
|--------|--------|------|
| 0 | NET_OK | 成功 |
| -1 | NET_ERR | 通用错误 |
| -2 | NET_ERR_BADF | 无效的 socket 描述符 |
| -3 | NET_ERR_INVAL | 无效参数 |
| -4 | NET_ERR_NOMEM | 内存不足 |
| -5 | NET_ERR_NOTCONN | 未连接 |
| -6 | NET_ERR_ISCONN | 已连接 |
| -7 | NET_ERR_WOULDBLOCK | 非阻塞模式下操作未完成 |
| -8 | NET_ERR_CONNRESET | 连接被重置 |
| -9 | NET_ERR_TIMEDOUT | 操作超时 |
| -10 | NET_ERR_NOSPACE | 发送缓冲区无空间 |
| -11 | NET_ERR_BUSY | 操作未完成BUSY |
### 12.7 CH395F 命令执行状态码
CH395F 底层驱动返回的状态码(`ch395f_get_cmd_status()``ch395f_poll_cmd_status()`
| 代码 | 宏定义 | 说明 |
|------|--------|------|
| 0x00 | CH395F_ERR_SUCCESS | 成功 |
| 0x10 | CH395F_ERR_BUSY | 忙,命令正在执行 |
| 0x11 | CH395F_ERR_MEM | 内存管理错误 |
| 0x12 | CH395F_ERR_BUF | 缓冲区错误 |
| 0x13 | CH395F_ERR_TIMEOUT | 超时 |
| 0x14 | CH395F_ERR_RTE | 路由错误 |
| 0x15 | CH395F_ERR_ABRT | 连接中止 |
| 0x16 | CH395F_ERR_RST | 连接复位 |
| 0x17 | CH395F_ERR_CLSD | 连接关闭 |
| 0x18 | CH395F_ERR_CONN | 无连接 |
| 0x19 | CH395F_ERR_VAL | 值错误 |
| 0x1A | CH395F_ERR_ARG | 参数错误 |
| 0x1B | CH395F_ERR_USE | 已被使用(常见于重复 OPEN 或冲突配置) |
| 0x1C | CH395F_ERR_IF | MAC 错误 |
| 0x1D | CH395F_ERR_ISCONN | 已连接 |
| 0x20 | CH395F_ERR_OPEN | 已打开 |
| 0x5F | CH395F_CMD_RET_ABORT | 命令中止 |
| 0xFA | CH395F_ERR_UNKNOW | 未知错误 |
### 12.8 故障排查
**问题Socket 4~7 的 CONNECT/RECV 中断不触发**
- 需使用 `GET_GLOB_INT_STATUS_ALL`(命令码 0x19返回 2 字节)才能检测 Socket 4~7 的中断
- 1 字节版 `GET_GLOB_INT_STATUS`(命令码 0x29仅支持 Socket 0~3
**问题CH395F 初始化超时**
- 芯片 INIT 需要约 200ms轮询间隔应为 20ms过短的查询可能干扰内部处理
- 超时值建议设为 4 秒以上
### 12.9 线程安全问题
**问题:`net_send()` / `net_recv()` / `net_close()` 可以在任意任务中调用吗?**
可以。这些公共 API 通过消息队列将请求委托给 `netTask` 串行执行,自身阻塞等待结果,是线程安全的。
**问题:`netTask` 内可以直接调用 `net_send()` / `net_recv()` 吗?**
不可以。`netTask` 中必须使用 `net_send_sock()` / `net_recv_sock()` 等内部版本,否则会死锁(任务向自身发送消息,永远无人处理)。内部版本直接操作 Socket 控制块,不经过消息队列。
**问题:`net_socket()` / `net_bind()` / `net_listen()` 线程安全吗?**
这些函数仅在系统初始化阶段(`osKernelStart()` 之前)调用,不存在并发访问。消息队列仅保护数据收发和连接关闭操作。
**问题:消息队列由谁创建?在哪里创建?**
由 CubeMX 在 `MX_FREERTOS_Init()` 中创建(`osMessageQueueNew(8, sizeof(net_msg_t), NULL)`),在 `osKernelInitialize()` 之后执行,确保 FreeRTOS 内核已就绪。