网络编程:WebSocket协议浅析
The following article is from Hello Wld Author colinsusie
欢迎关注colinsusie的微信公众号,colinsusie就是之前的colin大神哦!继续向colinsusie学习网络协议!
前言
当前好多手游都要求支持全平台,即要支持IOS和Android,也要支持原生App和H5,这让游戏的研发门槛越来越高。服务器这一端相对好一点,但也要考虑不同平台的通讯协议差异。综合各个平台的差异,只有HTTP和WebSocket是全平台支持的。HTTP适合于短连接的游戏,WebSocket则常用在长连接,通信比较频繁的游戏,比如像一些RPG,回合制,对战类的等等。
这一篇就来讲讲WebSocket协议的内容。
握手阶段
WebSocket以一个HTTP的请求和响应来进行握手,客户端请求的文本大概是这样:
GET / HTTP/1.1
Host: example.com:8000
Upgrade: websocket
Connection: Upgrade
Sec-WebSocket-Key: dGhlIHNhbXBsZSBub25jZQ==
Sec-WebSocket-Version: 13
请求方法必须是GET,协议必须是1.1以上,请求路径没有强制要求,这里是/
Upgrade 必须是websocket,Connection必须是Upgrade
Sec-WebSocket-Version 为WebSocket版本号,当然是13
Sec-WebSocket-Key 是客户端发来的一个Key,看下面响应描述。
服务器响应的文本为:
HTTP/1.1 101 Switching Protocols
Upgrade: websocket
Connection: Upgrade
Sec-WebSocket-Accept: s3pPLMBiTxaQ9kYGzzhZRbK+xOo=
响应码必须是101,表示Switching Protocols
Upgrade和Connection与上面一样
Sec-WebSocket-Accept根据上面请求的Sec-WebSocket-Key算出来的,它的算法是:Sec-WebSocket-Key + 258EAFA5-E914-47DA-95CA-C5AB0DC85B11,然后计算出这个字符串的SHA-1哈希值,最后用base64得到结果。
响应完之后,握手完成,接下来就可以交换数据帧。
数据帧格式
每一个数据帧都包含帧头+有效数据,帧头的格式如下(注意字节顺序都是网络字节序):
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 ... |
+---------------------------------------------------------------+
协议的大概内容是:
第1位FIN表示该帧是否为连续帧(即需要多个帧才能组成完整有效数据),如果为1表示单独帧,如果为0表示连续帧。我觉得这个有点过度设计了,连续帧应该由应用层自己解决,并且一个帧可以表示的长度是很长的,完全没必要设计这个连接帧。
RSV1~RSV3 这3位未用,默认为0。
opcode 为操作码,占4位,这个会决定该帧的类型,后面描述。
MASK 为掩码标志,占1位,如果为1,有效数据需要和Masking-key进行异或才得到原始数据,后面描述。
Payload len,Extended payload length 用于描述有效数据的大小,这个长度是动态的,后面描述。
Masking-key和MASK位结合,如果MASK为1,这个才会存在,否则不存在。
后面就是上层应用的有效数据。
MASK数据
如果MASK位为1,那么Masking-key这4个字节就存在,用于和Playload data进行解码,才能得到原始的数据,解码的伪代码如下:
var DECODED = "";
for (var i = 0; i < PayloadData.length; i++) {
DECODED[i] = PayloadData[i] ^ MaskKey[i % 4];
}
这其实就是一种异或加密,客户端发过来的数据规定必须要加密。服务器可以根据需要自己决定。
Payload Data的长度
帧头有一块内容用来表示有效数据的大小,这一块内容是动态长度的,它是这样计算有效数据大小的:
先读7位(Payload len),如果其值小于126,那么有效数据的长度就是它的值。
如果等于126,还要再读16位,这16位的值就是有效数据的长度。
如果等于127,还要再读64位,这64位的值就是有效数据的长度。
操作码
操作码分为两种,一种是数据类型,一种是控制码,描述如下:
0x1 表示内容是文本数据,并且总是以utf-8编码。
0x2 表示内容是二进制数据。
0x9 是一个控制帧,叫PING帧。
0xA 是一个控制帧,叫PONG帧,服务器收PING帧到后必须回应一个PONG帧,这其实就是一种心跳机制,且PING和PONG可以带有效数据,数据长度必须小于等125,即可以用前面的7位表示。
0x8 是一个控制帧,叫关闭帧,告诉对端我要关闭了,关闭帧可以带有效数据,其中前面2个字节是关闭码,后面跟着utf-8的判断原因文本。。
0x0 是一个控制帧,叫连续帧,假设连续帧有3帧,下面3帧的信息可以描述opcode和FIN怎么组合的:
Client:FIN=0,opcode=0x1,msg="and a"Client:FIN=0,opcode=0x0,msg="happy new"Client:FIN=1,opcode=0x0,msg="c"
最后收到的信息就是:anda happynewhappynew
断开挥手阶段
开始断开前,一端需要发送一个关闭控制帧。
另一端收到关闭帧后,需要发送一个关闭帧作为响应。
两端都发送并收到关闭帧后,就可以正常断开连接。
这个断开挥手阶段一直不大明白,TCP已经有断开过程了,WebSocket为什么还要自己设计一套挥手的过程,如果两端主动关闭,那么两端的TCP都会处于TIME_WAIT状态,这样会有什么好处呢?查看过几个实现,一般都是发送关闭帧后自己立即断开连接,并没有遵循WebSocket的协议说明等对端返回关闭帧才关闭。这个有人理解的话,欢迎告知。
关于WebSocket更详细的协议说明,请查看RFC6455