尧图网络 高端网站定制 · 原创设计
免费咨询热线
400-888-6620
免费获取方案
服务器持续监听实战:accept循环、backlog与事件驱动
标题里这个13.6看着像课程章节号但内容本身是老话题一个服务器写完了 bind、listen、accept跑起来只能接一个客户端接完就退出或者第二个客户端连上来就卡死。这几乎是每个学网络编程的人都会撞的第一堵墙。所谓让服务器端持续监听客户端的请求说白了就是把一次性的 demo 改造成为一个能长期活着、能反复接客、能同时应付多个连接、还能在出问题时自己扛住的服务端骨架。这件事听着简单——外面套个 while 循环不就完了真动手就会发现坑一个接一个循环里 accept 阻塞住了怎么优雅退出、客户端异常断开会不会把整个服务带崩、连接数一多是不是就得开线程、backlog 填多少才不会被内核截断、端口号 TIME_WAIT 状态下重启用不了怎么办、读了半包数据算不算一次完整请求。这篇东西写给两类人看刚学完 socket 基础、手上有个只能连一次的服务器想让它活下去的新手以及写过几版服务端、但总觉得自己的实现在高并发或者异常场景下不太稳、想补齐系统性认知的人。我会从操作系统内核在 listen 和 accept 之间到底做了什么讲起再给出三种截然不同的持续监听实现路线和选型理由然后手把手把代码从最小可用版一路改造成事件驱动版最后把我自己踩过、也见过别人踩的那些坑整理成一张能直接对照排查的表。代码以 Python 为主关键处会点出对应的系统调用语义换成 C 或者 Go 思路完全通用。1. 先搞清楚持续监听到底在持续什么1.1 一个只能接一次客的服务器长什么样大部分人写出来的第一版服务器大概是这样创建 socket、bind 端口、listen、然后 accept 一次、recv 一次、send 回去、close。跑起来测试也没问题用 telnet 连一下能收到回显看起来成功了。但这个程序的行为本质上是一次性的accept 返回之后主线程就进入了处理逻辑处理完直接退出进程。第二个客户端在这期间连上来会在内核的全连接队列里排队等着等到进程退出连接被 RST 掉。稍微聪明一点的改法是加个 for 循环把 accept 包起来这确实是持续监听的最小形态。但这时候会暴露第二个问题循环体的处理逻辑是串行的。第一个客户端连上来之后如果不发数据accept 就不会再被调用第二个客户端永远进不来。这就是典型的能持续监听但不能持续服务。很多人第一次遇到这个现象时会怀疑是防火墙、是端口没开、是自己代码写错了其实原因简单得可笑——你只有一个线程它正卡在 recv 上等第一个客户端说话。这里有个概念必须先掰清楚监听listen是内核在做的事accept 只是把内核已经替你完成的连接取出来。listen 调用一返回内核就开始在这个端口上接收 SYN、完成三次握手、把建好的连接塞进队列这个过程跟你有没有调用 accept 完全无关。所以严格来说持续监听从来不需要你写循环——内核一直在听。你需要循环的是持续取走并处理。注意不要用accept 之后服务器才和客户端建立连接这种理解去做设计。握手是内核完成的accept 拿到的已经是一条 ESTABLISHED 状态的连接。理解这一点后面 backlog 为什么会影响连接成功率、为什么客户端可能连上了但服务器还没反应过来就都能解释通了。1.2 真正要解决的四个问题把需求拆开看让服务器端持续监听其实包含四件互相独立的事混在一起想就会乱。第一件是取连接的循环。也就是外面那层 while保证任意时刻都有人在调用 accept把内核队列里的新连接搬出来。这个循环不能因为某一次 accept 失败就整个退出网络环境里 accept 返回错误是常态比如客户端在握手完成后、你 accept 之前就断开了内核可能返回 ECONNABORTED。第二件是同时服务的并发模型。取出来之后谁来处理是当前线程自己处理串行、交给新线程并发、还是注册到事件循环里等数据可读异步。这个选择直接决定了你能撑多少连接、代码复杂度有多高。第三件是连接的生命周期管理。一个连接什么时候算结束对端 close 了、读超时了、协议层报错了、服务要重启了。每个退出路径都必须把 fd 关掉否则跑几天就 EMFILE打开文件数超限表现是新连接全部失败但老连接还正常非常有迷惑性。第四件是服务自身的可控退出。这一点最容易被忽略。你的 while True 循环卡在 accept 上收到终止信号怎么办默认行为是进程被直接杀掉正在传输的数据丢了客户端收到 RST。稍微讲究一点的服务端都要求优雅停机停止接收新连接、等已有请求处理完、再退出。把这四件事分开想你就不会写出在 accept 循环里直接处理业务逻辑这种把三件事揉成一坨的代码了。2. 从 listen 到 accept内核替你做的那些事2.1 半连接队列与全连接队列的真实分工调用 listen(fd, backlog) 之后内核会为这个监听 socket 维护两个队列这是理解服务端行为的关键。半连接队列SYN queue存放的是收到了 SYN、回了 SYNACK、但还没收到客户端 ACK 的连接状态是 SYN_RECV。这个队列的长度上限跟 backlog 有关系但不是直接等于Linux 上还受内核参数net.ipv4.tcp_max_syn_backlog约束而且实际生效值有一套计算规则。全连接队列accept queue存放的是三次握手已经完成、处于 ESTABLISHED、但应用层还没调用 accept 取走的连接。这个队列的长度上限就是 min(backlog,net.core.somaxconn)。注意这个 min 很关键——你 listen 里传 1024如果系统 somaxconn 是 128老内核默认就是 128那实际只有 128。全连接队列满了会怎样在 Linux 默认配置下net.ipv4.tcp_abort_on_overflow 0内核会悄悄丢弃客户端发来的第三次握手的 ACK。客户端此时认为自己已经连上了开始发数据但服务器这边连接还没进队列数据无人认领客户端会重传重传到一定次数才报错。表现出来就是客户端连上了但是发数据后要卡好几秒才报错这个现象在压测时特别典型很多人会误判成业务代码慢。2.2 backlog 到底填多少才不心虚我给的建议是别纠结填一个大于你预期瞬时并发峰值的数并且顺手把 somaxconn 调上去。理由如下。backlog 的作用是缓冲。正常情况下 accept 循环会很快把连接取走队列里几乎不堆积。真正会堆起来的是两种情况一是流量突发比如秒杀开始那一瞬间二是有个慢操作卡住了 accept 循环比如你在 accept 之后、下一次 accept 之前做了耗时的日志落盘。前者是常态后者是你的代码问题靠调大 backlog 治标不治本。具体数值上我一般这么定场景backlog 建议值配套动作本地开发调试516无越小越容易复现队列满的问题内网微服务128512确认 somaxconn ≥ 该值面向公网的高并发服务10244096somaxconn、tcp_max_syn_backlog 同步调大长连接网关与最大连接数同数量级配合 fd 上限一起调调内核参数的命令很好记临时改用 sysctl -w永久改写进 /etc/sysctl.conf 或者 /etc/sysctl.d/ 下新建一个配置文件# 查看当前值 sysctl net.core.somaxconn sysctl net.ipv4.tcp_max_syn_backlog # 临时调整 sudo sysctl -w net.core.somaxconn4096 sudo sysctl -w net.ipv4.tcp_max_syn_backlog8192还有一个观察队列是否溢出的手段看内核统计netstat -s | grep -i listen # 输出里 Listens 下面的 overflowed 计数就是全连接队列溢出的次数这个数字如果一直在涨说明 accept 循环不够快或者 backlog 确实太小了。这是一个非常实用的排查动作比猜测靠谱得多。2.3 SO_REUSEADDR几乎每个服务端都该设的一项新手最常见的困扰之一把服务停了重启报Address already in use。明明端口没被别的程序占为什么绑不上原因是你的服务器作为主动关闭方或者被动关闭但进入了 TIME_WAIT关闭连接后那条连接的四元组会占用端口一段时间这个状态就是 TIME_WAITLinux 上默认持续 60 秒。TIME_WAIT 存在的意义是保证最后一个 ACK 能重传、以及防止延迟的旧报文污染新连接是有价值的机制不应该去关掉它。正确做法是在 bind 之前设置srv.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)它的语义是允许绑定一个处于 TIME_WAIT 状态的本地地址端口只要没有真正在 LISTEN 的 socket 占用。注意它不解决端口被另一个正在运行的程序占用的问题那种情况还是会报错这是应该的。顺带提一句 SO_REUSEPORT那个是让多个 socket甚至多个进程同时 bind 同一个端口内核做负载均衡是另一回事。有些框架为了多进程模型会用它但它有连接分布不均的问题慎用。3. 三条实现路线选哪条取决于你的天花板在哪3.1 路线一阻塞 IO 加循环 accept最快能跑起来这是所有教材的第一版。特点是代码量极小逻辑一眼看得懂缺点是同一时刻只能服务一个客户端。适合的场景比很多人想的多本地调试工具、内部一次性脚本、连接数固定且极少的场景、以及作为教学起点。不适合任何面向真实用户的服务。一个容易被忽略的细节走这条路的时候accept()返回之前收到中断信号会返回 EINTR。C 语言里必须自己判断 errno 然后继续循环Python 从 3.5 开始PEP 475在标准库层面自动重试你不太会碰到。写 C 的时候如果忘了处理 EINTR表现是服务偶尔莫名其妙退出很难查。3.2 路线二每连接一个线程简单粗暴但有硬顶accept 拿到连接后丢给一个新线程处理主线程立刻回去继续 accept。这样并发问题立刻解决了代码只多了几行。这条路线的问题不在能不能用而在用到多大就不行。线程在 Linux 上是真实的调度实体每个线程默认栈大小 8MB虚拟内存不是物理内存但也是消耗上下文切换在高并发下开销明显。撑几千个连接时你已经开始看到 CPU 花在切换上而不是干活上到上万个基本就到头了。还有一个更隐蔽的坑线程里的异常不会传播到主线程。如果处理线程里抛了个未捕获异常那个连接就静默地死了主线程毫不知情日志里可能什么都没有。所以这条路线的实现里处理函数必须自带 try/except 兜底并且一定要保证 finally 里 close 掉连接。我给它的定位是连接数规模在几百以内、每个连接的处理逻辑里可能有阻塞操作比如查数据库、并且你不打算自己写事件循环的时候用线程是最省心的。典型的内部管理后台、轻量的内部工具服务用这个完全够。3.3 路线三多路复用加事件循环长连接场景的正解这条路把等数据这件事交给内核的 epollLinux/ kqueueBSD、macOS/ IOCPWindows一个线程就能盯住成千上万个连接。Python 里标准库的 selectors 模块封装好了这层差异Windows 上也能跑虽然底层不是 epoll。代价是代码结构变了你不能写读一次、处理、回写这种线性流程了而是要拆成连接建立时注册回调、可读时触发回调、可写时触发回调、关闭时清理。这对刚入门的人是个思维跳跃但只要跨过去后面所有高并发服务器Nginx、Redis、Node.js都是同一套思路。三条路线的对比我整理成一张表选型时直接对号入座维度阻塞循环每连接一线程事件驱动多路复用代码复杂度极低低中等偏高可支撑连接数1数百到数千数万以上上下文切换开销无高极低能否处理阻塞操作能能不能会卡住整个循环典型故障模式卡在 recv 上不接新客线程爆炸、内存增长一个慢回调拖垮全部连接适用场景本地脚本、教学内部工具、连接少网关、推送、长连接4. 手把手从最小版本改造成能扛事的服务端4.1 第一步让循环不崩先给出最小可用的持续监听版本。目标很明确任意多个客户端依次连上来都能收到响应服务不退出。import socket import logging logging.basicConfig(levellogging.INFO, format%(asctime)s %(message)s) HOST 0.0.0.0 PORT 9000 BACKLOG 128 def handle(conn: socket.socket, addr) - None: try: data conn.recv(4096) if not data: logging.info(client %s closed before sending, addr) return conn.sendall(becho: data) except Exception as e: logging.warning(handle error with %s: %s, addr, e) finally: conn.close() def main() - None: srv socket.socket(socket.AF_INET, socket.SOCK_STREAM) srv.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1) srv.bind((HOST, PORT)) srv.listen(BACKLOG) logging.info(listening on %s:%d, HOST, PORT) while True: try: conn, addr srv.accept() except OSError as e: logging.warning(accept failed: %s, e) continue handle(conn, addr) if __name__ __main__: main()几个地方值得单独说。HOST 0.0.0.0表示监听所有网卡。如果你只想让本机访问写127.0.0.1这比在防火墙上做限制更直接测试阶段强烈建议先绑回环地址避免把半成品暴露出去。accept()外面套了 try/except 并且 continue这是不崩的关键。accept 可能因为各种原因失败对端在握手完成后立刻 RST返回 ECONNABORTED、fd 数量耗尽EMFILE、被信号打断EINTR。前两个必须吞掉继续循环不然后面的所有连接都没人接了。handle里用了 try/except/finally。finally 里的 close 是硬性要求任何返回路径都必须经过它。我见过太多因为一处 return 提前退出导致 fd 泄漏的代码。conn.recv(4096)返回空字节串代表对端已经关闭了连接收到 FIN。这个判断不能省否则你会对一个已经关闭的连接一直 send直到出错。这个版本依然只能串行处理第四个客户端连接时要等前一个处理完。但作为理解循环结构的起点它是对的。4.2 第二步给循环加上能被叫停的能力while True加阻塞 accept 的组合有个现实问题程序没法优雅退出。CtrlC 会抛 KeyboardInterrupt但如果你是在 Docker 里跑、或者用 systemd 管理收到的是 SIGTERM默认行为直接终止进程。我的做法是给监听 socket 设一个超时把无限阻塞变成可轮询import signal import socket import threading stop_event threading.Event() def _on_term(signum, frame): stop_event.set() srv.settimeout(1.0) while not stop_event.is_set(): try: conn, addr srv.accept() except socket.timeout: continue # 一秒到了回头看看要不要退出 except OSError: break # 用子线程或注册到事件循环处理 conn超时值选 1 秒是个经验值足够短停机响应及时又足够长不会因为频繁超时唤醒而空耗 CPU。有人设 0.01 秒那是把阻塞 IO 硬生生改成了忙轮询CPU 会难看得很。停机流程还有一步不能省——已经接进来的连接要处理完。简单实现是维护一个活跃连接集合收到停止信号后先关监听 socket不再接新客再等集合清空或者等一个固定的宽限期比如 10 秒到点强关。宽限期这个设计很重要不设的话一个卡住的客户端能让你永远退不掉。顺带说个容易忽略的srv.close()之后内核会立刻释放端口。如果你在同一进程里马上重启服务有 SO_REUSEADDR 就没事没有就等着报端口占用。4.3 第三步换成事件驱动一个线程看住一万个连接这是改造的重头戏。用 selectors 模块写出来的骨架大概长这样import selectors import socket import types sel selectors.DefaultSelector() def accept_wrapper(sock: socket.socket) - None: conn, addr sock.accept() conn.setblocking(False) # 事件驱动必须非阻塞 data types.SimpleNamespace(addraddr, inbb, outbb) sel.register(conn, selectors.EVENT_READ, datadata) def service_connection(key, mask) - None: sock key.fileobj data key.data if mask selectors.EVENT_READ: recv_data sock.recv(4096) if recv_data: data.outb recv_data sel.modify(sock, selectors.EVENT_READ | selectors.EVENT_WRITE, data) else: # 对端关闭必须注销并关闭 sel.unregister(sock) sock.close() if mask selectors.EVENT_WRITE and data.outb: sent sock.send(data.outb) data.outb data.outb[sent:] if not data.outb: sel.modify(sock, selectors.EVENT_READ, data) lsock socket.socket(socket.AF_INET, socket.SOCK_STREAM) lsock.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1) lsock.bind((0.0.0.0, 9000)) lsock.listen(1024) lsock.setblocking(False) sel.register(lsock, selectors.EVENT_READ, dataNone) while True: events sel.select(timeout1.0) for key, mask in events: if key.data is None: accept_wrapper(key.fileobj) else: service_connection(key, mask)这段代码里有三个必须理解的点否则改出来的东西一定会出 bug。第一所有 socket 必须非阻塞。事件循环只在内核说这个 fd 可读/可写的时候才去操作它但如果 socket 是阻塞的一次 recv 可能因为各种边缘情况卡住整个循环就停摆了。setblocking(False) 是硬要求不是优化。第二非阻塞 socket 上的 recv 可能抛 BlockingIOError。这在可读事件到达和你真正去读之间连接状态发生变化的竞态里会出现。实践中必须在读取处捕获 EAGAIN/EWOULDBLOCKPython 里是 BlockingIOError并当作暂时没数据处理而不是当错误。第三写操作不能假设一次写完。sock.send()返回的是实际写进去的字节数可能小于你要发的长度尤其是内核发送缓冲区满的时候。所以必须维护一个待发缓冲区把没写完的部分留着等下次可写事件。上面代码里的data.outb就是这个缓冲区data.outb[sent:]是保留未发送部分。这是新手写事件驱动服务器最常见的一个 bug——数据发一半就丢了而且是偶发的压测才能压出来。这个骨架离生产可用还有距离缺少空闲超时清理、缺少背压控制、缺少协议解析但结构是对的剩下的都是往上加。4.4 第四步处理一次 recv 不一定是完整请求这是所有初学者必然会踩的一脚TCP 是字节流不是消息流。你主观上发一条消息内核看来就是一堆字节它既不保证对端一次 recv 能收到全部也可能把两条消息粘在一起。演示一下这个现象的经典场景客户端连续 send 两次短字符串中间不加任何延迟。服务端一次 recv 读出来的是拼在一起的两段内容。反过来客户端 send 一个 10KB 的包服务端 recv(4096) 第一次只能拿到 4096 字节剩下的要再读两次。解决办法是在应用层定义消息边界。主流做法有四种方案实现方式优点缺点定长消息每条消息固定长度实现最简单长度不灵活浪费带宽分隔符用 \n 或 \r\n 结尾文本协议友好内容里不能出现分隔符需要转义长度前缀先发 4 字节长度再发内容二进制友好解析高效需要处理长度字段本身的半包自描述格式JSON、Protobuf 等扩展性好解析开销略高仍需要长度前缀或分隔符长度前缀是我用得最多的方案因为通用性最好。核心是一个读满 N 字节的函数def recv_exact(conn: socket.socket, n: int): buf bytearray() while len(buf) n: chunk conn.recv(n - len(buf)) if not chunk: return None # 对端提前关闭 buf.extend(chunk) return bytes(buf) def recv_message(conn: socket.socket): header recv_exact(conn, 4) if header is None: return None (length,) struct.unpack(!I, header) if length 0 or length 16 * 1024 * 1024: raise ValueError(fbad frame length: {length}) return recv_exact(conn, length)那个length 16MB的检查不是多余的防御性编程是必须的。不加的话客户端发一个 4 字节的长度头写个0xFFFFFFFF你的服务端就会试图分配 4GB 内存然后被 OOM killer 干掉。这个攻击成本极低是真实存在的任何暴露在公网的解析代码都要有长度上限。事件驱动版本里处理半包的思路是把待解析缓冲区挂在每个连接的上下文里每读到新数据就尝试从中切出完整帧切不出来就留着等下一次可读事件。这段逻辑必须写成循环因为一次读到 4KB 里可能含 3 个完整帧加半个帧只解析一个就走是不对的。5. 那些年踩过的坑附排查速查表5.1 服务端在听但客户端连不上先别急着怀疑代码。按顺序排查三件事。绑定地址对不对。如果你的服务绑的是 127.0.0.1那从别的机器上连是绝对连不上的这个错误我见过不止一次。反过来绑 0.0.0.0 才是对外可达。端口是否真的在 LISTEN 状态。命令就一条ss -lntp | grep 9000能看到 LISTEN 行并且进程号对得上说明服务端没问题问题在网络层。看不到说明你的 bind/listen 根本没成功去看程序日志。老系统上用netstat -lntp代替。中间有没有隔离。容器网络、云环境的访问控制列表、宿主机的防护规则都是可能的原因。排查手段是从目标机器上用 telnet 或者 nc 直连端口连不上就是网络层的事跟你的代码没关系。5.2 连接能建立但数据要卡好几秒这个特征几乎可以锁定到全连接队列溢出。验证方法很直接netstat -s | grep -A1 -i listen如果 overflowed 计数在增长就是它了。处理办法两个把 backlog 和 somaxconn 一起调大同时检查 accept 循环里有没有放慢操作。第二个才是根因。还有一种情况是 Nagle 算法和延迟 ACK 撞在一起导致的 40ms 左右延迟。现象是小包请求的响应时间稳定在 40 毫秒上下。解决办法是给连接设置 TCP_NODELAYconn.setsockopt(socket.IPPROTO_TCP, socket.TCP_NODELAY, 1)这个选项适合请求-响应模式、小包居多的协议。对于大批量数据传输的场景则没必要甚至可能因为小包太多而降低吞吐。5.3 跑一段时间之后新连接全失败典型的 fd 泄漏。判断方法# 看进程打开的文件描述符数量 ls /proc/pid/fd | wc -l # 看进程的 fd 上限 cat /proc/pid/limits | grep open files数字接近上限就是泄漏了。泄漏点一般在三处accept 之后处理抛异常、没走到 close事件驱动里对端关闭后忘了 unregister线程池里的任务被丢弃但连接没关。根治办法是把关闭连接这件事收敛到一个地方所有路径都走它。我在事件驱动的实现里会让每个连接的关闭都过一个统一的 close_conn 函数里面先 unregister 再 shutdown 再 close中间任何一个抛异常都不影响后面的步骤。这样即使有 bug泄漏面也被压到最小。顺带把 fd 上限调高这个是基础设施层面的常规操作# 临时生效 ulimit -n 65535 # 永久生效改 /etc/security/limits.conf # * soft nofile 65535 # * hard nofile 65535容器环境要注意宿主机调了不代表容器里调了很多容器运行时默认还是 1024。5.4 客户端已经断了服务端还在傻等TCP 的一个基本事实连接的一方断电、断网、进程被杀另一方可能完全感知不到。正常情况下对端关闭会发 FIN你 recv 会返回 0很好判断。但对端如果是拔网线、机器崩溃这种没有任何报文发过来你的连接会一直挂在那里占着 fd 和内存。解决办法是应用层心跳。约定一个空闲时长比如 30 秒超过这个时间没收到任何数据就主动探活或者直接关闭。实现上有两种风格被动式超时记录每个连接最后一次收到数据的时间事件循环的定时检查里扫一遍超时的清理掉。这是绝大多数场景的做法实现简单。主动式心跳服务端定时发 ping客户端必须回 pong连续 N 次没回就断。适合需要快速感知对端状态的场景但会带来额外的流量。不管哪种超时值一定要比客户端的心跳间隔大。我曾经把服务端超时设成 30 秒客户端的保活间隔也是 30 秒结果线上时不时有连接莫名被踢。原因是两者的计时起点不一样服务端的 30 秒到了客户端的包还在路上。后来把服务端改成 90 秒问题消失。这个经验值得记下来超时判定要留出至少 2 到 3 倍的心跳周期作为余量。5.5 CPU 占用异常升高事件驱动的服务器 CPU 跑满八成是进了忙轮询。最常见的原因是 select 的 timeout 设成了 0或者可写事件一直被注册着但根本没有数据要写。EPOLLOUT 是水平触发的——只要发送缓冲区有空位就会一直返回可写事件。如果你的代码收到可写事件但什么都不做下一轮循环它还会报形成死循环。所以写事件必须按需注册有数据要发才注册发完立刻改回只监听读。另一个原因是异常没有被正确处理。比如 recv 抛了 EAGAIN 之外的错误你 catch 住之后没有关闭连接也没有注销下一轮还是同一个 fd 报同样的事件日志刷屏加 CPU 飙升。这种情况看日志能立刻发现。5.6 排查速查表现象最可能原因快速验证处理方向启动报 Address already in use端口被占用或 TIME_WAIT 未释放ss -lntp | grep 端口设 SO_REUSEADDR或换端口客户端连接被拒绝服务未监听 / 绑了回环地址 / 网络隔离ss -lntp看有无 LISTEN改绑定地址检查访问控制连上了但响应慢几秒全连接队列溢出netstat -s | grep -i overflow调大 backlog 与 somaxconn加快 accept小包响应稳定慢 40msNagle 与延迟 ACK 交互抓包看时间间隔设置 TCP_NODELAY运行一段时间后新连接全失败fd 泄漏ls /proc/pid/fd | wc -l统一关闭路径调高 nofile连接无故被断空闲超时设得太紧核对心跳周期与超时值超时放大到心跳的 3 倍CPU 飙高忙轮询 / 写事件常驻top 看 sys 占比查 select timeout调大 timeout写事件按需注册收数据粘在一起或截断没做消息边界打印每次 recv 的字节数长度前缀或分隔符服务停不下来阻塞 accept 无限等待看能否响应 SIGTERM设超时维护停止标志位6. 几个让实现更结实的习惯写到这里基础的东西已经齐了。但同一份功能实现质量的差距往往体现在一些不影响跑通、只影响出事后能不能救回来的细节上。给每个连接一个标识。日志里只打印 socket fd 是不够的fd 会被复用你根本分不清哪行日志对应哪个客户端。我的做法是每个连接分配一个递增 ID日志里带上它和客户端地址端口出问题时能立刻串起一条连接的全生命周期。限制单连接的资源上限。包括单连接的接收缓冲区大小、单连接的消息速率、单连接的待发送队列长度。这三个不设上限一个恶意或者出 bug 的客户端就能吃掉全部内存。特别是待发送队列——如果对端只连不发不读你的写缓冲区会无限增长这是事件驱动服务器最经典的 OOM 路径。别在事件循环里做耗时操作。哪怕是一次 10 毫秒的文件写入乘以一万个连接就是 100 秒的排队。真要处理阻塞任务把任务丢给线程池让它在完成后再通过一个唤醒机制把结果交回事件循环。Python 里标准做法是 loop.call_soon_threadsafe 或者 asyncio 提供的 run_in_executor。先跑通串行版本再考虑并发。我见过太多人一上来就写 epoll 加线程池加内存池结果连为什么客户端收到的是半条消息都排查不出来。串行版本的好处是所有状态都是确定的你能一条一条看清楚数据怎么流动。等这条路跑顺了再换成事件驱动你关注的只是模型变化的差异而不是在十几种可能的原因里猜。测试时故意制造异常。正常关闭、过程中 kill -9 客户端、发一半数据就不发了、一次发 1MB、连发 1000 次不断开——这五种情况能覆盖掉绝大多数连接管理的 bug。我每次写完一个服务端骨架都会过一遍这几个场景比事后线上排查便宜太多。最后说说我自己的体会。这套让服务器持续监听的东西第一次写会觉得就是加个循环第二次写会觉得难点在并发模型写到第三第四次才会意识到真正的难度全在异常路径和资源边界上。内核替你扛下了握手和排队剩下的那些看起来琐碎的关闭、超时、上限、日志才是服务能不能长期稳定跑下去的决定因素。
RELATED

