tio-websocket-server 源码浅析
前言
之前写过一篇,当时是刚接触tio的时候,那时候记得读解码类的第一行代码 boolean fin = (first & 0x80) > 0; 时,这一行代码纠结了我一下午,因为当时误把一个字节当成了一位。后来又去看了看 & |等操作,才渐渐缓过神来,每次多看一遍都会有不一样的收货。其实本来想先写websocket篇的,不过在握手部分涉及到tio-http部分的编解码,所以后来索性先去研究tio-http源码了。
目录
- 握手过程
- 协议帧介绍
- 解码解析
- 编码解析
握手过程
之所以去分析tio-http的源码,是因为websocket(下文简称ws)的握手过程其实是一个http请求的处理过程。简单来讲,客户端(浏览器)发送一个特殊的http请求告诉服务端,这是一个ws的握手请求,麻烦处理一下。服务端经过验证,然后将请求结果返回。握手成功就可以进行ws通信了。那么怎么界定为客户端发起的是ws请求呢,如下图:
图中被红框圈起的部分。首先 Connection为Upgrade. 升级为什么呢?Upgrade:WebSocket。另外还要带上版本号,图中版本为13,外加一个base64编码的Sec-WebSocket-Key.另外,发起请求不是以http(s)://开头,而是以 ws(s)://开头。接下来我们看根据源码去详细了解服务器的握手处理过程。
首先,同样tio的老套路,请求进来,数据会流向继承自ServerAioHandler的WsServerAioHandler。
进入decode解码方法,因为是第一次连接,需要走握手流程。握手阶段就要用 HttpRequestDecoder 进行解析。解析成功之后,服务端对请求进行升级。升级成功之后发送响应消息给客户端完成握手过程。其中 IWsMsgHander的handshake方法可以进行业务级别的握手干预。
@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位PayloadLength。PayLoadLength的值的意思参考上文。
同样,利用 & 操作获取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 源码浅析相关推荐
- 通讯框架 t-io 学习——websocket 部分源码解析
前言 前端时间看了看t-io的websocket部分源码,于是抽时间看了看websocket的握手和他的通讯机制.本篇只是简单记录一下websocket握手部分. WebSocket握手 好多人都用过 ...
- harbor登录验证_Harbor 源码浅析
Harbor 源码浅析www.qikqiak.com Harbor 是一个CNCF基金会托管的开源的可信的云原生docker registry项目,可以用于存储.签名.扫描镜像内容,Harbor 通 ...
- 【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 ...
- 【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 ...
- 【flink】Flink 1.12.2 源码浅析 : yarn-per-job模式解析 yarn 提交过程解析
1.概述 转载:Flink 1.12.2 源码浅析 : yarn-per-job模式解析 [二] 请大家看原文去. 接上文Flink 1.12.2 源码分析 : yarn-per-job模式浅析 [一 ...
- Koa洋葱圈模型源码浅析(`await next()`为什么能够形成洋葱圈模型?)
Koa洋葱圈模型源码浅析 写在前面 什么是中间件? 为什么要使用中间件? auth中间件源码 Koa源码浅析 我们先来康一张gif图片 我们的探索流程图 listen函数 callback函数 cre ...
- System源码浅析- initializeSystemClass(initProperties)
文章目录 前情提要 System 描述 initializeSystemClass 描述 initProperties JVM_InitProperties 前情提要 在前面已经介绍过 System与 ...
- tio-http-server 源码浅析(二)Http请求的处理HttpRequestHandler
前言 在上一篇<tio-http-server 源码浅析(一)HttpRequestDecoder的实现>简单分析了HttpRequestDecoder的源码,并且已经得到了HttpReq ...
- libevent源码浅析: http库
libevent自带了一个http库,用它可以很简单的实现一个http服务器,本文非常简单地分析之.evhttp evhttp库有几个主要的结构体,它们之间的联系非常龌龊: 其中,结构体even ...
- Appium Server源码分析之作为Bootstrap客户端
Appium Server拥有两个主要的功能: 它是个http服务器,它专门接收从客户端通过基于http的REST协议发送过来的命令 他是bootstrap客户端:它接收到客户端的命令后,需要想办法把 ...
最新文章
- 2015年四级计算机网络课程,2015年计算机四级网络工程师考试辅导:网络互连技术...
- ArcGIS 10.7拆分多部件要素(Multipart Features)至单部件要素的两种方法
- spring基于注释的配置_基于注释的Spring MVC Web应用程序入门
- c语言 字符串map,C语言实现BitMap
- ubuntu 修改默认用户名_Tars框架在Ubuntu上的部署小结
- 与虚拟机连接出现ora-12514错误解决方法
- python下载教程-Python 如何入门?附Python教程下载
- Java word转pdf方法
- Redis运行时突然不能读取数据了
- Android开发----MaterialDesign设计下material-dialogs用法
- android webview加载H5链接时 没有加载权限弹框的问题
- 日期转换和日历的使用方法
- 学习笔记-DQPSK系统的调制与解调
- 计算机程序手工编织,丝绸编程秒杀计算机 《锦绣纪》致敬最强大脑
- andorid6.0 mtk6737平台 ctp调试方法
- java poi 操作word遇到的问题
- 利用 android手机DLNA功能,实现手机视频无线播放到电脑、电视
- MAC 合并多个jpg文件为PDF
- [C]循环语句(5/7)→ 用do……while语句循环
- 特么的. 最终把 amobbs 的站长阿莫(莫进明)给回骂了一顿.