34 KiB
BSD Socket API 使用指南
目录
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 包含头文件
#include "net_socket.h"
#include "net_select.h"
3.2 初始化网络
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)
- 触发事件回调
- 处理数据接收
while (1)
{
net_poll(); /* 建议至少每 10ms 调用一次 */
/* 其他应用代码... */
}
4. TCP 编程
4.1 TCP Server(单连接)
单连接模式下,1 个 Socket 既做监听又做数据通信:
#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,支持并发连接:
#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);
/* 数据 Socket(1~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
#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
#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
#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 的读写事件:
#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 结构体数组:
#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 定义回调函数
#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 注册回调
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 端口号转换
uint16_t port = 8080;
/* 主机序 -> 网络序(发送前转换) */
uint16_t net_port = net_htons(port);
/* 网络序 -> 主机序(接收后转换) */
uint16_t host_port = net_ntohs(net_port);
8.2 IP 地址转换
/* 字符串转网络序 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 填充地址结构体
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 读取地址信息
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 - 网络初始化
int net_init(const char *ip, const char *mask, const char *gateway);
| 参数 | 说明 |
|---|---|
ip |
IP 地址字符串,NULL 使用 DHCP |
mask |
子网掩码字符串,NULL 使用默认 |
gateway |
网关地址字符串,NULL 使用默认 |
| 返回值 | 0 成功,-1 失败 |
net_poll - 状态轮询
int net_poll(void);
| 参数 | 说明 |
|---|---|
| 返回值 | 有事件发生的 socket 数量 |
注意:必须在主循环中定期调用。
net_socket - 创建 Socket
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 - 绑定地址
int net_bind(int sockfd, const struct net_sockaddr *addr, int addrlen);
| 参数 | 说明 |
|---|---|
sockfd |
socket 描述符 |
addr |
地址结构体指针 |
addrlen |
地址结构体长度 |
| 返回值 | 0 成功,-1 失败 |
net_listen - 开始监听
int net_listen(int sockfd, int backlog);
| 参数 | 说明 |
|---|---|
sockfd |
socket 描述符 |
backlog |
最大并发连接数(0=单连接) |
| 返回值 | 0 成功,-1 失败 |
net_accept - 接受连接
int net_accept(int sockfd, struct net_sockaddr *addr, int *addrlen);
| 参数 | 说明 |
|---|---|
sockfd |
监听 socket 描述符 |
addr |
输出:客户端地址 |
addrlen |
输入输出:地址长度 |
| 返回值 | 新 socket 描述符,失败返回 -1 |
net_connect - 连接服务器
int net_connect(int sockfd, const struct net_sockaddr *addr, int addrlen);
| 参数 | 说明 |
|---|---|
sockfd |
socket 描述符 |
addr |
服务器地址 |
addrlen |
地址长度 |
| 返回值 | 0 成功,-1 失败 |
net_send - 发送数据
int net_send(int sockfd, const void *buf, int len, int flags);
| 参数 | 说明 |
|---|---|
sockfd |
socket 描述符 |
buf |
数据缓冲区 |
len |
数据长度 |
flags |
通常为 0 |
| 返回值 | 实际发送字节数,失败返回 -1 |
net_recv - 接收数据
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 数据
int net_sendto(int sockfd, const void *buf, int len, int flags,
const struct net_sockaddr *dest_addr, int addrlen);
net_recvfrom - 接收 UDP 数据
int net_recvfrom(int sockfd, void *buf, int len, int flags,
struct net_sockaddr *src_addr, int *addrlen);
net_close - 关闭 Socket
int net_close(int sockfd);
9.2 辅助 API
net_set_event_cb - 注册事件回调
int net_set_event_cb(int sockfd, net_event_cb_t cb, void *arg);
net_get_sock - 获取 Socket 控制块
net_sock_t *net_get_sock(int sockfd);
net_get_errno - 获取错误码
int net_get_errno(void);
9.3 字节序转换
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
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 操作宏
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(多连接)
#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 自动重连
#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 如何处理连接断开?
方式一:阻塞接收
int len = net_recv(sock, buf, sizeof(buf), 0);
if (len <= 0) {
/* 连接断开或错误 */
net_close(sock);
}
方式二:非阻塞接收
int len = net_recv(sock, buf, sizeof(buf), NET_MSG_DONTWAIT);
if (len < 0 && net_get_errno() == NET_ERR_WOULDBLOCK) {
/* 没有数据 */
} else if (len <= 0) {
/* 连接断开 */
}
方式三:事件回调
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 中断
- 检查多连接模式下数据 Socket(1~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 秒以上