作者:小小明

WebSocket的介绍

WebSocket是HTML5规范中新提出的客户端-服务器通讯协议,协议本身使用新的ws://URL格式。

WebSocket 是独立的、创建在 TCP 上的协议,和 HTTP 的唯一关联是使用 HTTP 协议的101状态码进行协议切换,使用的 TCP 端口是80,可以用于绕过大多数防火墙的限制。

WebSocket 使得客户端和服务器之间的数据交换变得更加简单,允许服务端直接向客户端推送数据而不需要客户端进行请求,两者之间可以创建持久性的连接,并允许数据进行双向传送。

目前常见的浏览器如 Chrome、IE、Firefox、Safari、Opera 等都支持 WebSocket,同时需要服务端程序支持 WebSocket。

Tornado的WebSocket模块

Tornado提供支持WebSocket的模块是tornado.websocket,其中提供了一个WebSocketHandler类用来处理通讯。

WebSocketHandler.open()

当一个WebSocket连接建立后被调用。

WebSocketHandler.on_message(message)

当客户端发送消息message过来时被调用,注意此方法必须被重写

WebSocketHandler.on_close()

当WebSocket连接关闭后被调用。

WebSocketHandler.write_message(message, binary=False)

向客户端发送消息messagea,message可以是字符串或字典(字典会被转为json字符串)。若binary为False,则message以utf8编码发送;二进制模式(binary=True)时,可发送任何字节码。

WebSocketHandler.close()

关闭WebSocket连接。

WebSocketHandler.check_origin(origin)

判断源origin,对于符合条件(返回判断结果为True)的请求源origin允许其连接,否则返回403。可以重写此方法来解决WebSocket的跨域请求(如始终return True)。

前端JavaScript编写

在前端JS中使用WebSocket与服务器通讯的常用方法如下:

var ws = new WebSocket("ws://127.0.0.1:8888/websocket"); // 新建一个ws连接
ws.onopen = function() {  // 连接建立好后的回调
   ws.send("Hello, world");  // 向建立的连接发送消息
};
ws.onmessage = function (evt) {  // 收到服务器发送的消息后执行的回调
   alert(evt.data);  // 接收的消息内容在事件参数evt的data属性中
};

web聊天室精简版

后端代码 server.py

# coding:utf-8

import tornado.web
import tornado.ioloop
import tornado.httpserver
import tornado.options
import os
import datetime

from tornado.web import RequestHandler
from tornado.options import define, options
from tornado.websocket import WebSocketHandler

define("port", default=9000, type=int)

class IndexHandler(RequestHandler):
    def get(self):
        self.render("index.html")

class ChatHandler(WebSocketHandler):

    users = set()  # 用来存放在线用户的容器

    def open(self):
        self.users.add(self)  # 建立连接后添加用户到容器中
        for u in self.users:  # 向已在线用户发送消息
            u.write_message(u"[%s]-[%s]-进入聊天室" % (self.request.remote_ip, datetime.datetime.now().strftime("%Y-%m-%d %H:%M:%S")))

    def on_message(self, message):
        for u in self.users:  # 向在线用户广播消息
            u.write_message(u"[%s]-[%s]-说:%s" % (self.request.remote_ip, datetime.datetime.now().strftime("%Y-%m-%d %H:%M:%S"), message))

    def on_close(self):
        self.users.remove(self) # 用户关闭连接后从容器中移除用户
        for u in self.users:
            u.write_message(u"[%s]-[%s]-离开聊天室" % (self.request.remote_ip, datetime.datetime.now().strftime("%Y-%m-%d %H:%M:%S")))

    def check_origin(self, origin):
        return True  # 允许WebSocket的跨域请求

if __name__ == '__main__':
    tornado.options.parse_command_line()
    app = tornado.web.Application([
            (r"/", IndexHandler),
            (r"/chat", ChatHandler),
        ],
        static_path = os.path.join(os.path.dirname(__file__), "static"),
        template_path = os.path.join(os.path.dirname(__file__), "template"),
        debug = True
        )
    http_server = tornado.httpserver.HTTPServer(app)
    http_server.listen(options.port)
    tornado.ioloop.IOLoop.current().start()

前端代码index.html

<!DOCTYPE html>
<html>
<head>
    <meta charset="utf-8">
    <meta name="viewport" content="width=device-width,initial-scale=1">
    <title>聊天室</title>
