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()
View Code

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
View Code

 

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>
View Code

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>
View Code

 

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

 


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM