socket.accept()需用try/except捕获osError(errno 4)并重试;recv()返回b”表示连接关闭,须显式判断;多客户端应避免单线程轮询,改用i/o多路复用、多线程或asyncio;bind()前须设so_reuseaddr。

socket.accept() 必须在 try/except 里包裹
阻塞式 accept() 可能被信号中断(比如 Ctrl+C、系统发送的 SIGCHLD),此时会抛出 OSError: [Errno 4] Interrupted system call。不捕获就崩,尤其在 linux 守护进程中很常见。
- 用
try/except OSError as e:捕获,检查e.errno == 4后直接重试accept() - 别只捕获
socket.error(python 3 已弃用)或宽泛的Exception - 如果用了
signal.signal(signal.SIGINT, handler),更要小心——自定义信号处理会让accept()更容易被中断
recv() 返回空字节意味着客户端已关闭连接
recv() 返回 b'' 不是错误,而是 TCP 连接被对方正常关闭(FIN 包到达)。很多新手误以为是网络故障,接着继续 recv() 或忽略,结果导致死循环或资源泄漏。
- 每次
recv()后必须显式判断:if not data: break - 不要依赖超时来“等数据”,
recv()在连接关闭时立刻返回空,不等超时 - 若协议需要粘包处理(如自定义消息头),也要先确认连接是否还活着,再解析;否则可能对
b''做解包,触发Struct.unpack()报错
多客户端场景下别用单线程轮询 recv()
一个线程里对多个 socket 调用 recv() 是典型阻塞陷阱:只要一个客户端不发数据,整个服务就卡住,其他连接全被饿死。
- 要么用
select()/poll()/epoll()做 I/O 多路复用(注意 windows 下select()是唯一通用选项) - 要么用
Threading.Thread为每个连接启一个线程(适合低并发,注意threading.stack_size()和 GIL 影响) - 要么用
asyncio+async def handle_client()(推荐,但得确保所有 I/O 都是 async 的,别混用time.sleep()或同步 DB 调用)
bind() 前记得 setsockopt(SO_REUSEADDR)
服务重启时经常报 OSError: [Errno 98] Address already in use,本质是 TIME_WAIT 状态占着端口。不设 SO_REUSEADDR,内核拒绝绑定。
立即学习“Python免费学习笔记(深入)”;
- 必须在
bind()前调用:s.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1) - Windows 和 Linux 都支持,无需条件判断
- 别和
SO_REUSEPORT混淆——后者是 Linux 3.9+ 才有,用于多进程负载均衡,普通服务端不需要
真正难的不是写通连接,而是让 accept() 不崩、recv() 不卡、多连接不互掐、重启不失败。这些点漏掉任何一个,上线后都得半夜爬起来看日志。