`
yajie
  • 浏览: 206791 次
  • 性别: Icon_minigender_1
  • 来自: 北京
文章分类
社区版块
存档分类
最新评论

webSocket 服务器端的简单实现

阅读更多

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字节作为请求实体的内容返回。如下实例:
------------------请求--------------------------------------------

Java代码  收藏代码
  1. GET /demo HTTP/1.1  
  2. Host: example.com  
  3. Connection: Upgrade  
  4. Sec-WebSocket-Key2: 12998 5 Y3 1  .P00  
  5. Sec-WebSocket-Protocol: sample  
  6. Upgrade: WebSocket  
  7. Sec-WebSocket-Key1: 4 @1  46546xW%0l 1 5  
  8. Origin: http://example.com  
  9. (\r\n)  
  10. ^n:ds[4U  


------------------响应--------------------------------------------

Java代码  收藏代码
  1. HTTP/1.1 101 WebSocket Protocol Handshake  
  2. Upgrade: WebSocket  
  3. Connection: Upgrade  
  4. Sec-WebSocket-Origin: http://example.com  
  5. Sec-WebSocket-Location: ws://example.com/demo  
  6. Sec-WebSocket-Protocol: sample  
  7. (\r\n)  
  8. 8jKS'y:G*Co,Wxa-  


------------------------------------------------------------------

把 第一个Key中的数字除以第一个Key的空白字符的数量,而第二个Key也是如此,这样得到两个整数,把每个整数写的四个字节里去,串为8个字 节,然后和请求实体里面的8个字节串为16字节,将这16个字节进行MD5加密(如实例中的结果:8jKS'y:G*Co,Wxa-),得到一个16字节 的数据作为响应实体的内容,返回给客户端,这样握手成功。


代码实现:

Java代码  收藏代码
  1. int len = 8; // in.available();  
  2. byte[] key3 = new byte[len];  
  3. if (in.read(key3) != len)  
  4.     throw new RuntimeException();  
  5. log.debug(HelpUtil.formatBytes(key3));  
  6. String key1 = requestHeaders.get("Sec-WebSocket-Key1");  
  7. String key2 = requestHeaders.get("Sec-WebSocket-Key2");  
  8. int k1 = HelpUtil.parseWebsokcetKey(key1);  
  9. int k2 = HelpUtil.parseWebsokcetKey(key2);  
  10.   
  11. byte[] sixteenByte = new byte[16];  
  12. System.arraycopy(HelpUtil.intTo4Byte(k1), 0, sixteenByte, 0, 4);  
  13. System.arraycopy(HelpUtil.intTo4Byte(k2), 0, sixteenByte, 4, 4);  
  14. System.arraycopy(key3, 0, sixteenByte, 8, 8);  
  15. byte[] md5 = MessageDigest.getInstance("MD5").digest(sixteenByte);  





在版本4之后,握手协议修改了:
------------------请求--------------------------------------------

Java代码  收藏代码
  1. GET /chat HTTP/1.1  
  2. Host: server.example.com  
  3. Upgrade: websocket  
  4. Connection: Upgrade  
  5. Sec-WebSocket-Key: dGhlIHNhbXBsZSBub25jZQ==  
  6. Sec-WebSocket-Origin: http://example.com  
  7. Sec-WebSocket-Protocol: chat, superchat  
  8. (\r\n)  



------------------响应--------------------------------------------

Java代码  收藏代码
  1. HTTP/1.1 101 Switching Protocols  
  2. Upgrade: websocket  
  3. Connection: Upgrade  
  4. Sec-WebSocket-Accept: me89jWimTRKTWwrS3aRrL53YZSo=  
  5. Sec-WebSocket-Nonce: AQIDBAUGBwgJCgsMDQ4PEC==  
  6. 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 加密,之后和前面的相同。完成握手协议。

Java代码  收藏代码
  1. public static final String GUID = "258EAFA5-E914-47DA-95CA-C5AB0DC85B11";  
  2. public static final String HEADER_CODE = "iso-8859-1";  
  3.   
  4. String code = requestHeaders.get("Sec-WebSocket-Key") + GUID;  
  5. byte[] bts = MessageDigest.getInstance("SHA1").digest(code.getBytes(HEADER_CODE));  
  6. code = HelpUtil.getBASE64(bts);  
  7. resMap.put("Sec-WebSocket-Accept", code);  




握手完成就是数据帧的传输了。

在版本 0 中, 数据帧比较的简单。数据帧以 0x00 开头,以0xFF结尾,中间的数据以utf-8编码的字符就可以了。当然这个简单的格式只能用来传输字符串。无法传输字节流。所以 版本 1 就做了修改了,后面的版本绝大部分是兼容的。
后面的这个帧结构就有点复杂了,如下所示(一行是4个字节,32 bit):

Java代码  收藏代码
  1.  0                   1                   2                   3  
  2.  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  
  3. +-+-+-+-+-------+-+-------------+-------------------------------+  
  4. |M|R|R|R| opcode|R| Payload len |    Extended payload length    |  
  5. |O|S|S|S|  (4)  |S|     (7)     |             (16/63)           |  
  6. |R|V|V|V|       |V|             |   (if payload len==126/127)   |  
  7. |E|1|2|3|       |4|             |                               |  
  8. +-+-+-+-+-------+-+-------------+ - - - - - - - - - - - - - - - +  
  9. |     Extended payload length continued, if payload len == 127  |  
  10. + - - - - - - - - - - - - - - - +-------------------------------+  
  11. |                               |         Extension data        |  
  12. +-------------------------------+ - - - - - - - - - - - - - - - +  
  13. :                                                               :  
  14. +---------------------------------------------------------------+  
  15. :                       Application data                        :  
  16. +---------------------------------------------------------------+  


(后续的版本略有修改)

获取数据长度

Java代码  收藏代码
  1. int dataLen = bt & PAYLOADLEN;  
  2.   
  3. if (dataLen == HAS_EXTEND_DATA) {// read next 16 bit  
  4.     bt = in.read();  
  5.     b2 = in.read();  
  6.     fram.setDateLength(HelpUtil.toShort((byte) bt, (byte) b2));  
  7. else if (dataLen == HAS_EXTEND_DATA_CONTINUE) {// read next 32 bit  
  8.     byte[] bts = new byte[8];  
  9.     if (in.read(bts) != 8){  
  10.         //fram.setOpcode  
  11.         throw new RuntimeException(  
  12.                 "reader Payload-Len-Extended-Continued data length < 64 bit");  
  13.     }  
  14.     fram.setDateLength(HelpUtil.toLong(bts));  
  15. else {  
  16.     fram.setDateLength(dataLen);  
  17. }  



[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的。

Java代码  收藏代码
  1.  0                   1                   2                   3  
  2.  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  
  3. +-+-+-+-+-------+-+-------------+-------------------------------+  
  4. |F|R|R|R| opcode|M| Payload len |    Extended payload length    |  
  5. |I|S|S|S|  (4)  |A|     (7)     |             (16/63)           |  
  6. |N|V|V|V|       |S|             |   (if payload len==126/127)   |  
  7. | |1|2|3|       |K|             |                               |  
  8. +-+-+-+-+-------+-+-------------+ - - - - - - - - - - - - - - - +  
  9. |     Extended payload length continued, if payload len == 127  |  
  10. + - - - - - - - - - - - - - - - +-------------------------------+  
  11. |                               |Masking-key, if MASK set to 1  |  
  12. +-------------------------------+-------------------------------+  
  13. | Masking-key (continued)       |          Payload Data         |  
  14. +-------------------------------- - - - - - - - - - - - - - - - +  
  15. :                     Payload Data continued ...                :  
  16. + - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - +  
  17. |                     Payload Data continued ...                |  
  18. +---------------------------------------------------------------+  


[opcode]  01标识数据内容是 文本,08标识 : 要求远端去关闭当前连接。
[MASK](即原先的RSV4)如果是 1 则数据是被 MASK 的。
[Masking-key] 如果MASK为 1 则有4字节的 Masking-key,用于与传输的数据 [Payload Data] 进行异或运算,4byte(32bit)进行一次运算,不足四位从前往后对应,如只有三位,则只与[Masking-key]的前三位进行运算。

解码 MASK 数据,使用了一个过滤流

Java代码  收藏代码
  1. @Override  
  2. public int read() throws IOException {  
  3.     if (readLength >= length)  
  4.         return -1;  
  5.     int b = 0;  
  6.     synchronized (lock) {  
  7.         if (readLength >= length)  
  8.             return -1;  
  9.         b = super.read();  
  10.         if (isMask) {  
  11.             b ^= maskKey[(int) (readLength % 4)];  
  12.         }  
  13.         readLength++;  
  14.     }  
  15.     return b;  
  16. }  



关于流的关闭:一般情况我们可以直接 使用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/

1
1
分享到:
评论

相关推荐

    IOS WEBSOCKET Objective-C实现

    IOS WEBSOCKET Objective-C实现,有服务器部分和客户端部分,实现非常简单,随后我们还会增加C版本的夸端实现代码;

    websocket案例.zip

    一个用websocket实现的简单聊天室功能. 1. 登录: 未使用数据库,采用以密码"a"为验证 2. 使用websocket协议进行通信 3. websocket服务器端 取到servelt api操作

    基于Netty的WebSocket实现,使用注解快速实现websocket服务.rar

    客户端首先向服务器端去建立连接,这个连接本身就是http协议只是在头信息中包含了一些websocket协议的相关信息,一旦http连接建立之后,服务器端读到这些websocket协议的相关信息就将此协议升级成websocket协议。...

    websocket_websocket客户端_客户端和服务器通信_websocket_websocket客户端

    一个简单的基于websocket实现客户端和服务器端通信的程序

    socket.io实现的websocket

    用node.js开源框架socket.io实现的简单websocket,浏览器与服务器端的长链接,html5支持的新特性,比传统的轮询机制要优越……

    WebSocket实现了消息推送、聊天室、客户端聊天

    服务器端是用C#窗体写的;处理使用了HPSocket,连接数量HPSocket支持多少就支持多少连接数量,目测至少支持1万;网页端主要是以实现功能为主;比较简单;数据传送使用json字符串;文本长读至少支持65536个字符,具体...

    websocket 依赖包,android和java都可以

    嗯 长连接,实现服务器的推送服务器端的websocket长连接;这是好东西,可以实现服务器端的信息向客户端的推送,实现起来也很简单。狠狠的踢开http(hahahahaha!);httpok也有封装这类东西。可以添加到android中也

    WebSocket+java+flash兼容低版本即时刷新

    ava做服务器端,WebSocket连接,Flash实现兼容低版本浏览器,代码简单

    服务器端调用客户端硬件设备解决方案

    流程: 1、客户端使用java 开发 WebSocket服务,以实现调用设备端接口。 2、服务器端程序通过 WebSocket通讯调用客户端本地设备,实现具体操作。 举例:服务器端(为简单验证用,临时搭建)

    一个服务器对多个客户端的MFC Socket编程示例(实现简单的即时通讯功能)

    2、首先运行服务器端工程,选默认的端口1008 3、然后运行客户端工程,选默认的端口1008和默认的服务器地址 4、再运行多个客户端进程 5、如果一切正常,可以每个客户端的消息发送,我们可以在服务端和各个客户端同步...

    Android使用Websocket实现聊天室

    类似于斗鱼TV的聊天室功能,与服务器端人商量后决定用WebSocket来做,但是在这之前我只知道Socket但是听都没有听过WebSocket,但是查看了相关的材料以后发现实现一个聊天室其实是很简单的!下面我们先来看看...

    Python实现同时兼容老版和新版Socket协议的一个简单WebSocket服务器

    最近在做的一个项目中需要使用到HTML5中引入的WebSocket技术,本来以为应该很容易就能搞定,谁知道在真正上手开发了以后才发现有很多麻烦的地方,虽然我们是一个以前端开发和设计见长的团队,而且作为一个二手程序猿...

    WebSocket客户端和服务端实例源码

    轮询,原理简单易懂,就是客户端通过一定的时间间隔以频繁请求的方式向服务器发送请求,来保持客户端和服务器端的数据同步。问题很明显,当客户端以固定频率向服务器端发送请求时,服务器端的数据可能并没有更新,...

    使用Android WebSocket实现即时通讯功能

    WebSocket协议就不细讲了,感兴趣的可以具体查阅资料,简而言之,它就是一个可以建立长连接的全双工(full-duplex)通信协议,允许服务器端主动发送信息给客户端。 Java-WebSocket框架 对于使用websocket协议,Android...

    LabVIEW WebSockets例程源码

    Websockets是一种在单个TCP链接上进行全双工通讯的协议,使得客户端和服务器之间的数据交换变得更加简单,允许服务器主动向客户端推送数据,在Websockets API中,浏览器和服务器只需要完成一次握手,两者之间就直接...

    微信小程序之WebSocket

    当然读者也可以使用socket.io,专为webSocket设计的js语言的服务端,用起来非常简单,它也对不支持webSocket的浏览器提供了兼容(flash或comet实现)。 笔者本人比较喜欢使用tornado,做了几年后台开发,使用最多的...

    WebSocket示例:带有Android客户端和浏览器客户端的Spring Boot WebSocket服务器

    Spring Boot WebSocket和Android客户端Spring Boot WebSocket服务器,具有浏览器客户端和简单的Android客户端。 Android客户端使用来在Android上实现协议 ,以向服务器订阅或发送消息。介绍服务器现在包括三个端点以...

Global site tag (gtag.js) - Google Analytics