相关推荐

uIP源码解析:2KB RAM单片机TCP/IP移植与排错

uIP源码解析:2KB RAM单片机TCP/IP移植与排错

简介:面向嵌入式网络开发初学者与需要为资源受限设备接入网络的工程师,这套uIP协议栈源码是理解轻量级TCP/IP实现机制的实用样本。包内共304个文件,其中29个c源文件与34个h头文件构成协议主体,183个html、3个css及若干png、gif图片…

📅 2026/10/26 4:44:20
零基础学AI应用开发:从Python到大模型API的实战路线

零基础学AI应用开发:从Python到大模型API的实战路线

不想劝退任何一个零基础的人,但我也必须说实话:AI应用开发这条路,现在确实比过去任何时候都更适合新人走。前提是,你得先把“AI应用开发”这件事理解对。很多初学者一听到“AI”两个字,脑子里浮现的是数学公式、神经网…

📅 2026/10/26 4:44:20
网络代理技术解析与企业级架构实践

网络代理技术解析与企业级架构实践

1. 网络代理技术基础解析网络代理技术作为现代互联网基础设施的重要组成部分,其核心原理是通过中间服务器转发客户端请求。这种架构设计最早可追溯至早期ARPANET时代,当时主要用于解决不同网络协议之间的兼容性问题。随着互联网商业化发展,代…

