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文档对比,还是多少能够理解一些的。再次感谢开源贡献者,向所有开源大神致敬。

  • 相关阅读:
    走进MongoDB(二)
    走进MongoDB(一)
    InnoDB锁与事务模型
    InnoDB引擎架构的主要组件构成
    InnoDB多版本存储引擎简介
    mysql 数据库备份与恢复
    mysql 字符集
    java常见问题笔记
    mysql5.7 Installing MySQL on Microsoft Windows Using a noinstall Zip Archive(mysql解压版安装)
    作业day03吴童
  • 原文地址:https://www.cnblogs.com/panzi/p/7823118.html
Copyright © 2011-2022 走看看