
简介这份资源面向嵌入式网络开发学习者与STM32进阶开发者提供一套以STM32F750为硬件平台、将单片机配置为TCP服务器并支持多个PC客户端并发连接的完整Demo工程可用于理解资源受限环境下的并发网络服务设计。压缩包共820个文件约60.36MB以217个h头文件与204个c源文件为核心配合109个o目标文件、109个d依赖文件及104个crf交叉引用文件另含少量html、txt、asm、s、exe与uvprojx工程文件覆盖lwIP协议栈、UCOSIII实时操作系统、以太网驱动及HAL库等模块。资源重点展示服务器初始化、监听接受连接、事件驱动并发处理、数据收发与错误关闭等关键流程并给出UCOSIII任务调度与lwIP多连接管理的具体实现便于读者对照源码梳理工程结构与排错思路。目前已有5560人学习下载适合希望深入掌握嵌入式TCP服务器并发模型的开发者参考。1. 一块 STM32 当 TCP 服务器PC 多客户端同时连它到底靠不靠谱前阵子帮一个做工业网关的朋友看代码他拿一块 STM32F407 跑 LwIP想让产线上的三台 PC 同时连上来读数据结果第二台一连上第一台就掉线。他以为是网口硬件问题换了两块板子都一样。这其实是嵌入式 TCP 服务器最典型的翻车点——不是硬件不行是 socket 管理没做对。STM32 单片机作 TCP 服务器、实现 PC 多客户端连接本质是在资源受限的 MCU 上跑一个精简协议栈用有限的 socket 描述符同时维护多条连接。它解决的是「设备侧主动提供网络服务、上位机免装驱动直接 socket 通信」这个需求适合做数据采集、远程控制、参数配置这类场景。这份 Demo 就是把这套流程跑通的最小可复现工程新手能照着搭熟手能看清并发上限和内存边界在哪。2. 选型与协议栈为什么是 LwIP 而不是自己撸 TCP2.1 裸机跑 TCP 的三种路线对比在 STM32 上做 TCP 服务器绕不开协议栈选型。常见做法有三条路一是用芯片厂商配套的以太网外设加 LwIP 协议栈二是用带硬件 TCP/IP 加速的独立网口芯片比如常见的 SPI 转以太网方案三是自己写精简 TCP 状态机。第三条基本是血泪经验劝退——TCP 的三次握手、超时重传、滑动窗口、拥塞控制任何一块写不严谨多客户端一压就崩调试成本远超收益。LwIP 是目前 STM32 生态里最成熟的开源协议栈支持 raw API、netconn API、socket API 三种编程接口。raw API 是回调式、零拷贝、内存占用最小但写起来反人类socket API 最接近 PC 端编程习惯但每层封装都要吃 RAM。这个 Demo 走的是 socket API 路线原因是多客户端场景下代码可读性和可维护性优先而且 STM32F4/F7/H7 系列的 RAM 足够撑起几个并发 socket。路线RAM 占用并发能力开发难度适用场景LwIP raw API最低强高资源极紧、连接数固定LwIP socket API中等中低多客户端、快速开发硬件协议栈芯片低MCU 侧受芯片限制低无以太网外设的 MCU2.2 LwIP 内存配置并发数的真正天花板很多人以为并发数取决于 CPU 主频其实在 LwIP 里能同时维持几条连接几乎完全由内存池决定。打开lwipopts.h下面这几个宏是必须调的// lwipopts.h 关键配置决定并发上限 #define MEM_SIZE (16 * 1024) // LwIP 堆大小socket API 依赖它 #define MEMP_NUM_TCP_PCB (8) // 同时存在的 TCP 控制块数量 #define MEMP_NUM_TCP_PCB_LISTEN (2) // 监听用 PCB 数量 #define MEMP_NUM_NETCONN (10) // netconn/socket 描述符上限 #define TCP_SND_BUF (2 * 1460) // 单连接发送缓冲约 2 个 MSS #define TCP_WND (2 * 1460) // 单连接接收窗口 #define PBUF_POOL_SIZE (16) // pbuf 池收包全靠它逻辑说明MEMP_NUM_NETCONN直接决定socket()能成功创建多少个描述符它必须大于等于「监听 socket 客户端连接数」。MEMP_NUM_TCP_PCB是底层 TCP 控制块通常要比 netconn 数再留一点余量。PBUF_POOL_SIZE太小的话多客户端同时发数据会丢包表现为连接还在但数据收不全。参数怎么改假设你要稳定支持 4 个 PC 客户端MEMP_NUM_NETCONN至少设 61 个监听 4 个连接 1 个余量MEMP_NUM_TCP_PCB设 8PBUF_POOL_SIZE建议 16 起步。改完这些宏MEM_SIZE也要跟着涨否则堆分配会失败。判断是否够用的方法在accept返回后打印剩余堆跑满客户端看余量。提示LwIP 的 socket API 在 accept 时会为每个连接分配 netconn 结构如果MEMP_NUM_NETCONN设小了第 N 个客户端连上来时 accept 直接返回错误现象就是「连不上」而不是「掉线」别和硬件问题搞混。3. 多客户端并发的实现从单连接循环到 select 轮询3.1 单线程 select 模型为什么适合 MCUPC 端写多客户端服务器第一反应是开线程一个连接一个线程。但 STM32 裸机没有操作系统线程就算上了 FreeRTOS给每个 socket 开任务也是奢侈——每个任务栈至少 512 字节起4 个连接就是 2KB RAM 没了。所以 MCU 上的标准做法是单线程 select多路复用用一个循环同时监听监听 socket 和所有已连接 socket谁有数据就处理谁。这个模型的好处是逻辑线性、无锁、无上下文切换开销。代价是任何一次处理不能阻塞太久否则其他连接的数据会积压。所以发送大块数据时要分片接收要设非阻塞。3.2 完整的多客户端服务器主循环下面这段是 Demo 的核心骨架可直接抄// tcp_server.c 多客户端 select 主循环 #include lwip/sockets.h #define MAX_CLIENTS 4 #define BUF_SIZE 512 static int client_fds[MAX_CLIENTS]; // 保存所有已连接客户端 void tcp_server_task(void *arg) { int listen_fd, max_fd, activity, i, n; struct sockaddr_in addr, cli_addr; socklen_t cli_len sizeof(cli_addr); fd_set readfds; char rx_buf[BUF_SIZE]; // 初始化客户端数组 for (i 0; i MAX_CLIENTS; i) client_fds[i] -1; listen_fd socket(AF_INET, SOCK_STREAM, 0); addr.sin_family AF_INET; addr.sin_addr.s_addr INADDR_ANY; // 监听本机所有网卡地址 addr.sin_port htons(8080); // 服务端口 bind(listen_fd, (struct sockaddr *)addr, sizeof(addr)); listen(listen_fd, MAX_CLIENTS); // backlog 设为最大客户端数 while (1) { FD_ZERO(readfds); FD_SET(listen_fd, readfds); max_fd listen_fd; for (i 0; i MAX_CLIENTS; i) { if (client_fds[i] 0) { FD_SET(client_fds[i], readfds); if (client_fds[i] max_fd) max_fd client_fds[i]; } } // 超时 100ms避免死等导致其他任务饿死 struct timeval tv {0, 100000}; activity select(max_fd 1, readfds, NULL, NULL, tv); if (activity 0) continue; // 有新客户端接入 if (FD_ISSET(listen_fd, readfds)) { int new_fd accept(listen_fd, (struct sockaddr *)cli_addr, cli_len); if (new_fd 0) { for (i 0; i MAX_CLIENTS; i) { if (client_fds[i] -1) { client_fds[i] new_fd; break; } } if (i MAX_CLIENTS) closesocket(new_fd); // 满了就拒 } } // 处理已连接客户端的数据 for (i 0; i MAX_CLIENTS; i) { int fd client_fds[i]; if (fd 0 FD_ISSET(fd, readfds)) { n recv(fd, rx_buf, BUF_SIZE, 0); if (n 0) { // 对端关闭或出错 closesocket(fd); client_fds[i] -1; } else { send(fd, rx_buf, n, 0); // 回显实际项目换成业务处理 } } } } }逻辑说明select每次循环重建 fd 集合把监听 fd 和所有活跃客户端 fd 都塞进去然后阻塞等待带 100ms 超时。有事件时先判断是不是新连接是就 accept 并存入数组再遍历客户端判断谁有数据可读。recv返回 0 或负数表示对端断开必须及时closesocket并清空槽位否则数组会被占满新客户端永远进不来。参数说明backlog设为MAX_CLIENTS表示内核允许的等待队列长度tv超时不能设 0变成纯轮询吃 CPU也不能设太大响应迟钝100ms 是常见折中。BUF_SIZE别超过PBUF_POOL单块大小太多否则一次 recv 要拼多个 pbuf。3.3 客户端槽位管理与连接回收上面代码里client_fds数组就是连接管理的核心。几个容易忽略的点一是 accept 后如果数组满了必须closesocket掉新连接不能不管否则 LwIP 的 netconn 会被耗尽二是recv出错后要立即回收槽位不能等下一轮三是如果客户端异常断电没有正常四次挥手recv可能长时间不返回错误这时要靠 TCP keepalive 或者应用层心跳来清理。LwIP 里开启 keepalive 的方式// 对每个已连接 socket 开启 keepalive int keepalive 1; int keepidle 5; // 5 秒无数据开始探测 int keepintvl 2; // 探测间隔 2 秒 int keepcnt 3; // 3 次无响应则断开 setsockopt(fd, SOL_SOCKET, SO_KEEPALIVE, keepalive, sizeof(keepalive)); setsockopt(fd, IPPROTO_TCP, TCP_KEEPIDLE, keepidle, sizeof(keepidle)); setsockopt(fd, IPPROTO_TCP, TCP_KEEPINTVL, keepintvl, sizeof(keepintvl)); setsockopt(fd, IPPROTO_TCP, TCP_KEEPCNT, keepcnt, sizeof(keepcnt));逻辑说明keepalive 让协议栈在连接空闲时自动发探测包对端无响应就判定断开并触发recv返回错误从而回收槽位。参数上keepidle别设太短否则正常空闲的客户端会被误杀工业场景一般 5~10 秒起步。4. 避坑与排查多客户端连不上的五个真实原因4.1 现象第二个客户端连上第一个就掉原因最常见的是MEMP_NUM_NETCONN设成了 1 或 2只够一个监听加一个连接。第二个连接 accept 时底层分配 netconn 失败协议栈可能复用或顶掉旧连接。 解决把MEMP_NUM_NETCONN调到「1 最大客户端数 余量」同时确认MEMP_NUM_TCP_PCB不小于它。4.2 现象连接建立成功但收不到数据原因PBUF_POOL_SIZE太小或者TCP_WND设得比 pbuf 能承载的还大导致接收窗口虚高但实际没内存收包数据被静默丢弃。 解决PBUF_POOL_SIZE至少 16TCP_WND不要超过PBUF_POOL_SIZE * 单块大小的量级。用netif的统计计数看是否有 drop。4.3 现象跑一段时间后新客户端连不上重启才好原因客户端异常断开时没有触发recv错误槽位一直占着数组满了。 解决开启 TCP keepalive或者应用层约定心跳包超时未收到就主动closesocket。别指望对端一定正常挥手。4.4 现象select 返回后处理某个 fd 卡死其他客户端无响应原因send在缓冲区满时会阻塞如果对端一直不收整个循环卡在这一条连接上。 解决把 socket 设为非阻塞或者send前用select判断可写。MCU 上更稳的做法是发送也走状态机分片发。4.5 现象PC 能 ping 通板子但 TCP 连不上原因bind时地址写成了具体 IP 而不是INADDR_ANY或者端口被别的服务占了也可能是防火墙拦了。 解决先用INADDR_ANY排除地址问题再用 PC 端telnet 板子IP 端口测端口通不通。注意 LwIP 的bind对地址字节序敏感htons别漏。5. 进阶技巧把并发上限和稳定性再压一压想让这套 Demo 从「能跑」变成「能上产线」有几个具体技巧值得试。第一把 socket API 换成 netconn API能省下每个连接约几百字节的封装开销代价是代码要改成回调或阻塞式可读性下降。第二发送路径改成「应用层环形缓冲 状态机分片发送」避免send阻塞主循环这是多客户端下最影响实时性的点。第三给每个客户端记录最后活跃时间主循环里定期扫描超时的主动踢掉比单纯依赖 keepalive 更可控。验证并发上限的方法很直接PC 端用脚本同时开 N 个连接每个连接每秒发一包跑十分钟看板子端有没有掉线、丢包、内存耗尽。下面这个 Python 脚本可以当压测工具# stress_test.py 多客户端并发压测 import socket, threading, time HOST, PORT 192.168.1.100, 8080 # 换成板子实际 IP CLIENTS 4 def worker(idx): s socket.socket(socket.AF_INET, socket.SOCK_STREAM) s.connect((HOST, PORT)) for i in range(600): # 每秒一包跑 10 分钟 s.sendall(fclient{idx} seq{i}\n.encode()) data s.recv(128) if not data: print(fclient{idx} disconnected at {i}) break time.sleep(1) s.close() threads [threading.Thread(targetworker, args(i,)) for i in range(CLIENTS)] for t in threads: t.start() for t in threads: t.join()逻辑说明每个线程模拟一个 PC 客户端持续发送并等待回显一旦recv返回空就说明被断开。跑的时候同时观察板子端串口打印的剩余堆和连接数就能定位是内存先耗尽还是逻辑先出错。参数上CLIENTS从 2 开始逐步加找到稳定上限别一上来就压 10 个。我自己的习惯是每次改完lwipopts.h的内存宏都先用这个脚本从 2 客户端压到目标值确认不掉线再进业务逻辑。从那以后我每次调 LwIP 并发参数都强制先跑一遍连接压测再写业务代码省得后面业务和协议栈问题混在一起查。希望帮到你。本文还有配套的精品资源点击获取