• websocket介绍


    手动实现的websocket

    你所见过的websocket

    你一定见过在网站中,有一个游客聊天的聊天框,比如人人影视。这个聊天框是如何实现即时通讯的呢,就是用到了websocket

    你可以打开浏览器的network,会看到有个ws://xxxxx,这就代表了是websocket做的

    那么什么是websocket?

    websocket就是一套协议。

    看名字,虽然有个websocket,但他和http协议一样,也要走socket。

    不同的是:http是短连接,处理完一个请求就断开;

    ​ websocket是连上就不断开,一直不断开,属于双工通道,服务端可以主动给客户端推送消息,客户端也可以主动给服务端推送消息

    当某一个客户端发送一条消息,服务端接收以后,再推送给所有的客户端,所以才会呈现出所有人都在即时通讯的效果

    服务端当然就是我们写的程序了,那客户端是浏览器,所以还需要浏览器支持才行。不要以为浏览器是都支持的,如果所有人都用chrome,前端开发工程师估计就没什么工作了。还有,如果所有的浏览器都支持,腾讯的webQQ,web微信,也不会使用长轮询来做这个事了。

    来看一下具体的代码实现

    import socket
    import base64
    import hashlib
    
    
    def get_headers(data):
        """
        将请求头格式化成字典
        :param data:
        :return:
        """
        header_dict = {}
        data = str(data, encoding='utf-8')
    
        for i in data.split('
    '):
            print(i)
        header, body = data.split('
    
    ', 1)
        header_list = header.split('
    ')
        for i in range(0, len(header_list)):
            if i == 0:
                if len(header_list[i].split(' ')) == 3:
                    header_dict['method'], header_dict['url'], header_dict['protocol'] = header_list[i].split(' ')
            else:
                k, v = header_list[i].split(':', 1)
                header_dict[k] = v.strip()
        return header_dict
    
    
    sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
    sock.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
    sock.bind(('127.0.0.1', 8002))
    sock.listen(5)
    
    conn, address = sock.accept()
    data = conn.recv(1024)
    
    headers = get_headers(data)  # 提取请求头信息
    
    
    # 对请求头中的sec-websocket-key进行加密
    response_tpl = "HTTP/1.1 101 Switching Protocols
    " 
                   "Upgrade:websocket
    " 
                   "Connection: Upgrade
    " 
                   "Sec-WebSocket-Accept: %s
    " 
                   "WebSocket-Location: ws://%s%s
    
    "
    
    magic_string = '258EAFA5-E914-47DA-95CA-C5AB0DC85B11'       #固定的,魔法字符串就是这个字符串
    value = headers['Sec-WebSocket-Key'] + magic_string
    ac = base64.b64encode(hashlib.sha1(value.encode('utf-8')).digest()) #把返回消息加密
    
    
    response_str = response_tpl % (ac.decode('utf-8'), headers['Host'], headers['url'])
    # 响应【握手】信息
    conn.send(bytes(response_str, encoding='utf-8'))
    
    info = conn.recv(8096)
    
    
    #下面是对浏览器发来的消息解密的过程
    payload_len = info[1] & 127
    if payload_len == 126:
        extend_payload_len = info[2:4]
        mask = info[4:8]
        decoded = info[8:] # 数据
    elif payload_len == 127:
        extend_payload_len = info[2:10]
        mask = info[10:14]
        decoded = info[14:]
    else:
        extend_payload_len = None
        mask = info[2:6]
        decoded = info[6:]
    
    bytes_list = bytearray()
    for i in range(len(decoded)):       #上面解密的最终结果,就是拿到这个decode,就是浏览器发来的真实的数据(加密的)
        chunk = decoded[i] ^ mask[i % 4]    #按位异或
        bytes_list.append(chunk)
    
    
    body = str(bytes_list, encoding='utf-8')
    print(body)
    

    客户端向服务端发送的请求里,有Sec-WebSocket-Key这样一个key,服务端回消息的时候,就要拿到这个key,加密后再发给浏览器,浏览器会判断自己加密后的值,与浏览器处理的是否一致,一致才能连接。加密的方式,用到一个magic_string,其实就是一段固定的字符串258EAFA5-E914-47DA-95CA-C5AB0DC85B11,加密后打包发给浏览器,浏览器验证通过后就可以通讯了,再来看看客户端:

    客户端就直接用浏览器运行这个html文件就行

    <!DOCTYPE html>
    <html lang="en">
    <head>
        <meta charset="UTF-8">
        <title>Title</title>
        <link rel="stylesheet" href="dist/css/bootstrap.css">
    </head>
    <body>
    
        <div>
            <input type="text" id="txt"/>
            <input type="button" id="btn" value="提交" onclick="sendMsg();"/>
            <input type="button" id="close" value="关闭连接" onclick="closeConn();"/>
        </div>
        <div id="content"></div>
    
    
    
        <script type="text/javascript">
              var socket = new WebSocket("ws://127.0.0.1:8002");
                socket.onopen = function () {
                    /* 与服务器端连接成功后,自动执行 */
                    var newTag = document.createElement('div');
                    newTag.innerHTML = "【连接成功】";
                    document.getElementById('content').appendChild(newTag);
                };
                socket.onmessage = function (event) {
                    /* 服务器端向客户端发送数据时,自动执行 */
                    var response = event.data;
                    var newTag = document.createElement('div');
                    newTag.innerHTML = response;
                    document.getElementById('content').appendChild(newTag);
                };
                socket.onclose = function (event) {
                    /* 服务器端主动断开连接时,自动执行 */
                    var newTag = document.createElement('div');
                    newTag.innerHTML = "【关闭连接】";
                    document.getElementById('content').appendChild(newTag);
                };
                function sendMsg() {
                    var txt = document.getElementById('txt');
                    socket.send(txt.value);
                    txt.value = "";
                }
                function closeConn() {
                    socket.close();
                    var newTag = document.createElement('div');
                    newTag.innerHTML = "【关闭连接】";
                    document.getElementById('content').appendChild(newTag);
                }
        </script>
    
    <script></script>
    </body>
    </html>
    

    这里面有三个方法:

    1. 连接上后,onopen会自动执行
    2. 发消息时,onmessage自动执行
    3. 断开连接,onclose自动执行

    客户端发送给服务端的数据,还有一层加密,必须通过解密才能拿到正确的消息

    payload_len = info[1] & 127
    if payload_len == 126:
        extend_payload_len = info[2:4]
        mask = info[4:8]
        decoded = info[8:] # 数据
    elif payload_len == 127:
        extend_payload_len = info[2:10]
        mask = info[10:14]
        decoded = info[14:]
    else:
        extend_payload_len = None
        mask = info[2:6]
        decoded = info[6:]
    
    bytes_list = bytearray()
    for i in range(len(decoded)):       #上面解密的最终结果,就是拿到这个decode,就是浏览器发来的真实的数据(加密的)
        chunk = decoded[i] ^ mask[i % 4]    #按位异或
        bytes_list.append(chunk)
    
    
    body = str(bytes_list, encoding='utf-8')
    

    这段就是解密的过程,用到位运算

    Django默认是不支持websocket的,虽然有个第三方的channels插件

    但是tornado默认就支持

    tornado实现websocket

    如果用tornado,客户端不能直接用浏览器运行了,而应该是运行tornado的一个模板文件

    服务端代码:

    #!/usr/bin/env python
    # -*- coding:utf-8 -*-
    import uuid
    import json
    import tornado.ioloop
    import tornado.web
    import tornado.websocket
    
    
    class IndexHandler(tornado.web.RequestHandler):
        def get(self):
            self.render('index.html')
    
    
    class ChatHandler(tornado.websocket.WebSocketHandler):
        # 用户存储当前聊天室用户
        waiters = set()
        # 用于存储历时消息
        messages = []
    
        def open(self):
            """
            客户端连接成功时,自动执行
            :return:
            """
            ChatHandler.waiters.add(self)
            uid = str(uuid.uuid4())
            self.write_message(uid)
    
            # 下面这段代码是给新加入的用户,显示历史信息的
            for msg in ChatHandler.messages:
                # {'uid':'xxx','message':asdfasd}
                content = self.render_string('message.html', **msg)
                self.write_message(content)
    
        def on_message(self, message):
            """
            客户端连发送消息时,自动执行
            :param message:
            :return:
            """
            msg = json.loads(message)
            ChatHandler.messages.append(msg)
    
            for client in ChatHandler.waiters:
                content = client.render_string('message.html', **msg)
                client.write_message(content)
    
        def on_close(self):
            """
            客户端关闭连接时,,自动执行
            :return:
            """
            ChatHandler.waiters.remove(self)
    
    
    def run():
        settings = {
            'template_path': 'templates',       # 配置模板文件
            'static_path': 'static',            # 配置静态文件路径
        }
        application = tornado.web.Application([         # 配置路由
            (r"/", IndexHandler),
            (r"/chat", ChatHandler),
        ], **settings)
        application.listen(8009)
        tornado.ioloop.IOLoop.instance().start()
    
    
    if __name__ == "__main__":
        run()
    

    模板文件(客户端代码):

    <!DOCTYPE html>
    <html lang="en">
    <head>
        <meta charset="UTF-8">
        <title>Python聊天室</title>
    </head>
    <body>
        <div>
            <input type="text" id="txt"/>
            <input type="button" id="btn" value="提交" onclick="sendMsg();"/>
            <input type="button" id="close" value="关闭连接" onclick="closeConn();"/>
        </div>
        <div id="container" style="border: 1px solid #dddddd;margin: 20px;min-height: 500px;">
    
        </div>
    
        <script src="/static/jquery-3.2.1.js"></script>
        <script type="text/javascript">
            $(function () {
                wsUpdater.start();
            });
            var wsUpdater = {
                socket: null,
                uid: null,
                start: function() {
                    var url = "ws://192.168.16.200:8009/chat";
                    wsUpdater.socket = new WebSocket(url);
                    wsUpdater.socket.onmessage = function(event) {
                        if(wsUpdater.uid){
                            wsUpdater.showMessage(event.data);
                        }else{
                            wsUpdater.uid = event.data;
                        }
                    }
                },
                showMessage: function(content) {
                    $('#container').append(content);
                }
            };
            function sendMsg() {
                var msg = {
                    uid: wsUpdater.uid,
                    message: $("#txt").val()
                };
                wsUpdater.socket.send(JSON.stringify(msg));
            }
    </script>
    
    </body>
    </html>
    

    原理都一样,但是用tornado实现起来,就清爽多了。

    ps:再说一下腾讯的长轮询,如果你登录webQQ,或者web微信,你可以在network里面找到 pending的字样,这就是表示是使用的长轮询(long polling)。

    长轮询与轮询(polling)的区别就是:

    ​ 轮询是过来以后看到没消息就立马去走了,但是长轮询不会立马走,而是在这等30秒(约定的时间)之后,如果一直没有消息,才返回,下一次来在等30秒,直到有消息了,这样有个缺点就是,拿到的消息并不是即时的。那腾讯这么大的公司,为什么不用性能更好的websocket呢?原因就是他是个大公司,必须要考虑兼容性,必须要保证所有的浏览器都能使用才行。

    你可以从这里拿到完整 的示例代码

    https://github.com/zEllis/websocket_demo

  • 相关阅读:
    hash_map和map的区别
    STL中map与hash_map容器的选择收藏
    ServletContextListener和ContextLoaderListener的区别
    解决Failed to execute goal org.apache.maven.plugins:maven-compiler-plugin:3.7.0:compile
    详解Tomcat线程池原理及参数释义
    Tomcat使用线程池配置高并发连接
    详解 Tomcat 的连接数与线程池
    ServletContextListener接口用法
    Spring Quartz定时任务如何获得ServletContext对象?
    如何在spring quartz类中拿到ServletContext
  • 原文地址:https://www.cnblogs.com/zhang-can/p/7994913.html
Copyright © 2020-2023  润新知