在线聊天室的搭建(2025.6.20)
2026/2/2大约 14 分钟
在线聊天室的搭建(2025.6.20)
前言:
在一番探索与ai的帮助下,实现了在线聊天室的功能,这里记录在案,防止忘记。另外还很多技术其实自己并不知道,代码也是ai写的,这些都需要自己去学习,变成自己的知识。
详细步骤:
首先创建如下的目录结构
websocket
├── public
│ └── index.html
├── server.py
└── start_server.sh
于是再把各部分的内容给出即可。
index.html
<!DOCTYPE html> <html lang="zh-CN"> <head> <meta charset="UTF-8"> <meta name="viewport" content="width=device-width, initial-scale=1.0"> <title>简易聊天室</title> <style> * { box-sizing: border-box; margin: 0; padding: 0; font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif; } body { background-color: #f5f7fa; display: flex; flex-direction: column; height: 100vh; padding: 15px; color: #333; } .container { max-width: 1000px; margin: 0 auto; background: white; border-radius: 12px; box-shadow: 0 4px 20px rgba(0, 0, 0, 0.08); display: flex; flex-direction: column; height: calc(100vh - 30px); overflow: hidden; } .header { background: linear-gradient(135deg, #4361ee, #3a0ca3); color: white; padding: 15px 20px; text-align: center; } .header h1 { font-weight: 600; font-size: 1.8rem; margin: 5px 0; } .header .subtitle { font-size: 0.9rem; opacity: 0.9; } .main-content { display: flex; flex: 1; overflow: hidden; } .chat-area { flex: 1; padding: 20px; overflow-y: auto; display: flex; flex-direction: column; background-color: #f8f9fe; } .message { max-width: 80%; margin-bottom: 18px; padding: 12px 16px; border-radius: 18px; word-break: break-word; line-height: 1.4; position: relative; box-shadow: 0 2px 5px rgba(0, 0, 0, 0.05); animation: fadeIn 0.3s ease-in-out; } @keyframes fadeIn { from { opacity: 0; transform: translateY(10px); } to { opacity: 1; transform: translateY(0); } } .received { align-self: flex-start; background: #ffffff; border: 1px solid #e0e0e0; border-bottom-left-radius: 5px; } .sent { align-self: flex-end; background: linear-gradient(135deg, #4361ee, #3a0ca3); color: white; border-bottom-right-radius: 5px; } .notification { align-self: center; background: #f0f2f5; color: #5c677d; border-radius: 20px; font-size: 0.85rem; padding: 8px 16px; margin: 8px 0; text-align: center; } .sender-name { font-size: 0.8rem; font-weight: 600; margin-bottom: 4px; opacity: 0.8; } .message-content { font-size: 1rem; line-height: 1.4; } .message-time { font-size: 0.7rem; text-align: right; margin-top: 5px; opacity: 0.65; } .input-area { display: flex; padding: 15px; border-top: 1px solid #eee; background: white; } #message-input { flex: 1; padding: 14px 18px; border: 1px solid #ddd; border-radius: 28px; outline: none; font-size: 1rem; transition: all 0.3s; box-shadow: 0 2px 8px rgba(0, 0, 0, 0.05); } #message-input:focus { border-color: #4361ee; box-shadow: 0 2px 12px rgba(67, 97, 238, 0.2); } #send-button { background: linear-gradient(135deg, #4361ee, #3a0ca3); color: white; border: none; border-radius: 28px; padding: 14px 28px; margin-left: 12px; cursor: pointer; font-weight: 600; transition: all 0.3s; box-shadow: 0 4px 10px rgba(67, 97, 238, 0.3); } #send-button:hover { transform: translateY(-2px); box-shadow: 0 6px 14px rgba(67, 97, 238, 0.4); } #send-button:active { transform: translateY(0); } #send-button:disabled { background: #ccc; box-shadow: none; cursor: not-allowed; transform: none; } .status-bar { padding: 10px 15px; background: #f8f9fe; border-top: 1px solid #eee; font-size: 0.85rem; color: #5c677d; display: flex; justify-content: space-between; } .status-item { display: flex; align-items: center; } .status-dot { width: 10px; height: 10px; border-radius: 50%; margin-right: 8px; } .disconnected { background-color: #ff6b6b; } .connecting { background-color: #ffd166; animation: pulse 1.5s infinite; } .connected { background-color: #06d6a0; } @keyframes pulse { 0% { opacity: 0.5; } 50% { opacity: 1; } 100% { opacity: 0.5; } } .config-panel { padding: 20px; background: #f8f9fe; border-left: 1px solid #eee; width: 300px; overflow-y: auto; } .config-section { margin-bottom: 20px; } .config-section h3 { margin-bottom: 12px; color: #3a0ca3; font-size: 1.1rem; padding-bottom: 8px; border-bottom: 1px solid #eee; } .config-item { margin-bottom: 15px; } .config-item label { display: block; margin-bottom: 6px; font-weight: 600; font-size: 0.9rem; color: #5c677d; } .config-item input { width: 100%; padding: 12px; border: 1px solid #ddd; border-radius: 8px; font-size: 1rem; transition: border-color 0.3s; } .config-item input:focus { border-color: #4361ee; outline: none; } .info-box { background-color: #e8f4ff; border-left: 4px solid #4361ee; padding: 12px; border-radius: 4px; font-size: 0.85rem; margin-top: 20px; } .info-box h4 { margin-top: 0; margin-bottom: 8px; color: #3a0ca3; } .info-box ul { padding-left: 20px; margin: 8px 0; } .info-box li { margin-bottom: 4px; } .debug-console { background-color: #1e1e1e; color: #d4d4d4; font-family: monospace; padding: 12px; border-radius: 4px; margin-top: 15px; max-height: 200px; overflow-y: auto; font-size: 0.85rem; display: none; } .debug-console.show { display: block; } .connection-info { font-size: 0.8rem; color: #777; padding: 8px; text-align: center; } .toggle-debug { background-color: #3a0ca3; color: white; border: none; border-radius: 4px; padding: 6px 12px; font-size: 0.8rem; cursor: pointer; float: right; } #debug-console-content { white-space: pre-wrap; overflow-anchor: none; } .center { display: flex; justify-content: center; align-items: center; height: 100%; } </style> </head> <body> <div class="container"> <div class="header"> <h1>简易聊天室</h1> <div class="subtitle">使用WebSocket技术的实时聊天应用</div> </div> <div class="main-content"> <div class="chat-area" id="chat-area"> <div class="center" id="welcome-message"> <div> <h2>欢迎使用聊天室</h2> <p>正在连接服务器...</p> </div> </div> </div> <div class="config-panel"> <button class="toggle-debug" id="toggle-debug">调试信息</button> <div class="config-section"> <h3>服务器配置</h3> <div class="config-item"> <label for="ws-server-input">WebSocket服务器地址</label> <input type="text" id="ws-server-input" placeholder="ws://IP地址或域名:端口" value="ws://8.134.12.212:8765"> </div> <button id="reconnect-button" class="toggle-debug">重新连接</button> </div> <div class="info-box"> <h4>连接帮助</h4> <ul> <li>默认WebSocket端口: 8765</li> <li>本地连接: ws://localhost:8765</li> <li>局域网连接: ws://[本地IP]:8765</li> <li>远程连接: ws://[公网IP]:8765</li> <li>确保服务器防火墙开放了WebSocket端口</li> </ul> </div> <div class="debug-console" id="debug-console"> <div id="debug-console-content"></div> </div> </div> </div> <div class="input-area"> <input type="text" id="message-input" placeholder="输入消息..." disabled> <button id="send-button" disabled>发送</button> </div> <div class="status-bar"> <div class="status-item"> <div class="status-dot disconnected" id="status-dot"></div> <span id="status-text">正在连接服务器...</span> </div> <div class="status-item" id="users-count"> <span>在线人数: 0</span> </div> </div> </div> <div class="username-modal" id="username-modal"> <div class="modal-content"> <h2>欢迎加入聊天室</h2> <p>请输入用户名</p> <input type="text" id="username-input" maxlength="15" placeholder="输入用户名"> <button id="confirm-username">进入聊天室</button> </div> </div> <script> // =================== // 常量与全局变量 // =================== const DEBUG = true; // 启用调试模式 const MAX_RECONNECT_ATTEMPTS = 10; const RECONNECT_DELAY = 3000; // 3秒 // DOM元素 const chatArea = document.getElementById('chat-area'); const messageInput = document.getElementById('message-input'); const sendButton = document.getElementById('send-button'); const usernameModal = document.getElementById('username-modal'); const usernameInput = document.getElementById('username-input'); const confirmButton = document.getElementById('confirm-username'); const statusDot = document.getElementById('status-dot'); const statusText = document.getElementById('status-text'); const usersCountElement = document.getElementById('users-count'); const wsServerInput = document.getElementById('ws-server-input'); const reconnectButton = document.getElementById('reconnect-button'); const debugConsole = document.getElementById('debug-console'); const debugConsoleContent = document.getElementById('debug-console-content'); const toggleDebugButton = document.getElementById('toggle-debug'); const welcomeMessage = document.getElementById('welcome-message'); // 全局变量 let username = ''; let websocket = null; let reconnectAttempts = 0; let connectionStatus = 'disconnected'; // disconnected, connecting, connected let onlineUsers = 0; // =================== // 初始化函数 // =================== async function init() { // 显示连接状态 updateConnectionStatus('disconnected'); // 添加事件监听器 addEventListeners(); // 从本地存储加载之前保存的WebSocket地址 const savedWsAddress = localStorage.getItem('ws_server_address'); if (savedWsAddress) { wsServerInput.value = savedWsAddress; } // 连接WebSocket connectWebSocket(); } // =================== // 添加事件监听器 // =================== function addEventListeners() { // 连接事件 confirmButton.addEventListener('click', handleConfirmUsername); reconnectButton.addEventListener('click', handleReconnect); // 消息事件 sendButton.addEventListener('click', sendMessage); messageInput.addEventListener('keypress', (e) => { if (e.key === 'Enter') sendMessage(); }); // 调试事件 toggleDebugButton.addEventListener('click', toggleDebugConsole); // 服务器地址更改事件 wsServerInput.addEventListener('change', () => { localStorage.setItem('ws_server_address', wsServerInput.value); }); } // =================== // WebSocket连接管理 // =================== function connectWebSocket() { // 清理之前的连接 if (websocket) { websocket.close(); websocket = null; } // 获取WebSocket服务器地址 let wsAddress = wsServerInput.value.trim(); if (!wsAddress) { // 尝试自动构建地址 wsAddress = tryGuessWsAddress(); } // 更新界面状态 updateConnectionStatus('connecting'); welcomeMessage.style.display = 'flex'; chatArea.innerHTML = ''; usersCountElement.innerHTML = ''; // 尝试连接 try { logDebug(`尝试连接到: ${wsAddress}`); websocket = new WebSocket(wsAddress); websocket.onopen = () => { logDebug("WebSocket连接已打开"); reconnectAttempts = 0; updateConnectionStatus('connected'); welcomeMessage.style.display = 'none'; // 显示用户名输入框 usernameModal.style.display = 'flex'; usernameInput.focus(); }; websocket.onmessage = (event) => { try { const data = JSON.parse(event.data); handleServerMessage(data); logDebug(`收到消息: ${event.data}`); } catch (e) { logDebug(`消息解析错误: ${e}`); } }; websocket.onerror = (error) => { logDebug(`WebSocket错误: ${JSON.stringify(error)}`); updateConnectionStatus('disconnected'); // 尝试重新连接 attemptReconnect(); }; websocket.onclose = () => { logDebug("WebSocket连接已关闭"); updateConnectionStatus('disconnected'); // 尝试重新连接 attemptReconnect(); }; } catch (error) { logDebug(`连接失败: ${error}`); updateConnectionStatus('disconnected'); showError(`连接失败: ${error.message || error}`); // 尝试重新连接 attemptReconnect(); } } function tryGuessWsAddress() { const host = window.location.h极光name; let port = 8765; // 默认WebSocket端口 // 如果HTTP端口存在且为80以外的值,尝试增加1作为WebSocket端口 if (window.location.port && window.location.port !== "80") { port = parseInt(window.location.port) + 1; } return `ws://${host}:${port}`; } function attemptReconnect() { if (reconnectAttempts < MAX_RECONNECT_ATTEMPTS) { reconnectAttempts++; const delay = RECONNECT_DELAY * Math.min(reconnectAttempts, 5); logDebug(`尝试重新连接 (${reconnectAttempts}/${MAX_RECONNECT_ATTEMPTS}),等待${delay / 1000}秒...`); updateConnectionStatus('connecting', `尝试重新连接 (${reconnectAttempts}/${MAX_RECONNECT_ATTEMPTS})`); setTimeout(() => { connectWebSocket(); }, delay); } else { updateConnectionStatus('disconnected', "无法连接到服务器,请检查配置"); } } function handleReconnect() { reconnectAttempts = 0; localStorage.setItem('ws_server_address', wsServerInput.value); connectWebSocket(); } // =================== // 用户身份处理 // =================== function handleConfirmUsername() { const name = usernameInput.value.trim(); if (name) { username = name; usernameModal.style.display = 'none'; if (websocket && websocket.readyState === WebSocket.OPEN) { // 发送用户名到服务器 - 确保格式正确 websocket.send(JSON.stringify({ "username": username })); // 启用输入框 messageInput.disabled = false; messageInput.focus(); sendButton.disabled = false; } } else { alert('用户名不能为空'); } } // =================== // 消息处理 // =================== function handleServerMessage(data) { switch (data.type) { case 'message': // 只显示别人发送的消息 if (data.username !== username) { addMessage(data.username, data.content, data.timestamp, true); } else { logDebug("忽略自己发送的消息(由服务器广播)"); } break; case 'notification': addNotification(data.content); if (data.content.includes("在线人数")) { updateUsersCount(data.content); } break; default: logDebug(`未知的消息类型: ${data.type}`); } } function sendMessage() { const content = messageInput.value.trim(); if (content && websocket && websocket.readyState === WebSocket.OPEN) { try { // 获取当前时间戳 const now = new Date(); const hours = String(now.getHours()).padStart(2, '0'); const minutes = String(now.getMinutes()).padStart(2, '0'); const seconds = String(now.getSeconds()).padStart(2, '0'); const timestamp = `${hours}:${minutes}:${seconds}`; // 在本地显示自己发送的消息 addMessage(username, content, timestamp, false); logDebug(`本地显示消息: ${content}`); // 发送消息到服务器 websocket.send(JSON.stringify({ "content": content })); // 清空输入框 messageInput.value = ''; logDebug(`发送消息: ${content}`); } catch (e) { logDebug(`发送消息失败: ${e}`); } } } // =================== // UI更新函数 // =================== function addMessage(sender, content, timestamp, isReceived = true) { // 确保聊天区域可见 if (welcomeMessage.style.display === 'flex') { welcomeMessage.style.display = 'none'; } const messageDiv = document.createElement('div'); messageDiv.className = `message ${isReceived ? 'received' : 'sent'}`; messageDiv.innerHTML = ` <div class="sender-name">${sender}</div> <div class="message-content">${content}</div> <div class="message-time">${timestamp}</div> `; chatArea.appendChild(messageDiv); // 滚动到底部 chatArea.scrollTop = chatArea.scrollHeight; logDebug(`添加消息: ${sender} - ${content.substring(0, 20)}`); } function addNotification(content) { const notifDiv = document.createElement('div'); notifDiv.className = 'message notification'; notifDiv.textContent = content; chatArea.appendChild(notifDiv); chatArea.scrollTop = chatArea.scrollHeight; // 提取在线人数 const countMatch = content.match(/(\d+)\s*个/); if (countMatch) { onlineUsers = parseInt(countMatch[1]); usersCountElement.innerHTML = `<span>在线人数: ${onlineUsers}</span>`; } } function updateUsersCount(text) { usersCountElement.innerHTML = `<span>${text}</span>`; } function updateConnectionStatus(status, message) { connectionStatus = status; statusDot.className = `status-dot ${status}`; const statusMessages = { disconnected: message || "连接已断开", connecting: message || "正在连接服务器...", connected: message || "已连接" }; statusText.textContent = statusMessages[status]; } function showError(message) { const errorDiv = document.createElement('div'); errorDiv.className = 'message notification'; errorDiv.style.backgroundColor = '#ff6b6b40'; errorDiv.textContent = `错误: ${message}`; chatArea.appendChild(errorDiv); } // =================== // 调试功能 // =================== function logDebug(message) { if (DEBUG) { const timestamp = new Date().toISOString().substring(11, 23); const logEntry = `${timestamp}: ${message}\n`; debugConsoleContent.textContent += logEntry; debugConsoleContent.scrollTop = debugConsoleContent.scrollHeight; // 始终显示调试面板 debugConsole.classList.add('show'); } } function toggleDebugConsole() { debugConsole.classList.toggle('show'); } // =================== // 初始化应用 // =================== document.addEventListener('DOMContentLoaded', init); </script> </body> </html>import asyncio import websockets import json from datetime import datetime import argparse import os import socket import logging from http.server import ThreadingHTTPServer, SimpleHTTPRequestHandler import threading import webbrowser import traceback from collections import namedtuple # 设置详细日志 logging.basicConfig( level=logging.INFO, format='%(asctime)s - %(levelname)s - %(message)s', handlers=[logging.StreamHandler()] ) logger = logging.getLogger(__name__) # 使用命名元组创建可哈希的客户端数据结构 ClientData = namedtuple('ClientData', ['ws', 'username']) connected_clients = set() class CustomHTTPRequestHandler(SimpleHTTPRequestHandler): """自定义HTTP请求处理器""" def __init__(self, *args, **kwargs): super().__init__(*args, directory='public', **kwargs) def guess_type(self, path): """重写MIME类型猜测方法""" if path.endswith(".js"): return "application/javascript" if path.endswith(".css"): return "text/css" return super().guess_type(path) def log_message(self, format, *args): """重写日志方法以减少输出""" logger.info(f"HTTP请求: {self.address_string()} - {format % args}") async def handle_client(websocket): """处理客户端连接 - 移除了'path'参数""" client_ip = websocket.remote_address[0] logger.info(f"新连接来自: {client_ip}") # 使用命名元组存储客户端数据 client_data = None username = "匿名用户" # 初始用户名 try: # 添加连接超时处理 try: # 等待用户名消息(最多10秒) username_msg = await asyncio.wait_for(websocket.recv(), timeout=10.0) username_data = json.loads(username_msg) # 确保消息中有username字段 if "username" not in username_data: await websocket.send(json.dumps({ "type": "error", "message": "缺少用户名字段,请重新连接" })) logger.error("客户端未提供用户名字段") return username = username_data["username"][:15] # 限制用户名长度 except asyncio.TimeoutError: await websocket.send(json.dumps({ "type": "error", "message": "连接超时,请重新连接" })) logger.error("等待用户名消息超时") return except json.JSONDecodeError: await websocket.send(json.dumps({ "type": "error", "message": "用户名格式错误,请重新连接" })) logger.error("用户名消息JSON解析错误") return logger.info(f"用户认证成功: {username} ({client_ip})") # 创建可哈希的客户端数据结构 client_data = ClientData(ws=websocket, username=username) connected_clients.add(client_data) # 通知所有人有新用户加入 join_msg = json.dumps({ "type": "notification", "content": f"{username} 加入了聊天室" }) await broadcast(join_msg, sender=websocket) # 发送欢迎消息 welcome_msg = json.dumps({ "type": "message", "username": "系统", "content": f"欢迎 {username} 加入聊天室!目前在线人数: {len(connected_clients)}", "timestamp": get_timestamp() }) await websocket.send(welcome_msg) # 持续接收消息 while True: try: # 设置接收超时(300秒 = 5分钟) message = await asyncio.wait_for(websocket.recv(), timeout=300.0) if message: try: # 解析消息并确保有content字段 msg_data = json.loads(message) if "content" not in msg_data: await websocket.send(json.dumps({ "type": "error", "message": "消息格式错误,缺少content字段" })) continue # 记录接收到的消息 log_message = msg_data["content"] if len(log_message) > 50: log_message = log_message[:47] + "..." logger.info(f"来自 {username} 的消息: {log_message}") # 创建广播消息 broadcast_msg = json.dumps({ "type": "message", "username": username, "content": msg_data["content"], "timestamp": get_timestamp() }) await broadcast(broadcast_msg, sender=websocket) except json.JSONDecodeError: await websocket.send(json.dumps({ "type": "error", "message": "无法解析JSON格式消息" })) except Exception as e: logger.error(f"处理消息时出错: {e}") traceback.print_exc() except asyncio.TimeoutError: # 超时后发送ping保持连接 try: await websocket.ping() except: break # 如果ping失败,断开连接 except (websockets.ConnectionClosed, ConnectionResetError, BrokenPipeError) as e: # 专门处理连接关闭异常 if isinstance(e, websockets.ConnectionClosed): logger.info(f"连接已关闭 ({username}): 代码={e.code}, 原因={e.reason}") else: logger.info(f"连接重置或关闭: {username}") break except Exception as e: logger.error(f"接收消息时发生错误: {e}") traceback.print_exc() break except (websockets.ConnectionClosed, ConnectionResetError, BrokenPipeError) as e: code = e.code if isinstance(e, websockets.ConnectionClosed) else "N/A" reason = e.reason if isinstance(e, websockets.ConnectionClosed) else "连接重置" logger.info(f"连接已关闭 ({username}): 代码={code}, 原因={reason}") except Exception as e: logger.error(f"连接错误: {e}") traceback.print_exc() finally: # 安全地移除客户端 if client_data and client_data in connected_clients: connected_clients.discard(client_data) # 通知其他人用户离开 try: if username != "匿名用户": logger.info(f"用户离开: {username}") leave_msg = json.dumps({ "type": "notification", "content": f"{username} 离开了聊天室" }) await broadcast(leave_msg) except: logger.warning("发送离开通知失败") async def broadcast(message, sender=None): """广播消息给所有客户端(除了发送者)""" if not connected_clients: return tasks = [] clients_to_remove = [] for client in list(connected_clients): # 使用副本进行迭代 try: # 跳过发送者 if client.ws == sender: continue # 发送消息 tasks.append(client.ws.send(message)) except (websockets.ConnectionClosed, ConnectionResetError, BrokenPipeError): # 如果连接已关闭,标记为需要移除 clients_to_remove.append(client) except Exception as e: logger.error(f"广播消息时出错: {e}") clients_to_remove.append(client) # 移除失效的客户端 for client in clients_to_remove: if client in connected_clients: connected_clients.discard(client) # 异步发送所有消息 if tasks: try: await asyncio.gather(*tasks) except Exception as e: logger.error(f"广播消息时发生错误: {e}") def get_timestamp(): """获取格式化时间""" return datetime.now().strftime("%H:%M:%S") def get_local_ip(): """获取本地IP地址""" try: s = socket.socket(socket.AF_INET, socket.SOCK_DGRAM) s.connect(("8.8.8.8", 80)) ip = s.getsockname()[0] s.close() return ip except: return "localhost" def start_http_server(port, host=''): """启动HTTP服务器""" logger.info(f"启动HTTP服务器: {host}:{port}") class ThreadedHTTPServer(ThreadingHTTPServer): daemon_threads = True server_address = (host, port) httpd = ThreadedHTTPServer(server_address, CustomHTTPRequestHandler) def run_server(): try: logger.info(f"HTTP服务器在端口 {port} 上运行") httpd.serve_forever() except Exception as e: logger.error(f"HTTP服务器错误: {e}") finally: httpd.server_close() logger.info("HTTP服务器已关闭") http_thread = threading.Thread(target=run_server) http_thread.daemon = True http_thread.start() return httpd async def start_websocket_server(host, port): """启动WebSocket服务器""" logger.info(f"启动WebSocket服务器: {host}:{port}") # 使用更强大的错误处理 server = await websockets.serve( handle_client, host, port, ping_interval=20, # 每20秒发送ping ping_timeout=120, # 等待pong回复120秒 close_timeout=10, # 关闭连接超时时间 max_size=10 * 1024 * 1024, # 10MB最大消息大小 max_queue=1000, # 更大的消息队列 ) # 记录服务器启动详情 logger.info(f"WebSocket服务器在 ws://{host}:{port} 上运行") logger.info(f"ping间隔: 20秒, ping超时: 120秒") return server def display_connection_info(http_port, ws_port): """显示连接信息""" local_ip = get_local_ip() print("\n" + "="*60) print(f"🎉 聊天服务器已成功启动!") print("="*60) print(f"🌐 HTTP服务器地址:") print(f" http://localhost:{http_port} (本机访问)") if local_ip != "localhost": print(f" http://{local_ip}:{http_port} (局域网内其他设备访问)") print(f"\n📡 WebSocket服务器地址:") print(f" ws://localhost:{ws_port} (本机访问)") if local_ip != "localhost": print(f" ws://{local_ip}:{ws_port} (局域网内其他设备访问)") print("\n💡 故障排除提示:") print("1. 如果无法连接,请确保防火墙允许端口: HTTP ({}) 和 WebSocket ({})".format(http_port, ws_port)) print("2. 对于云服务器,请检查安全组/防火墙规则") print("3. 如果有防病毒软件,请尝试暂时禁用") print("4. 检查浏览器控制台是否有错误 (按F12)") print("5. 确保网络连接稳定") print("="*60 + "\n") async def main(): parser = argparse.ArgumentParser(description='WebSocket聊天室服务器') parser.add_argument('--http-host', default='', help='HTTP服务器绑定地址') parser.add_argument('--http-port', type=int, default=8080, help='HTTP服务器端口') parser.add_argument('--ws-host', default='0.0.0.0', help='WebSocket服务器绑定地址') parser.add_argument('--ws-port', type=int, default=8765, help='WebSocket服务器端口') parser.add_argument('--no-browser', action='store_true', help='不要自动打开浏览器') parser.add_argument('--debug', action='store_true', help='启用调试模式') args = parser.parse_args() if args.debug: logging.getLogger().setLevel(logging.DEBUG) logger.info("调试模式已启用") # 创建public目录(如果不存在) if not os.path.exists('public'): os.makedirs('public') logger.info("已创建 public 目录") # 确保有默认的index.html文件 default_html_path = os.path.join('public', 'index.html') if not os.path.exists(default_html_path): logger.warning("未找到index.html文件,创建简单示例") with open(default_html_path, 'w') as f: f.write("""<!DOCTYPE html> <html> <head><title>聊天室</title></head> <body> <h1>聊天室服务器正在运行</h1> <p>请创建index.html文件放置在public目录</p> </body> </html>""") logger.info("已创建默认index.html文件") # 启动HTTP服务器 logger.info("启动HTTP服务器...") http_server = start_http_server(args.http_port, args.http_host) # 启动WebSocket服务器 logger.info("启动WebSocket服务器...") ws_server = await start_websocket_server(args.ws_host, args.ws_port) # 显示连接信息 display_connection_info(args.http_port, args.ws_port) # 在浏览器中自动打开(可选) if not args.no_browser: try: url = f"http://localhost:{args.http_port}" logger.info(f"在浏览器中打开: {url}") webbrowser.open(url) except Exception as e: logger.warning(f"无法自动打开浏览器: {e}") # 保持服务器运行 logger.info("服务器已启动并正在运行") try: # 记录运行状态 logger.info("使用 Ctrl+C 停止服务器") await asyncio.Future() # 永久运行 except KeyboardInterrupt: logger.info("\n服务器正在关闭...") finally: # 关闭WebSocket服务器 logger.info("关闭WebSocket服务器...") ws_server.close() await ws_server.wait_closed() logger.info("WebSocket服务器已关闭") # 关闭HTTP服务器 logger.info("关闭HTTP服务器...") http_server.shutdown() logger.info("HTTP服务器已关闭") if __name__ == "__main__": # 在Windows上设置更好的事件循环策略 if os.name == 'nt': asyncio.set_event_loop_policy(asyncio.WindowsSelectorEventLoopPolicy()) # 更详细的错误处理 try: asyncio.run(main()) except Exception as e: logger.error(f"服务器启动失败: {e}") traceback.print_exc() print("\n请检查端口是否可用,或尝试使用 --http-port 和 --ws-port 指定不同端口")start_server.sh
#! /bin/bash python3 server.py --http-port 8001
然后我们只需要在终端运行:
chmod +x start_server.sh
./start_server.sh于是我们的服务就成功启动了。