Tornado WebSocket简单聊天
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
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· AI与.NET技术实操系列(二):开始使用ML.NET
· 记一次.NET内存居高不下排查解决与启示
· 探究高空视频全景AR技术的实现原理
· 理解Rust引用及其生命周期标识(上)
· 浏览器原生「磁吸」效果!Anchor Positioning 锚点定位神器解析
· 全程不用写代码,我用AI程序员写了一个飞机大战
· DeepSeek 开源周回顾「GitHub 热点速览」
· 记一次.NET内存居高不下排查解决与启示
· MongoDB 8.0这个新功能碉堡了,比商业数据库还牛
· .NET10 - 预览版1新功能体验(一)