tio-websocket-server 源码浅析

前言

    之前写过一篇,当时是刚接触tio的时候,那时候记得读解码类的第一行代码 boolean fin = (first & 0x80) > 0; 时,这一行代码纠结了我一下午,因为当时误把一个字节当成了一位。后来又去看了看 & |等操作,才渐渐缓过神来,每次多看一遍都会有不一样的收货。其实本来想先写websocket篇的,不过在握手部分涉及到tio-http部分的编解码,所以后来索性先去研究tio-http源码了。

目录

  1. 握手过程
  2. 协议帧介绍
  3. 解码解析
  4. 编码解析

握手过程

    之所以去分析tio-http的源码,是因为websocket(下文简称ws)的握手过程其实是一个http请求的处理过程。简单来讲,客户端(浏览器)发送一个特殊的http请求告诉服务端,这是一个ws的握手请求,麻烦处理一下。服务端经过验证,然后将请求结果返回。握手成功就可以进行ws通信了。那么怎么界定为客户端发起的是ws请求呢,如下图:

tio-websocket-server 源码浅析_第1张图片

    图中被红框圈起的部分。首先 ConnectionUpgrade. 升级为什么呢?Upgrade:WebSocket。另外还要带上版本号,图中版本为13,外加一个base64编码的Sec-WebSocket-Key.另外,发起请求不是以http(s)://开头,而是以 ws(s)://开头。接下来我们看根据源码去详细了解服务器的握手处理过程。

    首先,同样tio的老套路,请求进来,数据会流向继承自ServerAioHandlerWsServerAioHandler。

进入decode解码方法,因为是第一次连接,需要走握手流程。握手阶段就要用 HttpRequestDecoder 进行解析。解析成功之后,服务端对请求进行升级。升级成功之后发送响应消息给客户端完成握手过程。其中 IWsMsgHanderhandshake方法可以进行业务级别的握手干预。

 @Override
  public Packet decode(ByteBuffer buffer, ChannelContext channelContext) throws AioDecodeException {
        WsSessionContext sessionContext = (WsSessionContext) channelContext.getAttribute();

        //没有握手
        if (!sessionContext.isHandshaked()){
            //调用Http解码方法
            HttpRequest request = HttpRequestDecoder.decode(buffer,channelContext);
            if (request == null){
                return null;
            }
            //升级 websocket 协议
            HttpResponse httpResponse = WsProtocol.updateToWebSocket(request,channelContext);
            if (httpResponse == null){
                throw new AioDecodeException("http协议升级到websocket协议失败");
            }

            //解析成为握手包
            WsRequest wsRequestPacket = new WsRequest();
            wsRequestPacket.setHandShake(true);
            return wsRequestPacket;
        }
        //其他代码略
    }

    其中,WsProtocol.updateToWebSocket 方法完成了具体的升级过程。

 /**
     * 升级websocket协议
     * */
    public static HttpResponse updateToWebSocket(HttpRequest request, ChannelContext channelContext){
        //获取请求头部信息
        Map headers = request.getHeaders();
        //获取 Sec_WebSocket_Key
        String Sec_WebSocket_Key = headers.get(HttpConst.RequestHeaderKey.Sec_WebSocket_Key);
        if (StringUtils.isNotBlank(Sec_WebSocket_Key)){
            //要添加一个 固定的值进行 SHA1编码,然后Base64编码
            String Sec_WebSocket_Key_Magic = Sec_WebSocket_Key + "258EAFA5-E914-47DA-95CA-C5AB0DC85B11";
            //sha1
            byte[] keyArray = SHA1Util.SHA1(Sec_WebSocket_Key_Magic);
            //base64
            String acceptKey = BASE64Util.byteArrayToBase64(keyArray);
            //构建响应
            HttpResponse httpResponse = new HttpResponse(request);
            //响应状态码为 101 Switching protocols
            httpResponse.setStatus(HttpResponseStatus.C101);

            Map respHeaders = new HashMap<>(3);
            //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;
    }

    最后在调用handler方法时,进行业务级别的判断。如果是握手包,发送一个handshake属性为true的包即可。因为,在调用encode方法时,会进行判断,如果为握手包的返回,则直接返回上文中已经被升级的响应。

 HttpResponse handshakeResp = sessionContext.getHandshakeResponsePacket();
 return HttpResponseEncoder.encode(handshakeResp ,groupContext,channelContext,false);

    那么到此为止,握手阶段结束。

ws 协议帧介绍

    tio-websocket-server 源码浅析_第2张图片

    上图就是ws的报文传输格式。

    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])

