7 个 Python 库让你的网络编程脱胎换骨
线上接口卡住了,日志里只剩一句:
ReadTimeout: HTTPSConnectionPool(host='api.xxx.com', port=443)
这种问题我一般不先怀疑业务代码。网络编程里最烦的不是“连不上”,而是“偶尔连不上、偶尔超时、偶尔粘包、偶尔一堆连接挂着不释放”。
Python 写网络程序,别只会 requests.get()。面试里问 7 个网络库,其实不是让你背名字,是看你知不知道不同问题该拿什么工具拆。
1、socket
socket 是底层货,绕不开。
只要你写 TCP、UDP,最后基本都绕到它身上。HTTP 再高级,底下也是 socket。
我面试时很喜欢问一个问题:TCP 一次 recv(1024) 能不能保证读到完整业务包?
不能。
TCP 是字节流,不管你上层业务边界。你发两次,对方可能一次收到;你发一次,对方也可能分几次收到。这个坑不踩一次,很难真记住。
import socket
import structdefread_exact(conn, size):
buf = bytearray()
while len(buf) < size:
chunk = conn.recv(size - len(buf))
ifnot chunk:
raise ConnectionError("client closed before packet completed")
buf.extend(chunk)
return bytes(buf)
defread_packet(conn):
header = read_exact(conn, 4)
body_len = struct.unpack("!I", header)[0]
if body_len > 2 * 1024 * 1024:
raise ValueError(f"packet too large: {body_len}")
return read_exact(conn, body_len)
这种代码看着土,但线上比花哨封装可靠。先读长度,再按长度读 body。别指望一次 recv 解决所有问题。
2、selectors
如果只会一个连接一个线程,连接数稍微上来,线程先把机器拖死。
selectors 是 Python 标准库里做 IO 多路复用的东西。Linux 下通常走 epoll,macOS 下走 kqueue,Windows 下也有对应实现。它适合写轻量 TCP 服务,尤其是你想理解事件驱动到底怎么回事的时候。
import selectors
import socketsel = selectors.DefaultSelector()
defaccept(server):
conn, addr = server.accept()
conn.setblocking(False)
sel.register(conn, selectors.EVENT_READ, read_client)
print("new conn:", addr)
defread_client(conn):
data = conn.recv(4096)
ifnot data:
sel.unregister(conn)
conn.close()
return
conn.sendall(b"ok:" + data)
server = socket.socket()
server.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
server.bind(("0.0.0.0", 9001))
server.listen()
server.setblocking(False)
sel.register(server, selectors.EVENT_READ, accept)
whileTrue:
for key, _ in sel.select(timeout=1):
key.data(key.fileobj)
这段代码不优雅,但它把网络编程最关键的东西摊开了:连接、事件、读写、关闭。
3、asyncio
asyncio 解决的是高并发 IO 等待问题。
很多人一看 async/await 就觉得高级,其实别神化。它不是让 CPU 跑得更快,而是让你在等待网络返回时,别傻站着。
适合什么?爬接口、批量调用内部服务、长连接网关、推送系统。
import asyncioasyncdefcall_device(device_id):
reader, writer = await asyncio.open_connection("127.0.0.1", 9001)
payload = f"PING {device_id}".encode()
writer.write(len(payload).to_bytes(4, "big") + payload)
await writer.drain()
size = int.from_bytes(await reader.readexactly(4), "big")
body = await reader.readexactly(size)
writer.close()
await writer.wait_closed()
return device_id, body.decode()
asyncdefmain():
tasks = [call_device(f"d{i}") for i in range(100)]
for item inawait asyncio.gather(*tasks, return_exceptions=True):
print(item)
asyncio.run(main())
这里我会特别看 readexactly。能写出这个,比只会 await reader.read(1024) 靠谱一点。
4、aiohttp
asyncio 管事件循环,aiohttp 更贴近 HTTP 场景。
比如你要批量调用 500 个设备接口,用同步请求一个个打,超时的时候会很难看。aiohttp 可以配合连接池、超时、并发控制一起用。
import asyncio
import aiohttpasyncdeffetch_status(session, device_id):
url = f"http://ops-api.local/device/{device_id}/status"
try:
asyncwith session.get(url, timeout=3) as resp:
if resp.status != 200:
return device_id, "bad_status", resp.status
return device_id, "ok", await resp.json()
except asyncio.TimeoutError:
return device_id, "timeout", None
asyncdefscan_devices(ids):
connector = aiohttp.TCPConnector(limit=80, limit_per_host=20)
asyncwith aiohttp.ClientSession(connector=connector) as session:
returnawait asyncio.gather(*(fetch_status session, i) for i in ids)
上面代码有个地方故意值得看:并发不是越大越好。limit 不设,打挂对方服务也不是没见过。
不过最后一行少了括号,应该这样写:
returnawait asyncio.gather(*(fetch_status(session, i) for i in ids))
这类小错,面试现场也挺常见。能自己发现,印象反而更好。
5、httpx
requests 好用,但它偏同步。httpx 的好处是同步、异步都支持,迁移成本低。
我一般在内部工具里用 httpx,尤其是要统一超时、重试、header 的地方。
import httpxclient = httpx.Client(
timeout=httpx.Timeout(connect=2, read=5, write=3, pool=2),
headers={"User-Agent": "ops-checker/1.0"}
)
defcheck_order(order_id):
r = client.get(f"https://api.local/orders/{order_id}")
r.raise_for_status()
return r.json()
这里别只写一个 timeout=5。连接超时、读取超时、连接池等待超时,最好分开。线上排查超时时,这几个不是一回事。
6、websockets
WebSocket 场景现在不少,监控大屏、设备实时状态、IM、日志推送都会用。
websockets 库比较直接,适合快速写一个长连接服务。
import asyncio
import websocketsclients = set()
asyncdefhandle(ws):
clients.add(ws)
try:
asyncfor msg in ws:
if msg == "ping":
await ws.send("pong")
finally:
clients.discard(ws)
asyncdefpush_alarm(text):
dead = []
for ws in clients:
try:
await ws.send(text)
except Exception:
dead.append(ws)
for ws in dead:
clients.discard(ws)
asyncdefmain():
asyncwith websockets.serve(handle, "0.0.0.0", 9100):
await asyncio.Future()
asyncio.run(main())
长连接别忘了清理断开的客户端。不清理,时间一长,集合里全是尸体连接。
7、scapy
scapy 不太像业务库,更像排障工具。
抓包、构造包、看 TCP/UDP 细节,它很有用。一般业务开发用不到,但你真遇到“服务端说没收到,客户端说发了”的问题,抓包比吵架好使。
from scapy.all import sniff, TCP, IPdefshow(pkt):
if IP in pkt and TCP in pkt:
print(
pkt[IP].src,
"->",
pkt[IP].dst,
"sport=", pkt[TCP].sport,
"dport=", pkt[TCP].dport,
"flags=", pkt[TCP].flags
)
sniff(filter="tcp port 9001", prn=show, store=False)
这个库别乱用在生产机上长时间跑。抓包本身也吃资源,排查完就停。
这 7 个库放在一起看,顺序其实很清楚。
socket 让你知道底层怎么收发数据。selectors 让你知道多连接怎么扛。asyncio 让你知道 IO 等待怎么让出去。aiohttp 和 httpx 解决 HTTP 客户端/服务端里的实际问题。websockets 处理长连接。scapy 留给那些日志解释不了的现场。
面试时别只背库名。能讲清楚一次 recv 为什么读不完整、超时为什么要拆开配、长连接为什么要清理、批量请求为什么要限并发,这题基本就稳了。