4、TCP服务器实现:多线程服务器、线程池、异步IO模型
说到TCP服务器,很多新手第一反应就是写个循环,accept一个连接就处理一个。嗯,这种做法在教科书上看着挺美,但放到生产环境里,分分钟让你崩溃。我早期做爬虫调度中心的时候,就吃过这个亏——一个慢请求堵住了后面所有连接,整个服务直接卡死。
今天咱们就来聊聊,怎么把TCP服务器做得既稳又快。说白了,就是解决一个核心问题:如何高效处理大量并发连接。
4.1 多线程服务器:最简单的并发方案
先看最直观的思路:来一个客户端,就开一个线程去伺候它。主线程只管接客,干活的事交给小弟们。
核心思想:每个连接独立线程,互不干扰。
import socket
import threading
def handle_client(conn, addr):
print(f'[新连接] {addr} 上线了')
try:
while True:
data = conn.recv(1024)
if not data:
break
conn.send(f'收到: {data.decode()}'.encode())
except ConnectionResetError:
pass
finally:
conn.close()
print(f'[断开] {addr} 离开了')
def start_server(host='0.0.0.0', port=8888):
server = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
server.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
server.bind((host, port))
server.listen(5)
print(f'[启动] 服务器监听 {host}:{port}')
while True:
conn, addr = server.accept()
# 来一个客户端,开一个线程
t = threading.Thread(target=handle_client, args=(conn, addr))
t.start()
if __name__ == '__main__':
start_server()
这段代码看着简单吧?但我在项目中遇到过一个问题:如果客户端突然暴增,比如爬虫集群同时发起1000个连接,系统就会疯狂创建线程。线程不是免费的,每个线程都要占用栈空间(默认8MB左右),1000个线程就是8GB内存,服务器直接跪了。
避坑指南:我曾经在一个数据采集项目中,用多线程模型处理500个爬虫节点的连接,结果服务器内存爆了。后来一查,每个线程的栈空间就占了8MB,500个线程就是4GB。所以多线程模型适合连接数可控的场景,比如几十个客户端。
4.2 线程池:控制并发数量
既然线程不能无限开,那就搞个池子呗。线程池说白了就是:提前创建好一批线程,有任务来了就丢给空闲线程处理,处理完了线程不销毁,继续等下一个任务。
Python的concurrent.futures模块直接提供了线程池实现,用起来非常方便。
import socket
from concurrent.futures import ThreadPoolExecutor
def handle_client(conn, addr):
print(f'[处理] {addr} 的连接')
try:
while True:
data = conn.recv(1024)
if not data:
break
conn.send(f'[线程池回复] {data.decode()}'.encode())
except:
pass
finally:
conn.close()
def start_threadpool_server(host='0.0.0.0', port=8889, max_workers=10):
server = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
server.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
server.bind((host, port))
server.listen(128) # 可以接受更多等待连接
print(f'[线程池服务器] 启动于 {host}:{port},最大线程数 {max_workers}')
# 创建线程池,最多10个线程同时干活
with ThreadPoolExecutor(max_workers=max_workers) as executor:
while True:
conn, addr = server.accept()
# 提交任务到线程池
executor.submit(handle_client, conn, addr)
if __name__ == '__main__':
start_threadpool_server()
你想想看,线程池的好处是什么?资源可控。就算有1000个客户端同时连上来,也只有10个线程在同时处理,剩下的都在排队。这样服务器就不会被压垮。
我的经验:线程池的大小怎么设?我个人习惯是 CPU核心数 * 2 + 1 左右。如果任务是IO密集型的(比如爬虫、数据库查询),可以适当放大到 CPU核心数 * 10。但如果是CPU密集型的,设太多反而会因为上下文切换降低效率。
4.3 异步IO模型:真正的性能王者
多线程和线程池虽然解决了并发问题,但本质上还是「一个线程处理一个连接」。如果连接数上万,线程切换的开销就非常恐怖了。
这时候就该异步IO登场了。异步IO的核心思想是:一个线程处理所有连接,哪个连接有数据来了,就去处理哪个。说白了就是「事件驱动」——没事就歇着,有事才干活。
Python里实现异步IO,最常用的就是selectors模块和asyncio。咱们先看一个基于selectors的例子:
import socket
import selectors
# 创建一个selector对象
sel = selectors.DefaultSelector()
def accept(sock, mask):
"""处理新连接"""
conn, addr = sock.accept()
print(f'[接受] {addr}')
conn.setblocking(False) # 设置为非阻塞
# 注册读事件,当conn可读时调用read回调
sel.register(conn, selectors.EVENT_READ, read)
def read(conn, mask):
"""处理客户端数据"""
try:
data = conn.recv(1024)
if data:
print(f'[收到] {data.decode()}')
conn.send(f'[异步回复] {data.decode()}'.encode())
else:
# 客户端关闭连接
print(f'[关闭] 客户端断开')
sel.unregister(conn)
conn.close()
except:
sel.unregister(conn)
conn.close()
def start_async_server(host='0.0.0.0', port=8890):
server = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
server.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
server.bind((host, port))
server.listen(128)
server.setblocking(False) # 服务器本身也非阻塞
# 注册服务器socket的读事件
sel.register(server, selectors.EVENT_READ, accept)
print(f'[异步服务器] 启动于 {host}:{port}')
try:
while True:
# 阻塞等待事件发生
events = sel.select()
for key, mask in events:
# key.data 就是注册时传入的回调函数
callback = key.data
callback(key.fileobj, mask)
except KeyboardInterrupt:
print('[关闭] 服务器停止')
finally:
sel.close()
if __name__ == '__main__':
start_async_server()
这段代码里,整个服务器只有一个线程。它通过sel.select()等待事件,哪个socket有数据来了,就调用对应的回调函数去处理。你想想看,如果有一万个连接,多线程模型需要一万个线程,而异步模型只需要一个线程——这性能差距可不是一星半点。
关键点:异步IO模型的核心是「非阻塞」+「事件循环」。非阻塞保证了一个socket的读写不会卡住整个流程,事件循环则负责调度哪些socket需要被处理。
4.4 三种模型的对比
说了这么多,咱们用一张表来总结一下:
| 模型 | 并发能力 | 资源消耗 | 编程复杂度 | 适用场景 |
|---|---|---|---|---|
| 多线程 | 低(几十到几百) | 高(每个线程8MB栈) | 低 | 少量客户端,简单任务 |
| 线程池 | 中(几百到几千) | 中(线程数可控) | 低 | IO密集型,连接数中等 |
| 异步IO | 高(上万甚至十万) | 低(单线程) | 高 | 高并发,长连接,实时通信 |
我个人习惯是:小项目用线程池,大项目用异步IO。线程池写起来简单,不容易出bug;异步IO虽然性能好,但回调地狱和状态管理确实让人头疼。不过Python的asyncio已经用async/await语法把回调地狱解决了,后面有机会咱们再细聊。
曾经踩过的坑:我在做一个实时数据推送服务时,用了多线程模型,结果客户端一多,线程切换导致CPU飙升到90%。后来换成异步IO,同样的硬件配置,CPU只用了15%,吞吐量反而翻了三倍。所以如果你的服务需要支撑大量长连接,别犹豫,直接上异步IO。
4.5 知识体系总览
下面这张图帮你理清三种模型的关系和选择逻辑:
嗯,这张图应该能帮你快速理解三种模型的核心区别。记住一个原则:没有银弹。多线程简单但资源消耗大,线程池平衡了性能和资源,异步IO性能最强但编程复杂度最高。选哪个,取决于你的业务场景和团队能力。
最后说一句,如果你刚开始学,我建议先从线程池入手。等把线程池玩明白了,再挑战异步IO。毕竟,一口吃不成胖子,对吧?
小技巧:在实际项目中,我经常把线程池和异步IO结合起来用。比如用异步IO处理网络层的海量连接,然后把业务逻辑丢到线程池里去执行。这样既享受了异步IO的高并发,又避免了在回调里写复杂业务逻辑的尴尬。
可可精品资料库