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

 

posted @ 2019-08-29 17:16  Foto_CShow  阅读(1243)  评论(0编辑  收藏  举报