[WebSocket/Python] 基于 python + websocket 实现简易的Web版聊天室
0 序
- 在开发 MCP Tool 时,了解到 基于 HTTP SSE 通信模式下 MCP Server/Tool;而与 HTTP SSE 通信模式较为接近的是 WebSocket 通信模式。
- 为此,对 WebSocket 进行一个简单的试验、实践。
1 基于 python + websocket 实现简易的Web版聊天室
- 创建一个基于 WebSocket + Python 的简易 Web 版聊天室。这个实现将包含完整的后端和前端代码,支持多人实时聊天、用户加入/离开通知等功能。
1 源码实现
chat.html - WebSocket 聊天室
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>WebSocket 聊天室</title>
<style>
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
body {
font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
height: 100vh;
display: flex;
justify-content: center;
align-items: center;
}
.chat-container {
width: 90%;
max-width: 800px;
height: 90vh;
background: rgba(255, 255, 255, 0.95);
border-radius: 20px;
box-shadow: 0 20px 60px rgba(0,0,0,0.3);
display: flex;
flex-direction: column;
overflow: hidden;
}
.chat-header {
background: linear-gradient(90deg, #667eea 0%, #764ba2 100%);
color: white;
padding: 20px;
text-align: center;
position: relative;
}
.chat-header h1 {
font-size: 24px;
margin-bottom: 5px;
}
.online-count {
font-size: 14px;
opacity: 0.9;
}
.connection-status {
position: absolute;
right: 20px;
top: 50%;
transform: translateY(-50%);
width: 12px;
height: 12px;
border-radius: 50%;
background: #ff4444;
transition: background 0.3s;
}
.connection-status.connected {
background: #00ff88;
box-shadow: 0 0 10px #00ff88;
}
.chat-messages {
flex: 1;
overflow-y: auto;
padding: 20px;
background: #f8f9fa;
}
.message {
margin-bottom: 15px;
animation: slideIn 0.3s ease-out;
}
@keyframes slideIn {
from {
opacity: 0;
transform: translateY(10px);
}
to {
opacity: 1;
transform: translateY(0);
}
}
.message-header {
display: flex;
align-items: center;
margin-bottom: 5px;
}
.username {
font-weight: bold;
color: #667eea;
margin-right: 10px;
}
.timestamp {
font-size: 12px;
color: #999;
}
.message-content {
background: white;
padding: 12px 16px;
border-radius: 15px;
box-shadow: 0 2px 5px rgba(0,0,0,0.05);
display: inline-block;
max-width: 80%;
word-wrap: break-word;
}
.message.own .message-header {
flex-direction: row-reverse;
}
.message.own .username {
color: #764ba2;
margin-right: 0;
margin-left: 10px;
}
.message.own .message-content {
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
color: white;
margin-left: auto;
}
.system-message {
text-align: center;
color: #888;
font-style: italic;
margin: 10px 0;
font-size: 14px;
}
.typing-indicator {
display: none;
padding: 10px 20px;
color: #999;
font-style: italic;
font-size: 14px;
}
.typing-indicator.active {
display: block;
}
.chat-input-area {
padding: 20px;
background: white;
border-top: 1px solid #eee;
}
.input-wrapper {
display: flex;
gap: 10px;
}
#messageInput {
flex: 1;
padding: 12px 20px;
border: 2px solid #e0e0e0;
border-radius: 25px;
outline: none;
font-size: 16px;
transition: border-color 0.3s;
}
#messageInput:focus {
border-color: #667eea;
}
#sendBtn {
padding: 12px 30px;
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
color: white;
border: none;
border-radius: 25px;
cursor: pointer;
font-size: 16px;
transition: transform 0.2s, box-shadow 0.2s;
}
#sendBtn:hover {
transform: translateY(-2px);
box-shadow: 0 5px 15px rgba(102, 126, 234, 0.4);
}
#sendBtn:active {
transform: translateY(0);
}
.login-modal {
position: fixed;
top: 0;
left: 0;
width: 100%;
height: 100%;
background: rgba(0,0,0,0.8);
display: flex;
justify-content: center;
align-items: center;
z-index: 1000;
}
.login-box {
background: white;
padding: 40px;
border-radius: 20px;
text-align: center;
animation: popIn 0.3s ease-out;
}
@keyframes popIn {
from {
transform: scale(0.8);
opacity: 0;
}
to {
transform: scale(1);
opacity: 1;
}
}
.login-box h2 {
margin-bottom: 20px;
color: #333;
}
#usernameInput {
width: 100%;
padding: 15px;
margin-bottom: 20px;
border: 2px solid #e0e0e0;
border-radius: 10px;
font-size: 16px;
outline: none;
}
#usernameInput:focus {
border-color: #667eea;
}
#joinBtn {
width: 100%;
padding: 15px;
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
color: white;
border: none;
border-radius: 10px;
font-size: 16px;
cursor: pointer;
transition: transform 0.2s;
}
#joinBtn:hover {
transform: scale(1.05);
}
.hidden {
display: none !important;
}
/* 滚动条样式 */
.chat-messages::-webkit-scrollbar {
width: 8px;
}
.chat-messages::-webkit-scrollbar-track {
background: #f1f1f1;
}
.chat-messages::-webkit-scrollbar-thumb {
background: #888;
border-radius: 4px;
}
.chat-messages::-webkit-scrollbar-thumb:hover {
background: #555;
}
</style>
</head>
<body>
<!-- 登录界面 -->
<div id="loginModal" class="login-modal">
<div class="login-box">
<h2>🚀 加入聊天室</h2>
<input type="text" id="usernameInput" placeholder="请输入你的昵称" maxlength="20">
<button id="joinBtn">进入聊天室</button>
</div>
</div>
<!-- 聊天界面 -->
<div class="chat-container hidden" id="chatContainer">
<div class="chat-header">
<h1>💬 WebSocket 聊天室</h1>
<div class="online-count">在线人数: <span id="onlineCount">0</span></div>
<div class="connection-status" id="connectionStatus"></div>
</div>
<div class="chat-messages" id="chatMessages">
<!-- 消息将在这里动态添加 -->
</div>
<div class="typing-indicator" id="typingIndicator">
有人正在输入...
</div>
<div class="chat-input-area">
<div class="input-wrapper">
<input type="text" id="messageInput" placeholder="输入消息..." maxlength="500">
<button id="sendBtn">发送</button>
</div>
</div>
</div>
<script>
let ws;
let username;
let reconnectAttempts = 0;
const maxReconnectAttempts = 5;
// DOM 元素
const loginModal = document.getElementById('loginModal');
const chatContainer = document.getElementById('chatContainer');
const usernameInput = document.getElementById('usernameInput');
const joinBtn = document.getElementById('joinBtn');
const messageInput = document.getElementById('messageInput');
const sendBtn = document.getElementById('sendBtn');
const chatMessages = document.getElementById('chatMessages');
const onlineCount = document.getElementById('onlineCount');
const connectionStatus = document.getElementById('connectionStatus');
// 加入聊天室
joinBtn.addEventListener('click', joinChat);
usernameInput.addEventListener('keypress', (e) => {
if (e.key === 'Enter') joinChat();
});
function joinChat() {
username = usernameInput.value.trim();
if (!username) {
alert('请输入昵称');
return;
}
loginModal.classList.add('hidden');
chatContainer.classList.remove('hidden');
connectWebSocket();
}
// 连接 WebSocket
function connectWebSocket() {
// 使用当前主机和端口,或指定服务器地址
const wsUrl = `ws://${window.location.hostname}:8765`;
ws = new WebSocket(wsUrl);
ws.onopen = () => {
console.log('WebSocket 连接成功');
connectionStatus.classList.add('connected');
reconnectAttempts = 0;
// 发送加入消息
ws.send(JSON.stringify({
type: 'join',
username: username,
timestamp: new Date().toISOString()
}));
};
ws.onmessage = (event) => {
const data = JSON.parse(event.data);
handleMessage(data);
};
ws.onclose = () => {
console.log('WebSocket 连接关闭');
connectionStatus.classList.remove('connected');
attemptReconnect();
};
ws.onerror = (error) => {
console.error('WebSocket 错误:', error);
};
}
// 重连机制
function attemptReconnect() {
if (reconnectAttempts < maxReconnectAttempts) {
reconnectAttempts++;
console.log(`尝试重连... (${reconnectAttempts}/${maxReconnectAttempts})`);
setTimeout(connectWebSocket, 3000);
} else {
addSystemMessage('连接已断开,请刷新页面重试');
}
}
// 处理收到的消息
function handleMessage(data) {
switch(data.type) {
case 'message':
addMessage(data.username, data.content, data.timestamp, data.username === username);
break;
case 'system':
addSystemMessage(data.content);
break;
case 'user_count':
onlineCount.textContent = data.count;
break;
case 'history':
// 加载历史消息
data.messages.forEach(msg => {
addMessage(msg.username, msg.content, msg.timestamp, msg.username === username);
});
break;
}
}
// 添加普通消息
function addMessage(user, content, timestamp, isOwn = false) {
const messageDiv = document.createElement('div');
messageDiv.className = `message ${isOwn ? 'own' : ''}`;
const time = new Date(timestamp).toLocaleTimeString('zh-CN', {
hour: '2-digit',
minute: '2-digit'
});
messageDiv.innerHTML = `
<div class="message-header">
<span class="username">${escapeHtml(user)}</span>
<span class="timestamp">${time}</span>
</div>
<div class="message-content">${escapeHtml(content)}</div>
`;
chatMessages.appendChild(messageDiv);
scrollToBottom();
}
// 添加系统消息
function addSystemMessage(content) {
const div = document.createElement('div');
div.className = 'system-message';
div.textContent = content;
chatMessages.appendChild(div);
scrollToBottom();
}
// 滚动到底部
function scrollToBottom() {
chatMessages.scrollTop = chatMessages.scrollHeight;
}
// HTML 转义防止 XSS
function escapeHtml(text) {
const div = document.createElement('div');
div.textContent = text;
return div.innerHTML;
}
// 发送消息
function sendMessage() {
const content = messageInput.value.trim();
if (!content || !ws || ws.readyState !== WebSocket.OPEN) return;
ws.send(JSON.stringify({
type: 'message',
username: username,
content: content,
timestamp: new Date().toISOString()
}));
messageInput.value = '';
messageInput.focus();
}
sendBtn.addEventListener('click', sendMessage);
messageInput.addEventListener('keypress', (e) => {
if (e.key === 'Enter') sendMessage();
});
// 页面关闭时发送离开消息
window.addEventListener('beforeunload', () => {
if (ws && ws.readyState === WebSocket.OPEN) {
ws.send(JSON.stringify({
type: 'leave',
username: username
}));
}
});
</script>
</body>
</html>
server.py - WebSocket 聊天室服务端
# server.py - WebSocket 聊天室服务端
import asyncio
import websockets
import json
import logging
from datetime import datetime
from collections import defaultdict
# 配置日志
logging.basicConfig(
level=logging.INFO,
format='%(asctime)s - %(levelname)s - %(message)s'
)
logger = logging.getLogger(__name__)
class ChatServer:
def __init__(self):
# 存储所有连接的客户端 {websocket: username}
self.clients = {}
# 消息历史记录(保留最近100条)
self.message_history = []
self.max_history = 100
async def register(self, websocket, username):
"""注册新客户端"""
self.clients[websocket] = username
logger.info(f"用户 {username} 加入聊天室,当前在线: {len(self.clients)}人")
# 发送历史消息给新用户
await self.send_history(websocket)
# 广播用户加入消息
await self.broadcast({
"type": "system",
"content": f"👋 {username} 加入了聊天室"
})
# 更新在线人数
await self.broadcast_user_count()
async def unregister(self, websocket):
"""注销客户端"""
if websocket in self.clients:
username = self.clients[websocket]
del self.clients[websocket]
logger.info(f"用户 {username} 离开聊天室,当前在线: {len(self.clients)}人")
# 广播用户离开消息
await self.broadcast({
"type": "system",
"content": f"👋 {username} 离开了聊天室"
})
# 更新在线人数
await self.broadcast_user_count()
async def send_history(self, websocket):
"""发送历史消息给新连接的用户"""
if self.message_history:
await websocket.send(json.dumps({
"type": "history",
"messages": self.message_history
}))
async def broadcast_user_count(self):
"""广播当前在线人数"""
await self.broadcast({
"type": "user_count",
"count": len(self.clients)
})
async def broadcast(self, message, exclude=None):
"""广播消息给所有客户端"""
if self.clients:
message_str = json.dumps(message)
# 创建发送任务列表
tasks = []
for client in self.clients:
if client != exclude and client.open:
tasks.append(asyncio.create_task(self.safe_send(client, message_str)))
if tasks:
await asyncio.gather(*tasks, return_exceptions=True)
async def safe_send(self, websocket, message):
"""安全发送消息(捕获异常)"""
try:
await websocket.send(message)
except Exception as e:
logger.error(f"发送消息失败: {e}")
async def handle_message(self, websocket, data):
"""处理收到的消息"""
msg_type = data.get("type")
if msg_type == "join":
username = data.get("username")
await self.register(websocket, username)
elif msg_type == "message":
username = data.get("username")
content = data.get("content", "").strip()
if not content:
return
# 保存到历史记录
message_data = {
"username": username,
"content": content,
"timestamp": data.get("timestamp", datetime.now().isoformat())
}
self.message_history.append(message_data)
# 限制历史记录长度
if len(self.message_history) > self.max_history:
self.message_history.pop(0)
# 广播消息
await self.broadcast({
"type": "message",
**message_data
})
elif msg_type == "leave":
await self.unregister(websocket)
async def handler(self, websocket, path):
"""WebSocket 连接处理器"""
try:
async for message in websocket:
try:
data = json.loads(message)
await self.handle_message(websocket, data)
except json.JSONDecodeError:
logger.error("收到无效的 JSON 数据")
except Exception as e:
logger.error(f"处理消息时出错: {e}")
except websockets.exceptions.ConnectionClosed:
logger.info("客户端连接关闭")
finally:
await self.unregister(websocket)
# 创建服务器实例
chat_server = ChatServer()
async def main():
"""启动 WebSocket 服务器"""
host = "0.0.0.0"
port = 8765
logger.info(f"启动聊天室服务器于 ws://{host}:{port}")
async with websockets.serve(chat_server.handler, host, port):
await asyncio.Future() # 永久运行
if __name__ == "__main__":
try:
asyncio.run(main())
except KeyboardInterrupt:
logger.info("服务器已停止")
2 使用说明
2.1 安装依赖
pip install websockets
2.2 启动服务端
python server.py
启动运行日志
(ai-env) PS D:\Workspace\xxx\xxx\websocket_chat> python server.py
2026-03-27 09:31:39,782 - INFO - 启动聊天室服务器于 ws://0.0.0.0:8765
2026-03-27 09:31:39,876 - INFO - server listening on 0.0.0.0:8765
2026-03-27 09:33:49,209 - INFO - connection open
2026-03-27 09:33:49,212 - INFO - 用户 johnny 加入聊天室,当前在线: 1人
2026-03-27 09:34:30,394 - INFO - connection open
2026-03-27 09:34:30,399 - INFO - 用户 jack 加入聊天室,当前在线: 2人
2026-03-27 09:41:05,575 - INFO - server closing
2026-03-27 09:41:05,577 - INFO - 用户 jack 离开聊天室,当前在线: 1人
2026-03-27 09:41:05,578 - INFO - connection closed
2026-03-27 09:41:05,578 - INFO - 用户 johnny 离开聊天室,当前在线: 0人
2026-03-27 09:41:05,579 - INFO - connection closed
2026-03-27 09:41:05,579 - INFO - server closed
2026-03-27 09:41:05,580 - INFO - 服务器已停止
2.3 运行客户端
- 直接在浏览器中打开 HTML 文件,或 使用 Python 简单 HTTP 服务器:
# 在 HTML 文件所在目录运行
python -m http.server 8080
# 然后访问 http://localhost:8080
启动/运行日志:
$ python -m http.server 8080
Serving HTTP on :: port 8080 (http://[::]:8080/) ...
::1 - - [27/Mar/2026 09:33:27] "GET / HTTP/1.1" 200 -
::1 - - [27/Mar/2026 09:33:27] code 404, message File not found
::1 - - [27/Mar/2026 09:33:27] "GET /favicon.ico HTTP/1.1" 404 -
::1 - - [27/Mar/2026 09:33:30] "GET /chat.html HTTP/1.1" 200 -
::1 - - [27/Mar/2026 09:34:08] "GET /chat.html HTTP/1.1" 304 -
2.4 体验/使用
- 用户们可通过 http://localhost:8080/chat.html 先后进入聊天室

3 功能特性
| 功能 | 说明 |
|---|---|
| 实时通信 | 基于 WebSocket 的全双工通信 |
| 多人聊天 | 支持多个客户端同时在线 |
| 用户系统 | 昵称登录,加入/离开通知 |
| 消息历史 | 新用户加入可看到最近100条消息 |
| 在线人数 | 实时显示当前在线用户数量 |
| 重连机制 | 客户端自动重连(最多5次) |
| XSS 防护 | 自动转义 HTML 特殊字符 |
| 响应式设计 | 适配移动端和桌面端 |
4 架构说明
┌─────────────┐ WebSocket ┌──────────────┐
│ 浏览器 │ ◄──────────────────────► │ Python 服务端 │
│ (HTML/JS) │ ws://localhost:8765 │ websockets │
└─────────────┘ └──────────────┘
▲ │
│ │
└────────────── 广播消息 ◄────────────────────┘
- 这个实现非常轻量,核心代码不到 200 行,但包含了生产环境所需的基本功能如异常处理、心跳检测(通过自动重连)、历史消息等。你可以基于此扩展更多功能如私聊、表情包、文件传输等。
Z FAQ for WebSocket/聊天室
Y 推荐文献
-
MCP Server/Tool 开发指南 - 博客园/数据知音
- 基于 HTTP SSE 模式的 MCP Server/Tool
-
SSE(Server Send Events) :服务器 => 浏览器的消息推送解决方案 - 博客园/千千寰宇
- HTTP SSE 通信模式(半双工) vs WebSocket 通信模式(全双工)
X 参考文献
本文作者:
千千寰宇
本文链接: https://www.cnblogs.com/johnnyzen
关于博文:评论和私信会在第一时间回复,或直接私信我。
版权声明:本博客所有文章除特别声明外,均采用 BY-NC-SA 许可协议。转载请注明出处!
日常交流:大数据与软件开发-QQ交流群: 774386015 【入群二维码】参见左下角。您的支持、鼓励是博主技术写作的重要动力!
本文链接: https://www.cnblogs.com/johnnyzen
关于博文:评论和私信会在第一时间回复,或直接私信我。
版权声明:本博客所有文章除特别声明外,均采用 BY-NC-SA 许可协议。转载请注明出处!
日常交流:大数据与软件开发-QQ交流群: 774386015 【入群二维码】参见左下角。您的支持、鼓励是博主技术写作的重要动力!

浙公网安备 33010602011771号