ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

STM32实现TCP服务器:LwIP+select轮询搞定多客户端并发连接

STM32实现TCP服务器:LwIP+select轮询搞定多客户端并发连接 简介面向嵌入式网络开发者的STM32 TCP服务器多客户端连接Demo基于STM32F750高性能MCU结合UCOSIII实时操作系统与lwIP轻量级协议栈演示在资源有限的嵌入式环境中实现并发连接管理的关键技术。整个工程共820个文件包含h/c源文件、o/d编译中间文件、crf文件、工程配置文件、hex烧录文件等压缩包大小60.36MB目录结构完整便于直接打开工程研读、编译验证或二次开发。核心代码覆盖服务器初始化、监听与接受连接、并发连接处理、数据收发、错误处理与连接关闭等模块并附有RTOS任务与lwIP网络配置集合的工程文件展示如何将UCOSIII任务管理与lwIP轻量级协议栈结合通过合理使用事件驱动或任务调度达到同时服务多个PC客户端的目标。已有5551人学习下载适合有一定单片机基础、希望进阶嵌入式网络编程的开发者可作为课程设计、项目实战或技术研究的重要参考。 最近在做一个现场采集的小项目需求其实不复杂一块STM32板子接传感器采集数据工位上三台电脑要同时实时看数据。很多朋友第一反应是“走MQTT啊让数据上云再推到前端”但仔细一想设备在现场、需求在局域网内没有公网接入板卡资源也不充裕电脑端只是想拿一个轻量级的TCP连接拉数据。绕一大圈上云纯属给自己制造麻烦。于是我把STM32直接做成了TCP服务器PC端多个客户端主动连上来板子负责监听、接收、再广播给所有在线连接。这个Demo跑通之后正好对应设备状态监控、小屏数据看板、本地调试工具这类场景。如果你手里有一块带以太网功能的STM32板子想搞明白“单片机怎么让多台电脑同时连接”这篇就是把你从零带到通。1. 为什么放着MQTT云平台不用偏要拿STM32当TCP服务器1.1 单机直连的局域网场景其实非常普遍先说个反直觉的结论不是所有设备数据都必须上云。MQTT适合设备多、节点分散、需要远程管理的大规模场景但如果是“一块板子、三台电脑、一个交换机”这种小闭环上云就是绕远路。本地TCP服务器模式最大的好处是PC端不用装任何中间件一个Socket连接进来就能收数据谁都能写客户端。很多工控现场就是这么干的。设备通过网线直连工控机板子开机就监听端口各工位电脑需要看数据就连一下不需要数据历史存储和远程控制的话这比走云平台省掉一大截网络抖动和调试成本。1.2 数据流向与整体架构设计这个Demo的架构很直白STM32上电初始化以太网拿到固定IP创建TCP服务器监听8080端口客户端连接后服务器记录这个连接任意一个客户端发来数据服务器把它转发给所有在线客户端客户端断开服务器自动清理连接不影响其他人也就是说STM32的角色是中心节点既做数据采集也做消息分发。PC端更像是分布式显示屏每台电脑收到的数据一致适合团队共用一组数据源的场景。1.3 本Demo的功能边界这里得说清楚Demo不是产品功能上有意做了裁剪不做掉线重连不做加密也不做大文件传输只聚焦在“多客户端连接”这件事本身。但恰恰是这种裁剪反而适合拿来理解TCP服务器在单片机上的工作方式。如果你后续要加功能核心代码不需要推翻重写把数据分发部分换成业务处理就行。这也是我选TCP服务器而不是定制协议栈的原因——Socket API是通用语言PC端和单片机端的思维模型是统一的。2. 硬件与协议栈选型内置MAC加PHY还是外挂W55002.1 三种主流实现方案对比做网络应用第一件事是选硬件方案。STM32接网络常见的路子有三条各有各的使用场景方案实现方式优点缺点适合场景内置MAC 外部PHYSTM32F407/F429等内置以太网MAC外接LAN8720A/DP83848等PHY芯片跑LwIP协议栈性能好协议栈可控连接数多移植和调试门槛高对PCB走线有要求正经产品、需要自定义协议外挂W5500W5500内部集成TCP/IP协议栈STM32走SPI操作代码简单硬件协议栈稳定最大8个Socket受芯片缓冲区限制快速出Demo小数据量物联网设备ESP8266/ESP32 WiFi透传用AT指令或SDK做TCP Server无线部署方便成本低稳定性一般WiFi环境干扰宿舍小玩意、个人毕设我手头正好有STM32F407VET6最小系统板加LAN8720A模块所以选了第一套。如果你想快速验证W5500也完全可以做多客户端但它的Socket数量有限8路看起来不少一旦有大缓冲需求每路分到的空间就很紧张了。选择内置MAC加PHY主要就是看中它后续扩展空间大真正的TCP服务器写法和PC上几乎一样。2.2 硬件连接与PHY地址注意LAN8720A是RMII接口管脚不多但有两个点非常容易翻车第一是RMII参考时钟。LAN8720A需要50MHz的REF_CLK可以从PHY外部晶振给也可以从STM32的MCO引脚输出。我用的是STM32的MCO1输出50MHz时钟到LAN8720A的CLKINCubeMX里要正确配置PLL和MCO引脚否则PHY完全不起来。第二是PHY地址。LAN8720A的PHY地址由RXER/PHYAD0引脚的电平决定模块上通常拉到低电平也就是地址0。CubeMX配置ETH时PHY Address要填0如果填成1或者其他值驱动读不到PHY状态就会一直Link Down。2.3 调试器连接的一个小经验顺带说一句调试下载的事。如果你用的是ST-Link在Keil里下载时报类似error: no stm32 target found的错先别急着怀疑工程大概率是ST-Link接触不良或者板子没上电。复位线、SWDIO、SWCLK三根线重新插拔一下再把下载速度降到1MHz试试。这个问题和TCP服务器本身没关系但卡在这一步会白耗一下午。3. LwIP移植和网络初始化跑通的第一步3.1 CubeMX里LwIP的关键配置我用STM32CubeMX生成基础工程LwIP选在中间件里打开ETH外设使能后核心配置就几个地方模式RMIIPHY Address0LwIP版本2.1.2或你习惯的稳定版IP模式静态IPDemo里给板子固定192.168.1.100网关掩码255.255.255.0网关地址192.168.1.1需要说明的是LwIP这套协议栈本身跑的是TCP/IP协议不涉及任何代理转发就是标准的以太网通信。它解决的问题是MCU内存小、CPU频率低没法直接跑完整BSD协议栈的问题所以用轻量化实现裁剪出一套可用的TCP/IP内核。3.2 多客户端相关的内存参数调整很多人移植完LwIP单客户端测试一切正常一接入多个客户端就开始丢包甚至连接不上。原因几乎都是内存配置没跟上。在CubeMX的LwIP配置里有这几个参数和连接数直接相关MEMP_NUM_TCP_SEG同时能缓存的TCP报文段数量默认值比较保守多客户端时建议调大MEMP_NUM_NETCONN同时活动的网络连接对象数量按“预计最大客户端数2”来设PBUF_POOL_SIZE收发包缓冲池大小直接影响吞吐TCP_MSS最大报文段长度默认1460走局域网可以不调TCP_WND接收窗口如果要做大量数据广播比默认值大一倍会明显改善我的Demo里最大客户端设成4个MEMP_NUM_NETCONN填6MEMP_NUM_TCP_SEG填32PBUF_POOL_SIZE填24实测五六个连接同时涌进来也稳。3.3 初始化代码和链接状态处理CubeMX生成的main函数里会自动调用MX_LWIP_Init()之后你要做的是周期性调用lwip_pkt_handle()这类系统处理函数。如果用CubeMX生成的模板它已经把MX_LWIP_Process()放在main loop里了不需要手动再管。建议上电后加一个IP获取成功的打印void ethernet_status_check(void) { struct netif *net_if netif_find(eth0); if (net_if netif_is_link_up(net_if)) { printf(IP address: %s\r\n, ipaddr_ntoa(net_if-ip_addr)); } }如果是静态IP检查一次就够了。确认PC能ping通192.168.1.100网络这层就算通了然后再进入服务器代码的编写。4. 多客户端连接的核心Socket链路与select轮询设计4.1 从bind到accept的完整链路LwIP提供了Socket API配合FreeRTOS可以像在PC上一样写网络应用。TCP服务器的第一步是建套接字、绑定端口、开启监听struct sockaddr_in server_addr; int server_fd socket(AF_INET, SOCK_STREAM, 0); if (server_fd 0) { printf(socket create err\r\n); return -1; } server_addr.sin_family AF_INET; server_addr.sin_addr.s_addr htonl(INADDR_ANY); server_addr.sin_port htons(8080); if (bind(server_fd, (struct sockaddr *)server_addr, sizeof(server_addr)) 0) { printf(bind err\r\n); return -1; } if (listen(server_fd, 5) 0) { printf(listen err\r\n); return -1; }listen的第二个参数是最大等待连接数可以理解成排队的长度。STM32资源有限设成5就够用再多没有意义因为后面的内存参数不支撑。4.2 为什么多客户端不能用阻塞式accept新手最容易踩的坑是这么写int client_fd accept(server_fd, ...); recv(client_fd, ...); // 然后就被阻塞在这里第二个客户端进不来单客户端时没问题多客户端就不行了。因为accept和recv一旦阻塞CPU就卡住了ST32没有能力再去监听新连接。解决办法是改用select机制。select的作用是同时监听多个socket哪个有事件处理哪个没有事件就超时返回这样单片机就能腾出手来轮询所有连接。4.3 select模式管理多客户端的完整代码先定义一个客户端连接表#define MAX_CLIENTS 4 static int client_fds[MAX_CLIENTS]; static int client_count 0; void client_fds_init(void) { for (int i 0; i MAX_CLIENTS; i) { client_fds[i] -1; } }然后在主循环里轮询所有socketfd_set read_fds; struct timeval tv {1, 0}; // 超时1秒 while (1) { FD_ZERO(read_fds); int max_fd server_fd; FD_SET(server_fd, read_fds); for (int i 0; i MAX_CLIENTS; i) { if (client_fds[i] 0) { FD_SET(client_fds[i], read_fds); if (client_fds[i] max_fd) max_fd client_fds[i]; } } int act select(max_fd 1, read_fds, NULL, NULL, tv); if (act 0) continue; // 有新客户端连接 if (FD_ISSET(server_fd, read_fds)) { struct sockaddr_in client_addr; socklen_t client_len sizeof(client_addr); int new_fd accept(server_fd, (struct sockaddr *)client_addr, client_len); printf(new connect: %d\r\n, new_fd); if (client_count MAX_CLIENTS) { client_fds[client_count] new_fd; client_count; } else { printf(max clients, reject\r\n); close(new_fd); } } // 检查已有客户端是否有数据 for (int i 0; i MAX_CLIENTS; i) { if (client_fds[i] 0 FD_ISSET(client_fds[i], read_fds)) { uint8_t buf[512]; int len recv(client_fds[i], buf, sizeof(buf), 0); if (len 0) { broadcast_data(client_fds[i], buf, len); } else if (len 0) { printf(client %d closed\r\n, client_fds[i]); close(client_fds[i]); client_fds[i] -1; client_count--; } } } }注意select的第一个参数是最大文件描述符加1这一行写错会连“同一个socket反复触发事件”这种怪问题都会出现。LwIP内部虽然和PC的select实现方式不一样但语义是统一的所以写法和Windows/Linux下几乎完全一致。4.4 广播函数一个客户端发所有客户端收Demo的亮点是数据分发。我这里实现了一个简单广播收到任何客户端的数据就转发给其他所有客户端这样每台电脑都能看到相同的内容void broadcast_data(int src_fd, uint8_t *data, int len) { for (int i 0; i MAX_CLIENTS; i) { if (client_fds[i] 0 client_fds[i] ! src_fd) { send(client_fds[i], data, len, 0); } } }实际项目里你可以把src_fd来源这一路的客户端排除掉也可以让发送者也收到自己的数据看需求。测试的时候用两台PC各自发一条消息会发现对面都收到同样的内容。5. PC端压力测试网络调试助手加Python同时开连5.1 Windows防火墙先放行端口第一次用PC连接STM32服务器如果连接直接超时十有八九是Windows防火墙拦了TCP入站连接。最简单的方法是在测试工具首次弹窗时勾选“专用网络”或者手动入站规则放行8080端口。这块和程序bug没关系但要先排除掉。5.2 网络调试助手验证基本收发我最先用的是以太网调试助手之类的TCP客户端工具填写IP192.168.1.100、端口8080点连接连上后发一条消息。如果板子上串口打印了new connect说明accept逻辑正常。接着再开第二个调试助手窗口同样连接同一个IP端口观察两个客户端是否都处于“已连接”状态用客户端A发消息看客户端B是否收到拔掉其中一个客户端的网线或强制关闭软件看另一个客户端是否还能正常收发这一步能把最基本的多连接、广播、断线检测全部验证一遍。如果强制关掉软件后板子长时间不响应新连接说明断线检测有问题后面第6章再展开。5.3 Python脚本模拟更多并发客户端调试助手适合人工验证但想模拟多个客户端并发连接还是Python更快。下面这段脚本创建5个线程同时连接板子每个线程发一条消息后等服务器广播import socket import threading import time SERVER_IP 192.168.1.100 SERVER_PORT 8080 CLIENT_NUM 5 def client_worker(idx): try: s socket.socket(socket.AF_INET, socket.SOCK_STREAM) s.settimeout(5) s.connect((SERVER_IP, SERVER_PORT)) msg fhello from pc-{idx}.encode() s.send(msg) data s.recv(1024) print(f[{idx}] recv: {data.decode()}) time.sleep(1) s.close() except Exception as e: print(f[{idx}] error: {e}) threads [] for i in range(CLIENT_NUM): t threading.Thread(targetclient_worker, args(i,)) t.start() threads.append(t) for t in threads: t.join() print(all clients done)运行结果里每个线程都打印出了收到的广播数据说明服务器确实同时维护了5个TCP连接并完成了数据分发。这个脚本我强烈建议保留以后改任何服务端逻辑跑一遍就知道有没有把多连接搞坏。5.4 连接数超过上限时的表现由于代码里设了MAX_CLIENTS 4当第5个客户端尝试连接时会被直接close。PC端表现是连接正常建立后又马上断开。这个行为是故意的防止内存被耗尽后系统崩溃。实际项目里你可以改成拒绝提示或者替换最早的空闲连接思路都一样。6. 实测踩坑记录异常断开、内存不足与防火墙拦截6.1 客户端异常断开服务器为什么收不到消息踩的第一个坑用调试助手连接后直接关闭整个软件板子几乎立刻打印client closed。但如果是拔网线或者电脑休眠recv返回的不是0而是一直阻塞。原因在于TCP协议本身的设计对端异常消失时本端不会立刻感知需要等TCP超时重传机制兜底。解决办法有两个方向。第一是给socket设置超时比如每3秒检查一次struct timeval timeout {3, 0}; setsockopt(client_fds[i], SOL_SOCKET, SO_RCVTIMEO, timeout, sizeof(timeout));这样recv最多阻塞3秒就返回-1配合errno是EWOULDBLOCK的话就继续等待相当于用超时做心跳。第二个方案是应用层心跳板子每5秒给所有客户端发一个特定的心跳字节客户端断开后send会失败从而清理连接。6.2 多连接后ping不通先查LwIP内存参数跑并发测试时发现一个现象前3个客户端正常第4个客户端一连上整个网络都卡了连ping板子都有延时。查下来不是CPU占用问题而是LwIP的内存池被连接占光了。特别是MEMP_NUM_TCP_SEG和PBUF_POOL_SIZE这两个值单个连接发送大数据时就会吃紧多连接情况更是立竿见影。后来把这两个参数分别在CubeMX里调大重新生成代码问题消失。如果你懒得精细调可以先用一个保守的经验值MEMP_NUM_TCP_SEG不小于32PBUF_POOL_SIZE不小于20MEMP_NUM_NETCONN不小于最大连接数加2。这几个值会多占一点RAM但换来的是稳定性。6.3 select轮询的高占用问题还有一个容易被忽略的点select的timeval如果设成0会变成纯忙轮询CPU占用极高。我最初为了响应快设置的是0秒0微秒结果板子温度都上去了。后来改成1秒超时体验几乎没有差别因为连接少的时候本来就没有频繁事件有事件时会立刻唤醒。6.4 数据粘包与DEMO边界最后说一个很多人测完都会问的问题客户端一帧数据超过512字节怎么办recv在LwIP上最多返回当前缓冲区的数据一帧大消息可能被拆成多次recv。Demo里没有做组包处理因为目标就是演示多连接。如果你要传输结构化数据就要定义帧头帧尾或者长度字段自己实现粘包拆包逻辑。这一步并不复杂但属于协议设计范畴和TCP服务器本身是两件事。回头来看整个Demo从硬件配置到跑通多客户端连接最难的部分不在代码量而在理解“阻塞与非阻塞”“单连接与多连接”这些网络编程思维模式的转换。LwIP的Socket API把复杂度屏蔽了不少底层的内存和协议栈配置才是单片机和PC开发的真正分水岭。希望这篇能帮你跳过我已经踩平的坑直接把精力放在自己的业务逻辑上。本文还有配套的精品资源点击获取
返回列表