实战项目8:聊天室程序¶
项目概述¶
使用 Python 标准库 socket 和 threading 实现一个基于 TCP 的多人在线聊天室。服务端负责管理连接和广播消息,客户端支持收发消息、修改昵称、私聊和查看在线用户。这是学习网络编程最经典的入门项目。
学习目标¶
- 理解 TCP Socket 编程的基本流程
- 掌握多线程并发处理多个客户端
- 学习自定义应用层协议(消息格式)
- 理解"一人读、一人写"的收发分离模型
- 了解粘包问题及其简单处理方案
功能需求¶
核心功能¶
- 多个客户端同时连接并聊天
- 服务端把消息广播给所有在线用户
- 用户进入/离开时通知全体
- 修改昵称、查看在线列表
- 私聊指定用户
项目结构¶
代码实现¶
1. 服务端(server.py)¶
import socket
import threading
import time
HOST, PORT = "0.0.0.0", 9000
clients = {} # socket -> 昵称
lock = threading.Lock()
def broadcast(msg, sender=None):
"""把消息发给除发送者外的所有客户端。"""
with lock:
targets = list(clients.items())
text = msg.encode("utf-8")
for sock, _ in targets:
if sock is sender:
continue
try:
sock.sendall(text)
except OSError:
remove_client(sock)
def online_names():
with lock:
return list(clients.values())
def remove_client(sock):
with lock:
name = clients.pop(sock, None)
if name:
try:
sock.close()
except OSError:
pass
broadcast(f"【系统】{name} 离开了聊天室")
def handle(sock, addr):
# 第一条消息作为昵称
try:
name = sock.recv(1024).decode("utf-8").strip() or f"用户{addr[1]}"
except OSError:
sock.close()
return
with lock:
# 昵称去重
base, i = name, 1
while name in clients.values():
i += 1
name = f"{base}{i}"
clients[sock] = name
sock.sendall(f"【系统】欢迎你,{name}!当前在线:{', '.join(online_names())}".encode())
broadcast(f"【系统】{name} 进入了聊天室", sender=sock)
print(f"[{time.strftime('%H:%M:%S')}] {name} 已连接 ({addr[0]})")
buffer = b""
while True:
try:
data = sock.recv(1024)
if not data:
break
except OSError:
break
buffer += data
# 按换行分包,处理 TCP 粘包
while b"\n" in buffer:
line, buffer = buffer.split(b"\n", 1)
text = line.decode("utf-8").strip()
if not text:
continue
now = time.strftime("%H:%M:%S")
if text == "/list":
sock.sendall(f"在线用户:{', '.join(online_names())}".encode())
elif text.startswith("/to "):
# 私聊:/to 昵称 消息
parts = text.split(maxsplit=2)
if len(parts) < 3:
sock.sendall("用法:/to 昵称 消息".encode())
continue
_, target, content = parts
with lock:
target_sock = next(
(s for s, n in clients.items() if n == target), None)
if target_sock:
target_sock.sendall(f"【私聊】{name}:{content}".encode())
else:
sock.sendall(f"【系统】用户 {target} 不在线".encode())
elif text == "/quit":
buffer = b""
break
else:
broadcast(f"[{now}] {name}:{text}", sender=sock)
if not buffer and text == "/quit" if False else False:
break
print(f"{name} 已断开")
remove_client(sock)
def main():
server = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
server.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
server.bind((HOST, PORT))
server.listen()
print(f"聊天室服务端已启动,监听 {HOST}:{PORT}")
while True:
sock, addr = server.accept()
threading.Thread(target=handle, args=(sock, addr), daemon=True).start()
if __name__ == "__main__":
main()
2. 客户端(client.py)¶
import socket
import threading
HOST, PORT = "127.0.0.1", 9000
def receive(sock):
"""独立线程:持续接收并打印服务端消息。"""
while True:
try:
data = sock.recv(1024)
except OSError:
print("\n连接已断开")
break
if not data:
print("\n服务端已关闭")
break
print("\r" + data.decode("utf-8") + "\n> ", end="", flush=True)
def main():
sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
sock.connect((HOST, PORT))
name = input("请输入昵称:").strip()
sock.sendall((name + "\n").encode())
threading.Thread(target=receive, args=(sock,), daemon=True).start()
print("输入消息回车发送,/list 查看在线,/to 昵称 消息 私聊,/quit 退出")
while True:
text = input("> ").strip()
if not text:
continue
if text == "/quit":
sock.sendall(b"/quit\n")
break
try:
sock.sendall((text + "\n").encode("utf-8"))
except OSError:
print("发送失败,连接已断开")
break
sock.close()
if __name__ == "__main__":
main()
运行指南¶
支持的特殊命令:
| 命令 | 作用 |
|---|---|
/list | 查看在线用户 |
/to 昵称 消息 | 向指定用户私聊 |
/quit | 退出聊天室 |
关键知识点¶
为什么要用多线程?¶
服务端要同时服务多个客户端。主线程负责 accept 新连接,每来一个连接就开一个新线程专门处理它的读写,互不阻塞。这是最朴素的并发模型,理解它之后再看 selectors、asyncio 会容易很多。
什么是 TCP 粘包?¶
TCP 是字节流协议,没有"消息边界":两条 send 的内容可能被合并成一次 recv 收到。解决办法是自定义边界,本项目用换行符分包(收到数据后按 \n 切分),生产环境更常用"消息头 + 长度字段"的方案。
广播为什么要加锁?¶
clients 字典会被多个线程同时读写(新连接插入、断开移除、广播遍历)。Python 的 dict 虽然单个操作是原子的,但"遍历期间有元素被删除"仍会出问题,所以用 threading.Lock 保护。
扩展功能建议¶
基础扩展¶
- 消息加上颜色区分(客户端 ANSI 转义码)
- 服务端保存聊天记录到文件
- 限制最大在线人数
进阶扩展¶
- 改用
selectors实现单线程 I/O 多路复用版本 - 用
asyncio重写服务端,对比理解两种并发模型 - 消息加入长度前缀的协议头,彻底解决粘包
高级扩展¶
- 基于 WebSocket(
websockets库)实现网页版聊天室 - 增加聊天房间(频道)功能
- 消息端到端加密
常见问题¶
Q: 客户端发送和接收会互相阻塞吗?¶
A: 不会。客户端把"接收消息"放在独立线程里,主线程只负责读取用户输入并发送,两边互不干扰。
Q: 为什么服务端要捕获 OSError?¶
A: 客户端异常断线(如直接关闭窗口)时,服务端对该 socket 的读写会抛异常。捕获后统一走 remove_client 清理资源,避免线程崩溃。
Q: 如何测试多个客户端?¶
A: 在同一台机器开多个终端运行 client.py 即可,局域网内其他电脑把 HOST 改成服务端 IP 也能连入。
项目总结¶
本项目实现了一个功能完整的多人聊天室,涵盖了:
- Socket 编程:TCP 连接的建立、读写与关闭
- 并发模型:一线程一连接的经典结构
- 协议设计:换行分包 + 命令字(
/list、/to) - 共享状态管理:加锁保护在线用户表
- 健壮性处理:断线检测、昵称去重、异常清理
下一步¶
- 用
asyncio重写本项目的服务端 - 挑战实战项目9:快速排序优化,回到算法训练
祝你开发愉快!