通讯框架 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-KeyString 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);//转化为base64String 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:websocketrespHeaders.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 maskedif (!hasMask) { //第9为为mask,必须为1 //throw new AioDecodeException("websocket client data must be masked"); } else {//注:有掩码的情况下,掩码占用4个字节,所以在这里headLength + 4headLength += 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 + 2headLength += 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) {//头部长度 + 8headLength += 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 = 00000101array[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文档对比,还是多少能够理解一些的。再次感谢开源贡献者,向所有开源大神致敬。
转载于:https://www.cnblogs.com/panzi/p/7823118.html
通讯框架 t-io 学习——websocket 部分源码解析相关推荐
- Android OpenGL ES 学习(十) – GLSurfaceView 源码解析GL线程以及自定义 EGL
OpenGL 学习教程 Android OpenGL ES 学习(一) – 基本概念 Android OpenGL ES 学习(二) – 图形渲染管线和GLSL Android OpenGL ES 学 ...
- [源码解析] 深度学习分布式训练框架 horovod (10) --- run on spark
[源码解析] 深度学习分布式训练框架 horovod (10) - run on spark 文章目录 [源码解析] 深度学习分布式训练框架 horovod (10) --- run on spark ...
- [源码解析] 深度学习分布式训练框架 horovod (11) --- on spark --- GLOO 方案
[源码解析] 深度学习分布式训练框架 horovod (11) - on spark - GLOO 方案 文章目录 [源码解析] 深度学习分布式训练框架 horovod (11) --- on spa ...
- 客户连接多个服务端_Dubbo源码解析之客户端Consumer
前面我们学习了Dubbo源码解析之服务端Provider.对服务提供方进行思路上的讲解,我们知道以下知识点.本篇文章主要对消费方进行讲解.废话不多说请看下文. 如何将对象方法生成Invoker 如何将 ...
- Unity中的UGUI源码解析之事件系统(9)-输入模块(下)
Unity中的UGUI源码解析之事件系统(9)-输入模块(下) 接上一篇文章, 继续介绍输入模块. StandaloneInputModule类是上一篇文章介绍的抽象类PointerInputModu ...
- 深度学习框架Caffe源码解析
作者:薛云峰(https://github.com/HolidayXue),主要从事视频图像算法的研究, 本文来源微信公众号:深度学习大讲堂. 原文:深度学习框架Caffe源码解析 欢迎技术投稿. ...
- Android 框架学习2:源码分析 EventBus 3.0 如何实现事件总线
Go beyond yourself rather than beyond others. 上篇文章 深入理解 EventBus 3.0 之使用篇 我们了解了 EventBus 的特性以及如何使用,这 ...
- [源码解析] 深度学习流水线并行 PipeDream(6)--- 1F1B策略
[源码解析] 深度学习流水线并行 PipeDream(6)- 1F1B策略 文章目录 [源码解析] 深度学习流水线并行 PipeDream(6)--- 1F1B策略 0x00 摘要 0x01 流水线比 ...
- sparkcore分区_Spark学习:Spark源码和调优简介 Spark Core (二)
本文基于 Spark 2.4.4 版本的源码,试图分析其 Core 模块的部分实现原理,其中如有错误,请指正.为了简化论述,将部分细节放到了源码中作为注释,因此正文中是主要内容. 第一部分内容见: S ...
最新文章
- 【廖雪峰Python学习笔记】字符串与编码
- If-Modified-Since和If-None-Match
- head在c语言中的作用,阅读以下说明和C语言函数,将应填入(n)处的字句写在对应栏内。【说明】 函数sort (NODE *head)的功能 - 赏学吧...
- 每天九点十分开始每半小时一次执行一个cron_趣讲 PowerJob 超强大的调度层,开始表演真正的技术了...
- FFmpeg使用遇到问题记录
- golang基础01
- 工作与生活平衡(2)运动也需要执行力
- 在web3上搭建ecshop网上商城
- 面试精讲之面试考点及大厂真题 - 分布式专栏 08 Redis中有哪些数据结构及底层实现原理
- eclipse maven项目 class类部署不到tomcat下_Springboot介绍以及用Eclipse搭建一个简单的Springboot项目教程
- Android 学习 笔记_07. XML文件解析
- [转]使用RDLC报表(1) -(4)
- G120变频器如何通过BOP-2操作面板或Startdrive复位出厂参数?
- docker 设置阿里云加速器
- win系统安装夜神模拟器、夜神模拟器链接到HbuilderX
- 关于PV、EV、AC、CV、SV、CPI、SPI、BAC、ETC、EAC的解析及计算
- python中oserror_Python:OSError:[Errno 2]没有这样的文件或目录:”
- windows+clion Process finished with exit code -1073741515 (0xC0000135)
- 论文阅读-Whisper语音识别(OpenAI)
- PointNet论文翻译
热门文章
- 中国九章量子计算机诞生!比最快的超算快一百万亿倍
- 中国航发9名劳模工匠变身“高级制造工程师”
- C#(WinForm)的Show()和ShowDialog()方法介绍
- 北京大学启用人脸识别系统:学生“刷脸”入校
- linux系统中使用chattr命令的,chattr命令怎么用
- djc加密数字货币_中国银行原副行长: quot;网络加密币quot;难以成为货币,央行数字货币只能是法定货币的数字化...
- RabbitMQ 消息确认机制 以及 原理解析
- 用户界面改变图片锚点
- php 回复id同过session 任意获取id不通过a,PHP会话ID相同但变量丢失
- xml签名和普通数字签名