webSocket 服务器端的简单实现
上周研究了一下HTML5.
发现很多令人激动的功能。
路漫漫其修远兮,吾将上下而求索!
1. 内置数据库
2. 支持WebSocket
3. 支持多线程
4. 支持本地存储
但是,仍然处于草案中的 WebSocket 竟然找不到合适的服务器,刚好工作比较闲,用来三天时间自己写了一个。
功能有点简单!设计上也有很大缺陷。只能简单的发送信息,和推送信息。
而且现在的协议还不成熟,不久就有一个版本出现!昨天看到才是V16,今天出V17了。
简
单介绍一下 WebSocket 它是实现了浏览器与服务器的全双工信息传输。Websocket协议基于Http 的 Upgrade
头和101的响应进行协议切换。经过简单的握手协议,建立一个长连接,按照协议的规则进行数据的传输。具体介绍可以参考google.
1.握手协议
版本0--3中:
握手通过请求头Sec-WebSocket-Key1 和 Sec-WebSocket-Key2 的值和 8 字节的请求实体,进行MD5加密,将加密结果,构造出一个16字节作为请求实体的内容返回。如下实例:
------------------请求--------------------------------------------
- GET?/demo?HTTP/1.1??
- Host:?example.com??
- Connection:?Upgrade??
- Sec-WebSocket-Key2:?12998?5?Y3?1??.P00??
- Sec-WebSocket-Protocol:?sample??
- Upgrade:?WebSocket??
- Sec-WebSocket-Key1:?4?@1??46546xW%0l?1?5??
- Origin:?http://example.com??
- (\r\n)??
- ^n:ds[4U??
------------------响应--------------------------------------------
- HTTP/1.1?101?WebSocket?Protocol?Handshake??
- Upgrade:?WebSocket??
- Connection:?Upgrade??
- Sec-WebSocket-Origin:?http://example.com??
- Sec-WebSocket-Location:?ws://example.com/demo??
- Sec-WebSocket-Protocol:?sample??
- (\r\n)??
- 8jKS'y:G*Co,Wxa-??
------------------------------
把
第一个Key中的数字除以第一个Key的空白字符的数量,而第二个Key也是如此,这样得到两个整数,把每个整数写的四个字节里去,串为8个字
节,然后和请求实体里面的8个字节串为16字节,将这16个字节进行MD5加密(如实例中的结果:8jKS'y:G*Co,Wxa-),得到一个16字节
的数据作为响应实体的内容,返回给客户端,这样握手成功。
代码实现:
- int?len?=?8;?//?in.available();??
- byte[]?key3?=?new?byte[len];??
- if?(in.read(key3)?!=?len)??
- ????throw?new?RuntimeException();??
- log.debug(HelpUtil.formatBytes(key3));??
- String?key1?=?requestHeaders.get("Sec-WebSocket-Key1");??
- String?key2?=?requestHeaders.get("Sec-WebSocket-Key2");??
- int?k1?=?HelpUtil.parseWebsokcetKey(key1);??
- int?k2?=?HelpUtil.parseWebsokcetKey(key2);??
- ??
- byte[]?sixteenByte?=?new?byte[16];??
- System.arraycopy(HelpUtil.intTo4Byte(k1),?0,?sixteenByte,?0,?4);??
- System.arraycopy(HelpUtil.intTo4Byte(k2),?0,?sixteenByte,?4,?4);??
- System.arraycopy(key3,?0,?sixteenByte,?8,?8);??
- byte[]?md5?=?MessageDigest.getInstance("MD5").digest(sixteenByte);??
在版本4之后,握手协议修改了:
------------------请求--------------------------------------------
- GET?/chat?HTTP/1.1??
- Host:?server.example.com??
- Upgrade:?websocket??
- Connection:?Upgrade??
- Sec-WebSocket-Key:?dGhlIHNhbXBsZSBub25jZQ==??
- Sec-WebSocket-Origin:?http://example.com??
- Sec-WebSocket-Protocol:?chat,?superchat??
- (\r\n)??
------------------响应--------------------------------------------
- HTTP/1.1?101?Switching?Protocols??
- Upgrade:?websocket??
- Connection:?Upgrade??
- Sec-WebSocket-Accept:?me89jWimTRKTWwrS3aRrL53YZSo=??
- Sec-WebSocket-Nonce:?AQIDBAUGBwgJCgsMDQ4PEC==??
- Sec-WebSocket-Protocol:?chat??
使
用请求头的值
Sec-WebSocket-Key,该值是BASE-64编码(base64-encoded)的,我们不需要转码,加上一个魔幻字符串:
"258EAFA5-E914-47DA-95CA-C5AB0DC85B11",(结果:
[dGhlIHNhbXBsZSBub25jZQ==258EAFA5-E914-47DA-95CA-C5AB0DC85B11])使用 SHA-1
加密,之后进行 BASE-64编码,将结果做为 Sec-WebSocket-Accept 头的值,返回给客户端。
如果服务器端有
Sec-WebSocket-Nonce 头,表示要在Sec-WebSocket-Key 的值,和魔幻字符串之间加入该
Sec-WebSocket-Nonce
头的值,即“dGhlIHNhbXBsZSBub25jZQ==AQIDBAUGBwgJCgsMDQ4PEC==258EAFA5-
E914-47DA-95CA-C5AB0DC85B11”,进行 SHA-1 加密,之后和前面的相同。完成握手协议。
- public?static?final?String?GUID?=?"258EAFA5-E914-47DA-95CA-C5AB0DC85B11";??
- public?static?final?String?HEADER_CODE?=?"iso-8859-1";??
- ??
- String?code?=?requestHeaders.get("Sec-WebSocket-Key")?+?GUID;??
- byte[]?bts?=?MessageDigest.getInstance("SHA1").digest(code.getBytes(HEADER_CODE));??
- code?=?HelpUtil.getBASE64(bts);??
- resMap.put("Sec-WebSocket-Accept",?code);??
握手完成就是数据帧的传输了。
在版本 0 中, 数据帧比较的简单。数据帧以 0x00 开头,以0xFF结尾,中间的数据以utf-8编码的字符就可以了。当然这个简单的格式只能用来传输字符串。无法传输字节流。所以 版本 1 就做了修改了,后面的版本绝大部分是兼容的。
后面的这个帧结构就有点复杂了,如下所示(一行是4个字节,32 bit):
- ?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??
- +-+-+-+-+-------+-+-------------+-------------------------------+??
- |M|R|R|R|?opcode|R|?Payload?len?|????Extended?payload?length????|??
- |O|S|S|S|??(4)??|S|?????(7)?????|?????????????(16/63)???????????|??
- |R|V|V|V|???????|V|?????????????|???(if?payload?len==126/127)???|??
- |E|1|2|3|???????|4|?????????????|???????????????????????????????|??
- +-+-+-+-+-------+-+-------------+?-?-?-?-?-?-?-?-?-?-?-?-?-?-?-?+??
- |?????Extended?payload?length?continued,?if?payload?len?==?127??|??
- +?-?-?-?-?-?-?-?-?-?-?-?-?-?-?-?+-------------------------------+??
- |???????????????????????????????|?????????Extension?data????????|??
- +-------------------------------+?-?-?-?-?-?-?-?-?-?-?-?-?-?-?-?+??
- :???????????????????????????????????????????????????????????????:??
- +---------------------------+??
- :???????????????????????Application?data????????????????????????:??
- +---------------------------+??
(后续的版本略有修改)
获取数据长度
- int?dataLen?=?bt?&?PAYLOADLEN;??
- ??
- if?(dataLen?==?HAS_EXTEND_DATA)?{//?read?next?16?bit??
- ????bt?=?in.read();??
- ????b2?=?in.read();??
- ????fram.setDateLength(HelpUtil.toShort((byte)?bt,?(byte)?b2));??
- }?else?if?(dataLen?==?HAS_EXTEND_DATA_CONTINUE)?{//?read?next?32?bit??
- ????byte[]?bts?=?new?byte[8];??
- ????if?(in.read(bts)?!=?8){??
- ????????//fram.setOpcode??
- ????????throw?new?RuntimeException(??
- ????????????????"reader?Payload-Len-Extended-Continued?data?length?<?64?bit");??
- ????}??
- ????fram.setDateLength(HelpUtil.toLong(bts));??
- }?else?{??
- ????fram.setDateLength(dataLen);??
- }??
[MORE] 表示一个数据通过多个帧进行传输, 如果是 0 表示后面还有数据帧,如果是 1 则表示是最后一个帧。
[RSV1][RSV2][RSV3][RSV4] 未做定义暂时全为零。
[opcode] 标识数据的格式,以及帧的控制,如:08标识数据内容是 文本,01标识:要求远端去关闭当前连接。
[Payload len] 如果小于126 表示后面的数据长度是 [Payload len] 的值。(最大125byte)
????????????? 等于 126 表示之后的16 bit位的数据值标识数据的长度。(最大65535byte)
????????????? 等于 127 表示之后的64 bit位的数据值标识数据的长度。(一个有符号长整型的最大值)
[Extension data]没有提及怎么使用。
[Application data] 为应用提供的数据。
版本7之后,添加了 MASK 的概念。相当于对数据加密。而且要求客户端必须是MASK的。
- ?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/63)???????????|??
- |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?...????????????????|??
- +---------------------------+??
[opcode]? 01标识数据内容是 文本,08标识 : 要求远端去关闭当前连接。
[MASK](即原先的RSV4)如果是 1 则数据是被 MASK 的。
[Masking-key]
如果MASK为 1 则有4字节的 Masking-key,用于与传输的数据 [Payload Data]
进行异或运算,4byte(32bit)进行一次运算,不足四位从前往后对应,如只有三位,则只与[Masking-key]的前三位进行运算。
解码 MASK 数据,使用了一个过滤流
- @Override??
- public?int?read()?throws?IOException?{??
- ????if?(readLength?>=?length)??
- ????????return?-1;??
- ????int?b?=?0;??
- ????synchronized?(lock)?{??
- ????????if?(readLength?>=?length)??
- ????????????return?-1;??
- ????????b?=?super.read();??
- ????????if?(isMask)?{??
- ????????????b?^=?maskKey[(int)?(readLength?%?4)];??
- ????????}??
- ????????readLength++;??
- ????}??
- ????return?b;??
- }??
关于流的关闭:一般情况我们可以直接 使用socket.close() 进行关闭,客户端JS状态会显示 webSocket.readyState 的值为 2 (正在关闭的状态)。需要我们通过握手去要求远端关闭流。
有三个版本:
在版本 0 时:传两个字节 (0xff,0x00);
在版本 1--6 时:传三个字节 (0x80,0x01,0x00);
在版本 7--以上 时:传两个字节 (0x88,0x00);
经测试 只有 在版本 7--以上 时:传两个字节 (0x88,0x00); 这时可以实现 webSocket.readyState 的值为 3。
估计是我的代码有问题。如有发现请告知,谢谢!
websocket 协议: http://tools.ietf.org/html/draft-ietf-hybi-thewebsocketprotocol-10 (其他版本查看相关链接)
源码SVN地址:http://lineblog.googlecode.com/svn/trunk/ 下面的目录
???????????? httpAnalysis/src/com/googlecode/lineblog/websocket/