</head>
<body>
    <div id="contents" style="height:500px;overflow:auto;"></div>
    <div>
        <textarea id="msg"></textarea>
        <a href="javascript:;" onclick="sendMsg()">发送</a>
    </div>
    <script src="{{static_url('js/jquery.min.js')}}"></script>
    <script type="text/javascript">
        var ws = new WebSocket("ws://192.168.43.153:9000/chat");
        ws.onmessage = function(e) {
            $("#contents").append("<p>" + e.data + "</p>");
        }
        function sendMsg() {
            var msg = $("#msg").val();
            ws.send(msg);
            $("#msg").val("");
        }
    </script>
</body>
</html>

注意:192.168.43.153改成你自己电脑的ip.

这样一个简单的web聊天室就出来,效果如下:

image-20200527141434691

image-20200527141502570

在同一局域网下的手机和电脑对聊,主要的是界面太丑!

web聊天室v0.1

现在呢,写一个界面好看点的聊天室,最终长这个样:

image-20200527141828500

第一次使用时,还可以选择头像和昵称:

image-20200527142005046

地址

代码地址:

https://gitee.com/wangzhouming/websocket_chat

体验地址:

http://xiaoxiaoming.xyz:8088/

项目结构

│  bot_util.py
│  random_name.py
│  server.py
│
├─conf
│  │  bot_conf.py
│  │  config.ini
│  │  config.py
│  └─ __init__.py
│
├─static
│  ├─css
│  │      bootstrap-theme.css
│  │      bootstrap-theme.css.map
│  │      bootstrap-theme.min.css
│  │      bootstrap-theme.min.css.map
│  │      bootstrap.css
│  │      bootstrap.css.map
│  │      bootstrap.min.css
│  │      bootstrap.min.css.map
│  │      style.css
│  │
│  ├─fonts
│  │      glyphicons-halflings-regular.eot
│  │      glyphicons-halflings-regular.svg
│  │      glyphicons-halflings-regular.ttf
│  │      glyphicons-halflings-regular.woff
│  │      glyphicons-halflings-regular.woff2
│  │
│  ├─images
│  │      0.jpg
│  │      1.jpg
│  │      10.jpg
│  │      11.jpg
│  │      12.jpg
│  │      13.jpg
│  │      14.jpg
│  │      15.jpg
│  │      16.jpg
│  │      17.jpg
│  │      18.jpg
│  │      19.jpg
│  │      2.jpg
│  │      20.jpg
│  │      21.jpg
│  │      22.jpg
│  │      23.jpg
│  │      24.jpg
│  │      25.jpg
│  │      26.jpg
│  │      3.jpg
│  │      4.jpg
│  │      5.jpg
│  │      6.jpg
│  │      7.jpg
│  │      8.jpg
│  │      9.jpg
│  │      code.jpg
│  │
│  └─js
│          bootstrap.js
│          bootstrap.min.js
│          jquery.min.js
│          vue.min.js
│
└─template
      base.html
       room.html
      signin.html

生成随机昵称代码编码

