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。这次协议变更非常大,主要集中在握手协议和数据传输的格式上。下面我们来详细介绍一下。

握手协议

我们先来看一下大致的区别:

  1. 最老的websocket草案标准中是没有安全key,草案7.5、7.6中有两个安全key,而现在的草案10中只有一个安全key,即将 7.5、7.6中http头中的”Sec-WebSocket-Key1″与”Sec-WebSocket-Key2″合并为了一个”Sec- WebSocket-Key”
  2. 把http头中Upgrade的值由”WebSocket”修改为了”websocket”;http头中的”-Origin”修改为了”Sec-WebSocket-Origin”;
  3. 增加了http头”Sec-WebSocket-Accept”,用来返回原来草案7.5、7.6服务器返回给客户端的握手验证,原来是以内容的形式返回,现在是放到了http头中;另外服务器返回客户端的验证方式也有变化。

服务器生成验证的方式变化较大,我们来做一介绍。

旧版:

1 GET / HTTP/1.1
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,即可完成握手。

新版:

1 GET / HTTP/1.1
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实现的解码和编码,兼容新旧版协议,仅供参考:

01 def send_data(self, raw_str):
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 相同。

 posted on 2012-09-09 10:42 落叶满长沙 阅读(...) 评论(...) 编辑 收藏