解码解析

   读取第一个字节。8位,包含了1位FIN,3位RSV,4位Opcode。

    first & 10000000 = 1(0) 0000000 所以,通过和 0x80 的 & 操作就得到第一位的值。

    234位的RSV暂且不管。

    同样的道理,first & 0x0f 得到后四位的值就是opcode的值。每个值代表的意思参考上文。

//读取第一个字节
byte first = buf.get();
//128 10000000 & frist 取第一位
boolean fin = (first & 0x80) > 0;
//01110000
@SuppressWarnings("unused")
int rsv = (first & 0x70) >>> 4;
//00001111 取后四位
byte opCodeByte = (byte)(first & 0x0f);

    接下来读取第二个字节。8位,包含了1位MASK,7位PayloadLengthPayLoadLength的值的意思参考上文。

    同样,利用 & 操作获取MASK值和PayLoadLength的值。转化为代码如下:

        byte second = buf.get();
        //11111111
        boolean hasMask = (second & 0xff) >> 7 == 1;
        if (!hasMask){

        }else{
            //mask 占 4 个字节
            headLength += 4;
        }
        //01111111
        int payloadLength = second & 0x7f;

        byte[] mask = null;
        if (payloadLength == 126){
            //payloadLength 长度为2
            headLength += 2;
            if(readableLength < headLength){
                return null;
            }
            payloadLength = ByteBufferUtils.readUB2WithBigEdian(buf);
        }else if (payloadLength == 127){
            //payloadLength 长度为8
            headLength += 8;
            if(readableLength < headLength){
                return null;
            }
            payloadLength = (int) buf.getLong();
        }

    然后读取4位的Masking-Key,在之后就是具体的内容了。

 if (hasMask){
   //读取mask key
   mask = ByteBufferUtils.readBytes(buf,4);
 }
 //读取payloadlength长度的内容
 byte[] array = ByteBufferUtils.readBytes(buf,payloadLength);
 //掩码解码
 if (hasMask){
    for (int i=0; i

     解码完毕,封装到 WsRequestPacket中,返回。

编码解析

    先构造第一字节。我们都知道第一位为FIN位,设置为1,中间三位为0,后四位为opcode。

     byte header0 = (byte)(0x8f & (response.getWsOpcode().getCode() | 0xf0));

     上述代码的意思很明确,例如opcode为 2,计算过程如下;

      00000010 | 11110000 = 11110010

      10001111 & 11110010 = 10000010 (第一位FIN 为1,后四位为Opcode 为 0010)

       然后根据bodyLength创建ByteBuffer。

 byte header0 = (byte)(0x8f & (response.getWsOpcode().getCode() | 0xf0));
        ByteBuffer buf = null;
        if (bodyLength < 126){
            buf = ByteBuffer.allocate(2 + bodyLength);
            buf.put(header0);
            buf.put((byte)bodyLength);
        }else if(bodyLength < (1 << 16) - 1){
            buf = ByteBuffer.allocate(2 + 2 + bodyLength);
            buf.put(header0);
            buf.put((byte)126);
            ByteBufferUtils.writeUB2WithBigEdian(buf,bodyLength);
        }else{
            buf = ByteBuffer.allocate(2 + 8 + bodyLength);
            buf.put(header0);
            buf.put((byte)127);
            //这行代码问过作者,他说需要确认一下,后来我的理解是这里由于bodyLength是int类型的只占有 32 位,
            //所以 8 位中的前四位 给 0。(我不知道是不是这个解释,需要确认)
            buf.put(new byte[]{0,0,0,0});
            ByteBufferUtils.writeUB4WithBigEdian(buf,bodyLength);
        }
        if (body != null && body.length >0){
            buf.put(body);
        }
        return buf;

总结

    本文根据ws的协议帧图,简单的分析了 tio-websocket-server 的编码解码过程。其实深入到数据的每个字节甚至每一位上还是挺有意思的。

    

转载于:https://my.oschina.net/panzi1/blog/1615399

你可能感兴趣的:(网络)