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
dGhlIHNhbXBsZSBub25jZQ== :
Origin: http://example.com
chat, superchat :
13 :
如果接受升级,服务端响应相关内容即可
101 Switching Protocols
Upgrade: websocket
Connection: Upgrade
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
开源跨平台通讯框架(支持TLS)
轻松实现高性能:tcp、http、websocket、redis、rpc和网关等服务应用
https://beetlex.io
如果你想了解某方面的知识或文章可以把想法发送到
henryfan@msn.com|admin@beetlex.io
本文分享自微信公众号 - dotNET跨平台(opendotnet)。
如有侵权,请联系 support@oschina.cn 删除。
本文参与“OSC源创计划”,欢迎正在阅读的你也加入,一起分享。
来源:oschina
链接:https://my.oschina.net/u/3772973/blog/4676010