Socket 进阶:并发、超时与协议设计
约 1417 字大约 5 分钟
2026-05-10
基础 socket 示例一次只能处理一个客户端。真实服务需要处理多个连接、超时、断线和消息边界。
- socket 超时
- 每个连接一个线程
- 使用线程池
- 消息边界
- 1“每连接一线程”最直观,但连接多了线程数会爆 —— 改用 ThreadPoolExecutor 限并发。
- 2消息边界两种方案:换行符(\n 切包,简单但要处理转义)、长度前缀(4 字节长度 + body,最常用)。
- 3永远 conn.settimeout(...) —— 空闲连接占住资源是长连接服务最常见的资源泄漏。
- 4TCP 粘包/拆包是字节流协议的本性:循环 recv + 自己累 buffer 解包,不能假设一次 recv 等于一条消息。
- 5真要做高并发服务别从 socket 起步:HTTP/WebSocket/gRPC + 成熟框架解决了 99% 的工程难题。
基础 socket 示例一次只能处理一个客户端。真实服务需要处理多个连接、超时、断线和消息边界。
socket 超时
conn.settimeout(10)设置超时后,长时间没有数据会抛出 socket.timeout。
每个连接一个线程
import socket
import threading
def handle_client(conn, addr):
with conn:
while True:
data = conn.recv(1024)
if not data:
break
conn.sendall(data)
server = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
server.bind(('0.0.0.0', 9000))
server.listen()
while True:
conn, addr = server.accept()
threading.Thread(target=handle_client, args=(conn, addr), daemon=True).start()这种方式简单,但连接很多时线程数量会失控。
使用线程池
可以用线程池限制并发连接处理数量。
from concurrent.futures import ThreadPoolExecutor
pool = ThreadPoolExecutor(max_workers=20)
while True:
conn, addr = server.accept()
pool.submit(handle_client, conn, addr)消息边界
TCP 没有消息边界。推荐使用长度前缀协议:
4 字节长度 + JSON 数据接收方先读 4 字节,知道后面消息长度,再循环读取完整消息。
心跳与断线
长连接需要考虑:
- 客户端异常断开
- 网络中断
- 空闲连接超时
- 心跳包检测连接是否存活
select 简介
select 可以在单线程中监听多个 socket 是否可读写,是 IO 多路复用的基础。
import select
readable, writable, errors = select.select(sockets, [], [], 1)实际项目中更常用 asyncio、selectors 或成熟网络框架。
为什么不建议自定义协议
自定义协议容易踩坑:
- 粘包/拆包
- 版本兼容
- 心跳
- 重连
- 加密
- 鉴权
- 压缩
- 错误处理
如果 HTTP/WebSocket 能满足需求,优先使用更高层协议。
为什么简单 Echo Server 不够用
基础篇里的 Echo Server 适合理解流程,但离真实服务还差几步:
- 不能同时服务多个客户端;
- 客户端不发数据时可能一直卡住;
- 不知道一条消息什么时候结束;
- 客户端断开后要正确清理连接;
- 服务端出错时不能直接崩掉。
进阶并不是为了把 socket 写得很复杂,而是理解真实网络程序必须面对的问题。
每个连接一个线程:直观但要控制数量
最容易理解的并发方式是:每来一个客户端,就开一个线程处理。
import socket
import threading
HOST = '127.0.0.1'
PORT = 9000
def handle_client(conn, addr):
print(f'客户端连接:{addr}')
with conn:
while True:
data = conn.recv(1024)
if not data:
break
conn.sendall(data)
print(f'客户端断开:{addr}')
with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as server:
server.bind((HOST, PORT))
server.listen()
print(f'监听 {HOST}:{PORT}')
while True:
conn, addr = server.accept()
thread = threading.Thread(target=handle_client, args=(conn, addr))
thread.start()这个版本能同时处理多个客户端,但也有问题:客户端很多时会创建大量线程。真实项目里通常要用线程池、异步 IO,或者直接使用成熟框架。
给连接设置超时
如果客户端连接后一直不发数据,recv() 会一直等。可以设置超时:
conn.settimeout(10)处理时捕获:
try:
data = conn.recv(1024)
except socket.timeout:
print('客户端太久没有发送数据,断开连接')
break超时不是异常情况的“失败”,而是一种保护机制:防止连接一直占着资源。
用换行符做消息边界
TCP 是字节流,不知道“消息”的概念。初学者可以先用换行符作为分隔符:
hello\n
world\n接收方不断累积数据,看到 \n 就切出一条消息:
buffer = b''
while True:
data = conn.recv(1024)
if not data:
break
buffer += data
while b'\n' in buffer:
line, buffer = buffer.split(b'\n', 1)
message = line.decode('utf-8')
print('收到消息:', message)这只是入门方案。真实协议可能会使用长度前缀、JSON Lines、Protocol Buffers 等更明确的格式。
什么时候不要自己写 socket 协议
大多数业务系统不需要从 socket 开始写协议。更常见的选择是:
| 需求 | 更常用方案 |
|---|---|
| 调用 Web API | HTTP + requests/httpx |
| 实时消息 | WebSocket |
| 内部服务通信 | HTTP/gRPC/消息队列 |
| 文件上传下载 | HTTP 或对象存储 SDK |
学习 socket 的价值在于理解底层通信、排查网络问题、理解协议设计的难点;不是鼓励所有项目都自己造协议。
总结
socket 进阶重点是并发连接处理、超时、消息边界和断线恢复。它能训练网络底层思维,但生产项目中应谨慎自定义协议。
- 进阶 socket 主要解决并发、超时、消息边界和连接清理问题。
- 每连接一个线程最直观,但连接很多时要考虑线程池、异步 IO 或成熟框架。
- TCP 没有天然消息边界,真实协议必须明确“如何判断一条消息结束”。
版权所有
版权归属:Shuo Liu
