前言

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

目录

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

握手过程

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

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

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

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

 @Overridepublic 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<String,String> headers = request.getHeaders();//获取 Sec_WebSocket_KeyString 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";//sha1byte[] keyArray = SHA1Util.SHA1(Sec_WebSocket_Key_Magic);//base64String acceptKey = BASE64Util.byteArrayToBase64(keyArray);//构建响应HttpResponse httpResponse = new HttpResponse(request);//响应状态码为 101 Switching protocolshttpResponse.setStatus(HttpResponseStatus.C101);Map<String,String> respHeaders = new HashMap<>(3);//Connection:UpgraderespHeaders.put(HttpConst.ResponseHeaderKey.Connection,HttpConst.ResponseHeaderValue.Connection.Upgrade);//Upgrade:WebSocketrespHeaders.put(HttpConst.ResponseHeaderKey.Upgrade,"WebSocket");//Sec_WebSocket_Accept:base64respHeaders.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 协议帧介绍

上图就是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();//11111111boolean hasMask = (second & 0xff) >> 7 == 1;if (!hasMask){}else{//mask 占 4 个字节headLength += 4;}//01111111int payloadLength = second & 0x7f;byte[] mask = null;if (payloadLength == 126){//payloadLength 长度为2headLength += 2;if(readableLength < headLength){return null;}payloadLength = ByteBufferUtils.readUB2WithBigEdian(buf);}else if (payloadLength == 127){//payloadLength 长度为8headLength += 8;if(readableLength < headLength){return null;}payloadLength = (int) buf.getLong();}

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

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

解码完毕,封装到 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

tio-websocket-server 源码浅析相关推荐

  1. 通讯框架 t-io 学习——websocket 部分源码解析

    前言 前端时间看了看t-io的websocket部分源码,于是抽时间看了看websocket的握手和他的通讯机制.本篇只是简单记录一下websocket握手部分. WebSocket握手 好多人都用过 ...

  2. harbor登录验证_Harbor 源码浅析

    Harbor 源码浅析​www.qikqiak.com Harbor 是一个CNCF基金会托管的开源的可信的云原生docker registry项目,可以用于存储.签名.扫描镜像内容,Harbor 通 ...

  3. 【flink】Flink 1.12.2 源码浅析 : yarn-per-job模式解析 TaskMasger 启动

    1.概述 转载:Flink 1.12.2 源码浅析 : yarn-per-job模式解析 [四] 上一篇: [flink]Flink 1.12.2 源码浅析 : yarn-per-job模式解析 Jo ...

  4. 【flink】Flink 1.12.2 源码浅析 : yarn-per-job模式解析 JobMasger启动 YarnJobClusterEntrypoint

    1.概述 转载:Flink 1.12.2 源码浅析 : yarn-per-job模式解析 [三] 上一章:[flink]Flink 1.12.2 源码浅析 : yarn-per-job模式解析 yar ...

  5. 【flink】Flink 1.12.2 源码浅析 : yarn-per-job模式解析 yarn 提交过程解析

    1.概述 转载:Flink 1.12.2 源码浅析 : yarn-per-job模式解析 [二] 请大家看原文去. 接上文Flink 1.12.2 源码分析 : yarn-per-job模式浅析 [一 ...

  6. Koa洋葱圈模型源码浅析(`await next()`为什么能够形成洋葱圈模型?)

    Koa洋葱圈模型源码浅析 写在前面 什么是中间件? 为什么要使用中间件? auth中间件源码 Koa源码浅析 我们先来康一张gif图片 我们的探索流程图 listen函数 callback函数 cre ...

  7. System源码浅析- initializeSystemClass(initProperties)

    文章目录 前情提要 System 描述 initializeSystemClass 描述 initProperties JVM_InitProperties 前情提要 在前面已经介绍过 System与 ...

  8. tio-http-server 源码浅析(二)Http请求的处理HttpRequestHandler

    前言 在上一篇<tio-http-server 源码浅析(一)HttpRequestDecoder的实现>简单分析了HttpRequestDecoder的源码,并且已经得到了HttpReq ...

  9. libevent源码浅析: http库

     libevent自带了一个http库,用它可以很简单的实现一个http服务器,本文非常简单地分析之.evhttp evhttp库有几个主要的结构体,它们之间的联系非常龌龊: 其中,结构体even ...

  10. Appium Server源码分析之作为Bootstrap客户端

    Appium Server拥有两个主要的功能: 它是个http服务器,它专门接收从客户端通过基于http的REST协议发送过来的命令 他是bootstrap客户端:它接收到客户端的命令后,需要想办法把 ...

最新文章

  1. 2015年四级计算机网络课程,2015年计算机四级网络工程师考试辅导:网络互连技术...
  2. ArcGIS 10.7拆分多部件要素(Multipart Features)至单部件要素的两种方法
  3. spring基于注释的配置_基于注释的Spring MVC Web应用程序入门
  4. c语言 字符串map,C语言实现BitMap
  5. ubuntu 修改默认用户名_Tars框架在Ubuntu上的部署小结
  6. 与虚拟机连接出现ora-12514错误解决方法
  7. python下载教程-Python 如何入门?附Python教程下载
  8. Java word转pdf方法
  9. Redis运行时突然不能读取数据了
  10. Android开发----MaterialDesign设计下material-dialogs用法
  11. android webview加载H5链接时 没有加载权限弹框的问题
  12. 日期转换和日历的使用方法
  13. 学习笔记-DQPSK系统的调制与解调
  14. 计算机程序手工编织,丝绸编程秒杀计算机 《锦绣纪》致敬最强大脑
  15. andorid6.0 mtk6737平台 ctp调试方法
  16. java poi 操作word遇到的问题
  17. 利用 android手机DLNA功能,实现手机视频无线播放到电脑、电视
  18. MAC 合并多个jpg文件为PDF
  19. [C]循环语句(5/7)→ 用do……while语句循环
  20. 特么的. 最终把 amobbs 的站长阿莫(莫进明)给回骂了一顿.

热门文章

  1. mysql rollback作用_mysql rollback 原理以及若干疑问
  2. 024空格沙悟净死亡
  3. SAS中的informat和input
  4. 状态机-简单、重要、高可应用性的思想
  5. 利用Bettercap实现密码的嗅探
  6. Spring Security介绍(4)
  7. IIS中应用程序池和站点通过命令启停方法
  8. Baxter robot 碰撞检测相关问题集合(自己学习用)
  9. 【Unity3D插件】KGFMapSystem插件分享《快速制作小地图插件》
  10. 【行业动态】特斯拉线圈如何撼动一个行业:秒开智能锁