Files
STM32F4-Base/docs/BSD_Socket_API_使用指南.md
2026-07-19 00:59:56 +08:00

1269 lines
34 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. [TCP 编程](#4-tcp-编程)
5. [UDP 编程](#5-udp-编程)
6. [I/O 多路复用](#6-io-多路复用)
7. [事件回调机制](#7-事件回调机制)
8. [字节序处理](#8-字节序处理)
9. [API 参考](#9-api-参考)
10. [完整示例](#10-完整示例)
11. [常见问题](#11-常见问题)
---
## 1. 概述
本项目基于 CH395F 以太网协议栈芯片,提供了一套兼容 Linux BSD Socket API 的网络编程接口。这套接口屏蔽了底层硬件细节,让嵌入式网络编程与标准 Linux 网络编程保持一致。
### 1.1 支持的功能
| 功能 | 说明 |
|------|------|
| TCP Client | 主动连接远程服务器 |
| TCP Server单连接 | 支持 1 个客户端连接 |
| TCP Server多连接 | 支持最多 7 个并发连接 |
| UDP Client | 发送 UDP 数据报 |
| UDP Server | 接收 UDP 数据报 |
| select/poll | I/O 多路复用 |
| 事件回调 | 异步事件通知 |
### 1.2 与 Linux Socket 的差异
| 特性 | Linux Socket | 本实现 |
|------|--------------|--------|
| Socket 数量 | 无限制 | 最多 8 个 |
| TCP Server 多连接 | 无限制 | 最多 7 个并发连接 |
| 非阻塞模式 | 支持 | 通过 `net_poll()` 实现 |
| 错误码 | errno | `net_get_errno()` |
| 头文件 | `<sys/socket.h>` | `"net_socket.h"` |
---
## 2. 架构说明
```
┌─────────────────────────────────────────────────────────┐
│ 应用层 (App) │
├─────────────────────────────────────────────────────────┤
│ net_socket.h (BSD Socket API) │ ← 用户接口
├─────────────────────────────────────────────────────────┤
│ net_socket.c + net_select.c (状态机+select) │ ← 核心逻辑
├─────────────────────────────────────────────────────────┤
│ ch395f.c/h (底层驱动) │ ← 硬件驱动
├─────────────────────────────────────────────────────────┤
│ CH395F 硬件 │
└─────────────────────────────────────────────────────────┘
```
### 2.1 文件说明
| 文件 | 说明 |
|------|------|
| `net_config.h` | 配置宏定义(最大 Socket 数、超时时间等) |
| `net_types.h` | 类型定义地址结构、Socket 控制块等) |
| `net_socket.h` | BSD Socket API 头文件 |
| `net_socket.c` | API 核心实现 |
| `net_select.h` | select/poll API 头文件 |
| `net_select.c` | select/poll 实现 |
### 2.1a 底层驱动改进v2.1
ch395f.c/h 已完成以下优化:
| 改进项 | 说明 |
|--------|------|
| 命令执行等待 | `open/close/listen/connect/disconnect` 所有长命令内部调用 `poll_cmd_status` 等待完成20ms × 200 次 = 4s 超时) |
| 全局中断 | 使用 `GET_GLOB_INT_STATUS_ALL`2字节版命令码 0x19支持全部 8 个 Socket |
| 错误码 | 与芯片手册完全对齐的 17 个状态码定义 |
| FUN_PARA 标志 | `CH395F_FUN_PARA_TCP_SERVER` / `SOCKET_CLOSE` / `DISABLE_SEND_OK` 宏 |
| 新增函数 | `ch395f_set_arp()` / `ch395f_set_ttl()` / `ch395f_get_unreach_info()` / `ch395f_poll_cmd_status()` |
| KeepAlive | 参数单位为毫秒(非秒),必须为 500 的倍数 |
### 2.2 Socket 状态机
```
net_socket()
┌──────────── CREATED ────────────┐
│ │ │
│ net_bind() │
│ │ │
│ ▼ │
│ BOUND ─────────────────┤
│ │ │ │
│ net_listen() net_connect() │
│ │ │ │
│ ▼ ▼ │
│ LISTENING CONNECTING │
│ │ │ │
│ net_accept() │ (连接成功) │
│ │ │ │
│ ▼ ▼ │
│ TCP_ACCEPT ESTABLISHED ◄──┤
│ │ │ │
│ (分配连接)│ net_send/recv() │
│ │ │ │
│ ▼ ▼ │
│ ESTABLISHED │
│ │ │
│ net_close() │
│ │ │
│ ▼ │
└────── CLOSED ──────────────────┘
```
---
## 3. 快速开始
### 3.1 包含头文件
```c
#include "net_socket.h"
#include "net_select.h"
```
### 3.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(); /* 必须在主循环中调用 */
/* 其他任务... */
}
}
```
### 3.3 主循环要求
**重要**`net_poll()` 必须在主循环中定期调用,它负责:
- 轮询 CH395F INT# 引脚电平GPIO 轮询,无需 EXTI 中断)
- 通过 `GET_GLOB_INT_STATUS_ALL`2字节版读取所有 8 个 Socket 的中断状态
- 更新所有 Socket 的连接状态CONNECT / DISCONNECT / RECV / TIMEOUT
- 触发事件回调
- 处理数据接收
```c
while (1)
{
net_poll(); /* 建议至少每 10ms 调用一次 */
/* 其他应用代码... */
}
```
---
## 4. TCP 编程
### 4.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. 开始监听(单连接模式 backlog=0 */
if (net_listen(listen_sock, 0) != 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));
/* 接收和发送数据 */
while (1)
{
len = net_recv(client_sock, buf, sizeof(buf) - 1, 0);
if (len <= 0) {
printf("连接断开\r\n");
break;
}
buf[len] = '\0';
printf("收到: %s\r\n", buf);
/* 回显数据 */
net_send(client_sock, buf, len, 0);
}
net_close(client_sock);
}
}
}
```
### 4.2 TCP Server多连接
多连接模式使用 1 个监听 Socket + N 个数据 Socket支持并发连接
```c
#include "net_socket.h"
#include "net_select.h"
#define MAX_CLIENTS 4
void tcp_server_multi(void)
{
int listen_sock;
struct net_sockaddr_in addr;
int i;
/* 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);
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");
return;
}
/* 3. 启动监听backlog 设为并发连接数) */
if (net_listen(listen_sock, MAX_CLIENTS) != 0) {
printf("监听失败\r\n");
return;
}
printf("TCP Server 启动,支持 %d 个并发连接\r\n", MAX_CLIENTS);
/* 4. 使用 select 等待事件 */
while (1)
{
net_fd_set readfds;
struct net_timeval timeout;
NET_FD_ZERO(&readfds);
/* 监听 Socket 加入读集合 */
NET_FD_SET(listen_sock, &readfds);
/* 数据 Socket1~4加入读集合 */
for (i = 1; i <= MAX_CLIENTS; i++) {
NET_FD_SET(i, &readfds);
}
timeout.tv_sec = 0;
timeout.tv_usec = 100000; /* 100ms 超时 */
int ret = net_select(listen_sock + MAX_CLIENTS + 1,
&readfds, NULL, NULL, &timeout);
if (ret > 0)
{
/* 检查数据 Socket */
for (i = 1; i <= MAX_CLIENTS; i++)
{
if (NET_FD_ISSET(i, &readfds))
{
net_sock_t *sock = net_get_sock(i);
if (sock != NULL && sock->state == NET_SOCK_STATE_ESTABLISHED)
{
char buf[256];
int len = net_recv(i, buf, sizeof(buf) - 1, 0);
if (len > 0) {
buf[len] = '\0';
printf("Socket %d: %s\r\n", i, buf);
net_send(i, buf, len, 0); /* 回显 */
} else {
printf("Socket %d 断开\r\n", i);
}
}
}
}
}
}
}
```
### 4.3 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);
}
```
---
## 5. UDP 编程
### 5.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);
}
```
### 5.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);
}
}
}
```
---
## 6. I/O 多路复用
### 6.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) {
/* 超时 */
}
}
}
```
### 6.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);
/* 处理数据... */
}
}
}
}
```
### 6.3 事件掩码说明
| 事件 | 说明 |
|------|------|
| `NET_POLLIN` | 有数据可读,或有新连接到达(监听 Socket |
| `NET_POLLOUT` | 发送缓冲区有空间,可以写入数据 |
| `NET_POLLERR` | 发生错误 |
| `NET_POLLHUP` | 连接挂起(对端关闭) |
| `NET_POLLNVAL` | 无效的文件描述符 |
---
## 7. 事件回调机制
可以通过注册回调函数,异步获取 Socket 事件通知:
### 7.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;
}
}
```
### 7.2 注册回调
```c
void event_callback_example(void)
{
int sock;
/* 创建 Socket ... */
/* 注册事件回调 */
net_set_event_cb(sock, my_event_callback, NULL);
/* 主循环 */
while (1)
{
net_poll();
}
}
```
---
## 8. 字节序处理
网络协议使用**大端序**(网络字节序),而 STM32 使用**小端序**(主机字节序)。发送和接收数据时需要进行字节序转换。
### 8.1 端口号转换
```c
uint16_t port = 8080;
/* 主机序 -> 网络序(发送前转换) */
uint16_t net_port = net_htons(port);
/* 网络序 -> 主机序(接收后转换) */
uint16_t host_port = net_ntohs(net_port);
```
### 8.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" */
```
### 8.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;
```
### 8.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);
}
```
---
## 9. API 参考
### 9.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, int backlog);
```
| 参数 | 说明 |
|------|------|
| `sockfd` | socket 描述符 |
| `backlog` | 最大并发连接数0=单连接) |
| 返回值 | 0 成功,-1 失败 |
#### net_accept - 接受连接
```c
int net_accept(int sockfd, struct net_sockaddr *addr, int *addrlen);
```
| 参数 | 说明 |
|------|------|
| `sockfd` | 监听 socket 描述符 |
| `addr` | 输出:客户端地址 |
| `addrlen` | 输入输出:地址长度 |
| 返回值 | 新 socket 描述符,失败返回 -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=对端关闭,-1=错误 |
#### 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);
```
### 9.2 辅助 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);
```
### 9.3 字节序转换
```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 -> 字符串 */
```
### 9.4 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);
```
### 9.5 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 是否在集合中 */
```
---
## 10. 完整示例
### 10.1 TCP Echo Server多连接
```c
#include "net_socket.h"
#include "net_select.h"
#include <stdio.h>
#include <string.h>
#define SERVER_PORT 8080
#define MAX_CLIENTS 4
void tcp_echo_server(void)
{
int listen_sock;
struct net_sockaddr_in addr;
int i;
/* 初始化网络 */
net_init("192.168.1.100", "255.255.255.0", "192.168.1.1");
/* 创建监听 Socket */
listen_sock = net_socket(AF_INET, SOCK_STREAM, 0);
if (listen_sock < 0) {
printf("创建 Socket 失败\r\n");
while(1);
}
/* 绑定 */
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("0.0.0.0");
if (net_bind(listen_sock, (struct net_sockaddr *)&addr, sizeof(addr)) != 0) {
printf("绑定失败\r\n");
while(1);
}
/* 监听 */
if (net_listen(listen_sock, MAX_CLIENTS) != 0) {
printf("监听失败\r\n");
while(1);
}
printf("TCP Echo Server 启动,端口 %d\r\n", SERVER_PORT);
/* 主循环 */
while (1)
{
net_fd_set readfds;
struct net_timeval timeout;
NET_FD_ZERO(&readfds);
NET_FD_SET(listen_sock, &readfds);
for (i = 1; i <= MAX_CLIENTS; i++) {
NET_FD_SET(i, &readfds);
}
timeout.tv_sec = 0;
timeout.tv_usec = 100000;
int ret = net_select(listen_sock + MAX_CLIENTS + 1,
&readfds, NULL, NULL, &timeout);
if (ret > 0)
{
for (i = 1; i <= MAX_CLIENTS; i++)
{
if (NET_FD_ISSET(i, &readfds))
{
net_sock_t *sock = net_get_sock(i);
if (sock == NULL) continue;
if (sock->state == NET_SOCK_STATE_ESTABLISHED)
{
char buf[256];
int len = net_recv(i, buf, sizeof(buf) - 1, 0);
if (len > 0) {
buf[len] = '\0';
printf("客户端 %d: %s\r\n", i, buf);
net_send(i, buf, len, 0);
} else {
printf("客户端 %d 断开\r\n", i);
net_close(i);
}
}
}
}
}
}
}
```
### 10.2 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_TCP_ACCEPT ||
sock_info->state == NET_SOCK_STATE_CLOSED))
{
/* 连接失败或断开 */
printf("连接失败,准备重连...\r\n");
net_close(sock);
sock = -1;
}
}
}
}
```
---
## 11. 常见问题
### 11.1 net_poll() 必须调用吗?
**是的**`net_poll()` 负责轮询 CH395F 的中断状态并更新所有 Socket 的状态。如果不调用,连接状态不会更新,数据也无法接收。
建议至少每 10ms 调用一次。
### 11.2 Socket 数量有限制吗?
是的CH395F 最多支持 8 个 Socket索引 0~7。其中
- TCP Server 多连接模式1 个监听 Socket + 最多 7 个数据 Socket
- TCP Client最多 8 个
- UDP最多 8 个
### 11.3 TCP Server 多连接模式有什么要求?
**模式工作原理:**
- CH395F 多连接模式下Socket 0 专职监听Socket 1~7 由芯片自动分配
- 客户端连接时CH395F 自动查找协议类型为 TCP、源端口与监听端口相同、处于关闭状态的 Socket
- 找到后自动打开该 Socket 并分配连接,触发 CONNECT 中断
**硬件要求:**
- CH395F 固件版本 >= 0x44支持 Socket 4~7
### 11.4 发送数据有什么限制?
- 每次发送的数据长度不能超过 `NET_SEND_BUF_SIZE`(默认 4096 字节)
- TCP 需要等待发送缓冲区空闲(`send_ready` 标志)
- UDP 单次发送最大 1460 字节
### 11.5 如何处理连接断开?
**方式一:阻塞接收**
```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) {
/* 连接断开 */
}
}
```
### 11.6 select 和 poll 的区别?
| 特性 | select | poll |
|------|--------|------|
| 描述符集合 | fd_set位掩码 | pollfd 数组 |
| 最大 fd 数 | 32 | 无限制 |
| 使用方式 | NET_FD_SET 宏 | 直接操作结构体 |
两者功能相同,推荐使用 `net_select()`,更接近 Linux 编程习惯。
### 11.7 错误码说明
| 错误码 | 宏定义 | 说明 |
|--------|--------|------|
| 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 |
### 11.8 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 | 未知错误 |
### 11.9 故障排查
**问题TCP Client 连接超时,无 CONNECT 中断**
- 检查多连接模式下数据 Socket1~7是否设置了 `SET_PROTO_TCP` + `SET_SOUR_PORT`(与监听端口相同)
- 如果不设置CH395F 不知道哪些 Socket 可用于自动分配TCP SYN 被静默丢弃
**问题:数据 Socket OPEN 返回 0x1B已被使用**
- CH395F 多连接模式下,数据 Socket 由芯片自动管理,软件不应手动 `OPEN_SOCKET`
- 只需 `SET_PROTO_TCP` + `SET_SOUR_PORT`CH395F 在有连接时自动打开
**问题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 秒以上