用 asyncio 实现多房间核心逻辑:rooms 字典维护各房间 StreamWriter 集合,JOIN/ROOM/LEAVE 协议解析实现用户进出与消息定向广播,严格避免同步 I/O 和粘包,telnet 可直连,单文件≤150行、内存稳定10MB内。

用 socket 实现多房间核心逻辑,别碰 threading 就完事
纯 socket + select 或 asyncio 是唯一靠谱路径。用 threading 每个客户端起一个线程,房间一多(比如 50+ 用户分在 10 个房间),线程调度开销和锁竞争会直接让服务端卡死或丢消息。
推荐走 asyncio:它天然支持单线程内并发处理大量连接,房间状态只需用字典维护,例如 rooms = {"lobby": set(), "python": set(), "js": set()},每个 set 存的是对应房间的 asyncio.StreamWriter 实例。
- 用户登录时发
"JOIN:python",服务端解析后把 writer 加入rooms["python"] - 发消息时带前缀
"ROOM:python:hello world",服务端提取房间名,只往rooms["python"]里所有 writer 写数据 - 断开连接时,遍历所有
rooms.values(),用discard()安全移除该 writer(不用remove()避免 KeyError)
asyncio 下如何安全广播而不阻塞其他房间
关键不是“怎么发”,而是“发给谁”和“不发给谁”。广播必须严格限定在目标房间的 writer 集合内,且不能用同步 I/O(如 print() 或文件写入)混在里面——那会阻塞整个 event loop。
- 所有网络写操作必须用
writer.write()+await writer.drain(),后者不能省,否则缓冲区满会导致后续 write 报ConnectionResetError - 不要在广播循环里做字符串拼接或 JSON 序列化耗时操作,提前序列化好再进循环
- 如果某 client 的
writer.is_closing()为True,跳过它,避免RuntimeError: writer is closing
示例片段:
立即学习“Python免费学习笔记(深入)”;
安全 Python 代码沙箱:在隔离的沙箱中执行任意 Python 代码,预装 requests、NumPy、pandas 库。返回 stdout、stderr。
for writer in rooms.get(room_name, set()):
if not writer.is_closing():
try:
writer.write(message_bytes)
await writer.drain()
except (ConnectionResetError, BrokenPipeError):
rooms[room_name].discard(writer)
客户端怎么识别自己在哪个房间、并切换房间
服务端不主动推房间列表,客户端靠协议约定来管理状态。最简方案是:所有消息都带显式房间标识,客户端只解析并显示当前所在房间的消息。
- 登录后默认进
"lobby",客户端本地记下current_room = "lobby" - 输入
/join python,客户端拼出"JOIN:python"发给服务端,并更新本地current_room - 收到服务端消息如
"ROOM:python:hi from server",客户端检查前缀是否匹配current_room,只显示匹配的 -
/leave就发"LEAVE",服务端从所有 rooms 里移除该 writer,客户端重置current_room = "lobby"
注意:服务端不校验 JOIN 的房间名合法性(比如是否已存在),新房间会自动创建——这是简化设计,也是故意为之。
为什么不用 WebSockets 而坚持原生 socket
因为“简易”二字有明确边界:不需要浏览器兼容、不涉及 HTTPS、不需跨域、不打算加历史消息或用户头像。一旦引入 websockets 库或 Flask-SocketIO,就等于接受 HTTP 生命周期、握手开销、额外依赖和更复杂的错误码映射(比如 1006 vs ConnectionAbortedError)。
原生 asyncio socket 能做到:
- telnet 直连测试:
telnet localhost 8888就能聊天 - 单文件服务端 ≤ 150 行
- 内存占用稳定在 10MB 内(100 连接)
真正容易被忽略的点是:客户端必须自己处理粘包。服务端按行发送(\n 结尾),客户端就得按行接收,不能假设 reader.read(1024) 一定读到完整消息——得缓存未完成的行,等下次 read 再拼。这点不处理,/join 和消息内容就会错乱。

















