Tornado WebSocket实时聊天实例

发布于:2025-05-31 ⋅ 阅读:(22) ⋅ 点赞:(0)

在 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()) # 给每个连接一个唯一ID
        ChatSocketHandler.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 # 暂时允许所有来源

    @classmethod
    def 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:
                continue
            try:
                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 = 8888
    app.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>

如何运行:

  1. 保存文件: 将 Python 代码保存为 server.py,将 HTML 代码保存为 client.html
  2. 安装 Tornado: 如果你还没有安装,请执行:
    pip install tornado
    
  3. 运行服务器: 打开终端或命令提示符,导航到保存 server.py 的目录,然后运行:
    python server.py
    
    你应该会看到类似 “WebSocket server started on port 8888” 的输出。
  4. 打开客户端: 在你的 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 选项。