
简介这份资源聚焦TCP协议下的异步通信实现面向具备一定网络编程基础、希望深入理解高并发网络应用开发的学习者与开发者。包内包含TCP服务端与TCP客户端两套异步通信示例代码可用于对照学习异步I/O模型、回调函数、事件循环、线程池等关键技术点帮助理解连接建立、数据收发等环节在非阻塞模式下的处理流程。资源共2个文件均为cpp源码文件压缩包整体约2KB体积轻量便于直接阅读与调试。目前已有189人学习下载适合作为网络编程入门到进阶的参考素材。通过研读这两份源码读者可以梳理异步TCP通信的基本框架掌握套接字创建、绑定、监听、连接及异步收发等核心接口的配合方式并在此基础上自行扩展多客户端并发处理、事件驱动调度等实践为构建高性能网络服务打下基础。1. 从 tc.zip 说起一个 TCP 异步通信骨架到底解决什么问题很多人第一次看到tc.zip_TCP 服务端_TCP客户端异步_tcp 异步这个标题脑子里冒出来的是一堆问号这到底是一个现成的代码包还是一套要自己搭的通信骨架其实它指向的需求非常具体——你要让一个 TCP 服务端同时扛住几百上千个客户端连接而客户端这边发完请求不用傻等回复可以继续干别的事。这就是 TCP 异步通信要解决的核心矛盾连接数一多同步阻塞模型就崩了线程池再大也扛不住 C10K。我见过太多项目一开始用同步 Socket 写得好好的上线后连接数一过两百就开始各种玄学超时日志里全是Connection reset和SocketTimeoutException。异步模型不是银弹但它能把「一个连接一个线程」的线性成本压成事件驱动这是质变。这篇文章面向的是需要自己动手搭 TCP 长连接服务的后端或嵌入式工程师我会把服务端和客户端两侧的异步骨架都拆开讲包括选型理由、最小可跑代码、参数怎么调、以及那些只有踩过才知道的坑。读完你至少能判断这个方向值不值得投入以及第一版代码该从哪写起。2. TCP 异步的底层逻辑为什么 select 和 epoll 是分水岭2.1 同步阻塞到底卡在哪先把这个说清楚后面选型才不会拍脑袋。同步阻塞模型下accept()拿到一个连接后通常交给一个独立线程去read()线程在数据没来时会一直挂起。连接数少的时候没问题但每个线程默认栈空间在 Linux 上通常是 8MB一千个连接就是 8GB 虚拟内存上下文切换开销也随连接数线性上涨。更致命的是如果某个客户端发了半包数据就卡住那个线程就一直占着后面的请求全排队。异步模型的核心思路是不让线程去等数据而是让操作系统在数据就绪时通知你。这就引出了 I/O 多路复用。select是最早的方案但它有两个硬伤——单进程能监听的 fd 数量有上限通常 1024而且每次调用都要把整个 fd 集合从用户态拷贝到内核态再线性扫描一遍连接数一多就是 O(n) 的灾难。poll解决了数量上限但拷贝和扫描的问题还在。2.2 epoll 到底改了什么epoll是 Linux 下的分水岭。它用三个系统调用epoll_create、epoll_ctl、epoll_wait把「注册关注」和「等待就绪」拆开了。fd 只需要在epoll_ctl时注册一次内核用红黑树管理事件就绪时通过回调把 fd 放进就绪链表epoll_wait直接返回就绪链表复杂度是 O(1)。这就是为什么高并发服务端基本都跑在 epoll 上。但要注意epoll 有两种触发模式水平触发LT和边缘触发ET。LT 是默认模式只要 fd 可读每次epoll_wait都会通知你编程简单但可能重复通知。ET 只在状态变化时通知一次必须配合非阻塞 fd 一次性把数据读干净否则会丢事件。我一般建议新手先用 LT 把逻辑跑通确认没问题再切 ET 压性能因为 ET 下漏读一个字节都可能导致连接假死排查起来非常痛苦。2.3 异步的两种实现路径Reactor 与 Proactor选型上还有一个岔路口。Reactor 模式是「就绪通知」——内核告诉你 fd 可读了你自己去read()。Proactor 是「完成通知」——内核帮你把数据读到缓冲区读完再通知你。Linux 原生 AIO 属于后者但生态和稳定性一直不如 epoll所以绝大多数 TCP 异步服务端走的都是 Reactor 路线。Windows 的 IOCP 是典型的 Proactor跨平台库比如 Boost.Asio 会在不同平台做适配。落到代码层面Reactor 又分单 Reactor 单线程、单 Reactor 多线程、主从 Reactor 多线程。单线程适合连接数不多、业务逻辑轻的场景主从 Reactor 是主流主线程只管accept从线程池管读写和业务。你如果只是做一个内部工具或中等规模服务单 Reactor 多线程就够了别一上来就上主从复杂度会吃掉你大量调试时间。3. 服务端异步骨架从 epoll 注册到连接生命周期管理3.1 最小可跑的 epoll 服务端下面这段 Python 代码用select.epoll演示了最核心的骨架虽然生产环境更推荐用 asyncio 或直接上 C/Go但用它理解流程最直观。import socket import select # 创建监听 socket设为非阻塞 server socket.socket(socket.AF_INET, socket.SOCK_STREAM) server.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1) server.bind((0.0.0.0, 9000)) server.listen(128) server.setblocking(False) # 创建 epoll 对象注册监听 fd 的可读事件 epoll select.epoll() epoll.register(server.fileno(), select.EPOLLIN) # fd - socket 映射方便事件回来时找到对应连接 connections {} try: while True: # 阻塞等待事件超时 1 秒 events epoll.poll(timeout1) for fd, event in events: if fd server.fileno(): # 有新连接进来accept 后注册到 epoll conn, addr server.accept() conn.setblocking(False) epoll.register(conn.fileno(), select.EPOLLIN) connections[conn.fileno()] conn print(fnew connection from {addr}) elif event select.EPOLLIN: # 某个连接可读收数据 conn connections.get(fd) if conn is None: continue try: data conn.recv(4096) except ConnectionResetError: data b if not data: # 对端关闭清理资源 epoll.unregister(fd) conn.close() del connections[fd] print(fconnection {fd} closed) else: # 回显实际业务在这里处理 conn.sendall(data) finally: epoll.unregister(server.fileno()) epoll.close() server.close()这段代码的逻辑链条是监听 fd 注册可读事件事件循环里先判断是不是监听 fd是就accept新连接并注册不是就说明是已连接 fd 可读recv收数据收到空数据代表对端关闭做清理。参数上listen(128)的 backlog 是半连接队列长度高并发下可以调到 512 或 1024但最终受内核somaxconn限制。epoll.poll(timeout1)的超时设 1 秒是为了让循环有机会处理其他逻辑纯事件驱动可以设 -1 一直阻塞。3.2 连接生命周期管理的三个关键点第一fd 泄漏是异步服务端最常见的慢性病。每次accept后注册关闭时必须unregister再close顺序反了在某些内核版本上会报错。我习惯用一个connections字典统一管理关闭时先查字典再操作避免重复关闭。第二写事件不要一直注册。EPOLLOUT只在发送缓冲区满、需要等待可写时才注册数据发完立刻注销否则 epoll 会一直通知你可写CPU 直接跑满。这是新手最容易翻车的地方之一。第三心跳和超时。TCP 连接可能因为网络中间设备静默断开对端不一定会发 FIN。你需要一个定时器扫描最后活跃时间超过阈值就主动关闭。常见做法是在事件循环里每轮检查一次或者用timerfd注册到 epoll 里。3.3 参数调优backlog、缓冲区与 TCP_NODELAY服务端有几个参数值得单独说。SO_REUSEADDR基本必开否则服务重启时会因为 TIME_WAIT 状态报Address already in use。TCP_NODELAY建议开它会禁用 Nagle 算法减少小包延迟代价是网络包数量增加内网环境基本无脑开。接收缓冲区SO_RCVBUF和发送缓冲区SO_SNDBUF默认值在 Linux 上通常是 128KB 左右高吞吐场景可以调到 256KB 或 512KB但别盲目调大内存占用会随连接数线性增长。backlog 参数前面说了配合内核net.core.somaxconn一起调。这些参数没有万能值压测时用ss -lnt看 Send-Q 和 Recv-Q 有没有堆积有就说明缓冲区或处理速度跟不上。4. 客户端异步发完不等回复的三种写法4.1 回调式异步客户端客户端异步的核心诉求是发起请求后不阻塞主线程收到响应再处理。最原始的做法是回调。import socket import selectors sel selectors.DefaultSelector() def start_connection(host, port, msg): addr (host, port) sock socket.socket(socket.AF_INET, socket.SOCK_STREAM) sock.setblocking(False) # 非阻塞 connect 会立刻返回 EINPROGRESS sock.connect_ex(addr) # 注册可写事件连接建立后会触发 sel.register(sock, selectors.EVENT_WRITE, datamsg) def handle_write(sock, msg): # 连接建立后发送数据然后改注册可读事件 sock.send(msg) sel.modify(sock, selectors.EVENT_READ, datamsg) def handle_read(sock): data sock.recv(4096) if data: print(fresponse: {data}) else: sel.unregister(sock) sock.close() start_connection(127.0.0.1, 9000, bhello) while True: events sel.select(timeout1) for key, mask in events: sock key.fileobj if mask selectors.EVENT_WRITE: handle_write(sock, key.data) elif mask selectors.EVENT_READ: handle_read(sock)这里的关键是connect_ex在非阻塞模式下会立刻返回连接真正建立后 fd 变为可写epoll 通知你这时才能send。发送完把注册事件从写改成读等响应。逻辑说明selectors是 Python 对 epoll/kqueue/select 的封装跨平台。参数上sel.select(timeout1)的超时控制事件循环节奏实际项目中通常配合协程或线程池。4.2 协程式异步asyncio 的写法Python 里更现代的写法是asyncio它把回调地狱藏起来了。import asyncio async def tcp_client(msg): # open_connection 内部就是非阻塞 connect 事件循环 reader, writer await asyncio.open_connection(127.0.0.1, 9000) writer.write(msg) await writer.drain() # 等待发送缓冲区可写 data await reader.read(4096) print(fresponse: {data}) writer.close() await writer.wait_closed() async def main(): # 并发发 100 个请求互不阻塞 tasks [tcp_client(fmsg-{i}.encode()) for i in range(100)] await asyncio.gather(*tasks) asyncio.run(main())await reader.read()在数据没来时会挂起当前协程事件循环去跑别的任务这就是异步的本质。writer.drain()是等发送缓冲区降到低水位避免内存暴涨。参数上asyncio.open_connection可以传limit控制读取缓冲区大小默认 64KB。4.3 异步客户端的重连与超时策略生产环境的客户端必须处理断线重连。常见做法是给每个连接维护一个状态机连接中、已连接、断开重试。重试间隔用指数退避比如 1s、2s、4s、8s上限 30s避免服务端刚重启就被大量重连打垮。超时方面asyncio.wait_for(reader.read(4096), timeout5)可以给单次读取加超时。注意超时后要主动关闭连接否则协程取消但 fd 可能还挂着。我一般会在连接对象上记录last_active时间后台起一个任务定期扫描超过 60 秒没活动的连接直接关掉重连。5. 避坑与排查异步 TCP 最容易翻车的五个地方5.1 现象连接数一上去就报 Too many open files原因进程 fd 上限没调。Linux 默认单进程 1024异步服务端很容易超过。解决ulimit -n 65535临时生效永久生效改/etc/security/limits.conf同时确认 systemd 服务里LimitNOFILE也设了否则重启又回去。5.2 现象ET 模式下偶尔丢数据连接假死原因边缘触发要求一次把数据读干净如果recv只读了一次就返回剩余数据不会再触发事件。解决ET 模式下必须循环recv直到抛出BlockingIOError或返回空。我血泪经验是切 ET 之前先把 LT 逻辑压测通过切完再压一遍对比 QPS 和错误率。5.3 现象客户端发了数据服务端半天收不到原因Nagle 算法在攒小包。解决服务端和客户端都设TCP_NODELAY。另一个可能是send返回的字节数小于请求长度TCP 不保证一次发完必须循环发或检查返回值。5.4 现象压测时 CPU 飙到 100%但 QPS 很低原因EPOLLOUT一直注册着epoll 空转通知可写。解决只在send返回EAGAIN时才注册写事件发完立刻modify回只读。用strace -c看epoll_wait调用次数异常高基本就是这个原因。5.5 现象服务运行几天后内存持续上涨原因连接关闭时没清理关联的缓冲区或定时器。解决每个连接对象关闭时走统一的cleanup路径把 fd、缓冲区、定时器、回调引用全部置空。用objgraph或tracemalloc抓泄漏点别靠猜。6. 进阶技巧用压测数据反推你的异步骨架该不该重构异步骨架搭完只是开始真正决定要不要继续投入的是压测数据。我一般用wrk或自己写一个基于 asyncio 的压测客户端重点看三个指标连接建立速率、单连接吞吐、P99 延迟。如果连接建立速率上不去瓶颈通常在accept队列或 fd 上限如果单连接吞吐低但 CPU 没跑满多半是缓冲区太小或系统调用太频繁如果 P99 延迟抖动大检查是否有慢业务逻辑阻塞了事件循环。一个具体技巧是在事件循环里埋一个耗时统计记录每轮epoll_wait到处理完所有事件的耗时。如果某轮超过 10ms说明有阻塞操作混进来了异步最怕的就是在事件循环里做同步 I/O 或重计算。我自己的习惯是任何超过 1ms 的逻辑都扔到线程池事件循环只做收发和状态流转。验证重构是否值得可以做一个对照实验把当前骨架的连接数从 1000 逐步压到 10000记录内存、CPU、P99。如果 5000 连接时 P99 已经超过业务容忍阈值而 CPU 还有余量那瓶颈就在你的代码结构而不是机器重构有意义。反之如果 CPU 先打满先考虑加机器或换语言别急着改架构。最后说个我踩过的坑别在没压测的情况下直接上主从 Reactor多出来的线程间通信和锁竞争可能让性能不升反降。先用单 Reactor 多线程把数据跑出来确认瓶颈真的在 accept 再拆。希望帮到你。本文还有配套的精品资源点击获取