zoukankan      html  css  js  c++  java
  • 通讯框架 t-io 学习——websocket 部分源码解析

    前言

      前端时间看了看t-io的websocket部分源码,于是抽时间看了看websocket的握手和他的通讯机制。本篇只是简单记录一下websocket握手部分。

    WebSocket握手

      好多人都用过websocket,不过有的都是在框架之上,只知道连接某个地址,然后调用js API就可以使用websocket了。但是通过阅读t-io的源码才稍微有点明白,服务端到底做了什么。将t-io的websocket demo运行起来之后,我们看一下请求。

      可以看到,请求头部分:

      Connection:Upgrade 固定

      Upgrade:websocket 固定

      Host:为websocket请求地址

      Sec-WebSocket-Version:13,websocket协议版本号

      Sec-WebSocket-Key:发送给服务端需要校验的key,是一个Base64 encode的值,这个是浏览器随机生成的。那么服务端如果响应的话,需要做如下操作:将 Key 追加固定字符串 :“258EAFA5-E914-47DA-95CA-C5AB0DC85B11”,然后进行SHA-1加密,在转化为base64.

      服务端响应如下:

      Status Code:101 Switching Protocols

      sec-websocket-accept:为上文中转化为base64的串。

      upgrade:升级为websocket协议

      握手成功,可以进行通讯。

    握手源码

      代码来源:tio/websocket/server/WsServerAioHandler.java

    public static HttpResponse updateWebSocketProtocol(HttpRequest request, ChannelContext channelContext) {
         //首先获取请求头部信息
            Map<String, String> headers = request.getHeaders();
         //获取Sec-WebSocket-Key
            String Sec_WebSocket_Key = headers.get(HttpConst.RequestHeaderKey.Sec_WebSocket_Key);
    
         //如果key是空的话,肯定不会握手成功
            if (StringUtils.isNotBlank(Sec_WebSocket_Key)) {
           //追加固定串
                String Sec_WebSocket_Key_Magic = Sec_WebSocket_Key + "258EAFA5-E914-47DA-95CA-C5AB0DC85B11";
           //SHA-1加密
                byte[] key_array = SHA1Util.SHA1(Sec_WebSocket_Key_Magic);
           //转化为base64
                String acceptKey = BASE64Util.byteArrayToBase64(key_array);
           //构造响应体
                HttpResponse httpResponse = new HttpResponse(request, null);
           //响应状态码 101 Switching Protocols
                httpResponse.setStatus(HttpResponseStatus.C101);
    
                Map<String, String> respHeaders = new HashMap<>();
           //Connection:upgrade
                respHeaders.put(HttpConst.ResponseHeaderKey.Connection, HttpConst.ResponseHeaderValue.Connection.Upgrade);
           //Upgrade:websocket
                respHeaders.put(HttpConst.ResponseHeaderKey.Upgrade, "WebSocket");
           //Sec-WebSocket-Accept:生成的base64串
                respHeaders.put(HttpConst.ResponseHeaderKey.Sec_WebSocket_Accept, acceptKey);
           //设置响应头
                httpResponse.setHeaders(respHeaders);
           //返回响应信息 握手成功
                return httpResponse;
            }
            return null;
        }

    WebSocket 数据帧解析

      注:博客部分内容来源于:https://github.com/zhangkaitao/websocket-protocol/wiki/5.%E6%95%B0%E6%8D%AE%E5%B8%A7  有兴趣的同学可以直接读本链接内容。

      相信很多人从其他博客中也看过这个图,当然啦,这个图是官方出品的权威数据帧格式图。

      其实我第一眼看的时候确实看不懂,不过没关系,一点一点的看。

      FIN:1bit,指示这个消息是否为最后片段,1是,0否。如果不是最后片段,则服务端需要将所有消息接受完并组装成一个完整的消息才可以。(t-io中目前只支持FIN=1)

      RSV123每个长度为1bit,目前就都是固定 0。

      opcode:4bit,数据操作类型。

    • %x0 代表一个继续帧
    • %x1 代表一个文本帧
    • %x2 代表一个二进制帧
    • %x3-7 保留用于未来的非控制帧
    • %x8 代表连接关闭
    • %x9 代表ping
    • %xA 代表pong
    • %xB-F 保留用于未来的控制帧

      MASK:1bit,是否掩码,1掩码,0非掩码。从客户端发送到服务端的这个值必须为1,否则服务端不接受。服务端返回到客户端的这个值必须为 0.

      Payload len:负载数据的长度,7bit。由于7bit只能存储0-127,所以为了能够表示准确的长度,在这个值为0-125区间的时候,payload length的长度就是该值。当 值为126的时候,后边两个字节(16位)的值表示长度。当值为127的时候,后边8字节(64位)的值表示长度。

      Mask key:掩码,0或4个bit。值取决于MASK是否为1.在有掩码的情况下,数据就要根据掩码来解析。否则不用解析。解析规则为:每个字节的值与掩码的索引(字节索引值对4取模)异或运算。(array[i] = array[i] ^ mask[i % 4])

      其实说实话我也没弄得非常懂,但是基本了解了以上这些知识之后,我们就可以读懂源码的意思了。

    数据帧解析源码

      代码来源:tio/websocket/common/WsServerDecoder.java

      代码中的注释为我自己的理解所添加的注释,不一定正确。(由于源码中有部分注释,我的注释添加“注”字以作区分)

    public static WsRequest decode(ByteBuffer buf, ChannelContext channelContext) throws AioDecodeException {
    WsSessionContext imSessionContext = (WsSessionContext) channelContext.getAttribute();
    List<byte[]> lastParts = imSessionContext.getLastParts();
    
    //第一阶段解析
    int initPosition = buf.position();
    int readableLength = buf.limit() - initPosition;
    
    int headLength = WsPacket.MINIMUM_HEADER_LENGTH;
        
    if (readableLength < headLength) {
    return null;
    }
    //注:读取第一个字节 这里以 0x81举例 它的二进制为:10000001
    byte first = buf.get();
    //注:这个 0xff还是很有意思的,当byte类型想转为int类型的时候,比如: int res = byteValue & 0xff;
    //int b = first & 0xFF; //转换成32位
    // 0x80(127) 10000000
    // 0x81(128) 10000001
    // 此行代码说实话,我是用了很长的时间才理解,说来惭愧,刚开始连 & 操作符啥意思都不清楚。
    // 按位与运算符“&”是双目运算符。其功能是参与运算的两数各对应的二进位相与。只要对应的二个二进位都为1时,结果位就为1。
    // 参与运算的两个数均以补码出现。
    // 0x80 & 0x81 10000000
    boolean fin = (first & 0x80) > 0; //得到第8位 10000000>0
    //注:这段我不理解什么意思,为什么要右移4位
    @SuppressWarnings("unused")
    int rsv = (first & 0x70) >>> 4;//得到5、6、7 为01110000 然后右移四位为00000111
    //注:获取操作码
    //0x0f 00001111 (按位与操作,前四位都为0,那么操作结果就是opCode的值)
    byte opCodeByte = (byte) (first & 0x0F);//后四位为opCode 00001111
    //注:转换OpCode
    Opcode opcode = Opcode.valueOf(opCodeByte);
    if (opcode == Opcode.CLOSE) {
    //Aio.remove(channelContext, "收到opcode:" + opcode);
    //return null;
    }
    if (!fin) {
        log.error("{} 暂时不支持fin为false的请求", channelContext);
        Aio.remove(channelContext, "暂时不支持fin为false的请求");
        return null;
    //下面这段代码不要删除,以后若支持fin,则需要的
    
    //            if (lastParts == null) {
    
    //                lastParts = new ArrayList<>();
    
    //                imSessionContext.setLastParts(lastParts);
    
    //            }
    
    } else {
        imSessionContext.setLastParts(null);
    }
    
    //注:开始解析第二个字节。8-16位,第八位为mask掩码值1或者0,后7位为payload length
    byte second = buf.get(); //向后读取一个字节
    //注:又是 & 操作。 0xff:11111111
    // 11111111 & 10000001 = 10000001  向右移动七位,只剩下第一位的值 00000001
    //所以该操作过后就知道第一位为 0 或者 1 ,得知 payload Data是否经过掩码处理
    boolean hasMask = (second & 0xFF) >> 7 == 1; //用于标识PayloadData是否经过掩码处理。如果是1,Masking-key域的数据即是掩码密钥,用于解码PayloadData。客户端发出的数据帧需要进行掩码处理,所以此位是1。
    
    
    // Client data must be masked
    
    if (!hasMask) { //第9为为mask,必须为1
    //throw new AioDecodeException("websocket client data must be masked");
    } else {
        //注:有掩码的情况下,掩码占用4个字节,所以在这里headLength + 4
        headLength += 4;
    }
    //注:第一位为mask位置,后7位为payload length
    //0x7f : 01111111
    //&操作过后得到payload的值
    //读取后7位  Payload legth,如果<126则payloadLength
    int payloadLength = second & 0x7F;
    byte[] mask = null;
    //注:如果payloadLength = 126,那么说明这个值不是真正的payloadLength,后边两个字节才表示真正的length
    //为126读2个字节,后两个字节为payloadLength
    if (payloadLength == 126) {
        //需要多占两个字节表示payloadLength。headlength + 2
        headLength += 2;
    if (readableLength < headLength) {
        return null;
    }
    
    payloadLength = ByteBufferUtils.readUB2WithBigEdian(buf);
      log.info("{} payloadLengthFlag: 126,payloadLength {}", channelContext, payloadLength);
    
    }
    //注:如果payloadLength = 127,则后 8个字节 64位长度的值表示payloadLength
    //127读8个字节,后8个字节为payloadLength
    else if (payloadLength == 127) {
        //头部长度 + 8
        headLength += 8;
    if (readableLength < headLength) {
        return null;
    }
    //注:我猜测getLong方法就读取buf中下一位长整数,即64位的payloadLength(first ,second都已经读取完)
    //|first|second|payloadLength|
    payloadLength = (int) buf.getLong();
      log.info("{} payloadLengthFlag: 127,payloadLength {}", channelContext, payloadLength);
    }
    
    if (payloadLength < 0 || payloadLength > WsPacket.MAX_BODY_LENGTH) {
    throw new AioDecodeException("body length(" + payloadLength + ") is not right");
    }
    
    if (readableLength < headLength + payloadLength) {
      return null;
    }
    
    if (hasMask) {
        //注:有掩码,掩码长度为4个字节,读取掩码的值
        mask = ByteBufferUtils.readBytes(buf, 4);
    }
    
    //第二阶段解析
    WsRequest websocketPacket = new WsRequest();
    //注:设置各种属性值
    websocketPacket.setWsEof(fin);
    websocketPacket.setWsHasMask(hasMask);
    websocketPacket.setWsMask(mask);
    websocketPacket.setWsOpcode(opcode);
    websocketPacket.setWsBodyLength(payloadLength);
    
    if (payloadLength == 0) {
        return websocketPacket;
    }
    //注:读取payloadLength长度的body值
    byte[] array = ByteBufferUtils.readBytes(buf, payloadLength);
    if (hasMask) {
        //注:有掩码,所以需要通过掩码解析
        for (int i = 0; i < array.length; i++) {
            //^操作 位值相同为0 ,不同为1
            // 00001111 ^ 00001010 = 00000101
            array[i] = (byte) (array[i] ^ mask[i % 4]);
        }
    }
    
    if (!fin) {
    //lastParts.add(array);
    
        log.error("payloadLength {}, lastParts size {}, array length {}", payloadLength, lastParts.size(), array.length);
        return websocketPacket;
    } else {
        int allLength = array.length;
        if (lastParts != null) {
        for (byte[] part : lastParts) {
          allLength += part.length;
      }
    byte[] allByte = new byte[allLength];
    
    int offset = 0;
    for (byte[] part : lastParts) {
        System.arraycopy(part, 0, allByte, offset, part.length);
        offset += part.length;
    }
    System.arraycopy(array, 0, allByte, offset, array.length);
        array = allByte;
    }
    
    websocketPacket.setBody(array);
    
    if (opcode == Opcode.BINARY) {
    
    } else {
        try {
            String text = null;
            text = new String(array, WsPacket.CHARSET_NAME);
            websocketPacket.setWsBodyText(text);
        } catch (UnsupportedEncodingException e) {
            log.error(e.toString(), e);
            }
        }
    }
        return websocketPacket;
    }

    总结

      由于本人也是小菜鸟,能看懂的就那么多了,很多代码都读不懂。哎,大神就是大神啊,编码都精准到每一个bit上了。不过通过阅读源码和websocket文档对比,还是多少能够理解一些的。再次感谢开源贡献者,向所有开源大神致敬。

  • 相关阅读:
    noi 2011 noi嘉年华 动态规划
    最小乘积生成树
    noi 2009 二叉查找树 动态规划
    noi 2010 超级钢琴 划分树
    noi 2011 阿狸的打字机 AC自动机
    noi 2009 变换序列 贪心
    poj 3659 Cell Phone Network 动态规划
    noi 2010 航空管制 贪心
    IDEA14下配置SVN
    在SpringMVC框架下建立Web项目时web.xml到底该写些什么呢?
  • 原文地址:https://www.cnblogs.com/panzi/p/7823118.html
Copyright © 2011-2022 走看看