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. 如果发现此文章有错误请不吝评论指出,感激不尽。

posted @ 2021-02-06 17:06  Mz1  阅读(92)  评论(0编辑  收藏  举报