BeetleX之Websocket协议分析详解

语言: CN / TW / HK

        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

分享到: