# 简单客户端-服务器实现 ## 🎯 学习目标 通过本章学习,您将能够: - 从零开始实现一个完整的客户端-服务器应用 - 理解网络编程的实际开发流程 - 掌握调试和测试网络应用的方法 - 为Chat-Room项目的开发做好准备 ## 🚀 项目概述 我们将实现一个简单的聊天系统,包含以下功能: - 多客户端连接支持 - 实时消息广播 - 用户昵称管理 - 基本的错误处理 ```mermaid graph TB subgraph "简单聊天系统架构" S[聊天服务器
ChatServer] C1[客户端1
Alice] C2[客户端2
Bob] C3[客户端3
Charlie] C1 <--> S C2 <--> S C3 <--> S end subgraph "消息流程" M1[Alice发送消息] --> M2[服务器接收] M2 --> M3[广播给所有客户端] M3 --> M4[Bob和Charlie收到消息] end style S fill:#f9f,stroke:#333,stroke-width:4px ``` ## 🖥️ 服务器端实现 ### 完整的聊天服务器 ```python #!/usr/bin/env python3 """ 简单聊天服务器实现 演示Socket编程的基本概念和实践 """ import socket import threading import json import time from typing import Dict, List, Optional class SimpleChatServer: """ 简单聊天服务器 功能: 1. 接受多个客户端连接 2. 管理用户昵称 3. 广播消息给所有客户端 4. 处理客户端断开连接 """ def __init__(self, host: str = "localhost", port: int = 8888): self.host = host self.port = port self.server_socket: Optional[socket.socket] = None self.running = False # 客户端管理 self.clients: Dict[socket.socket, str] = {} # {socket: nickname} self.clients_lock = threading.Lock() # 线程安全 print(f"初始化聊天服务器: {host}:{port}") def start(self): """启动服务器""" try: # 创建服务器Socket self.server_socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM) # 设置Socket选项 self.server_socket.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1) # 绑定地址和端口 self.server_socket.bind((self.host, self.port)) # 开始监听 self.server_socket.listen(10) self.running = True print(f"🚀 聊天服务器启动成功!") print(f"📡 监听地址: {self.host}:{self.port}") print(f"⏳ 等待客户端连接...") # 主循环:接受客户端连接 self._accept_connections() except Exception as e: print(f"❌ 服务器启动失败: {e}") finally: self.stop() def _accept_connections(self): """接受客户端连接的主循环""" while self.running: try: # 接受新连接 client_socket, client_address = self.server_socket.accept() print(f"🔗 新客户端连接: {client_address}") # 为每个客户端创建处理线程 client_thread = threading.Thread( target=self._handle_client, args=(client_socket, client_address), daemon=True ) client_thread.start() except OSError: # 服务器Socket被关闭 break except Exception as e: print(f"❌ 接受连接时发生错误: {e}") def _handle_client(self, client_socket: socket.socket, client_address): """ 处理单个客户端 每个客户端在独立线程中运行 """ nickname = None try: # 1. 客户端昵称注册 nickname = self._register_client(client_socket) if not nickname: return # 2. 发送欢迎消息 self._send_welcome_message(client_socket, nickname) # 3. 广播用户加入消息 self._broadcast_system_message(f"🎉 {nickname} 加入了聊天室") # 4. 处理客户端消息 self._process_client_messages(client_socket, nickname) except Exception as e: print(f"❌ 处理客户端 {client_address} 时发生错误: {e}") finally: # 5. 清理客户端连接 self._cleanup_client(client_socket, nickname) def _register_client(self, client_socket: socket.socket) -> Optional[str]: """ 注册客户端昵称 Returns: 客户端昵称,如果注册失败返回None """ try: # 发送昵称请求 request_msg = { "type": "nickname_request", "message": "请输入您的昵称:" } self._send_message(client_socket, request_msg) # 接收昵称响应 response = self._receive_message(client_socket) if not response or response.get("type") != "nickname_response": print("❌ 客户端昵称注册失败") return None nickname = response.get("nickname", "").strip() # 验证昵称 if not self._validate_nickname(nickname): error_msg = { "type": "error", "message": "昵称无效,请使用3-20个字符" } self._send_message(client_socket, error_msg) return None # 检查昵称是否已被使用 with self.clients_lock: if nickname in self.clients.values(): error_msg = { "type": "error", "message": f"昵称 '{nickname}' 已被使用" } self._send_message(client_socket, error_msg) return None # 注册成功 self.clients[client_socket] = nickname print(f"✅ 客户端注册成功: {nickname}") return nickname except Exception as e: print(f"❌ 客户端注册失败: {e}") return None def _validate_nickname(self, nickname: str) -> bool: """验证昵称格式""" if not nickname: return False if len(nickname) < 3 or len(nickname) > 20: return False if not nickname.replace('_', '').replace('-', '').isalnum(): return False return True def _send_welcome_message(self, client_socket: socket.socket, nickname: str): """发送欢迎消息""" welcome_msg = { "type": "system", "message": f"🎊 欢迎 {nickname}!您已成功连接到聊天室", "timestamp": time.time() } self._send_message(client_socket, welcome_msg) # 发送在线用户列表 with self.clients_lock: online_users = list(self.clients.values()) users_msg = { "type": "user_list", "users": online_users, "message": f"📋 当前在线用户 ({len(online_users)}): {', '.join(online_users)}" } self._send_message(client_socket, users_msg) def _process_client_messages(self, client_socket: socket.socket, nickname: str): """ 处理客户端消息的主循环 持续接收和处理客户端发送的消息 """ while self.running: try: message = self._receive_message(client_socket) if not message: break # 处理不同类型的消息 msg_type = message.get("type") if msg_type == "chat": self._handle_chat_message(message, nickname) elif msg_type == "ping": self._handle_ping_message(client_socket) else: print(f"⚠️ 未知消息类型: {msg_type}") except Exception as e: print(f"❌ 处理 {nickname} 的消息时发生错误: {e}") break def _handle_chat_message(self, message: dict, sender_nickname: str): """处理聊天消息""" content = message.get("content", "").strip() if not content: return # 构造广播消息 broadcast_msg = { "type": "chat", "sender": sender_nickname, "content": content, "timestamp": time.time() } # 广播给所有客户端 self._broadcast_message(broadcast_msg) print(f"💬 [{sender_nickname}]: {content}") def _handle_ping_message(self, client_socket: socket.socket): """处理心跳消息""" pong_msg = { "type": "pong", "timestamp": time.time() } self._send_message(client_socket, pong_msg) def _send_message(self, client_socket: socket.socket, message: dict) -> bool: """ 发送消息给指定客户端 消息格式:[4字节长度][JSON数据] """ try: # 序列化消息 message_json = json.dumps(message, ensure_ascii=False) message_bytes = message_json.encode('utf-8') # 发送长度头 length_header = len(message_bytes).to_bytes(4, byteorder='big') client_socket.send(length_header) # 发送消息体 client_socket.send(message_bytes) return True except Exception as e: print(f"❌ 发送消息失败: {e}") return False def _receive_message(self, client_socket: socket.socket) -> Optional[dict]: """ 接收完整消息 Returns: 解析后的消息字典,如果失败返回None """ try: # 接收长度头(4字节) length_data = self._receive_exact(client_socket, 4) if not length_data: return None # 解析消息长度 message_length = int.from_bytes(length_data, byteorder='big') # 接收消息体 message_data = self._receive_exact(client_socket, message_length) if not message_data: return None # 解析JSON消息 message_json = message_data.decode('utf-8') message = json.loads(message_json) return message except Exception as e: print(f"❌ 接收消息失败: {e}") return None def _receive_exact(self, client_socket: socket.socket, length: int) -> Optional[bytes]: """ 接收指定长度的数据 确保接收到完整的数据 """ data = b"" while len(data) < length: chunk = client_socket.recv(length - len(data)) if not chunk: return None data += chunk return data def _broadcast_message(self, message: dict): """广播消息给所有客户端""" with self.clients_lock: disconnected_clients = [] for client_socket in self.clients.keys(): if not self._send_message(client_socket, message): disconnected_clients.append(client_socket) # 清理断开的客户端 for client_socket in disconnected_clients: nickname = self.clients.get(client_socket, "未知用户") self._cleanup_client(client_socket, nickname) def _broadcast_system_message(self, content: str): """广播系统消息""" system_msg = { "type": "system", "message": content, "timestamp": time.time() } self._broadcast_message(system_msg) print(f"📢 系统消息: {content}") def _cleanup_client(self, client_socket: socket.socket, nickname: Optional[str]): """清理客户端连接""" try: client_socket.close() except: pass with self.clients_lock: if client_socket in self.clients: nickname = self.clients[client_socket] del self.clients[client_socket] if nickname: print(f"👋 {nickname} 离开了聊天室") self._broadcast_system_message(f"👋 {nickname} 离开了聊天室") def stop(self): """停止服务器""" print("🛑 正在关闭服务器...") self.running = False # 关闭所有客户端连接 with self.clients_lock: for client_socket in list(self.clients.keys()): try: client_socket.close() except: pass self.clients.clear() # 关闭服务器Socket if self.server_socket: try: self.server_socket.close() except: pass print("✅ 服务器已关闭") def main(): """服务器主函数""" server = SimpleChatServer() try: server.start() except KeyboardInterrupt: print("\n收到中断信号") finally: server.stop() if __name__ == "__main__": main() ``` ## 💻 客户端实现 ### 完整的聊天客户端 ```python #!/usr/bin/env python3 """ 简单聊天客户端实现 演示Socket编程的客户端开发 """ import socket import threading import json import time from typing import Optional class SimpleChatClient: """ 简单聊天客户端 功能: 1. 连接到聊天服务器 2. 注册用户昵称 3. 发送和接收消息 4. 处理服务器断开 """ def __init__(self, host: str = "localhost", port: int = 8888): self.host = host self.port = port self.socket: Optional[socket.socket] = None self.connected = False self.nickname = "" # 接收线程 self.receive_thread: Optional[threading.Thread] = None print(f"初始化聊天客户端: {host}:{port}") def start(self): """启动客户端""" try: # 1. 连接服务器 if not self._connect_to_server(): return # 2. 注册昵称 if not self._register_nickname(): return # 3. 启动接收线程 self._start_receive_thread() # 4. 开始用户输入循环 self._input_loop() except KeyboardInterrupt: print("\n用户中断程序") except Exception as e: print(f"❌ 客户端运行错误: {e}") finally: self.disconnect() def _connect_to_server(self) -> bool: """连接到服务器""" try: print(f"🔗 正在连接服务器 {self.host}:{self.port}...") # 创建Socket self.socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM) self.socket.settimeout(10.0) # 10秒连接超时 # 连接服务器 self.socket.connect((self.host, self.port)) self.connected = True print("✅ 连接服务器成功!") return True except Exception as e: print(f"❌ 连接服务器失败: {e}") return False def _register_nickname(self) -> bool: """注册昵称""" try: # 接收昵称请求 request = self._receive_message() if not request or request.get("type") != "nickname_request": print("❌ 服务器昵称请求格式错误") return False # 获取用户输入的昵称 print(request.get("message", "请输入昵称:")) nickname = input("👤 昵称: ").strip() if not nickname: print("❌ 昵称不能为空") return False # 发送昵称响应 response = { "type": "nickname_response", "nickname": nickname } if not self._send_message(response): return False # 等待服务器响应 result = self._receive_message() if not result: return False if result.get("type") == "error": print(f"❌ 注册失败: {result.get('message')}") return False self.nickname = nickname print(f"✅ 昵称注册成功: {nickname}") return True except Exception as e: print(f"❌ 昵称注册失败: {e}") return False def _start_receive_thread(self): """启动消息接收线程""" self.receive_thread = threading.Thread( target=self._receive_loop, daemon=True ) self.receive_thread.start() print("📡 消息接收线程已启动") def _receive_loop(self): """消息接收循环""" while self.connected: try: message = self._receive_message() if not message: break self._handle_received_message(message) except Exception as e: if self.connected: print(f"❌ 接收消息错误: {e}") break print("📡 消息接收线程已停止") def _handle_received_message(self, message: dict): """处理接收到的消息""" msg_type = message.get("type") if msg_type == "chat": # 聊天消息 sender = message.get("sender") content = message.get("content") timestamp = message.get("timestamp", time.time()) time_str = time.strftime("%H:%M:%S", time.localtime(timestamp)) print(f"[{time_str}] {sender}: {content}") elif msg_type == "system": # 系统消息 content = message.get("message") timestamp = message.get("timestamp", time.time()) time_str = time.strftime("%H:%M:%S", time.localtime(timestamp)) print(f"[{time_str}] 📢 {content}") elif msg_type == "user_list": # 用户列表 print(f"📋 {message.get('message')}") elif msg_type == "pong": # 心跳响应 print("💓 服务器心跳正常") else: print(f"⚠️ 未知消息类型: {msg_type}") def _input_loop(self): """用户输入循环""" print("\n🎉 欢迎来到聊天室!") print("💡 输入消息并按回车发送") print("💡 输入 '/quit' 退出程序") print("💡 输入 '/ping' 测试连接") print("-" * 50) while self.connected: try: user_input = input().strip() if not user_input: continue # 处理特殊命令 if user_input == "/quit": print("👋 再见!") break elif user_input == "/ping": self._send_ping() continue # 发送聊天消息 chat_msg = { "type": "chat", "content": user_input } if not self._send_message(chat_msg): print("❌ 发送消息失败") break except EOFError: # Ctrl+D break except Exception as e: print(f"❌ 输入处理错误: {e}") break def _send_ping(self): """发送心跳消息""" ping_msg = { "type": "ping", "timestamp": time.time() } if self._send_message(ping_msg): print("💓 发送心跳消息") else: print("❌ 发送心跳失败") def _send_message(self, message: dict) -> bool: """发送消息到服务器""" if not self.connected or not self.socket: return False try: # 序列化消息 message_json = json.dumps(message, ensure_ascii=False) message_bytes = message_json.encode('utf-8') # 发送长度头 length_header = len(message_bytes).to_bytes(4, byteorder='big') self.socket.send(length_header) # 发送消息体 self.socket.send(message_bytes) return True except Exception as e: print(f"❌ 发送消息失败: {e}") self.connected = False return False def _receive_message(self) -> Optional[dict]: """接收完整消息""" if not self.connected or not self.socket: return None try: # 接收长度头 length_data = self._receive_exact(4) if not length_data: return None # 解析消息长度 message_length = int.from_bytes(length_data, byteorder='big') # 接收消息体 message_data = self._receive_exact(message_length) if not message_data: return None # 解析JSON消息 message_json = message_data.decode('utf-8') message = json.loads(message_json) return message except Exception as e: if self.connected: print(f"❌ 接收消息失败: {e}") self.connected = False return None def _receive_exact(self, length: int) -> Optional[bytes]: """接收指定长度的数据""" data = b"" while len(data) < length: try: chunk = self.socket.recv(length - len(data)) if not chunk: return None data += chunk except Exception: return None return data def disconnect(self): """断开连接""" if not self.connected: return print("🔌 正在断开连接...") self.connected = False # 关闭Socket if self.socket: try: self.socket.close() except: pass self.socket = None # 等待接收线程结束 if self.receive_thread and self.receive_thread.is_alive(): self.receive_thread.join(timeout=2.0) print("✅ 连接已断开") def main(): """客户端主函数""" print("🎮 简单聊天客户端") print("=" * 30) # 获取服务器地址 host = input("服务器地址 (默认 localhost): ").strip() or "localhost" port_input = input("服务器端口 (默认 8888): ").strip() port = int(port_input) if port_input else 8888 # 创建并启动客户端 client = SimpleChatClient(host, port) client.start() if __name__ == "__main__": main() ``` ## 🧪 测试和调试 ### 测试脚本 ```python #!/usr/bin/env python3 """ 聊天系统测试脚本 """ import subprocess import time import threading def test_server_client(): """测试服务器和客户端""" print("🧪 开始测试聊天系统") # 1. 启动服务器 print("1️⃣ 启动服务器...") server_process = subprocess.Popen( ["python", "simple_server.py"], stdout=subprocess.PIPE, stderr=subprocess.PIPE ) # 等待服务器启动 time.sleep(2) # 2. 启动多个客户端进行测试 print("2️⃣ 启动测试客户端...") # 这里可以添加自动化测试逻辑 # 例如:模拟客户端连接、发送消息等 print("✅ 测试完成") # 清理 server_process.terminate() if __name__ == "__main__": test_server_client() ``` ## 🎯 实践练习 ### 练习1:添加私聊功能 ```python # 在服务器中添加私聊处理 def _handle_private_message(self, message: dict, sender_nickname: str): """ 处理私聊消息 消息格式: { "type": "private", "target": "目标用户昵称", "content": "消息内容" } """ # TODO: 实现私聊功能 pass ``` ### 练习2:添加聊天室功能 ```python # 添加多个聊天室支持 class ChatRoom: """聊天室类""" def __init__(self, name: str): self.name = name self.members = set() # 成员集合 self.messages = [] # 消息历史 def add_member(self, nickname: str): """添加成员""" # TODO: 实现成员添加 pass def remove_member(self, nickname: str): """移除成员""" # TODO: 实现成员移除 pass def broadcast_message(self, message: dict): """广播消息给房间成员""" # TODO: 实现房间内广播 pass ``` ## ✅ 学习检查 完成本章学习后,请确认您能够: - [ ] 独立实现完整的客户端-服务器应用 - [ ] 处理多客户端并发连接 - [ ] 实现可靠的消息传输机制 - [ ] 添加基本的错误处理和异常管理 - [ ] 测试和调试网络应用 - [ ] 完成实践练习 ## 📚 下一步 简单客户端-服务器实现完成后,请继续学习: - [第3章:简单聊天室](../07-simple-chat/protocol-design.md) --- **恭喜!您已经完成了Socket网络编程的学习!** 🎉