def random_name():
    xing = '赵钱孙李周吴郑王冯陈褚卫蒋沈韩杨朱秦尤许何吕施张孔曹严华金魏陶姜戚谢邹喻柏水窦章云苏潘葛' \
           '奚范彭郎鲁韦昌马苗凤花方俞任袁柳酆鲍史唐费廉岑薛雷贺倪汤滕殷罗毕郝邬安常乐于时傅皮卞齐康' \
           '伍余元卜顾孟平黄和穆萧尹姚邵湛汪祁毛禹狄米贝明臧计伏成戴谈宋茅庞熊纪舒屈项祝董梁杜阮蓝闵' \
           '席季麻强贾路娄危江童颜郭梅盛林刁钟徐邱骆高夏蔡田樊胡凌霍虞万支柯昝管卢莫经房裘缪干解应宗' \
           '丁宣贲邓郁单杭洪包诸左石崔吉钮龚程嵇邢滑裴陆荣翁荀羊於惠甄曲家封芮羿储靳汲邴糜松井段富巫' \
           '乌焦巴弓牧隗山谷车侯宓蓬全郗班仰秋仲伊宫宁仇栾暴甘钭厉戎祖武符刘景詹束龙叶幸司韶郜黎蓟薄' \
           '印宿白怀蒲邰从鄂索咸籍赖卓蔺屠蒙池乔阴鬱胥能苍双闻莘党翟谭贡劳逄姬申扶堵冉宰郦雍卻璩桑桂' \
           '濮牛寿通边扈燕冀郏浦尚农温别庄晏柴瞿阎充慕连茹习宦艾鱼容向古易慎戈廖庾终暨居衡步都耿满弘' \
           '匡国文寇广禄阙东欧殳沃利蔚越夔隆师巩厍聂晁勾敖融冷訾辛阚那简饶空曾毋沙乜养鞠须丰巢关蒯相' \
           '查后荆红游竺权逯盖益桓公万俟司马上官欧阳夏侯诸葛闻人东方赫连皇甫尉迟公羊澹台公冶宗政濮阳' \
           '淳于单于太叔申屠公孙仲孙轩辕令狐钟离宇文长孙慕容鲜于闾丘司徒司空丌官司寇仉督子车颛孙端木' \
           '巫马公西漆雕乐正壤驷公良拓跋夹谷宰父谷梁晋楚闫法汝鄢涂钦段干百里东郭南门呼延归海羊舌微生' \
           '岳帅缑亢况郈有琴梁丘左丘东门西门商牟佘佴伯赏南宫墨哈谯笪年爱阳佟第五言福'
    ming = '伟刚勇毅俊峰强军平保东文辉力明永健世广志义兴良海山仁波宁贵福生龙元全国胜学祥才发武新利清' \
           '飞彬富顺信子杰涛昌成康星光天达安岩中茂进林有坚和彪博诚先敬震振壮会思群豪心邦承乐绍功松善' \
           '厚庆磊民友裕河哲江超浩亮政谦亨奇固之轮翰朗伯宏言若鸣朋斌梁栋维启克伦翔旭鹏泽晨辰士以建家' \
           '致树炎德行时泰盛秀娟英华慧巧美娜静淑惠珠翠雅芝玉萍红娥玲芬芳燕彩春菊兰凤洁梅琳素云莲真环' \
           '雪荣爱妹霞香月莺媛艳瑞凡佳嘉琼勤珍贞莉桂娣叶璧璐娅琦晶妍茜秋珊莎锦黛青倩婷姣婉娴瑾颖露瑶' \
           '怡婵雁蓓纨仪荷丹蓉眉君琴蕊薇菁梦岚苑筠柔竹霭凝晓欢霄枫芸菲寒欣滢伊亚宜可姬舒影荔枝思丽秀' \
           '飘育馥琦晶妍茜秋珊莎锦黛青倩婷宁蓓纨苑婕馨瑗琰韵融园艺咏卿聪澜纯毓悦昭冰爽琬茗羽希'
    result = []
    result.append(random.choice(xing))
    for i in range(random.randrange(1, 4)):
        result.append(random.choice(ming))
    return "".join(result)

调用腾讯AI闲聊接口的核心代码

import hashlib
import random
import time
from urllib import parse

import requests

from conf.bot_conf import app_id, app_key

nonce_max = 16 ** 30


def get_nonce_str():
    '获取随机字符串,用于保证签名不可预测'
    return hex(random.randrange(nonce_max))[2:]


def get_textchat_params(question) -> dict:
    params = {
        'app_id': app_id,
        'time_stamp': str(int(time.time())),
        'nonce_str': get_nonce_str(),
        'session': '10000',
        'question': question,
    }
    sign = gen_sign_string(params.items())
    params["sign"] = sign
    return params


def gen_sign_string(params) -> str:
    p = list(sorted(params))
    p.append(('app_key', app_key))
    sign_str = parse.urlencode(p)
    # 对字符串sign_before进行MD5运算,得到接口请求签名
    sign = hashlib.md5(sign_str.encode('UTF-8')).hexdigest().upper()
    return sign


# 聊天的API地址
textchat_url = "https://api.ai.qq.com/fcgi-bin/nlp/nlp_textchat"


def textchat(question):
    params = get_textchat_params(question.encode('utf-8'))
    r = requests.post(textchat_url, params)
    json = r.json()
    return json['data']['answer']

服务端核心代码

def parseUser(r: Optional[str]) -> Optional[dict]:
    if r:
        return json.loads(base64.b64decode(r).decode("utf-8"))
    return None


class IndexHandler(RequestHandler):
    def get_current_user(self):
        """在此完成用户的认证逻辑"""
        user: dict = parseUser(self.get_secure_cookie("user"))
        return user

    @tornado.web.authenticated
    def get(self):
        current_user = self.current_user
        logging.info(f"当前登陆的用户:{current_user}")
        self.render("room.html", title="web聊天室", user=current_user, host=options.host, port=options.port)


