[转载]websocket最新协议的握手实现


现在能找到的实现握手协议的代码基本上是76草案的,76草案已经过期,Firefox在强制升级到6.0以后,不再支持76草案,而且WebSocket对象也不存在了,转而使用自家的对象:MozWebSocket,所以需要修改你的javascript代码:

var support = "MozWebSocket" in window ? 'MozWebSocket' : ( "WebSocket" in window ? 'WebSocket' : null ) ;  if( support ) {         ws = new window[support]('ws://localhost:8080', 'my-custom-chat-protocol'); }else{         alert("Your browser doesn't support websocket!"); }

虽然Firefox在和Chrome血拼版本号,谁敢说Firefox疯狂升级版本号而没却有什么更新?看看Firefox:把WebSocket升级为MozWebSocket也算是一大超级创新!因为你刚刚发布好了程序,和朋友们出去吃着火锅还唱着歌,突然客户打电话来说:原来跑的好好的代码不能运行了!然后翻遍 mozilla.org ,终于在一个小角落了发现了声明:6.0以后没有了WebSocket对象,取而代之的是MozWebSocket,就算你可以随便修改名称,但是你是不是得保留原有的?

好了,这篇文章主要是记录不同语言的握手协议实现,当然了,没有涵盖到的语言大同小异,对这修改一下就可以了。

因为我平常主要使用 ruby、php、javascript,而 nodejs基本上用来做测试的比较多。

如下,我获取到的一次请求头部,最新版本的websocket协议没有了key1和key2这两个罗嗦的玩意,看来html5小组也在精简实现规则,这里不是全部的头部,比如应该有 cookie和User-Agent、Accept等,但都与实现无关,所以不贴出来了。

 

GET /pub/chat?q=me HTTP/1.1 Host: localhost:8080 Connection: keep-alive, Upgrade Sec-WebSocket-Version: 7 Sec-WebSocket-Origin: null Sec-WebSocket-Protocol: my-custom-chat-protocol Sec-WebSocket-Key: /4VCUCTU2R4ycJl99yQWXw== Pragma: no-cache Cache-Control: no-cache Upgrade: websocket

 

好,没有了Sec-WebSocket-Key1 和 Sec-WebSocket-Key2,只有一个Sec-WebSocket-Key,对于整天跟编码打交到的程序员,一眼就可以看出来:这个是一个经过base64编码后的数据,不过你不需要解码该数据,需要把这个字符串连接上一个固定的字符串:

 

258EAFA5-E914-47DA-95CA-C5AB0DC85B11

 

至于为什么是上面这个一堆,我没有深入研究,实际上websocket的草案我也是大致的浏览,因为平常很忙,没有细心去钻研。

把Sec-WebSocket-Key:后的字符串,即:/4VCUCTU2R4ycJl99yQWXw== 连接上那一串固定字符串,生成一个这样:

 

/4VCUCTU2R4ycJl99yQWXw==258EAFA5-E914-47DA-95CA-C5AB0DC85B11

 

假设该字符串存储在变量 key 中:

对该字符串先用 sha1安全散列算法计算出二进制的值,然后用base64对其进行编码,即可以得到握手后的字符串:

Ruby计算握手字符串:

 

require 'digest/sha1' require 'base64' # 注意:这里要用 strict_encode64 方法 response_key = Base64.strict_encode64(Digest::SHA1.digest( key ))

Nodejs计算:

 

 

var crypto = require('crypto'); response_key = crypto.createHash('sha1').update( key ).digest('base64');

 

PHP计算:

 

// sha1函数第二个参数为 true,sha1返回的为二进制格式数据 $response_key = base64_encode(sha1($key, true))

 

最终的结果应该是:

 

i/yxBvO+uGlGAOVqFUhEdVQS8mM=

 

对照实现看看是否一样,

生成后,返回给客户端:

 

HTTP/1.1 101 Switching Protocols\r\n  Upgrade: websocket\r\n  Connection: Upgrade\r\n  Sec-WebSocket-Accept: i/yxBvO+uGlGAOVqFUhEdVQS8mM=\r\n\r\n

 

好,用你的socket输出给浏览器,就可以完成握手了。

下篇介绍如何用C/C++来实现。

 

websocket通讯协议(10版本)简介

工作中用到了websocket 协议10版本的,英文的协议请看这里:

 

http://tools.ietf.org/html/draft-ietf-hybi-thewebsocketprotocol-10

 

这篇文章相当于工作的总结吧。

 

首先, 你需要简单了解一下为什么会诞生websocket通讯协议,web上的通讯一般都是基于HTTP(超文本传输协议)的通讯,故而没有建立长时间的网络连接的方法,一般的通讯都是这样子的:

 

请求

浏览器———————>服务器

<———————–

