Tornado WebSocket简单聊天

2023-05-11,,

Tornado实现了对socket的封装:tornado.web.RequestHandler

工程目录:

1、主程序 manage.py

import tornado.web
import tornado.httpserver
from tornado.options import define, options, parse_command_line from chat.views import IndexHandler, LoginHandler, ChatHandler
from util.settings import TEMPLATE_PATH, STATIC_PATH define("port", default=8180, help='run on the port', type=int) def make_app():
return tornado.web.Application(handlers=[
(r'/', IndexHandler),
(r'/login', LoginHandler),
(r'/chat', ChatHandler),
],
pycket={
'engine': 'redis',
'storage': {
'host': 'fot.redis.cache.net',
'port': 6379,
'password': 'yKigE3ZF0mGBSP4/M=',
'db_sessions': 5,
'db_notifications': 11,
'max_connections': 2 ** 31,
},
'cookies': {
'expires_days': 30,
'max_age': 100
},
},
login_url='/login',
template_path=TEMPLATE_PATH,
static_path=STATIC_PATH,
debug=True,
cookie_secret='cqVJzSSjQgWzKtpHMd4NaSeEa6yTy0qRicyeUDIMSjo='
) if __name__ == '__main__':
tornado.options.parse_command_line()
app = make_app()
http_server = tornado.httpserver.HTTPServer(app)
http_server.listen(options.port)
tornado.ioloop.IOLoop.current().start()

2、配置 settings.py

import os

BASE_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))

TEMPLATE_PATH = os.path.join(BASE_DIR, 'templates')

STATIC_PATH = os.path.join(BASE_DIR, 'static')

3、聊天程序 views.py

# -*- coding: utf-8 -*-
import datetime
import json import tornado.web
import tornado.websocket
from tornado.web import authenticated # 导入装饰器
from pycket.session import SessionMixin # 设置BaseHandler类,重写函数get_current_user
class BaseHandler(tornado.web.RequestHandler, SessionMixin):
def get_current_user(self): # 前面有绿色小圆圈带个o,再加一个箭头表示重写
current_user = self.session.get('user') # 获取加密的cookie
if current_user:
return current_user
return None # 基类
class BaseWebSocketHandler(tornado.websocket.WebSocketHandler, SessionMixin):
def get_current_user(self):
current_user = self.session.get('user') if current_user:
return current_user
return None # 跳转
class IndexHandler(BaseHandler):
@authenticated # 内置装饰器,检查是否登录
def get(self):
self.render('chat.html') class LoginHandler(BaseHandler):
def get(self):
self.render('index.html') # 跳转页面带上获取的参数 def post(self, *args, **kwargs):
user = self.get_argument('nickname', '')
if user: self.session.set('user', user) # 设置加密cookie
self.redirect('/') # 跳转到之前的路由
else:
self.render('index.html') class ChatHandler(BaseWebSocketHandler):
# 定义接收/发送聊天消息的视图处理类,继承自websocket的WebSocketHandler
# 定义一个集合,用来保存在线的所有用户 online_users = set() # 从客户端获取cookie信息 # 重写open方法,当有新的聊天用户进入的时候自动触发该函数
def open(self): # 新用户上线,加入集合
self.online_users.add(self)
# 将新用户加入的信息发送给所有用户 for user in self.online_users:
user.write_message('[%s]join room' % self.current_user) # 重写on_message方法,当聊天消息有更新时自动触发的函数
def on_message(self, message):
msgobj = {'msg': message} for user in self.online_users:
msgobj['key'] = '%s-%s-sea: ' % (self.current_user, datetime.datetime.now().strftime('%Y-%m-%d %H:%M:%S'))
user.write_message(json.dumps(msgobj)) # 重写on_close方法,当有用户离开时自动触发的函数
def on_close(self):
# 移除用户
self.online_users.remove(self)
for user in self.online_users:
user.write_message('[%s]remove room' % self.current_user) # 重写check_origin方法, 解决WebSocket的跨域请求
def check_origin(self, origin):
return True

4、前端登录 index.html

<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>聊天室登录首页</title>
<script src="../static/jquery-3.4.1.js"></script>
</head>
<body>
<div>
<div style="width:60%;">
<div>
聊天室个人登录
</div>
<div>
<form method="post" action="/login" style="width:80%">
<p>昵称:<input type="text" placeholder="请输入昵称" name="nickname"></p>
<button type="submit">登录</button>
</form>
</div>
</div>
</div>
</body>
</html>

5、前端聊天室 chat.html

