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