Tornado WebSocket实时聊天实例
在 Python 3 Tornado 中使用 WebSocket 非常直接。你需要创建一个继承自 tornado.websocket.WebSocketHandler
的类,并实现它的几个关键方法。
下面是一个简单的示例,演示了如何创建一个 WebSocket 服务器,该服务器会接收客户端发送的消息,并在其前面加上 "Echo: " 前缀后回显给客户端。同时,它还会将收到的消息广播给所有连接的客户端。
1. 服务器端 (Python - server.py
)
import tornado.ioloop
import tornado.web
import tornado.websocket
import logging
import uuid # 用于给客户端一个唯一ID (可选)logging.basicConfig(level=logging.INFO)class ChatSocketHandler(tornado.websocket.WebSocketHandler):# 使用一个类级别的集合来存储所有活动的 WebSocket 连接clients = set()client_details = {} # 可选:存储客户端更多信息def open(self):"""当一个新的 WebSocket 连接建立时调用"""self.client_id = str(uuid.uuid4()) # 给每个连接一个唯一IDChatSocketHandler.clients.add(self)ChatSocketHandler.client_details[self] = {"id": self.client_id}logging.info(f"New client connected: {self.client_id} from {self.request.remote_ip}")self.write_message(f"Welcome! Your ID is {self.client_id}")self.broadcast(f"Client {self.client_id} has joined.", exclude_self=True)def on_message(self, message):"""当从客户端接收到消息时调用"""logging.info(f"Received message from {self.client_id}: {message}")# 简单的回显# self.write_message(f"You said: {message}")# 广播消息给所有客户端self.broadcast(f"{self.client_id} says: {message}")def on_close(self):"""当 WebSocket 连接关闭时调用"""ChatSocketHandler.clients.remove(self)if self in ChatSocketHandler.client_details:del ChatSocketHandler.client_details[self]logging.info(f"Client {self.client_id} disconnected.")self.broadcast(f"Client {self.client_id} has left.", exclude_self=True)def check_origin(self, origin):"""允许跨域 WebSocket 连接。在生产环境中,你应该更严格地检查 origin。例如:allowed_origins = ["http://localhost:8000", "https://yourdomain.com"]return origin in allowed_origins"""logging.info(f"Checking origin: {origin}")return True # 暂时允许所有来源@classmethoddef broadcast(cls, message, exclude_self=False, sender=None):"""辅助方法,向所有连接的客户端广播消息"""logging.info(f"Broadcasting message: {message}")for client in cls.clients:if exclude_self and sender and client == sender:continuetry:client.write_message(message)except tornado.websocket.WebSocketClosedError:logging.warning(f"Failed to send to a closed socket for client {cls.client_details.get(client, {}).get('id', 'unknown')}")except Exception as e:logging.error(f"Error sending message to client {cls.client_details.get(client, {}).get('id', 'unknown')}: {e}")def make_app():return tornado.web.Application([(r"/ws", ChatSocketHandler), # 将 /ws 路径映射到处理器])if __name__ == "__main__":app = make_app()port = 8888app.listen(port)logging.info(f"WebSocket server started on port {port}")tornado.ioloop.IOLoop.current().start()
2. 客户端 (HTML + JavaScript - client.html
)
<!DOCTYPE html>
<html>
<head><title>Tornado WebSocket Chat</title><style>#chatbox {width: 400px;height: 300px;border: 1px solid #ccc;overflow-y: scroll;padding: 10px;margin-bottom: 10px;}.message {margin-bottom: 5px;}</style>
</head>
<body><h1>Tornado WebSocket Chat</h1><div id="chatbox"></div><input type="text" id="messageInput" placeholder="Type your message here..." size="50"><button onclick="sendMessage()">Send</button><script>const chatbox = document.getElementById('chatbox');const messageInput = document.getElementById('messageInput');// 确保 WebSocket URL 与服务器端配置一致const socket = new WebSocket("ws://localhost:8888/ws"); socket.onopen = function(event) {addMessageToChatbox("System: Connected to WebSocket server.");console.log("WebSocket connection opened:", event);};socket.onmessage = function(event) {console.log("Message from server:", event.data);addMessageToChatbox("Server: " + event.data);};socket.onclose = function(event) {if (event.wasClean) {addMessageToChatbox(`System: Connection closed cleanly, code=${event.code} reason=${event.reason}`);} else {addMessageToChatbox('System: Connection died');}console.log("WebSocket connection closed:", event);};socket.onerror = function(error) {addMessageToChatbox("System: WebSocket Error: " + error.message);console.error("WebSocket Error:", error);};function sendMessage() {const message = messageInput.value;if (message.trim() !== "") {socket.send(message);// addMessageToChatbox("You: " + message); // 也可以等服务器广播回来messageInput.value = "";}}messageInput.addEventListener("keypress", function(event) {if (event.key === "Enter") {sendMessage();}});function addMessageToChatbox(message) {const messageElement = document.createElement('div');messageElement.classList.add('message');messageElement.textContent = message;chatbox.appendChild(messageElement);chatbox.scrollTop = chatbox.scrollHeight; // 自动滚动到底部}</script>
</body>
</html>
如何运行:
- 保存文件: 将 Python 代码保存为
server.py
,将 HTML 代码保存为client.html
。 - 安装 Tornado: 如果你还没有安装,请执行:
pip install tornado
- 运行服务器: 打开终端或命令提示符,导航到保存
server.py
的目录,然后运行:
你应该会看到类似 “WebSocket server started on port 8888” 的输出。python server.py
- 打开客户端: 在你的 Web 浏览器中打开
client.html
文件 (可以直接双击文件,或者通过file:///path/to/client.html
访问)。你可以打开多个浏览器窗口或标签页来模拟多个客户端。
关键点解释:
tornado.websocket.WebSocketHandler
: 这是处理 WebSocket 连接的核心类。open()
: 当客户端成功建立 WebSocket 连接后,此方法被调用。你可以在这里进行初始化操作,比如将客户端实例添加到一个列表中以便后续广播。on_message(message)
: 当服务器从客户端接收到一条消息时,此方法被调用。message
参数是客户端发送的数据(通常是字符串,也可以配置为接收二进制数据)。on_close()
: 当连接关闭时(无论是客户端主动关闭还是服务器关闭,或者由于网络错误),此方法被调用。在这里进行清理工作,比如从活动客户端列表中移除该连接。write_message(message)
: 此方法用于向连接的客户端发送消息。check_origin(self, origin)
: 这个方法用于安全目的,决定是否接受来自特定源(origin)的 WebSocket 连接。默认情况下,Tornado 会拒绝跨域的 WebSocket 连接。在开发环境中,返回True
可以方便测试。在生产环境中,你应该仔细配置允许的源列表以防止 CSRF 类型的攻击。clients = set()
: 这是一个类级别的集合,用于跟踪所有当前连接的客户端WebSocketHandler
实例。这使得向所有客户端广播消息成为可能。broadcast()
(自定义方法): 这是一个我们添加的类方法,用于方便地向clients
集合中的所有客户端发送消息。- 客户端
WebSocket
API:new WebSocket("ws://localhost:8888/ws")
: 创建一个新的 WebSocket 连接。ws://
表示普通的 WebSocket,如果是加密的,则使用wss://
。socket.onopen
: 连接成功建立时的回调。socket.onmessage
: 收到服务器消息时的回调。event.data
包含消息内容。socket.onclose
: 连接关闭时的回调。socket.onerror
:发生错误时的回调。socket.send(message)
: 向服务器发送消息。
进一步的考虑:
- 错误处理: 更健壮的错误处理,例如
write_message
可能会因为客户端突然断开而失败。 - 消息格式: 对于复杂应用,通常使用 JSON 作为消息格式,方便传输结构化数据。你需要在服务器端
json.loads()
接收到的消息,并在发送前json.dumps()
。 - 认证与授权: 对于需要用户登录的应用,你需要在 WebSocket 连接建立时(可能通过
open()
或在 HTTP 升级握手阶段)进行用户认证。 - 状态管理: 对于更复杂的应用,你可能需要在服务器端为每个客户端维护更复杂的状态。
- 扩展性: 对于大量并发连接,单个 Tornado 进程可能不够。你可能需要考虑使用多个进程(例如通过 supervisord 运行多个 Tornado 实例)并使用像 Redis Pub/Sub 这样的消息队列来在进程间广播 WebSocket 消息。
- SSL/TLS (
wss://
): 在生产环境中,务必使用wss://
(WebSocket Secure) 来加密通信。这需要在 Tornado 应用启动时配置 SSL 选项。