<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title> WebSocket </title>
<style>
*{
margin: 0;
padding: 0;
}
.box{
width: 800px;
margin-left: auto;
margin-right: auto;
margin-top: 25px;
}
#text{
width: 685px;
height: 130px;
border: 1px solid skyblue;
border-radius: 10px;
font-size: 20px;
text-indent: 1em;
resize:none;
outline: none;
}
#text::placeholder{
color: skyblue;
}
.btn{
width: 100px;
margin: -27px 0 0px 8px;
}
#messages{
padding-left: 10px;
font-size: 25px;
}
#messages li{
list-style: none;
color: #000;
line-height: 30px;
font-size: 18px; }
</style>
</head>
<body>
<div class="box">
<div>
<textarea id="text" placeholder="请输入您的内容"></textarea>
<a href="javascript:WebSocketSend();" class="btn btn-primary">发送</a>
</div>
<ul id="messages">
</ul>
</div>
<script src="../static/jquery-3.4.1.js"></script>
<script type="text/javascript">
var mes = document.getElementById('messages');
var wsUrl = "ws://"+ window.location.host +"/chat";
var Socket = ''; if('WebSocket' in window){ /*判断浏览器是否支持WebSocket接口*/
/*创建创建 WebSocket 对象,协议本身使用新的ws://URL格式*/ createWebSocket();
}else{
/*浏览器不支持 WebSocket*/
alert("您的浏览器不支持 WebSocket!");
} function createWebSocket() {
try {
Socket = new WebSocket(wsUrl); init();
} catch(e) {
console.log('catch');
reconnect(wsUrl); //调用心跳
}
} function init() {
/*连接建立时触发*/
Socket.onopen = function () {
alert("连接已建立,可以进行通信"); heartCheck.start(); //调用心跳
};
/*客户端接收服务端数据时触发*/
Socket.onmessage = function (ev) {
var received_msg = ev.data; /*接受消息*/
var jopmsg = '';
try {
received_msg = JSON.parse(received_msg);
console.log(received_msg['msg']); if(received_msg['msg'] == '121')
jopmsg = '121'; received_msg = received_msg['key'] + received_msg['msg'];
}catch (e) { } //发送信息为121时为心跳,不记录到页面(只是个约定)
if(jopmsg !== '121'){
var aLi = "<li>" + received_msg + "</li>";
mes.innerHTML += aLi;
} heartCheck.start(); //调用心跳
};
/*连接关闭时触发*/
Socket.onclose = function () {
mes.innerHTML += "<br>连接已经关闭..."; reconnect(wsUrl); //关闭连接重新连接
};
} function WebSocketSend() {
/*form 里的Dom元素(input select checkbox textarea radio)都是value*/
var send_msg = document.getElementById('text').value;
//或者JQ中获取
// var send_msg = $("#text").val();
/*使用连接发送消息*/
Socket.send(send_msg);
$("#text").val('');
} var lockReconnect = false;//避免重复连接
function reconnect(url) {
if(lockReconnect) {
return true;
}; lockReconnect = true;
//没连接上会一直重连,设置延迟避免请求过多
setTimeout(function () {
createWebSocket(url);
lockReconnect = false;
}, 5000); } //心跳检测
var heartCheck = {
timeout: 10000, //每隔三秒发送心跳
num: 3, //3次心跳均未响应重连
timeoutObj: null,
serverTimeoutObj: null,
start: function(){
var _this = this;
var _num = this.num;
this.timeoutObj && clearTimeout(this.timeoutObj);
this.serverTimeoutObj && clearTimeout(this.serverTimeoutObj);
this.timeoutObj = setTimeout(function(){
//这里发送一个心跳,后端收到后,返回一个心跳消息,
//onmessage拿到返回的心跳就说明连接正常
Socket.send("121"); // 心跳包
_num--;
//计算答复的超时次数
if(_num === 0) {
Socket.colse();
}
}, this.timeout)
}
} </script>
</body>
</html>

6、运行效果: 输入 http://127.0.0.1:8180

7、部署到线上参考:https://www.cnblogs.com/cj8988/p/11288892.html

注 :nginx需要添加一个配置 (在 server {} 里添加下面配置)

    location /chat {
proxy_pass http://tornados;
proxy_http_version 1.1;
proxy_connect_timeout 4s; #配置点1
proxy_read_timeout 120s; #配置点2,如果没效,可以考虑这个时间配置长一点
proxy_send_timeout 120s;

proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection "upgrade";
}

8、注意,由于nginx超时问题,过段时间websocket会自动断开,所有前端需要设置心跳。

前端 chat.html 中   :

   //心跳检测
var heartCheck = {
timeout: 10000, //每隔三秒发送心跳
num: 3, //3次心跳均未响应重连
timeoutObj: null,
serverTimeoutObj: null,
start: function(){
var _this = this;
var _num = this.num;
this.timeoutObj && clearTimeout(this.timeoutObj);
this.serverTimeoutObj && clearTimeout(this.serverTimeoutObj);
this.timeoutObj = setTimeout(function(){
//这里发送一个心跳,后端收到后,返回一个心跳消息,
//onmessage拿到返回的心跳就说明连接正常
Socket.send("121"); // 心跳包
_num--;
//计算答复的超时次数
if(_num === 0) {
Socket.colse();
}
}, this.timeout)
}
}

在需要的地方调用:

heartCheck.start();

参考文档:
  https://www.jianshu.com/p/93b1788f055c
    
  https://www.lishuaishuai.com/html/759.html
  https://www.cnblogs.com/cj8988/p/11288892.html

Tornado WebSocket简单聊天的相关教程结束。

《Tornado WebSocket简单聊天.doc》

下载本文的Word格式文档,以方便收藏与打印。