Python网络编程源码,Socket与多线程,实现聊天室与文件传输

在网络应用开发中,掌握底层套接字(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校验、滑动窗口确认机制。本示例采用循环接收直至达到声明长度,满足小文件即时传输需求。

🛠️ 部署与调试建议

  1. 环境准备
    pip install tkinter  # Windows通常内置,Linux需安装tk-dev
  2. 运行步骤
    • 启动服务端:python server.py
    • 修改客户端IP后运行多个实例模拟多用户
  3. 常见问题排查
    • 端口占用:更换端口或关闭冲突进程
    • 防火墙拦截:临时开放对应端口测试
    • 编码异常:统一强制UTF-8编解码

✨ 扩展方向

  • [ ] 添加AES加密通信通道
  • [ ] 数据库持久化历史记录
  • [ ] WebSocket版浏览器客户端
  • [ ] 群组管理与权限控制

会员自媒体 Python Python网络编程源码,Socket与多线程,实现聊天室与文件传输 https://yuelu1.cn/26297.html

相关文章

猜你喜欢