前言

  前端时间看了看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) > ; //得到第8位 10000000>0
//注:这段我不理解什么意思,为什么要右移4位
@SuppressWarnings("unused")
int rsv = (first & 0x70) >>> ;//得到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) >> == ; //用于标识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 += ;
}
//注:第一位为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 == ) {
//需要多占两个字节表示payloadLength。headlength + 2
headLength += ;
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 == ) {
//头部长度 + 8
headLength += ;
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 < || 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, );
} //第二阶段解析
WsRequest websocketPacket = new WsRequest();
//注:设置各种属性值
websocketPacket.setWsEof(fin);
websocketPacket.setWsHasMask(hasMask);
websocketPacket.setWsMask(mask);
websocketPacket.setWsOpcode(opcode);
websocketPacket.setWsBodyLength(payloadLength); if (payloadLength == ) {
return websocketPacket;
}
//注:读取payloadLength长度的body值
byte[] array = ByteBufferUtils.readBytes(buf, payloadLength);
if (hasMask) {
//注:有掩码,所以需要通过掩码解析
for (int i = ; i < array.length; i++) {
//^操作 位值相同为0 ,不同为1
// 00001111 ^ 00001010 = 00000101
array[i] = (byte) (array[i] ^ mask[i % ]);
}
} 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 = ;
for (byte[] part : lastParts) {
System.arraycopy(part, , allByte, offset, part.length);
offset += part.length;
}
System.arraycopy(array, , 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文档对比,还是多少能够理解一些的。再次感谢开源贡献者,向所有开源大神致敬。

通讯框架 t-io 学习——websocket 部分源码解析的更多相关文章

  1. Laravel框架下路由的使用(源码解析)

    本篇文章给大家带来的内容是关于Laravel框架下路由的使用(源码解析),有一定的参考价值,有需要的朋友可以参考一下,希望对你有所帮助. 前言 我的解析文章并非深层次多领域的解析攻略.但是参考着开发文 ...

  2. 深入学习 esp8266 wifimanager源码解析(打造专属自己的web配网)

    QQ技术互动交流群:ESP8266&32 物联网开发 群号622368884,不喜勿喷 单片机菜鸟博哥CSDN 1.前言 废话少说,本篇博文的目的就是深入学习 WifiManager 这个gi ...

  3. Django框架 之 admin管理工具(源码解析)

    浏览目录 单例模式 admin执行流程 admin源码解析 单例模式 单例模式(Singleton Pattern)是一种常用的软件设计模式,该模式的主要目的是确保某一个类只有一个实例存在.当你希望在 ...

  4. spring cloud深入学习(四)-----eureka源码解析、ribbon解析、声明式调用feign

    基本概念 1.Registe 一一服务注册当eureka Client向Eureka Server注册时,Eureka Client提供自身的元数据,比如IP地址.端口.运行状况指标的Uri.主页地址 ...

  5. 集合类学习之ArrayList源码解析

    1.概述 ArrayList是List接口的可变数组的实现.实现了所有可选列表操作,并允许包括 null 在内的所有元素.除了实现 List 接口外,此类还提供一些方法来操作内部用来存储列表的数组的大 ...

  6. Django框架rest_framework中APIView的as_view()源码解析、认证、权限、频率控制

    在上篇我们对Django原生View源码进行了局部解析:https://www.cnblogs.com/dongxixi/p/11130976.html 在前后端分离项目中前面我们也提到了各种认证需要 ...

  7. [源码解析] 深度学习分布式训练框架 horovod (6) --- 后台线程架构

    [源码解析] 深度学习分布式训练框架 horovod (6) --- 后台线程架构 目录 [源码解析] 深度学习分布式训练框架 horovod (6) --- 后台线程架构 0x00 摘要 0x01 ...

  8. [源码解析] 深度学习分布式训练框架 Horovod (1) --- 基础知识

    [源码解析] 深度学习分布式训练框架 Horovod --- (1) 基础知识 目录 [源码解析] 深度学习分布式训练框架 Horovod --- (1) 基础知识 0x00 摘要 0x01 分布式并 ...

  9. [源码解析] 深度学习分布式训练框架 horovod (8) --- on spark

    [源码解析] 深度学习分布式训练框架 horovod (8) --- on spark 目录 [源码解析] 深度学习分布式训练框架 horovod (8) --- on spark 0x00 摘要 0 ...

随机推荐

  1. windown快速安装xgboost

    记录xgboost的快速安装方式,该方式适合pyhton3.5/3.6版本. 系统: win10 64bit python版本:3.6 1. 下载xgboost编译好的whl包 下载路径为:http: ...

  2. ERROR! MySQL server PID file could not be found!的解决方法

    启动MySQL服务 [root@test vhosts]# /etc/init.d/mysqld restart 提示错误: ERROR! MySQL server PID file could no ...

  3. 一步一个坑 - WinDbg调试.NET程序

    引言 第一次用WinDbg来排查问题,花了很多时间踩坑,记录一下希望对后面的同学有些帮助. 客户现场软件出现偶发性的界面卡死现象一直找不出原因,就想着让客户用任务管理器生成了一个dump文件发给我,我 ...

  4. OpenCV中的绘图函数-OpenCV步步精深

    OpenCV 中的绘图函数 画线 首先要为画的线创造出环境,就要生成一个空的黑底图像 img=np.zeros((512,512,3), np.uint8) 这是黑色的底,我们的画布,我把窗口名叫做i ...

  5. 【特效】单选按钮和复选框的美化(只用css)

    表单的默认样式都是比较朴素的,实际页面中往往需要美化他们.这里先说说单选按钮和复选框,有了css3,这个问题就变的好解决了.利用input与label相关联,对label进行美化并使其覆盖掉原本的in ...

  6. MyServer

    //一.设置一个8089端口的本地IP服务器 1 package myserver; import java.io.IOException; import java.net.ServerSocket; ...

  7. Thrift总结(三)Thrift框架

    1.数据类型 基本类型: bool:布尔值,true 或 false,对应 Java 的 boolean byte:8 位有符号整数,对应 Java 的 byte i16:16 位有符号整数,对应 J ...

  8. Extjs6(四)——侧边栏导航根据路由跳转页面

    本文基于ext-6.0.0 之前做的时候这个侧边栏导航是通过tab切换来切换页面的,但是总感觉不太对劲,现在终于发现怎么通过路由跳转了,分享给大家,可能有些不完善的地方,望大家读后可以给些指点.欢迎留 ...

  9. Centos 7.0 execute yum update ——File "/usr/libexec/urlgrabber-ext-down", line 75, in <module> 解决方式

    [打开这个文件:/usr/lib/python2.7/site-packages/urlgrabber/grabber.py找到elif errcode in (42, 55,56)   用  eli ...

  10. 长话短说 之 js的原型和闭包

    原型链:undefined, number, string, boolean 属于简单的值类型,函数.数组.对象.null.new obj()都是引用类型.检测值类型用typeof x 即可,检测引用 ...