zoukankan      html  css  js  c++  java
  • webSocket服务器端的简单实现

    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. byte[] sixteenByte = new byte[16];
    11. System.arraycopy(HelpUtil.intTo4Byte(k1), 0, sixteenByte, 0, 4);
    12. System.arraycopy(HelpUtil.intTo4Byte(k2), 0, sixteenByte, 4, 4);
    13. System.arraycopy(key3, 0, sixteenByte, 8, 8);
    14. 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. String code = requestHeaders.get("Sec-WebSocket-Key") + GUID;
    4. byte[] bts = MessageDigest.getInstance("SHA1").digest(code.getBytes(HEADER_CODE));
    5. code = HelpUtil.getBASE64(bts);
    6. 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. if (dataLen == HAS_EXTEND_DATA) {// read next 16 bit
    3. bt = in.read();
    4. b2 = in.read();
    5. fram.setDateLength(HelpUtil.toShort((byte) bt, (byte) b2));
    6. } else if (dataLen == HAS_EXTEND_DATA_CONTINUE) {// read next 32 bit
    7. byte[] bts = new byte[8];
    8. if (in.read(bts) != 8){
    9. //fram.setOpcode
    10. throw new RuntimeException(
    11. "reader Payload-Len-Extended-Continued data length < 64 bit");
    12. }
    13. fram.setDateLength(HelpUtil.toLong(bts));
    14. } else {
    15. fram.setDateLength(dataLen);
    16. }



    [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/
    或者后面的地址下载源码

  • 相关阅读:
    [色彩校正] Gamma Correction
    需要齐次坐标的原因之二 所有的变换运算(平移、旋转、缩放)都可以用矩阵乘法来搞定
    需要齐次坐标的原因之一
    数据库连接类
    简单的数组排序
    OfficePage封装代码
    新闻管理数据模板
    最新page页码生成控件代码
    新闻管理cs页面
    快速收录新域名网站
  • 原文地址:https://www.cnblogs.com/sky7034/p/2203446.html
Copyright © 2011-2022 走看看