在 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>
如何运行:
- 保存文件: 将 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 选项。