ARTICLE DETAIL

资讯详情

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

TCP/UDP多客户端处理核心:从epoll事件循环到对端表管理

TCP/UDP多客户端处理核心:从epoll事件循环到对端表管理 简介面向多客户端网络通信场景的C/Qt工程代码包围绕服务器同时维持多客户端连接、收发与转发消息展开并配套XML报文解析与生成、定时发送、多包发送及多线程发送等实现模块适合有一定网络编程基础的开发者参考学习。包体共一百一十五个文件以cpp源文件、h头文件为主还包含pro工程文件、makefile构建脚本、ini与user配置以及o编译目标文件压缩包仅二百九十一KB目录按tcpServer、tcpClient、tool等模块划分便于按层次阅读。已有九百三十一人学习下载可作为快速搭建多客户端通信原型的参考资料。从代码结构与内容预览看客户端界面、服务器界面、XML解析、多任务管理等关键部分均有对应实现可借此理解TCP连接管理与UDP广播在Qt框架中的落地方式同时借鉴XML消息构造、定时任务与多包发送的工程化写法对设计高并发网络应用具有参考价值。1. 多客户端一多就乱套先看连接管理和事件驱动这两条路做上位机、网关或者设备服务端的人大概率都经历过这个场景第一版只对接一台设备程序写得很顺等现场真的把几十个 TCP 客户端接上来或者几个 UDP 终端同时往同一端口灌数据服务端要么卡死、要么丢包、要么连接数一多 accept 都报错。这个标题要解决的就是“很多个客户端同时连过来服务端怎么收、怎么回、怎么不互相干扰”这整件事。先说结论TCP 的多客户端核心难点在“连接管理”——谁来了、谁走了、谁一直占着资源不吭声UDP 的多客户端核心难点在“对端表”和缓冲——包本身没有连接概念但你得替每个远端维护“它还在不在”。新手最容易犯的错是用多线程一连接一线程去堆 TCP结果几百个连接一上来上下文切换直接打满 CPU老手则知道Linux 下用 epoll 做事件循环UDP 用对端表加超时清理才是经得起现场考验的处理方式。这篇文章就把这两套方案的原理、骨架代码和踩坑记录一次讲透适合正在写服务端程序、想把 C10K 这条路走通的人。2. 先把 TCP 和 UDP 的“多客户端”拆开连接模型与处理路径差异2.1 TCP 的多客户端是“连接管理”UDP 的多客户端是“对端表”TCP 是面向连接的协议。一个客户端连上来服务端 accept 之后拿到一个独立的 socket 文件描述符后续的收发都通过这个 fd 完成。所以 TCP 多客户端处理的本质是管理一组 fd哪些 fd 有数据可读、哪些 fd 可以写、哪些 fd 很久没动静该清理、哪些 fd 被对端重置了要回收。数据在 TCP 里是字节流没有消息边界还得自己做分包这是另一层麻烦。UDP 完全不同。服务端只 bind 一个端口不需要 accept 任何东西每个客户端发来的包都从同一个 socket 口进来你只能靠 recvfrom 返回的对端地址来区分“这是谁发的”。所以 UDP 多客户端处理的本质是维护一张对端表key 是 ipportvalue 是业务上下文和最近活跃时间。连接不是你建立的而是你“记录”下来的。这也是为什么很多用 UDP 做设备接入的网关内部都会把“收到过包的源地址”当成一个虚拟连接来管理超时没有新包到达就判定这个远端离线。2.2 两个最小骨架先跑通收发再谈并发很多文章一上来就讲 epoll、讲多线程其实第一步应该先让单客户端收发跑通你才分得清“数据没收到”是协议栈问题还是并发模型的问题。TCP 侧最朴素的服务端骨架长这样int lfd socket(AF_INET, SOCK_STREAM, 0); bind(lfd, (struct sockaddr *)addr, sizeof(addr)); listen(lfd, 128); /* backlog 128半连接队列上限 */ while (1) { int cfd accept(lfd, NULL, NULL); /* 阻塞在这里等新连接 */ while (1) { int n read(cfd, buf, sizeof(buf)); /* 阻塞读 */ if (n 0) break; write(cfd, buf, n); /* 原样回显 */ } close(cfd); }这段代码只能服务一个客户端因为第二个 while 把整个进程卡住了。但它把 TCP 的模型讲得很清楚每个客户端需要一个专门的 fd你的任务就是找到一种方式让所有 fd 都能被同时照顾到。UDP 侧的最小骨架则精简得多int sfd socket(AF_INET, SOCK_DGRAM, 0); bind(sfd, (struct sockaddr *)addr, sizeof(addr)); while (1) { int n recvfrom(sfd, buf, sizeof(buf), 0, (struct sockaddr *)cli_addr, cli_len); sendto(sfd, buf, n, 0, (struct sockaddr *)cli_addr, cli_len); /* 回给同一个源地址 */ }这里没有 accept没有 per-client fd所有客户端共享一个 socket。你只需要把 cli_addr 当成一个识别标识它代表“这个包是 10.0.0.5:40001 发来的”。注意 recvfrom 的最后一个参数在调用前要初始化成 sizeof(cli_addr)否则内核可能写越界这是刚写 UDP 服务端最容易翻车的地方。2.3 选型判断丢包容忍度、消息量、实时性明确了两种模型的差异选型就不再是“TCP 更可靠所以无脑用 TCP”。我一般看三个维度。第一是丢包容忍度允许少量数据丢失、且每条消息独立可解析的比如设备状态上报、心跳包UDP 更省资源要求不丢包、要确认的比如文件下发、命令控制老老实实走 TCP。第二是消息量大量小包高频上报UDP 头开销小、省连接资源一个 socket 全收了但消息量大又要求有序处理得回到 TCP。第三是实时性UDP 没有重传和拥塞控制时延抖动小对实时性要求极高的场景有天然优势。还有一点容易被忽略UDP 服务端天然不会因为 accept 队列满而拒绝客户端因为它根本没有 accept 队列TCP 客户端连不上时你还能看到半连接队列爆满的数据UDP 只会默默丢包抗冲击的能力反而好一些。3. TCP 多客户端处理用 epoll 把 accept、read、write 收进一个事件循环3.1 每连接一个线程为什么先翻车句柄、上下文切换、锁单线程阻塞式 accept 只能服务一个客户端很多人第一时间想到的方案是“来一个客户端就建一个线程”。简单场景确实没问题四个八个客户端跑得挺好。现场一上量问题就来了。首先是句柄压力。一个线程默认栈要 8MB 虚拟内存1000 个线程光栈就预占了 8GB 虚拟地址空间虽然多数没被实际访问但内核调度器的负担是真的。更重要的是上下文切换1000 个线程里大部分都在阻塞等读一旦网络数据到达调度器要把对应线程唤醒、抢 CPU、切进去读几个字节然后又切走。CPU 时间大半花在切换上真正的业务逻辑反而跑不动。再加上锁竞争——多个线程同时操作共享缓冲或者业务状态锁粒度放粗了吞吐掉得厉害放细了写起来极其痛苦。你是在用“线程的数量”掩盖“IO 模型不当”的问题。3.2 epoll 关键参数LT/ET、EPOLLRDHUP、backlogLinux 下处理多客户端现成且可靠的手段就是 epoll。它的思路是让内核帮你盯着所有 fd哪个 fd 有读事件、写事件、对端关闭事件它只把活跃的 fd 放进返回数组你的进程单线程遍历这个数组逐个处理。核心参数有三个需要真的理解。第一个是事件模式。默认水平触发 LT只要 fd 还有数据没读完每次 epoll_wait 都会返回它你不用一次读完处理起来不容易漏边缘触发 ET只在“空→非空”这个变化的瞬间上报一次要求你必须把数据读干净否则剩下的数据要等下一次新数据到达才再触发容易卡住所以使用 ET 的前提是被监控 fd 必须是非阻塞并且循环读直到 EAGAIN。新手我建议先用 LT把逻辑跑通再考虑 ET 省那点系统调用。第二个是 EPOLLRDHUP对端关闭连接正常 close 或半关闭时这个标志会被置位比 EPOLLHUP 更早拿到关闭通知能让你及时清理连接不会等到读写报错才发现对面的设备已经下线。第三个参数不是 epoll 的而是 listen 的 backlog它决定内核里等待 accept 的半连接与全连接队列能装多少个默认值太小比如 128时大量客户端同时 connect 会把队列塞爆新连接直接被内核拒绝。服务端设计目标连接数越大backlog 要相应调大。3.3 非阻塞 epoll 多客户端骨架与四个必调参数下面这段是我常用的最小骨架去掉业务细节保留了多客户端处理的关键路径。注意几个重点监听 fd 和非阻塞 fd 都放进同一个 epoll 实例读事件来了就循环 accept 直到 EAGAIN连接 fd 设置非阻塞并加入 EPOLLIN 与 EPOLLRDHUP其他事件统一按关闭处理。#include stdio.h #include stdlib.h #include string.h #include unistd.h #include fcntl.h #include errno.h #include sys/epoll.h #include sys/socket.h #include netinet/in.h #define MAX_EVENTS 1024 #define PORT 9000 static int set_nonblock(int fd) { int flags fcntl(fd, F_GETFL, 0); return fcntl(fd, F_SETFL, flags | O_NONBLOCK); } int main(void) { int lfd socket(AF_INET, SOCK_STREAM, 0); int opt 1; setsockopt(lfd, SOL_SOCKET, SO_REUSEADDR, opt, sizeof(opt)); struct sockaddr_in addr { .sin_family AF_INET, .sin_port htons(PORT), .sin_addr.s_addr htonl(INADDR_ANY) }; bind(lfd, (struct sockaddr *)addr, sizeof(addr)); listen(lfd, 1024); /* backlog 调大扛突发连接 */ set_nonblock(lfd); int ep epoll_create1(0); struct epoll_event ev {.events EPOLLIN, .data.fd lfd}; epoll_ctl(ep, EPOLL_CTL_ADD, lfd, ev); struct epoll_event evs[MAX_EVENTS]; char buf[4096]; while (1) { int n epoll_wait(ep, evs, MAX_EVENTS, 1000); /* 超时 1s便于定期做清理 */ for (int i 0; i n; i) { int fd evs[i].data.fd; if (fd lfd) { /* 新连接accept 到 EAGAIN避免 LT 模式反复唤醒 */ while (1) { int cfd accept(lfd, NULL, NULL); if (cfd -1) break; set_nonblock(cfd); struct epoll_event cev { .events EPOLLIN | EPOLLRDHUP, .data.fd cfd }; epoll_ctl(ep, EPOLL_CTL_ADD, cfd, cev); } } else { /* 普通客户端事件 */ if (evs[i].events (EPOLLIN | EPOLLRDHUP)) { int r read(fd, buf, sizeof(buf)); if (r 0) { /* 收到数据透传或解析后按需挂 EPOLLOUT 再异步写回 */ write(fd, buf, r); /* 简单回显演示用 */ } else if (r 0 || errno ECONNRESET) { /* 对端关闭或异常重置清理 fd */ epoll_ctl(ep, EPOLL_CTL_DEL, fd, NULL); close(fd); } /* r 0 且 errno EAGAIN本轮数据读完正常 */ } } } /* 这里的 1s 超时恰好用来跑连接超时检查见避坑章节 */ } return 0; }逻辑说明监听 fd 只注册 EPOLLIN有新连接到来时触发循环 accept 把待处理的连接一次取完避免客户端同时在线的数量超过 accept 队列后排长队。普通客户端 fd 注册 EPOLLIN 和 EPOLLRDHUPread 返回 0 说明对端正常关闭ECONNRESET 说明对端没关 socket 直接没了两种情况都要从 epoll 里摘掉并 close否则 fd 泄漏。读数据返回 EAGAIN 不是错误是告诉你在非阻塞模式下本轮可读数据已经读完这对后续切换 ET 模式尤其重要。参数说明里最值得调的是四个listen 的 backlog 影响客户端突发连接数现场 500 台设备同时重连时 backlog1024 比默认值稳健得多epoll_wait 的超时参数不设 -1 而设 1000ms是为了定期执行连接超时扫描不会永远堵在等待事件连接 fd 只注册 EPOLLIN 不注册 EPOLLOUT是避免“一直可写”的 fd 让 epoll_wait 空转刷 CPU写操作应该在有数据要发时临时注册 EPOLLOUT写完再摘掉SO_REUSEADDR 解决服务端重启后 TIME_WAIT 端口占用导致的 bind 失败这个基本每个服务端都要加。这套模型单线程就能扛几千个连接因为工作的代价只和活跃连接数相关空连接不消耗 CPU。4. UDP 多客户端处理用对端表把无连接改成“准连接”4.1 UDP 不需要 acceptrecvfrom 返回的 addr 就是客户端 ID很多从 TCP 转过来的人会下意识找“UDP 的 accept”然后发现根本没有这个接口于是慌了。其实 UDP 的处理模型比 TCP 简单内核不维护每个远端的连接状态谁来都从同一个 socket 进recvfrom 的第二个返回参数会给你一个 sockaddr_in这里面的 ip 和 port 合在一起就是天然的客户端标识。你要做的是把这张标识表在应用层建起来。服务端收到一条数据先提着 src addr 去表里查查到了说明是老朋友更新最近活跃时间没查到说明来了个新对端分配一段业务上下文塞进表里。这张表解决了两件事一是让你能够按“连接”的粒度做业务处理比如给每个设备维护自己的序列号、超时计数、下发状态二是让你能够在宕机恢复或者对端重启之后通过超时机制把已经不存在的设备清理出内存。TCP 的连接是内核帮你记的UDP 的连接是你自己一笔一笔记的这是整个 UDP 多客户端处理的认知分水岭。4.2 对端表怎么设计addr 作 keylast_active 作心跳对端表的结构要处理三个问题用什么做 key、多久判定离线、满了怎么办。key 用 ipport 拼接成字符串或者用一个 64 位整数把 ipv4 和 port 压进去都行千万不要只用 ip因为同一个 NAT 出口下多台设备会共用公网 ipport 才是区分它们的标志。离线判定用一个 last_active 时间戳每次收到这个对端的包就刷新服务端的后台清理任务每几秒扫一遍表把超过阈值的条目删除。阈值怎么设取决于业务心跳周期心跳 5 秒一次的话30 秒没有包基本可以判定掉线心跳本身没保证的话阈值就要放大一个数量级。表满处理是个容易被忽略的边界。对端地址理论上有 6 万多个端口可变化但业务里不可能存这么多。我会给表设一个上限满了之后优先淘汰最久没有数据的条目其次淘汰发送频率最低的而不是直接拒绝新对端。UDP 的好处在于一次收不到也不会“断连”新对端的包先收下如果表满了就腾一个位置给它业务层顶多少掉一个离线设备的上下文。4.3 UDP 多对端收发骨架一张哈希表加一次循环收包UDP 多客户端处理没有并发模型的问题单线程循环收包就够了。下面这个骨架用宏和结构体示意对端表的基本操作重点在于理解“收到包 - 找对端 - 更新活跃时间 - 处理业务”这条链路。#include stdio.h #include string.h #include time.h #include sys/socket.h #include netinet/in.h #include arpa/inet.h #define MAX_PEERS 256 #define PEER_TIMEOUT 30 struct peer { struct sockaddr_in addr; /* 客户端地址作为身份标识 */ time_t last_active; /* 最后一次收包时间 */ int in_use; }; static struct peer peers[MAX_PEERS]; static struct peer *find_or_alloc_peer(struct sockaddr_in *addr) { for (int i 0; i MAX_PEERS; i) { if (peers[i].in_use memcmp(peers[i].addr, addr, sizeof(*addr)) 0) return peers[i]; } /* 没找到分配一个新条目满了就覆盖最老的 */ struct peer *victim NULL; time_t oldest (time_t)-1; for (int i 0; i MAX_PEERS; i) { if (!peers[i].in_use) { victim peers[i]; break; } if (peers[i].last_active oldest) { oldest peers[i].last_active; victim peers[i]; } } memcpy(victim-addr, addr, sizeof(*addr)); victim-in_use 1; return victim; } int main(void) { int sfd socket(AF_INET, SOCK_DGRAM, 0); int rcvbuf 4 * 1024 * 1024; /* 接收缓冲区调大到 4MB减少协议栈丢包 */ setsockopt(sfd, SOL_SOCKET, SO_RCVBUF, rcvbuf, sizeof(rcvbuf)); struct sockaddr_in addr { .sin_family AF_INET, .sin_port htons(9001), .sin_addr.s_addr htonl(INADDR_ANY) }; bind(sfd, (struct sockaddr *)addr, sizeof(addr)); char buf[2048]; struct sockaddr_in cli_addr; socklen_t cli_len sizeof(cli_addr); while (1) { /* recvfrom 会告诉我们“这个包是谁发的” */ int n recvfrom(sfd, buf, sizeof(buf), 0, (struct sockaddr *)cli_addr, cli_len); if (n 0) continue; struct peer *p find_or_alloc_peer(cli_addr); p-last_active time(NULL); /* 业务处理解析 buf必要时 sendto 回包给 cli_addr */ /* 定期清理超时对端这里每次循环顺带检查一次 */ time_t now time(NULL); for (int i 0; i MAX_PEERS; i) { if (peers[i].in_use (now - peers[i].last_active) PEER_TIMEOUT) { peers[i].in_use 0; printf(peer offline: %s\n, inet_ntoa(peers[i].addr.sin_addr)); } } } return 0; }逻辑说明find_or_alloc_peer 先把来源地址和表里的条目逐条比对找到直接复用找不到就分配空位表满则覆盖最老的条目这样新客户端永远有机会接入。recvfrom 的 cli_addr 在每次调用前重新设 cli_len是因为某些内核实现在返回时会修改这个值。超时清理用时间差判断不走信号或者定时器在这个单线程骨架里最省事。参数说明里有三个值得细说。SO_RCVBUF 调大是 UDP 高吞吐场景必备内核接收队列满的时候会直接丢包而且不会通知应用层你把缓冲区从默认值调到 4MB 甚至 8MB是成本最低的抗突发手段。recvfrom 的缓冲区长度要按业务最大消息体设置图中是 2048如果设备上报的包超过这个长度数据会被静默截断这个 bug 极难排查。对端表上限 MAX_PEERS 要参考现场设备数量256 不够就调大但不要盲目设到几十万每次收包都在表里做线性查找表太大要换哈希或者红黑树别为省事牺牲 CPU。4.4 要不要调用 connect常见做法与边界UDP 的 connect 和 TCP 的 connect 完全是两回事。UDP 调用 connect 不会触发网络握手它只是在本地把“默认对端地址”记下来之后可以省略 sendto 的地址参数直接 write。这个操作有两个好处一是内核只接受这个地址发来的包其他来源的包直接丢弃相当于在内核里做了一层过滤二是性能上略好一点省去每次发送时重新绑定目的地址的路径查找。缺点是只能跟一个对端通信多客户端场景想保留这个过滤能力就得为每个客户端创建一个 socketfd 开销上来了。我的常见做法是设备数量固定且 IP 已知的工业现场用 connect 方式每台设备一个 socket配合 poll 监听设备动态上下线、对端来自 IP 池或者 NAT 的不使用 connect走上面的对端表方案。后者更通用因为一个 socket 吃下所有远端不用管 fd 数量。5. 避坑与排查连接丢掉、收包丢失、CPU 打满的五个真实原因5.1 accept 报 EMFILE新客户端接不进来现象服务端运行一段时间后新设备怎么都连不上日志里 accept 返回 -1errno 是 EMFILE。老连接看上去都正常但新连接就被挡在外面。原因进程的文件描述符触及上限。Linux 默认单进程允许打开的 fd 数量是 1024服务端套接字、日志文件、配置文件句柄都要占名额每个 TCP 客户端至少吃掉 1 个 fd连到几百个就顶到墙了内核直接拒绝继续创建新的 fd。解决临时生效执行ulimit -n 65535再启动服务进程正式环境在 systemd 服务配置里加LimitNOFILE65535。另外 epoll 本身也算一个 fd但它不随连接数增长不用单独考虑。这里的关键是改完之后要重启服务进程才生效不是改完就立刻能用。5.2 UDP 收包丢包服务端毫无感知现象客户端明明发了数据应用层就是收不到或者收不全但客户端那边没有任何发送报错。你用 tcpdump 抓包发现包确实到了网卡。原因UDP 是“发了就不管”的协议丢包发生地可能在网卡队列、协议栈接收缓冲区或者应用层 recvfrom 调用不够快。最典型的是内核接收缓冲区被打满之前的包还没被 read 走新包直接把最老的挤掉了。解决第一步调大套接字接收缓冲区setsockopt(SOL_SOCKET, SO_RCVBUF)同时用sysctl net.core.rmem_max放开内核允许的最大值否则你设置多大都没用第二步看应用层消费速度有没有可能在 recvfrom 里做了过重的处理导致没有及时进入下一次读取第三步用网卡统计确认ethtool -S eth0看 rx_dropped 和 rx_missed这两个指标能区分是协议栈丢的还是驱动丢的。排除这类问题别靠猜上面三个证据链全看一遍。5.3 TCP 连接偶发 RST内核 timestamp 校验惹的祸现象客户端连接在运行数小时后忽然被重置断开重连又正常。抓包发现对端回了一个 RST而且这个时间点前后没有业务逻辑的主动断开调用。原因这是一个 Linux TCP 协议栈时间戳timestamps选项引发的经典问题。连接协商启用了 TCP 时间戳后内核用这个时间戳做 PAWS 保护防止旧连接的延迟包污染新连接。一旦后端服务从一台机器迁移到另一台或经过 NAT 地址映射发生变化TCP 时间戳的连续性被打破新握手包可能被判定为旧的重复包直接丢弃客户端表现就是连接建立失败或稳定后 RST。解决先确认是不是这个原因临时关闭内核时间戳看现象是否消失sysctl -w net.ipv4.tcp_timestamps0Windows 客户端侧也有对应开关常见做法是在管理员命令行执行netsh int tcp set global timestampsdisabled。注意这只是一个排查手段时间戳选项在高速长链路下对性能有实际作用生产环境要权衡不要因为一次偶发问题就长期关闭。只要确认是搬迁或 NAT 引发的更稳的处理是让连接重试带退避而不是裸奔关闭内核保护。5.4 TCP 粘包半包消息边界被弄丢了现象服务端收到的数据一会儿多条消息粘在一起一会儿一条消息被拆成两半。客户端一次 send 了 100 字节服务端一次 read 出 240 字节。原因TCP 是字节流协议不保证 send 一次就对应 read 一次。内核只保证字节顺序不保证消息边界。粘包是因为多次发送的数据被合并成一段放进接收队列半包是因为 send 的数据还没到齐应用层已经读走了一部分。解决必须在应用层定义消息边界。常见做法有两种一是固定长度消息所有包都是 64 字节读满一个定长就能解析但业务扩容比较痛苦二是包头加负载包头里固定前 4 字节放消息总长度服务端先读 4 字节得知负载大小再接着读对应的字节数读不够就缓存攒够一个完整消息再处理。做 Modbus TCP 这类已经有工业协议的场景协议本身带了长度字段按协议解析就行自研协议一定要在一开始就设计好这个字段等现场跑起来再改是牵一发动全身的活。5.5 ET 模式卡死读不完数据事件不再触发现象切换到 epoll 的 EPOLLET 模式后处理完一批数据就再也收不到后续事件了CPU 也不高客户端还在发数据服务端像死了一样。抓包发现内核其实收到了。原因ET 模式只在文件描述符从无数据变为有数据的那个边沿触发一次应用层必须在这个事件里把数据全部读走读到 EAGAIN。如果业务处理逻辑中途 break 出来或者只读了一次就返回事件循环剩下的数据一直留在缓冲区里但永远不会再有新的边沿事件产生了。解决ET 模式必须配合非阻塞 fd 和循环读read 返回 EAGAIN 才结束本轮读取。另外要注意 EPOLLOUT 的注册时机写事件同样只在缓冲区从满变为不满时触发一次不能依赖它持续通知可写。新手我建议开局就用 LT少一个坑确认要上 ET 减系统调用先跑双客户端多数据量的压测别拿单连接测测不出来的。6. 验证与压测用 iperf3 和并发脚本检验多客户端处理能力6.1 用 iperf3 的 UDP 模式验证吞吐与丢包写完服务端别拿“手机连一次收发成功”当验收标准那只能证明你的代码能跑不能证明你能扛多客户端。我每次都用 iperf3 做 UDP 打流先起服务端iperf3 -s -u -i 1客户端这边同时开多条流模拟多个 UDP 对端同时灌数据iperf3 -c 127.0.0.1 -u -b 100M -l 1400 -P 4 -t 60-u指定 UDP-b 100M是目标带宽-l 1400是每个包大小-P 4表示用 4 条独立数据流。跑完后重点看输出的Lost/Total Datagrams比例这一项在服务端和应用层之间加了缓冲的情况下正常情况下应该接近 0如果大量丢失结合避坑章节里的 SO_RCVBUF 检查很快能定位是套接字缓冲问题还是业务处理太慢。UDP 打流能验证你的接收路径扛不扛得住高包速条件允许把-P加到 16 条流顺带看 CPU 有没有被软中断打满。6.2 用 ss 命令盯住连接表验收 TCP 多客户端TCP 侧我习惯用一段短脚本快速拉起几百个并发连接然后看服务端的连接表是否稳定。下面这个 Python 脚本每 10 毫秒起一个线程去连服务端连上后保持 3 秒再关模拟设备批量上下线import socket import threading import time def client(idx): try: s socket.create_connection((127.0.0.1, 9000), timeout3) s.sendall(bhello %d % idx) time.sleep(3) s.close() except Exception as e: print(client %d failed: %s % (idx, e)) for i in range(300): t threading.Thread(targetclient, args(i,)) t.start() time.sleep(0.01)跑这段脚本的同时另开一个终端看服务端的连接状态ss -sss -s看整体套接字统计重点看establish的数量有没有持续上升到接近预期值再用ss -tnp | grep 9000 | wc -l统计具体端口上的连接数。验收标准有两个一是在并发连接建立的过程中accept 有没有报 EMFILE 或 backlog 溢出二是脚本结束后所有连接是否都进入 CLOSE_WAIT 状态迟迟不释放——如果大量 CLOSE_WAIT 残留说明你的服务端没有正确关闭对端 close 后的 fd这是比丢包更隐蔽的连接泄漏问题。这套验证流程跑完多客户端处理能力基本就测实了。我自己刚做服务端那阵子最深的教训就是“能收到包”和“能处理”是两回事把连接数、句柄数、丢包数三个指标打出来再动代码能省下一大半现场排查的功夫。希望帮到你。本文还有配套的精品资源点击获取
返回列表