📅 2026/10/26 4:44:20
MORE NEWS

更多资讯

📰

Windows开机加速:5种启动项管理方法详解

1. 为什么Windows开机越来越慢?每次按下电源键后,看着屏幕上转圈的加载图标,是不是感觉等待时间越来越长?这个问题困扰着绝大多数Windows用户。开机速度变慢的核心原因,往往在于系统启动时自动加载的程序过多。这些程序…

📰

Edge兼容性修复实战:从翻车到可复现的完整指南

1. 从一次真实的翻车现场说起上个月帮一个朋友处理他公司内网系统的问题,前端同事信誓旦旦说"Chrome上跑得好好的",结果业务部门清一色用Windows自带的Edge打开,页面直接白屏,控制台一片红。这不是个例,我在…

📰

Aspose.Words for Java 14.9 离线破解版技术解析

简介:本资源是面向Java开发者与后端工程师的Aspose Words for Java 14.9破解版工具包,专为需在非商业场景下实现Word文档自动化处理(如生成报告、批量替换、格式转换、页眉页脚操作等)的技术人员设计。资源包含3个核心文件&#x…

📰

企业微信 iPad 方案怎么选?官方 API、自动化能力与风险边界

企业微信 iPad 方案怎么选?官方 API、自动化能力与风险边界 摘要 围绕“企业微信 iPad 协议”的讨论,常把设备形态、客户端自动化和企业级接口混在一起。选方案时更重要的问题不是能不能模拟某个客户端,而是业务是否获得了明确授权、数据能否…

📰

YOLOv10与红外成像在太阳能板缺陷检测中的应用

1. 项目概述:红外太阳能板缺陷检测的智能化突破去年夏天参观光伏电站时,现场工程师指着成排的太阳能板告诉我:"人工巡检2000块板子需要3个人忙活一整天,最头疼的是细微裂纹和热斑,等肉眼能看见时往往已经造成发电…

📰

PyTorch安装验证全链路指南:CUDA、驱动、Python版本精准校准

1. 这不是“点几下就完事”的安装,而是深度学习环境的第一道门槛 PyTorch 安装与验证,听起来像教科书里最基础的一节,但实际操作中,它往往是新手掉进的第一个深坑——你以为只是执行一条 pip install torch ,结果卡…

TODAY

今日更新

THIS WEEK

本周精选

THIS MONTH

本月热门

读完文章,想聊聊您的网站?

告诉我们您的行业与需求,资深顾问一对一梳理方案与报价,全程免费。

📞 💬