在 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 選項。