WebSocket是用于浏览器或其他客户端,建立与web服务器的双向、可靠通信渠道的协议。与其他方法相比的最大好处是,不需要使用多个XML HTTP请求来完成,或者是必须让一个正常的HTTP链接尽可能长时间的保持Open.
历时11年,WebSocket终于被批准成为IETF的建议标准:RFC6455.其前身是WHATWG (Web Hypertext
Application Technology Working Group)的工作。而Web Socket的API,是W3C的工作。
WebSocket可以只打开一个到服务器的链接,并且在此链接上交换信息。其优势在于减少了传统方法的复杂性,提高了可靠性和降低了浏览器和客户端之间的负载。这样做的一个重要原因是, 很多防火墙屏蔽80以外的端口,迫使越来越多的应用迁移到HTTP上来了。
目前多数浏览器已经支持WebSocket,如iOS Safari 4.2, Opera Mobile 11,chrome 14, Firefox5等,但IE9不支持。
如果用websocket 的话 也许 有的server链接不上,可能是因为11年的websocket草案的变迁中,有的浏览器支持的是旧版本的websocket,比如iPhone4上的safari使用的WebSocket是旧版的握手协议,那么就要使用就的握手协议来制做服务器端
起初,Mozilla基金会的Mozilla Firefox会在4版本支持websocket。Opera软件公司方面在Opera 10.7和11.0的预览版本中也支持了websocket。然而,基于安全因素的考虑,两家宣布将暂时移除该功能。FireFox预计于版本6重新实现WebSockets RFC Version -07 ,但此版本实现并不向后兼容,故旧版本的服务器实现软件有可能无法顺利运行。版本6之中的WebSocket功能将会默认打开。
如今只有Safari支持旧版本的协议,Chrome和Firefox最新版都已升级至Hybi-10(协议地址)。因此,我们再来看一下WebSocket新版协议Hybi-10。这次协议变更非常大,主要集中在握手协议和数据传输的格式上。下面我们来详细介绍一下。
握手协议
我们先来看一下大致的区别:
- 最老的websocket草案标准中是没有安全key,草案7.5、7.6中有两个安全key,而现在的草案10中只有一个安全key,即将 7.5、7.6中http头中的”Sec-WebSocket-Key1″与”Sec-WebSocket-Key2″合并为了一个”Sec- WebSocket-Key”
- 把http头中Upgrade的值由”WebSocket”修改为了”websocket”;http头中的”-Origin”修改为了”Sec-WebSocket-Origin”;
- 增加了http头”Sec-WebSocket-Accept”,用来返回原来草案7.5、7.6服务器返回给客户端的握手验证,原来是以内容的形式返回,现在是放到了http头中;另外服务器返回客户端的验证方式也有变化。
服务器生成验证的方式变化较大,我们来做一介绍。
旧版:
2 Upgrade: WebSocket
3 Connection: Upgrade
4 Host: 127.0.0.1:1337
5 Origin: http://127.0.0.1:8000
6 Cookie: sessionid=xxxx; calView=day; dayCurrentDate=1314288000000
7 Sec-WebSocket-Key1: cV`p1* 42#7 ^9}_ 647 08{
8 Sec-WebSocket-Key2: O8 415 8x37R A8 4
9 ;“######
旧版生成Token的方法如下:
取出Sec-WebSocket-Key1中的所有数字字符形成一个数值,这里是1427964708,然后除以Key1中的空格数目,得到一个数 值,保留该数值整数位,得到数值N1;对Sec-WebSocket-Key2采取同样的算法,得到第二个整数N2;把N1和N2按照Big- Endian字符序列连接起来,然后再与另外一个Key3连接,得到一个原始序列ser_key。Key3是指在握手请求最后,有一个8字节的奇怪的字符 串“;”######”,这个就是Key3。然后对ser_key进行一次md5运算得出一个16字节长的digest,这就是老版本协议需要的 token,然后将这个token附在握手消息的最后发送回Client,即可完成握手。
新版:
2 Upgrade: websocket
3 Connection: Upgrade
4 Host: 127.0.0.1:1337
5 Sec-WebSocket-Origin: http://127.0.0.1:8000
6 Sec-WebSocket-Key: erWJbDVAlYnHvHNulgrW8Q==
7 Sec-WebSocket-Version: 8
8 Cookie: csrftoken=xxxxxx; sessionid=xxxxx
新版生成Token的方法如下:
首先服务器将key(长度24)截取出来,如4tAjitqO9So2Wu8lkrsq3w==,用它和自定义的一个字符串(长度 36)258EAFA5-E914-47DA-95CA-C5AB0DC85B11连接起来,然后把这一字符串进行SHA-1算法加密,得到长度为20字 节的二进制数据,再将这些数据经过Base64编码,最终得到服务端的密钥,也就是ser_key。服务器将ser_key附在返回值Sec- WebSocket-Accept后,至此握手成功。
数据报文格式
旧版协议比较简单,仅仅是在原始数据前加了个’\x00′,在最后面加了个’\xFF’,即假如Client发送一个字符串’test’,实际上WebSocket Server收到的数据是:’x00test\xFF’,所以只需要剥离掉首尾那两个字符就可以了。
新版的协议对这部分规定比较复杂,以下是其格式标准:(下图在Firefox可能会出现错乱,请换用Chrome)
FIN:1位,用来表明这是一个消息的最后的消息片断,当然第一个消息片断也可能是最后的一个消息片断;
RSV1, RSV2, RSV3: 分别都是1位,如果双方之间没有约定自定义协议,那么这几位的值都必须为0,否则必须断掉WebSocket连接;
Opcode:4位操作码,定义有效负载数据,如果收到了一个未知的操作码,连接也必须断掉,以下是定义的操作码:
- %x0 表示连续消息片断
- %x1 表示文本消息片断
- %x2 表未二进制消息片断
- %x3-7 为将来的非控制消息片断保留的操作码
- %x8 表示连接关闭
- %x9 表示心跳检查的ping
- %xA 表示心跳检查的pong
- %xB-F 为将来的控制消息片断的保留操作码
Mask:1位,定义传输的数据是否有加掩码,如果设置为1,掩码键必须放在masking-key区域,客户端发送给服务端的所有消息,此位的值都是1;
Payload length: 传输数据的长度,以字节的形式表示:7位、7+16位、或者7+64位。如果 这个值以字节表示是0-125这个范围,那这个值就表示传输数据的长度;如果这个值是126,则随后的两个字节表示的是一个16进制无符号数,用来表示传 输数据的长度;如果这个值是127,则随后的是8个字节表示的一个64位无符合数,这个数用来表示传输数据的长度。多字节长度的数量是以网络字节的顺序表 示。负载数据的长度为扩展数据及应用数据之和,扩展数据的长度可能为0,因而此时负载数据的长度就为应用数据的长度。
Masking-key:0或4个字节,客户端发送给服务端的数据,都是通过内嵌的一个32位值作为掩码的;掩码键只有在掩码位设置为1的时候存在。
Payload data: (x+y)位,负载数据为扩展数据及应用数据长度之和。
Extension data:x位,如果客户端与服务端之间没有特殊约定,那么扩展数据的长度始终为0,任何的扩展都必须指定扩展数据的长度,或者长度的计算方式,以及在握手时如何确定正确的握手方式。如果存在扩展数据,则扩展数据就会包括在负载数据的长度之内。
Application data:y位,任意的应用数据,放在扩展数据之后,应用数据的长度=负载数据的长度-扩展数据的长度。
以下是Python实现的解码和编码,兼容新旧版协议,仅供参考:
02 if self.sockets[self]['new_version']:
03 back_str = []
04 back_str.append(‘\x81‘)
05 data_length = len(raw_str)
06
07 if data_length <= 125:
08 back_str.append(chr(data_length))
09 else:
10 back_str.append(chr(126))
11 back_str.append(chr(data_length >> 8))
12 back_str.append(chr(data_length & 0xFF))
13
14 back_str = “”.join(back_str) + raw_str
15 self.transport.write(back_str)
16 else:
17 back_str = ‘\x00%s\xFF‘ % (raw_str)
18 self.transport.write(back_str)
19
20 def parse_recv_data(self, msg):
21 raw_str = ”
22
23 if self.sockets[self]['new_version']:
24 code_length = ord(msg[1]) & 127
25
26 if code_length == 126:
27 masks = msg[4:8]
28 data = msg[8:]
29 elif code_length == 127:
30 masks = msg[10:14]
31 data = msg[14:]
32 else:
33 masks = msg[2:6]
34 data = msg[6:]
35
36 i = 0
37 for d in data:
38 raw_str += chr(ord(d) ^ ord(masks[i%4]))
39 i += 1
40 else:
41 raw_str = msg.split(“\xFF“)[0][1:]
42
43 return raw_str
PS: 在FireFox6的版本里,WebSocket 被更名为 MozWebSocket,但是该 class 的成员与用法皆与 WebSocket 相同。