响应

 

这种连接都是客户端发起的,服务器回复数据后关闭连接。

就好像你用浏览器访问百度输入www.baidu.com后,浏览器发起请求,百度的服务器将该页面的html超文本传给你的浏览器后关闭连接。

 

这种连接时间很短的, 而且服务器无法主动传送数据。

 

举几个例子:

优酷, 土豆这些网站可以在网上播放电影,播放电影需要持续传送数据的,故只能内嵌flash播放器,用flash中的flash socket持续传送数据。

 

用html, js等编写一个及时聊天软件是很困难的,关键就在于不能建立持续的连接,服务器不能主动传送数据给客户端。只能隔一段时间客户端发起请求主动询问服务器有无数据?

 

websocket可以建立稳定的连接,能解决上述的问题。

 

先说下原理,稍后会把代码文件穿上来给大家下载。

 

websocket通讯过程:

 

1.客户端发起连接请求

websocket客户端首先发起一个连接请求,发送的数据格式如下:

 

 

GET /10.15.1.218:12345/chat?key=value\r\n

HTTP/1.1\r\n
Upgrade: websocket\r\n
Connection: Upgrade\r\n
Host: 10.15.1.218:12345\r\n
Sec-WebSocket-Origin: null\r\n
Sec-WebSocket-Key: 4tAjitqO9So2Wu8lkrsq3w==\r\n
Sec-WebSocket-Version: 8\r\n\r\n

 

这是类似于HTTP的头,注意每行数据结尾结束符是”\r\n”, 最后的结束符是”\r\n\r\n”。

 

请求头第1行详解:

“GET /”后面是服务器的IP和端口(10.15.1.218:12345)必须有。’/'的后面是你自己字符串,(chat),随便你传什么,这部分是可选的。字符串 ‘?’后面是一些参数(key=value),是什么你自己定义, 这部分也是可选的。像下面这三种都是合法的:

 

GET /10.15.1.218:12345\r\n

或者

GET /10.15.1.218:12345/chat\r\n

或者

GET /10.15.1.218:12345/chat?key=value\r\n

 

第2, 3, 4, 5, 6行:

 

HTTP/1.1\r\n
Upgrade: websocket\r\n
Connection: Upgrade\r\n
Host: 10.15.1.218:12345\r\n
Sec-WebSocket-Origin: null\r\n

 

这些都基本是固定的格式与内容,Host: 后面是服务器(被连接者)的IP和Port。

 

第7行:

 

Sec-WebSocket-Key: 4tAjitqO9So2Wu8lkrsq3w==\r\n

Sec-WebSocket-Key后面的那一串东西,那一串长度为24的字符串是客户端随机生成的,我们暂时叫他cli_key,服务器必须用它经过一定的运算规则生成服务器端的key,暂时叫做ser_key,然后把ser_key发回去,客户端验证正确后,握手成功!

 

第8行:

 

Sec-WebSocket-Version: 8\r\n\r\n

 

之所以版本为8的原因,我不太清楚。10版本的通讯协议中客户端发出的都是8。

 

chrome 14浏览器中实现了websocket客户端,不用自己实现。可以去下载一个,当websocket客户端用。

 

2.制作服务端的密钥

我们的服务器将key1(长度24)截取出来

 

4tAjitqO9So2Wu8lkrsq3w==

 

用它和自定义的一个字符串(长度36):

 

258EAFA5-E914-47DA-95CA-C5AB0DC85B11

连接起来,像这样:

 

4tAjitqO9So2Wu8lkrsq3w==258EAFA5-E914-47DA-95CA-C5AB0DC85B11

 

然后把这一长串经过SHA-1算法加密,得到长度为20字节的二进制数据,

再将这些数据经过Base64编码,最终得到服务端的密钥,也就是ser_key:

 

bEVeGLZrb9fS3Rj8WzExJdCsedg=

 

3.服务端返回密钥

 

然后需要把密钥返回给客户端,完成握手,发送的数据格式如下:

 

HTTP/1.1 101 Switching Protocols\r\n

Upgrade: websocket\r\n

Connection: Upgrade\r\n

Sec-WebSocket-Accept: bEVeGLZrb9fS3Rj8WzExJdCsedg=\r\n\r\n

 

至此,算是握手成功了!

 

4.传输数据(简单的介绍数据长度小于126的数据传输,传输大于等于126字节的数据头部(head)可就不止2个字节了,去看英文文档中介绍头部的部分)

 

必须有掩码

客户端———————–>服务器

<———————–

掩码(可选)

 

协议中规定客户端发向服务器的数据必须有掩码,比如需要发送一个字符串“Hello”,

 

“Hello“的ascii码:

H        e              l            l            o

十六进制         0×48     0×65        0x6c     0x6c     0x6f

