Websocket应用协议已经普及多年了,它是HTTP1.1的内部升级协议,主要作用是补充HTTP1.1无法灵活地主动推送消息给客户端的缺陷问题。在这里主要介绍一下使用组件如何扩展一个完整的Websocket协议。
协议介绍
Websocket并不复杂,但协议文档内容还是很全面的,以下是协议原文
https://tools.ietf.org/html/rfc6455。其实一个简单的图可以看出Websocket协议结构。
在这里主要介绍组件是如何实现的就不详细介绍内容了。
存储顺序
在协议中有一个地方需要关注存储顺序,那就是消息长度描述。不同语言平台对于基础值类型的存储顺序都不一样分别是:大端和小端。这个协议使用的是大端存储顺序,但.NET则是使用小端存储顺序;所以使用组件解Weboskcet协议前要更改一下流读写的存储顺序。
IServer.Options.LittleEndian = false;
组件可以通过配置来统一更改网络流针对大小端读写配置,应用中也可以默认用小端读出来后再移位转换也是可以。
分析状态
虽然Websocket已经有协议描述,但在分析过程中还是需要一些状态来处理。在TCP流中无法知道当前buffer里的情况,有可能不到一个消息帧,或存在多个消息帧;更有可能当前流的尾部可能只两个字节内容的playload len 127的情况;为了应对存在不同状态的网络流,在分析协议过程需要制定各种状态,以便于下一次网络数据到来直接跑到相关状态分配处理。
public enum DataPacketLoadStep
{//量开始状态None,//分析完头部信息Header,//分析完成内容长度信息Length,//内容在校检状态Mask,//分析完成Completed
}
握手处理
其实Websocket设计作为http 1.1的一个升级协议,所以在连接开始是通过http协议作为应用握手确认;确认后双方即可随意发送基于websocket协议描述的帧数据。
当服务端收到HTTP请求存在Upgrade头部信息的内容是Websocket的情况说明客户端要求升级到Websocket协议。
GET /chat HTTP/1.1Host: server.example.comUpgrade: websocketConnection: UpgradeSec-WebSocket-Key: dGhlIHNhbXBsZSBub25jZQ==Origin: http://example.comSec-WebSocket-Protocol: chat, superchatSec-WebSocket-Version: 13
如果接受升级,服务端响应相关内容即可
HTTP/1.1 101 Switching ProtocolsUpgrade: websocketConnection: UpgradeSec-WebSocket-Accept: s3pPLMBiTxaQ9kYGzzhZRbK+xOo=
组件FastHttpApi对应代码
https://github.com/IKende/FastHttpApi/blob/master/src/HttpApiServer.cs#L691
数据帧解包
WebSocket的数据帧解释比起http协议麻烦些,毕竟http协议都是换行拆分即可;而WebSocket则需要涉及到位信息处理。
internal DataPacketLoadStep Read(PipeStream stream){if (mLoadStep == DataPacketLoadStep.None){//当前流是否满足解释头两个字节需求if (stream.Length >= 2){byte value = (byte)stream.ReadByte();this.FIN = (value & CHECK_B8) > 0;this.RSV1 = (value & CHECK_B7) > 0;this.RSV2 = (value & CHECK_B6) > 0;this.RSV3 = (value & CHECK_B5) > 0;this.Type = (DataPacketType)(byte)(value & 0xF);value = (byte)stream.ReadByte();this.IsMask = (value & CHECK_B8) > 0;this.PayloadLen = (byte)(value & 0x7F);mLoadStep = DataPacketLoadStep.Header;}}if (mLoadStep == DataPacketLoadStep.Header){//是否满足解释帧长度需求if (this.PayloadLen == 127){if (stream.Length >= 8){Length = stream.ReadUInt64();mLoadStep = DataPacketLoadStep.Length;}}else if (this.PayloadLen == 126){if (stream.Length >= 2){Length = stream.ReadUInt16();mLoadStep = DataPacketLoadStep.Length;}}else{this.Length = this.PayloadLen;mLoadStep = DataPacketLoadStep.Length;}}if (mLoadStep == DataPacketLoadStep.Length){if (IsMask){if (stream.Length >= 4){this.MaskKey = new byte[4];stream.Read(this.MaskKey, 0, 4);mLoadStep = DataPacketLoadStep.Mask;}}else{mLoadStep = DataPacketLoadStep.Mask;}}if (mLoadStep == DataPacketLoadStep.Mask){//根据不同长度判断可读开度内容if (this.Length == 0){mLoadStep = DataPacketLoadStep.Completed;}else{if ((ulong)stream.Length >= this.Length){if (this.IsMask)ReadMask(stream);Body = this.DataPacketSerializer.FrameDeserialize(this, stream);mLoadStep = DataPacketLoadStep.Completed;}}}return mLoadStep;}
看完以上代码相信会有人问,写这么复杂干什么吗,几个字节的长度都需要判断吗?一次接收的信息不可能几个字节都没有。出现这情况的主要原因是当某端推送大量的消息,这些消息经过不同的网络环境和MTU限制后,可能出现帧的头部内容被拆到两个接收缓冲区中,所以在处理上需要完全考虑这种情况。
数据帧封包
void IDataResponse.Write(PipeStream stream)
{byte[] header = new byte[2];if (FIN)header[0] |= CHECK_B8;if (RSV1)header[0] |= CHECK_B7;if (RSV2)header[0] |= CHECK_B6;if (RSV3)header[0] |= CHECK_B5;header[0] |= (byte)Type;if (Body != null){ArraySegment<byte> data = this.DataPacketSerializer.FrameSerialize(this, Body);try{if (MaskKey == null || MaskKey.Length != 4)this.IsMask = false;//是否有掩码if (this.IsMask){header[1] |= CHECK_B8;int offset = data.Offset;for (int i = offset; i < data.Count; i++){data.Array[i] = (byte)(data.Array[i] ^ MaskKey[(i - offset) % 4]);}}int len = data.Count;//大于135小于unit16长度的消息头写入if (len > 125 && len <= UInt16.MaxValue){header[1] |= (byte)126;stream.Write(header, 0, 2);stream.Write((UInt16)len);}//大于unit16长度头写入else if (len > UInt16.MaxValue){header[1] |= (byte)127;stream.Write(header, 0, 2);stream.Write((ulong)len);}else{//小于126长度写入header[1] |= (byte)data.Count;stream.Write(header, 0, 2);}//写入掩码if (IsMask)stream.Write(MaskKey, 0, 4);//写入消息内容stream.Write(data.Array, data.Offset, data.Count);}finally{this.DataPacketSerializer.FrameRecovery(data.Array);}}else{//没有消息体,只写入消息头stream.Write(header, 0, 2);}
}
封包就简单了,除了判断长度写入不同的头信息外其他都是直接写入。以上代码可以查看
https://github.com/IKende/FastHttpApi/blob/master/src/WebSockets/DataFrame.cs
【BeetleX通讯框架代码详解】
BeetleX
开源跨平台通讯框架(支持TLS)
轻松实现高性能:tcp、http、websocket、redis、rpc和网关等服务应用
https://beetlex.io
如果你想了解某方面的知识或文章可以把想法发送到
henryfan@msn.com|admin@beetlex.io