asyncio.start_server 打造异步 TCP 服务
副标题:Python 3.12 StreamReader/Writer 与背压控制实战
摘要
Python 3.12 原生的 asyncio.start_server
提供了优雅的方式构建高并发 TCP 服务。本教程在 macOS/Linux 下用 pip + venv 初始化 NetLab 工程,基于 StreamReader/StreamWriter
实现一个带背压控制的异步 Echo 服务,支持成千上万连接。我们将对比同步与异步 TCP,在性能基准中验证并发 1000 连接的表现,并涵盖超时、限流、异常兜底等安全边界处理。
导语:痛点与场景
编写 TCP 服务器时,传统同步 socket 编程在面对成百上千并发连接时会阻塞线程或占用大量线程资源,无法高效扩展。
asyncio.start_server
利用协程事件循环让单线程也能管理超大并发数,适合聊天服务器、游戏网关、物联网终端接入等场景。
关键点包括:
- 用
StreamReader
/StreamWriter
收发数据 - 通过背压防止发送缓冲溢出
- 正确管理连接生命周期与异常
知识地图
graph TD
A[asyncio TCP Server] --> B[StreamReader/Writer]
B --> C[Echo 逻辑]
A --> D[背压 flow_control]
D --> E[安全(超时/限流)]
A --> F[性能调优]
环境与工程初始化
1. 目录
mkdir netlab && cd netlab
mkdir -p netlab/{common,clients,servers,protocols} tests bench scripts
touch netlab/__init__.py
touch netlab/common/{settings.py,logging.py,utils.py}
2. 虚拟环境与依赖
python3.12 -m venv .venv
source .venv/bin/activate
pip install --upgrade pip
pip install pydantic-settings structlog pytest pytest-asyncio
3. requirements.txt
pydantic-settings>=2.3
structlog>=24.1
pytest>=8.0
pytest-asyncio>=0.23
核心实现
我们实现:
- 异步 TCP Echo 服务(带背压),基于 asyncio.start_server
- 客户端并发连接测试
- 错误码与日志封装
1. 配置(netlab/common/settings.py
)
from pydantic_settings import BaseSettings
class Settings(BaseSettings):
tcp_host: str = "127.0.0.1"
tcp_port: int = 8888
max_clients: int = 1000
read_timeout: float = 10.0
class Config:
env_file = ".env"
settings = Settings()
2. 日志(netlab/common/logging.py
)
import logging
import structlog
def setup_logging() -> None:
logging.basicConfig(format="%(message)s", level=logging.INFO)
structlog.configure(
processors=[
structlog.processors.TimeStamper(fmt="iso"),
structlog.processors.add_log_level,
structlog.processors.EventRenamer("event"),
structlog.processors.JSONRenderer(),
],
logger_factory=structlog.stdlib.LoggerFactory(),
)
logger = structlog.get_logger()
3. 异步 TCP 服务(netlab/servers/async_tcp_echo.py
)
import asyncio
from async_timeout import timeout as aio_timeout # pip install async-timeout
from netlab.common.settings import settings
from netlab.common.logging import setup_logging, logger
class TcpServerError(Exception):
"""TCP Server custom error."""
def __init__(self, code: str, message: str):
super().__init__(message)
self.code = code
self.message = message
async def handle_echo(reader: asyncio.StreamReader, writer: asyncio.StreamWriter):
addr = writer.get_extra_info("peername")
logger.info("client_connected", addr=addr)
try:
while True:
# 超时控制
try:
async with aio_timeout(settings.read_timeout):
data = await reader.read(1024)
except asyncio.TimeoutError:
logger.warning("read_timeout", addr=addr)
break
if not data:
break
logger.info("server_recv", data=data.decode().strip(), addr=addr)
writer.write(data)
await writer.drain() # 背压点
logger.info("client_disconnected", addr=addr)
except Exception as e:
logger.error("server_error", error=str(e))
raise TcpServerError("SERVER_ERROR", str(e))
finally:
writer.close()
await writer.wait_closed()
async def main():
setup_logging()
server = await asyncio.start_server(
handle_echo, settings.tcp_host, settings.tcp_port, limit=2**16
)
addr = ", ".join(str(sock.getsockname()) for sock in server.sockets)
logger.info("server_start", addr=addr)
async with server:
await server.serve_forever()
if __name__ == "__main__":
asyncio.run(main())
4. 异步 TCP 客户端(netlab/clients/async_tcp_client.py
)
import asyncio
from netlab.common.settings import settings
from netlab.common.logging import setup_logging, logger
async def tcp_client(message: str) -> str:
setup_logging()
reader, writer = await asyncio.open_connection(settings.tcp_host, settings.tcp_port)
writer.write(message.encode())
await writer.drain()
logger.info("client_send", data=message)
data = await reader.read(1024)
logger.info("client_recv", data=data.decode())
writer.close()
await writer.wait_closed()
return data.decode()
测试与验证
# tests/test_async_tcp.py
import asyncio
import threading
import time
import pytest
from netlab.servers.async_tcp_echo import main as server_main
from netlab.clients.async_tcp_client import tcp_client
@pytest.fixture(scope="module", autouse=True)
def server():
def run_srv():
asyncio.run(server_main())
t = threading.Thread(target=run_srv, daemon=True)
t.start()
time.sleep(1)
@pytest.mark.asyncio
async def test_echo():
msg = "hello"
resp = await tcp_client(msg)
assert resp == msg
运行:
pytest -v
时序图
性能与调优
基准脚本(bench/bench_tcp.py
)
import asyncio
import time
from netlab.clients.async_tcp_client import tcp_client
from netlab.common.settings import settings
async def bench(n):
tasks = [tcp_client(f"ping-{i}") for i in range(n)]
await asyncio.gather(*tasks)
if __name__ == "__main__":
N = 1000
start = time.perf_counter()
asyncio.run(bench(N))
elapsed = time.perf_counter() - start
print(f"{N} connections in {elapsed:.2f}s -> {N/elapsed:.2f} req/s")
示例结果(本地):
连接数 | 耗时(s) | RPS |
---|---|---|
1000 | 0.85 | 1176 |
优化点:
- 调整
limit
参数优化缓冲区 - 使用
ujson
/orjson
序列化协议数据 - 调整 OS socket 缓冲区 (
SO_SNDBUF
/SO_RCVBUF
)
安全与边界
- 超时:用
async_timeout
包装reader.read
- 重试:客户端连接失败重试
- 限流:服务端使用
Semaphore
限制同时连接 - 异常兜底:捕获处理所有连接异常,防止服务崩溃
- mTLS:可在
start_server
中传入ssl.SSLContext
实现 TLS 加密
常见坑与排错清单
- 忘记
await writer.drain()
导致背压失效 - 未关闭连接导致 fd 泄漏
- 无超时控制易被慢连接拖垮
进一步扩展
- 协议解析:封装长度前缀帧
- 广播功能:管理连接集合
- TLS 支持 (
ssl
模块 + asyncio)
小结与思考题
本文用 asyncio.start_server
实现了高并发 TCP Echo 服务,演示背压控制与安全处理。
思考:在 10 万连接场景下,如何配合多进程 / 多机进一步扩容?
完整代码清单
保存上述文件,运行服务:
python -m netlab.servers.async_tcp_echo
运行单个客户端:
python -c "import asyncio;from netlab.clients.async_tcp_client import tcp_client;asyncio.run(tcp_client('test'))"
运行性能测试:
python bench/bench_tcp.py