十进制             72        101          108      108       111

 

 

 

但是实际发出的数据是这样的:

 

——————head—–掩码0—-1——-2——-3—–H—-e—–l——l——-o————–

0×81 0×85   0×37  0xfa   0×21    0x3d 0x7f 0x9f 0x4d 0×51 0×58

 

head头部我不在这里说了,需要很多文字才能说明,自己去英文协议中相关地方查看一下吧,head后是4字节的掩码,随机生成的,再后面是数据了,你可能发现数据变了,这些数据是 hello的ascii码和掩码做异或运算算出来的。

 

运算规则是这样的, 第零个字符’H'和第零个掩码异或, 第一个字符’e'和第一个掩码异或……

 

 

其实就是用c语言表示就是res = str[n]^mask[n%4]

 

 

服务器发出数据可以有掩码, 也可以没有掩码

 

发出一个“Hello”字符串可以发出和客户端一样的数据,也可以发出像下面的无掩码的:

 

0×81 0×05 0×48 0×65 0x6c 0x6c 0x6f
H       e       l        l       o

 

这就是头部信息加上原始数据啦。

 

5. 关闭连接

 

这部分很简单,或许你可以去英文协议中找到它看一看。

 

 

基于Websocket草案10协议的升级及基于Netty的握手实现

最近发现,WEBWW在chrome14及FF6.5中没法与后台建立连接了,后面经过查找原因,是chrome14中使用最新的websocket协议草案,而chrome12中使用的websocket协议标准还是草案7.5、7.6的标准;现在草案的最新版本是草案10,草案的链接地址为:http://tools.ietf.org/html/draft-ietf-hybi-thewebsocketprotocol-10,本次协议变更比较大,主要体现在安全性和可扩展性上:

1、握手的标准:

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”;

3)、把http头中的”-Origin”修改为了”Sec-WebSocket-Origin”;

4)、增加了http头”Sec-WebSocket-Accept”,用来返回原来草案7.5、7.6服务器返回给客户端的握手验证,原来是以内容的形式返回,现在是放到了http头中;另外服务器返回客户端的验证方式也变了,后面会有介绍。

2、数据传输的格式:

以下是一个格式标准图:

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位,任意的应用数据,放在扩展数据之后,应用数据的长度=负载数据的长度-扩展数据的长度。

数据帧协议是按照扩展的巴科斯范式(ANBF:Augmented Backus-Naur Form RFC5234)组成的:

ws-frame = frame-fin
frame-rsv1
frame-rsv2
frame-rsv3
frame-opcode
frame-masked
frame-payload-length
[ frame-masking-key ]
frame-payload-data

frame-fin = %x0 ; 表示这不是当前消息的最后一帧,后面还有消息
/ %x1 ; 表示这是当前消息的最后一帧

frame-rsv1 = %x0
; 1 bit, 如果没有扩展约定,该值必须为0

frame-rsv2 = %x0
; 1 bit, 如果没有扩展约定,该值必须为0

frame-rsv3 = %x0
; 1 bit, 如果没有扩展约定,该值必须为0

frame-opcode = %x0 ; 表示这是一个连续帧消息
/ %x1 ; 表示文本消息
/ %x2 ; 表示二进制消息
/ %x3-7 ; 保留
/ %x8 ; 表示客户端发起的关闭
/ %x9 ; ping(用于心跳)
/ %xA ; pong(用于心跳)
/ %xB-F ; 保留

frame-masked = %x0 ; 数据帧没有加掩码,后面没有掩码key
/ %x1 ; 数据帧加了掩码,后面有掩码key

frame-payload-length = %x00-7D
/ %x7E frame-payload-length-16
/ %x7F frame-payload-length-63
; 表示数据帧的长度

frame-payload-length-16 = %x0000-FFFF
; 表示数据帧的长度

frame-payload-length-63 = %x0000000000000000-7FFFFFFFFFFFFFFF
; 表示数据帧的长度

frame-masking-key = 4( %0×00-FF ) ; 掩码key,只有当掩码位为1时出现

frame-payload-data = (frame-masked-extension-data
frame-masked-application-data) ; 当掩码位为1时,这里的数据为带掩码的数据,扩展数据及应用数据都带掩码
/ (frame-unmasked-extension-data
frame-unmasked-application-data) ; 当掩码位为0时,这里的数据为不带掩码的数据,扩展数据及应用数据都不带掩码

frame-masked-extension-data = *( %x00-FF ) ; 目前保留,以后定义

frame-masked-application-data = *( %x00-FF )

frame-unmasked-extension-data = *( %x00-FF ) ; 目前保留,以后定义

frame-unmasked-application-data = *( %x00-FF )

原文地址:https://www.cnblogs.com/fx2008/p/2738798.html