WebSocket 是用于浏览器与服务器之间进行双相连接的协议,可以用于创建基于浏览器的实时聊天工具。Tornado 自身支持 WebSocket 协议,也可以用来接收网站管理员的编辑指令。
根据官方文档,可以通过继承 tornado.websocket.WebSocketHandler
处理来自 WebSocket 协议的请求:
class EchoWebSocket(tornado.websocket.WebSocketHandler):
def open(self):
print("WebSocket opened")
def on_message(self, message):
self.write_message(u"You said: " + message)
def on_close(self):
print("WebSocket closed")
重载的 open
、on_message
、on_close
方法分别用于处理连接创建时、接收信息时和连接关闭时的相关操作。对应的浏览器端操作为:
var ws = new WebSocket("ws://"+window.location.host+"/websocket");
ws.onopen = function() {
ws.send("Hello, world");
};
ws.onmessage = function (evt) {
console.log(evt.data);
};
首先需要创建聊天页面:
class ChatWebHandler(BaseController):
def get(self):
self.render("chat/index.html")
def post(self):
# 登录用户信息存入 Cookie
uid = self.get_body_argument("uid")
self.set_secure_cookie("uid", uid)
self.redirect("/chat")
router = [
(r'/chat', ChatWebHandler),
]
如果是多人聊天,需要保存所有的请求对象,并在有消息进入时更新所有连接:
from datetime import datetime
class EchoWebSocket(tornado.websocket.WebSocketHandler):
pool = set()
def open(self):
EchoWebSocket.pool.add(self)
def on_close(self):
EchoWebSocket.pool.remove(self)
def on_message(self, message):
uid = self.get_secure_cookie("uid")
uid = uid.decode() if uid is not None else "匿名"
EchoWebSocket.update(dict(
uid = uid,
msg = message,
t = datetime.now().strftime("%m-%d %H:%M:%S")
))
@classmethod
def update(cls, msg):
for chat in cls.pool:
chat.write_message(msg)
router = [
(r'/ws', WebSocketHandler), # WebSocket 地址为 ws://host/ws
(r'/chat', ChatWebHandler) # 聊天室地址为 http://host/chat
]
浏览器客户端接收和发送消息:
$(document).ready(function(){
var input = $('#msgInput');
var frame = $('#msgFrame');
var wraper= $('#msgWraper');
var ws = new WebSocket("ws://" + window.location.host + "/ws");
// 发送消息
$('#msgForm').on('submit', function (e) {
var msg = input.val();
input.val('');
ws.send(msg);
return false;
});
// 接收消息
ws.onmessage = function (evt) {
var data = JSON.parse(evt.data);
// 更新到界面
frame.append($('<li style="margin-left:12px;"><b>'+ data.uid+': </b>'+ data.message + '<span style="float:right; margin-right:12px;">' + data.t +'</span></li>'))
if (frame.height() >= wraper.height()) {
wraper.scrollTop(frame.height());
};
};
});
更新:异步处理
由于 tornado.websocket.WebSocketHandler
不支持异步请求,因此当需要与数据库进行交互的时候,例如保留聊天记录等,则只能通过同步阻塞的方式处理。而且若采用 Motor 这种异步驱动,也很难与 WebSocket 交互。解决方案是讲异步操作通过 IOLoop.current().spawn_callback
完成:
async def callback(self, msg, db):
res = await self.db.do_with(msg)
self.write_message(res)
def on_message(self, message):
tornado.ioloop.IOLoop.current().spawn_callback(lambda : self.callback(db, message))
更新2:Nginx 反向代理设置
参考 官方文档:
map $http_upgrade $connection_upgrade {
default upgrade;
'' close;
}
location /ws {
proxy_pass http://pyhub_daily;
proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection $connection_upgrade;
proxy_set_header Host 'daily.pyhub.cc';
}
更新3:Nginx 连接超时问题
发现每个一分钟左右没有回复就会被服务器断开连接,经过搜索发现需要设定 Nginx 读取超时时间:
proxy_read_timeout 500;