class SigninHandler(RequestHandler):
    index = 0

    def get(self):
        self.render("signin.html", title="web聊天室", name=random_name(), user=None, maximgid=27,
                    random_img_id=random.randrange(27))

    def post(self):
        SigninHandler.index += 1
        logging.info(f"注册接口被调用,当前索引:{SigninHandler.index}")
        name = self.get_body_argument("name")
        imgid = self.get_body_argument("imgid")
        user = {
            "id": SigninHandler.index,
            "name": name,
            "imageid": imgid
        }
        logging.info(f"写入cookie的对象:{user}")
        value = base64.b64encode(json.dumps(user).encode("utf-8"))
        self.set_secure_cookie("user", value)
        self.redirect("/")


class SignoutHandler(RequestHandler):
    def get(self):
        self.clear_cookie('user')
        self.redirect('/signin')


bot_ = {
    "id": 0,
    "name": "机器人小小鸟",
    "imageid": "bot"
}


class ChatHandler(WebSocketHandler):
    users = set()  # 用来存放在线用户的容器
    user = None  # 当前用户
    users_json = [bot_]  # 准备发给客户端的用户列表数据
    messageIndex = 0

    def createMessage(self, type, user, data):
        ChatHandler.messageIndex += 1
        # print(self.messageIndex)
        message = {
            "id": ChatHandler.messageIndex,
            "type": type,
            "user": user,
            "data": data
        }
        logging.info(f"创建一条消息:{message}")
        return json.dumps(message)

    def open(self):
        self.user = parseUser(self.get_secure_cookie("user"))
        if self.user is None:
            self.close(4001, 'Invalid user')
            return
        logging.info(f"{self.user}连接了WebSocket服务端...")
        self.users.add(self)  # 建立连接后添加用户到容器中
        self.users_json.append(self.user)

        # message = self.createMessage('join', self.user, self.user)
        message = self.createMessage('join', self.user, f"{self.user['name']}进入了聊天室.")
        for u in self.users:  # 向已在线用户发送消息
            u.write_message(message)
        # 通知当前上线的用户,在线用户列表
        self.write_message(self.createMessage('list', self.user, self.users_json))

    def on_message(self, message):
        msg = self.createMessage('chat', self.user, message.strip())
        self.send_msg(msg)
        msg = textchat(message.strip())
        if msg:
            msg = self.createMessage('chat', bot_, msg)
            self.send_msg(msg)

    def send_msg(self, msg):
        for u in self.users:  # 向在线用户广播消息
            u.write_message(msg)

    def on_close(self):
        self.users.remove(self)  # 用户关闭连接后从容器中移除用户
        self.users_json.remove(self.user)
        msg = self.createMessage('left', self.user, f"{self.user['name']}离开了聊天室.")
        self.send_msg(msg)

    def check_origin(self, origin):
        logging.info(origin)  # http://192.168.40.1:9000
        return True  # 允许WebSocket的跨域请求

前端登陆页面实现头像选择的核心代码

<form action="/signin" method="post">
<div class="form-group">
<label>头像:
<select name=imgid class="form-control"
onchange="document.images['idface'].src='/static/images/'+options[selectedIndex].value+'.jpg';">
{% for imgid in range(maximgid) %}
<option value="{{imgid}}" {% if imgid==random_img_id %} selected {% end %}>头像{{imgid}}
</option>
{% end %}
</select>
</label>
<img src="/static/images/{{random_img_id}}.jpg" id=idface>

<br/>
<label>昵称:</label>
<input type="text" name="name" class="form-control" placeholder="你的昵称" value="{{ name }}">
<p class="help-block">你的昵称</p>
</div>
<button type="submit" class="btn btn-primary">登陆</button>
</form>

聊天室前端页面vue双向数据绑定代码示例

<div id="message-list">
<div style="margin-bottom:25px;" v-for="msg in messages">
<div v-if="msg.type === 'join' || msg.type === 'left'">
<div class="media-left">
<img class="media-object mini"
v-bind:src="'/static/images/' + msg.user.imageid + '.jpg'">
</div>
<div class="media-body">
<h4 class="media-heading" v-text="msg.data"></h4>
</div>
</div>
<div v-if="msg.type === 'chat'">
<div class="media">
<div class="media-left">
<img class="media-object" style="width:48px; height:48px;"
v-bind:src="'/static/images/' + msg.user.imageid + '.jpg'">
</div>
<div class="media-body">
<h4 class="media-heading" v-text="msg.user.name"></h4>
<span v-text="msg.data"></span>
</div>
</div>
</div>
</div>
</div>

js部分:

vmMessageList = new Vue({
    el: '#message-list',
    data: {
    	messages: []
    }
});

function addMessage(list, msg) {
    list.push(msg);
    $('#message-list').parent().animate({
    	scrollTop: $('#message-list').height()
    }, 1000);
}

本文转载:CSDN博客