BeetleX之Websocket协议分析详解

你离开我真会死。 提交于 2020-10-28 07:31:06

        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)        {            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




本文分享自微信公众号 - dotNET跨平台(opendotnet)。
如有侵权,请联系 support@oschina.cn 删除。
本文参与“OSC源创计划”,欢迎正在阅读的你也加入,一起分享。

易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!