现在能找到的实现握手协议的代码基本上是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 )