zoukankan      html  css  js  c++  java
  • websocket | 小白的websocket探险之旅

    这个故事要从快一年前说起,那个时候刚刚会一点web,试图爬b站的直播弹幕。
    但是呢,那个时候直播的弹幕已经是websocket了,我也只会http,这彻底懵逼啊。
    然后学了学,没搞懂,毕竟网上的那些东西,比较乱比较杂,看不懂也是没办法。
    啊,然后就放弃了。
    直到最近,我才想起来还有这么一回事儿,这下懂得多了一丢丢了,决定自己尝试一下
    也强烈建议想搞明白的小伙伴自己用socket之类的底层库实现一下
    首先我去学了一下python的异步,这个东西在网络编程中也是绕不过去的砍儿,主要是asyncio和aiohttp。
    那么好文章来一篇:https://www.jianshu.com/p/50384d0d3940
    就不多说了。有的坑我放在文章末尾了~
    这里主要说一下websocket是怎么一回事儿。
    为了打倒谜语人,这里我用最最人话的语言来描述。同时这也是最基础的websocket协议模型。
    请注意,这里自始至终都只有一个连接。

    1 客户端向服务器发起一个tcp连接,服务器接收到这个连接。ok.

    2 客户端发一个http的头,主要内容如下:

    最关键的就是下面的头:
    headers = {
    'Connection': 'Upgrade',
    'Upgrade': 'websocket',
    'Sec-WebSocket-Key': 'YMhExId+8G8+ZU5rYvzbog==',            # 随机的 问题不大
    }
    数据包大概长这个样子:
    GET /message HTTP/1.1
    Host: 127.0.0.1:8000
    Connection: Upgrade
    Pragma: no-cache
    Cache-Control: no-cache
    User-Agent: Mozilla/5.0 (Windows NT 10.0; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/69.0.3497.100 Safari/537.36
    Upgrade: websocket
    Sec-WebSocket-Version: 13
    Accept-Encoding: gzip, deflate
    Accept-Language: zh-CN,zh;q=0.9
    Sec-WebSocket-Key: qWKwu9NLYSJGcQEJfYwd1w==
    

    总之就是要升级协议。

    3 服务器确认这是一个对的请求头,回一个包:

    长这个样子:

    HTTP/1.1 101 Switching Protocols
    Date: Sat, 06 Feb 2021 08:31:05 GMT
    Server: Application/debug ktor/debug
    Access-Control-Allow-Origin: *
    Upgrade: websocket
    Connection: Upgrade
    Sec-WebSocket-Accept: /jTsEkuH22eK770NKkQf8cVZTz0=
    

    4 这个时候类似于http请求的握手阶段就结束了,注意,此时连接没有断开,并且已经可以开始在这个tcp通道上自由传输数据了!但是:

    websocket有固定的数据包格式
    什么意思呢?
    就是虽然我告诉你,这个时候我们可以自由对话了,但是你一定要按一定的格式去说话,不然连接就要断了。
    这也是我一开始天真的地方,我以为握手结束就可以freestyle了,实际上并不是。
    这里贴出我参考的文章:https://www.cnblogs.com/nuccch/p/10947256.html
    重点就是其中的websocket数据包格式。

     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 ...                |
    +---------------------------------------------------------------+
    

    当然,这么一坨东西谁看的懂呢?反正我是看不明白。
    然后继续去网上找,找到了这样一个详细一点的文章:https://blog.csdn.net/someonemt5/article/details/79312927
    用人话说就是,你要说一句话,我可能分几段给你传过去,然后再给你包装一下每一段,这样传过去以后还能再拼起来。
    这就是所谓的分帧传输,一小段就是一个帧。
    下面重点解释这个帧是什么鬼东西
    下面这个代码框里头是比较标准的解释,不想看可以跳过。

    FIN:1 bit,代表是否是尾帧
    
    RSV1、RSV2、RSV3:每个1 bit,保留的,若建立连接时使用了扩展(Sec-WebSocket-Extension),那么这些位的含义应该已协商好。
    
    opcode:4 bit,定义payload data的类型:
    
    0x0 :continuation frame
    
    0x1 :text frame
    
    0x2 :binary frame
    
    0x3 - 0x7 :保留,for non-control frame
    
    0x8 :close frame
    
    0x9 :ping frame
    
    0xA :pong frame
    
    0xB - 0xF :保留,for control-frame
    
    MASK:表示payload data是否被masked
    
    Payload length:7 bits 或 7 bits + 16 bits 或 7 + 64 bits。若值为
    
    0-125 :则Payload data的长度即为该值
    
    126 :那么接下来的2个字节才是Payload Data的长度(unsigned)
    
    127 :那么接下来的8个字节才是Payload Tada的长度(unsigned)
    
    Masking-Key:0 or 4 bytes,只有客户端给服务端发的包且这个包的MASK字段为1,才有该字段
    
    Payload Data:包括Extension Data和Application Data,若handshake时使用了Sec-WebSocket-Extension,则Extension Data的长度由Sec-WebSocket-Extension的值指定,或由其推导出,Application Data的长度为Payload length - Extension Data length。若没使用Sec-WebSocket-Extension,则Extension Data 长度为0,Application Data的长度为Payload length
    
     
    关于masking:
    masking和unmasking算法是一样的:maskedData[i] = originData[i] ^ maskingKey[ i mod 4 ]   or originData[i] =  maskedData[i] ^ maskingKey[ i mod 4 ]
    

    现在开始人话解释这个数据包是什么样一个结构:
    首先,1bit的标志,告诉你这个数据包是不是一个完整的,还是说要等后面的包过来拼起来。大多数情况下是1。
    然后是3bit的保留,说了保留,基本就没什么用处。
    然后是4bit的opcode,表示这个包是用来干嘛的,最常见的形式就是0001,text包,里头包的是数据。
    第一个byte就是上面的内容。
    紧着着1bit的内容是mask,可以不用太在意。
    然后是7bit的数据包长度说明,7bit的无符号数是0-127,这里如果这7bit是0-125,那么真正的数据长度就是那么多,接下来就是数据了。如果此时这7bit等于126,则这7bit后面的2bytes是真正的数据长度,这2bytes后面就是真正的数据。如果这里的7bit等于127,则后面的8bytes才是真正的数据长度,之后是真正的数据。

    那么最简单的解释就到此了,了解了基本原理之后对于协议的细节就可以更轻松的理解了~

    下面是我写的一个客户端拆数据包的简单实例,由于只是测试用的,所以只考虑了text包且7bit的payloadlen==126的时候的情况,嘛,改的话多加点判断就好了。
    使用的是asyncio库,异步真香.jpg

    while self.connected == True:
    	# 拆数据帧
    	tmp = await self._reader.read(1)   # FIN + opcode
    	num = str(bin(int.from_bytes(tmp,byteorder='big',signed=False)))[2:]
    	print(' >  frame1(FIN): ', end='')
    	print(num[:4])
    	print(' >  frame2(opcode): ', end='')
    	print(num[4:])
    	tmp = await self._reader.read(1)   # mask(1) + payloadlen(7)
    	num = int.from_bytes(tmp,byteorder='big',signed=False)
    	mask = num >> 7    # pick the highest bit
    	print(' >  frame3(mask): ', end='')     # here the mask is always 0, when recv msg from the server
    	print(str(bin(mask))[2:])
    	payload_len = num & 0b01111111   # pick the bit from 1 to 7
    	print(' >  frame4(payload_len): ', end='')
    	print(str(bin(payload_len))[2:], end='')
    	print(' - '+ str(payload_len))
    	tmp = await self._reader.read(2)   # if payload_len == 126, here is the true length
    	print(' >  frame5(): ', end='')
    	data_len = int.from_bytes(tmp,byteorder='big',signed=False)
    	print(data_len)
    	# 读取真正的数据
    	tmp = await self._reader.read(data_len)
    	print(tmp.decode('utf-8'))
    	print("长度:"+str(len(tmp)))
    

    asyncio使用的时候,reader实例使用的时候,
    readline读不了不可见字符会阻塞出问题。
    直接read()默认read(-1)的话会等连接结束一并读取。
    所以使用websocket的时候读取确定的字节数比较方便。
    另外,websocket传输中数据基本上都是大端序,顺着读就好了。

    overです。
    P.S. 如果发现此文章有错误请不吝评论指出,感激不尽。

  • 相关阅读:
    C语言知识点
    VS Studio 相关知识点
    类——继承、复合、委托
    类(传入的形参为指针形式)-字符串的实现
    类(传入的形参为非指针形式)-复数的实现
    C++需要掌握的重点内容
    目标跟踪相关资料
    夏天在威海,冬天在昆明
    短时目标跟踪
    php实现中文反转字符串的方法
  • 原文地址:https://www.cnblogs.com/Mz1-rc/p/14382116.html
Copyright © 2011-2022 走看看