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.1 Host: server.example.com Upgrade: websocket Connection: Upgrade Sec-WebSocket-Key: dGhlIHNhbXBsZSBub25jZQ== Origin: http://example.com Sec-WebSocket-Protocol: chat, superchat Sec-WebSocket-Version: 13
如果接受升级,服务端响应相关内容即可
HTTP/1.1 101 Switching Protocols Upgrade: websocket Connection: Upgrade Sec-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) { ArraySegmentdata = 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
如果你想了解某方面的知识或文章可以把想法发送到