zoukankan      html  css  js  c++  java
  • WebSocket

    介绍

    WebSocket协议是基于TCP的一种新的协议。WebSocket最初在HTML5规范中被引用为TCP连接,作为基于TCP的套接字API的占位符。它实现了浏览器与服务器全双工(full-duplex)通信。其本质是保持TCP连接,在浏览器和服务端通过Socket进行通信。

    上古时期的浏览器有些是不支持WebSocket的,下面来介绍如何在浏览器中创建一个websocket对象

    var socket = new WebSocket("ws://127.0.0.1:8002/xxoo");
    

    socket发送接收数据

    ws.send() # 发送消息
    ws.onmessage = function (event) {
                console.log(event.data) # 收到数据执行的函数,event.data就是收到的数据
            }
    ws.onclose = function (event) {
        // 服务端主动断开了连接
    }
    
    ws.onopen = function(e){
        // 连接成功
    }

    Socket服务器

    import socket
    
    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)
    # 获取客户端socket对象
    conn, address = sock.accept()
    # 获取客户端的【握手】信息
    data = conn.recv(8096)
    
    print(str(data,encoding='utf-8'))

    浏览器Socket

    <!DOCTYPE html>
    <html lang="en">
    <head>
        <meta charset="UTF-8">
        <meta http-equiv="X-UA-Compatible" content="IE=edge">
        <meta name="viewport" content="width=device-width, initial-scale=1">
        <title>Title</title>
    </head>
    <body>
    <script>
    ws = new WebSocket('ws://127.0.0.1:8002/')
    </script>
    </body>
    </html>

    执行结果

    # 浏览器打印
    GET / HTTP/1.1
    Host: 127.0.0.1:8002
    Connection: Upgrade
    Pragma: no-cache
    Cache-Control: no-cache
    Upgrade: websocket
    Origin: http://localhost:53512
    Sec-WebSocket-Version: 13
    User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/66.0.3359.117 Safari/537.36
    Accept-Encoding: gzip, deflate, br
    Accept-Language: zh-CN,zh;q=0.9
    Cookie: _ga=GA1.1.414503566.1517490000; csrftoken=hm3ml70razspGU9n46ay8z7KaouRS1XrFjBgjqU2ANy8lZOKWPZJMHpNDz1QXNZ1
    Sec-WebSocket-Key: Gm61RLyES5nWI3UxzOES0g==
    Sec-WebSocket-Extensions: permessage-deflate; client_max_window_bits
    
    # 浏览器端
    WebSocket connection to 'ws://127.0.0.1:8002/' failed: Connection closed before receiving a handshake response
    

    当客户端向服务端发送连接请求时,不仅连接还会发送【握手】信息,并等待服务端响应,至此连接才创建成功!

    客户端的响应内容

    • 从请求【握手】信息中提取 Sec-WebSocket-Key
    • 利用magic_string 和 Sec-WebSocket-Key 进行hmac1加密,再进行base64加密
    • 将加密结果响应给客户端
    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'))
    

    服务端响应后,连接就建成了,接下来就可以收发数据了.

    客户端和服务端传输数据时,需要对数据进行【封包】和【解包】。客户端的JavaScript类库已经封装【封包】和【解包】过程,但Socket服务端需要手动实现。

    解包详解

    0                   1                   2                   3
     0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1
    +-+-+-+-+-------+-+-------------+-------------------------------+
    |F|R|R|R| opcode|M| Payload len |    Extended payload length    |
    |I|S|S|S|  (4)  |A|     (7)     |             (16/64)           |
    |N|V|V|V|       |S|             |   (if payload len==126/127)   |
    | |1|2|3|       |K|             |                               |
    +-+-+-+-+-------+-+-------------+ - - - - - - - - - - - - - - - +
    |     Extended payload length continued, if payload len == 127  |
    + - - - - - - - - - - - - - - - +-------------------------------+
    |                               |Masking-key, if MASK set to 1  |
    +-------------------------------+-------------------------------+
    | Masking-key (continued)       |          Payload Data         |
    +-------------------------------- - - - - - - - - - - - - - - - +
    :                     Payload Data continued ...                :
    + - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - +
    |                     Payload Data continued ...                |
    +---------------------------------------------------------------+
    
    # 根据第二个字节的后七位判断头部大小,
    # 127是向后64位,也就是8字节,头部共10字节
    # 127是向后16位,也就是2字节,头部共4字节
    # Masking-key在数据部分的前四位,后面才是真正数据的加密部分
    # 解密过程就将Masking-key与加密的数据进行位运算
    

    官方解释

    The MASK bit simply tells whether the message is encoded. Messages from the client must be masked, so your server should expect this to be 1. (In fact, section 5.1 of the spec says that your server must disconnect from a client if that client sends an unmasked message.) When sending a frame back to the client, do not mask it and do not set the mask bit. We'll explain masking later. Note: You have to mask messages even when using a secure socket.RSV1-3 can be ignored, they are for extensions.

    The opcode field defines how to interpret the payload data: 0x0 for continuation, 0x1 for text (which is always encoded in UTF-8), 0x2 for binary, and other so-called "control codes" that will be discussed later. In this version of WebSockets, 0x3 to 0x7 and 0xB to 0xF have no meaning.

    The FIN bit tells whether this is the last message in a series. If it's 0, then the server will keep listening for more parts of the message; otherwise, the server should consider the message delivered. More on this later.

    Decoding Payload Length

    To read the payload data, you must know when to stop reading. That's why the payload length is important to know. Unfortunately, this is somewhat complicated. To read it, follow these steps:

    1. Read bits 9-15 (inclusive) and interpret that as an unsigned integer. If it's 125 or less, then that's the length; you're done. If it's 126, go to step 2. If it's 127, go to step 3.
    2. Read the next 16 bits and interpret those as an unsigned integer. You're done.
    3. Read the next 64 bits and interpret those as an unsigned integer (The most significant bit MUST be 0). You're done.

    Reading and Unmasking the Data

    If the MASK bit was set (and it should be, for client-to-server messages), read the next 4 octets (32 bits); this is the masking key. Once the payload length and masking key is decoded, you can go ahead and read that number of bytes from the socket. Let's call the data ENCODED, and the key MASK. To get DECODED, loop through the octets (bytes a.k.a. characters for text data) of ENCODED and XOR the octet with the (i modulo 4)th octet of MASK. In pseudo-code (that happens to be valid JavaScript):

    var DECODED = "";
    for (var i = 0; i < ENCODED.length; i++) {
        DECODED[i] = ENCODED[i] ^ MASK[i % 4];
    }

    Now you can figure out what DECODED means depending on your application.

    python的解包过程

    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)):
        chunk = decoded[i] ^ mask[i % 4]
        bytes_list.append(chunk)
    body = str(bytes_list, encoding='utf-8')
    print(body)
    

    封包

    def send_msg(conn, msg_bytes):
        """
        WebSocket服务端向客户端发送消息
        :param conn: 客户端连接到服务器端的socket对象,即: conn,address = socket.accept()
        :param msg_bytes: 向客户端发送的字节
        :return: 
        """
        import struct
    
        token = b"x81"
        length = len(msg_bytes)
        if length < 126:
            token += struct.pack("B", length)
        elif length <= 0xFFFF:
            token += struct.pack("!BH", 126, length)
        else:
            token += struct.pack("!BQ", 127, length)
    
        msg = token + msg_bytes
        conn.send(msg)
        return True
    

    框架中使用

    • django: channel, 不过还是感觉dwebsocket好用一些,
    • flask: gevent-websocket
    • tornado: 内置

    Flask

    安装

    pip3 install gevent-websocket
    

    作用

    • - 处理Http、Websocket协议的请求 -> socket
    • - 封装Http、Websocket相关数据 -> request

    基本结构

    from geventwebsocket.handler import WebSocketHandler
    from gevent.pywsgi import WSGIServer
    from flask import Flask,render_template,request
    
    
    app = Flask(__name__)
    @app.route('/test')
    def test():
    	ws = request.environ.get('wsgi.websocket') # 如果是websocket请求会得到socket对象,否则None
            if ws:
                while:
    	        ws.receive() # 收数据
    	        ws.send(message) # 发数据
    	        # ws.close() # 关闭
    	return render_template('index.html')
    				
    if __name__ == '__main__':
    	http_server = WSGIServer(('0.0.0.0', 5000,), app, handler_class=WebSocketHandler)
    	http_server.serve_forever() 
    

    tornado

    Tornado是一个轻量级的Web框架,异步非阻塞+内置WebSocket功能

    安装

    pip3 install tornado 
    

    示例

    import tornado
    from tornado.web import Application
    from tornado.web import RequestHandler
    from tornado.websocket import WebSocketHandler
    
    # HTTP继承RequestHandler类
    class IndexHandler(RequestHandler):
    
    	def get(self, *args, **kwargs):
    		# self.write('Hello World') 响应字符串
    		self.render('index.html') # 响应模板
    
    	def post(self, *args, **kwargs):
    		user = self.get_argument('user') # 提交数据中取值
    		self.write('成功')
    
    WS_LIST = []
    # WebSocket继承WebSocketHandler类
    class MessageHandler(WebSocketHandler):
    
    	def open(self, *args, **kwargs):
                    # 连接时执行的
    		WS_LIST.append(self)
    
    	def on_message(self, message):
                    # self.close() 服务端主动断开连接,
                    # 收到数据时执行的,message就是收到的数据
    		for ws in WS_LIST:
    			ws.write_message(message) # 发送数据
    
    	def on_close(self):
                    # 客户端关闭连接时执行的
    		WS_LIST.remove(self)
    
    
    
    settings = {
    	'template_path':'templates',
    	'static_path':'static',
    } # 写配置
    
    app = Application([
    	(r"/index", IndexHandler),
    	(r"/message", MessageHandler),
    ],**settings) # 路由及配置
    
    if __name__ == '__main__':
    	app.listen(address='0.0.0.0',port=9999)
    	tornado.ioloop.IOLoop.instance().start() # 启动
    

    django-- 不用app, 不用中间件, 只需一个装饰器

    from django.shortcuts import render, HttpResponse
    from dwebsocket import require_websocket
    
    def index(request):
        return render(request,"index.html")
    
    
    
    @require_websocket
    def ws(request):
        # if not request.is_websocket():#判断是不是websocket连接
        #     try:#如果是普通的http方法
        #         message = request.GET['message']
        #         return HttpResponse(str(message))
        #     except:
        #         return render(request,'index.html')
        # else:
        #     # for message in request.websocket:
        #     #     request.websocket.send(message)#发送消息到客户端
            while True:
                res = request.websocket.wait()
                print(res) # 字节
                request.websocket.send(res)
    

      

  • 相关阅读:
    超强问卷调查系统源码购买及二次开发
    asp.net core mvc上传大文件解决方案
    asp.net core mvc发布后显示异常错误信息的方法
    基于.net core 2.0+mysql+AceAdmin搭建一套快速开发框架
    改造kindeditor支持asp.net core mvc上传文件
    Centos7 Nginx安装使用
    Centos7 守护进程supervisord 安装使用
    Centos7 .net core 2.0安装使用
    Centos7 Mysql安装
    Centos7 Redis安装
  • 原文地址:https://www.cnblogs.com/wwg945/p/9053071.html
Copyright © 2011-2022 走看看