在网络应用开发中,掌握底层套接字(Socket)编程与并发处理机制至关重要。本文将通过一个完整的实战项目——基于TCP协议的聊天室+文件传输系统,深入剖析Python网络编程的核心技术与实现思路。文中所有代码均可直接复用,只需注意运行环境为Python 3.6+,且同一局域网内测试效果最佳。
🏗️ 架构概览
系统采用C/S架构:
-
服务端:单进程+多线程模型,主线程监听连接,子线程处理客户端通信
-
客户端:双线程并行,分别负责消息接收与用户输入发送
-
传输协议:自定义简单头部约定,区分聊天消息与文件数据
⚙️ 核心源码实现
📁 server.py(服务端)
import socket
import threading
import os
from datetime import datetime
class ChatServer:
def __init__(self, host='0.0.0.0', port=8888):
self.server = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
self.server.bind((host, port))
self.clients = {} # {client_socket: (name, addr)}
self.lock = threading.Lock()
def broadcast(self, msg, sender=None):
"""广播消息给所有客户端"""
with self.lock:
for sock in list(self.clients.keys()):
try:
if sock != sender:
sock.send(msg.encode('utf-8'))
except Exception as e:
print(f"广播异常: {e}")
self.remove_client(sock)
def handle_client(self, client_sock, addr):
"""处理单个客户端连接"""
try:
name = client_sock.recv(1024).decode().strip()
welcome_msg = f"[{datetime.now():%H:%M}] {name} 进入聊天室"
with self.lock:
self.clients[client_sock] = (name, addr)
self.broadcast(welcome_msg)
print(f"{addr} 注册为: {name}")
while True:
data = client_sock.recv(4096)
if not data:
break
msg_type = data[0]
# 文本消息
if msg_type == 1:
text = data[1:].decode('utf-8')
formatted = f"[{datetime.now():%H:%M}] {name}: {text}"
self.broadcast(formatted, client_sock)
# 文件传输请求
elif msg_type == 2:
header = data[1:257].decode().rstrip('\x00')
filename, filesize = header.split('|')[:2]
filesize = int(filesize)
file_data = data[257:]
recv_size = len(file_data)
# 分段接收剩余文件数据
while recv_size < filesize:
chunk = client_sock.recv(min(8192, filesize - recv_size))
if not chunk:
break
file_data += chunk
recv_size += len(chunk)
if recv_size == filesize:
save_path = f"server_files/{filename}"
os.makedirs("server_files", exist_ok=True)
with open(save_path, 'wb') as f:
f.write(file_data)
notify = f"[文件] {name} 上传了 {filename} ({filesize//1024}KB)"
self.broadcast(notify)
else:
print(f"文件 {filename} 接收不完整")
except ConnectionResetError:
pass
finally:
self.remove_client(client_sock)
def remove_client(self, sock):
"""移除断开连接的客户端"""
with self.lock:
if sock in self.clients:
name, _ = self.clients[sock]
leave_msg = f"[{datetime.now():%H:%M}] {name} 离开聊天室"
del self.clients[sock]
self.broadcast(leave_msg)
print(f"{name} 已下线")
def run(self):
"""启动服务端"""
self.server.listen(10)
print(f"🚀 服务器启动于端口 8888,等待连接...")
while True:
try:
client_sock, addr = self.server.accept()
thread = threading.Thread(target=self.handle_client, args=(client_sock, addr))
thread.daemon = True
thread.start()
except KeyboardInterrupt:
break
self.server.close()
if __name__ == '__main__':
ChatServer().run()
💻 client.py(客户端)
import socket
import threading
import os
import sys
import tkinter as tk
from tkinter import filedialog, scrolledtext
class ChatClient:
def __init__(self, host='127.0.0.1', port=8888):
self.client = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
self.host, self.port = host, port
self.name = None
def connect(self, username):
"""连接到服务器并发送用户名"""
self.client.connect((self.host, self.port))
self.client.send(username.encode())
self.name = username
return True
def send_text(self, message):
"""发送文本消息(类型码=1)"""
header = bytes([1])
payload = message.encode('utf-8')
self.client.sendall(header + payload)
def send_file(self, filepath):
"""发送文件(类型码=2)"""
if not os.path.exists(filepath):
return False
filename = os.path.basename(filepath)
filesize = os.path.getsize(filepath)
# 构建256字节固定头:文件名|文件大小
header_info = f"{filename}|{filesize}".ljust(255)[:255] + '\x00'
with open(filepath, 'rb') as f:
file_content = f.read()
packet = bytes([2]) + header_info.encode() + file_content
self.client.sendall(packet)
return True
def receive_messages(self, callback):
"""持续接收消息的线程函数"""
try:
while True:
data = self.client.recv(16384)
if not data:
callback("[系统] 与服务器断开连接")
break
msg_type = data[0]
if msg_type == 1: # 文本
text = data[1:].decode('utf-8')
callback(text)
elif msg_type == 2: # 文件通知(简化为文本提示)
info = data[1:].decode()
callback(info)
except (ConnectionAbortedError, ConnectionResetError):
callback("[系统] 连接异常终止")
def close(self):
self.client.close()
def main():
# GUI界面初始化(此处省略详细Tkinter布局代码)
root = tk.Tk()
root.title("PyChat Client")
# 连接配置面板
conn_frame = tk.Frame(root)
tk.Label(conn_frame, text="昵称:").grid(row=0, column=0)
name_entry = tk.Entry(conn_frame)
name_entry.grid(row=0, column=1)
tk.Label(conn_frame, text="服务器IP:").grid(row=1, column=0)
ip_entry = tk.Entry(conn_frame)
ip_entry.insert(0, "127.0.0.1")
ip_entry.grid(row=1, column=1)
status_label = tk.Label(root, text="未连接", fg="red")
chat_area = scrolledtext.ScrolledText(root, state='disabled')
def update_chat(content):
chat_area.config(state='normal')
chat_area.insert(tk.END, content + '\n')
chat_area.yview(tk.END)
chat_area.config(state='disabled')
client = ChatClient()
def on_connect():
name = name_entry.get().strip()
if not name:
return
try:
host = ip_entry.get()
client.__init__(host=host)
if client.connect(name):
status_label.config(text=f"已连接: {name}", fg="green")
threading.Thread(
target=client.receive_messages,
args=(update_chat,),
daemon=True
).start()
except Exception as e:
update_chat(f"[错误] 连接失败: {str(e)}")
connect_btn = tk.Button(conn_frame, text="连接", command=on_connect)
connect_btn.grid(row=2, columnspan=2)
conn_frame.pack(pady=5)
status_label.pack()
chat_area.pack(fill=tk.BOTH, expand=True, padx=10, pady=5)
# 底部输入区域
input_frame = tk.Frame(root)
msg_entry = tk.Entry(input_frame)
msg_entry.pack(side=tk.LEFT, fill=tk.X, expand=True, padx=(0,5))
def on_send():
msg = msg_entry.get().strip()
if msg and hasattr(client, 'send_text'):
client.send_text(msg)
msg_entry.delete(0, tk.END)
send_btn = tk.Button(input_frame, text="发送", command=on_send)
send_btn.pack(side=tk.RIGHT)
def upload_file():
path = filedialog.askopenfilename(title="选择要发送的文件")
if path and hasattr(client, 'send_file'):
threading.Thread(target=client.send_file, args=(path,)).start()
upload_btn = tk.Button(input_frame, text="📎", command=upload_file)
upload_btn.pack(side=tk.RIGHT, padx=5)
input_frame.pack(fill=tk.X, padx=10, pady=5)
root.protocol("WM_DELETE_WINDOW", lambda: (client.close(), root.quit()))
root.mainloop()
if __name__ == '__main__':
main()
🔑 关键技术详解
1. Socket基础封装
-
TCP流式套接字保证数据有序可靠传输
-
AF_INET+SOCK_STREAM组合适用于绝大多数局域网/公网场景 -
缓冲区大小根据传输类型动态调整(文本4K,文件16K)
2. 多线程并发模型
-
服务端:每接入一个客户端创建独立线程,互不影响
-
客户端:UI主线程与网络接收线程分离,避免界面卡顿
-
线程守护模式确保程序退出时自动回收资源
3. 传输协议设计
通过首字节标识载荷类型,扩展性强:
协议帧结构:
┌─────────┬──────────────────────┐
│ 类型(1B)│ 有效载荷 │
└─────────┴──────────────────────┘
类型定义:
1 = UTF-8文本消息
2 = 文件传输(含256B文件名+大小头)
4. 文件断点续传优化点
实际生产环境中可增加MD5校验、滑动窗口确认机制。本示例采用循环接收直至达到声明长度,满足小文件即时传输需求。
🛠️ 部署与调试建议
-
环境准备
pip install tkinter # Windows通常内置,Linux需安装tk-dev -
运行步骤
-
启动服务端:
python server.py -
修改客户端IP后运行多个实例模拟多用户
-
-
常见问题排查
-
端口占用:更换端口或关闭冲突进程
-
防火墙拦截:临时开放对应端口测试
-
编码异常:统一强制UTF-8编解码
-
✨ 扩展方向
-
[ ] 添加AES加密通信通道
-
[ ] 数据库持久化历史记录
-
[ ] WebSocket版浏览器客户端
-
[ ] 群组管理与权限控制