BeetleX之WebSocket詳解

smark發表於2019-02-27

對於BeetleX來說編寫WebSocket服務是一件非常簡單的事情,當你實現一個Web Api應用的同時這些API方法也是WebSocket服務方法。接下來主要講解如何通過JavaScript呼叫BeetleXWebSocket服務方法或定義一個適合自己資料格式的WebSocket服務。

引用元件

通過Nuget引用最新版本的BeetleX.FastHttpApi或通過下載原始碼編譯元件

 

實現服務

由於元件支援基於類方法的方式來制定服務,所以定義一個服務非常簡單,以下是一個基於Websockethello world服務:

    [BeetleX.FastHttpApi.Controller]
    class Program
    {
        private static BeetleX.FastHttpApi.HttpApiServer mApiServer;

        static void Main(string[] args)
        {
            mApiServer = new BeetleX.FastHttpApi.HttpApiServer();
            mApiServer.Debug();
            mApiServer.Register(typeof(Program).Assembly);
            mApiServer.Open();
            Console.Write(mApiServer.BaseServer);
            Console.Read();
        }

        public string Hello(string name)
        {
            return $"{name} {DateTime.Now}";
        }
    }

JavaScript呼叫

由於元件定義一個呼叫規範,針對上面的方法呼叫有著一定的格式要求,大體的json格式如下:

{
     url:''
     params:{{"name":"value"},{"name1":"value1"}}
}
  • url

    描述請求的方法路徑,針對以上示例對應的路徑是'/Hello',元件預設大小寫不敏感。

  • params

    用於描述方法對應的引數列表

針對以上示例方法呼叫json如下:

{
      url: '/Hello', 
      params: { name: 'test' }
}

自己處理資料

元件的服務要求指定的請求格式和對應的響應格式,這樣對於一些使用者來說有些限制,如果不希望元件提供的格式而是自己制定資料方式的話可以繫結WebSocket資料接收事件,當事件繫結後元件會把接收的資料直接路由給事件來處理,不會再按原有的方式來解析處理。繫結事件如下:

  mApiServer.WebSocketReceive = (o, e) =>
            {
                Console.WriteLine(e.Frame.Body);
                var freame = e.CreateFrame($"{DateTime.Now}" + e.Frame.Body.ToString());
                e.Response(freame);
            };

不過這裡的處理方式還是以文字為主,只是文字的格式解釋和輸出格式更多的進行控制。

處理非文字資料

預設情況都以文字的方式來處理資料,實際上websocket是支援二進位制流的;如果希望在元件的基礎上自己處理二進位制流資料需要制定一個資料解析器,解析器的介面規範如下:

    public interface IDataFrameSerializer
    {
        object FrameDeserialize(DataFrame data, PipeStream stream);//反序列化物件方法

        ArraySegment<byte> FrameSerialize(DataFrame packet, object body);//序列化方法

        void FrameRecovery(byte[] buffer);//Buffer回收方法

    }

元件預設的解析器實現如下:

        public virtual object FrameDeserialize(DataFrame data, PipeStream stream)
        {
            return stream.ReadString((int)data.Length);
        }

        private System.Collections.Concurrent.ConcurrentQueue<byte[]> mBuffers = new System.Collections.Concurrent.ConcurrentQueue<byte[]>();

        public virtual ArraySegment<byte> FrameSerialize(DataFrame data, object body)
        {
            byte[] result;
            if (!mBuffers.TryDequeue(out result))
            {
                result = new byte[this.Options.MaxBodyLength];
            }
            string value;
            if (body is string)
                value = (string)body;
            else
                value = Newtonsoft.Json.JsonConvert.SerializeObject(body);
            int length = Options.Encoding.GetBytes(value, 0, value.Length, result, 0);
            return new ArraySegment<byte>(result, 0, length);
        }

        public virtual void FrameRecovery(byte[] buffer)
        {
            mBuffers.Enqueue(buffer);
        }

在制定完成資料解析器後把它設定到FrameSerializer屬性上即可

HttpApiServer.FrameSerializer= new CustomFrameSerializer();

連線驗證

當通過瀏覽器訪問websocket服務的時候,在連線建立過程存在一個握手通訊包,這個通訊包一般都帶有使用者的Cookie,通過這個Cookie即可以驗證連線的來源,從而確定連線的有效性。元件提供一個WebSocketConnect事件來擴充套件這個驗證機制,事件制定如下:

            mApiServer.WebSocketConnect = (o, e) => {
                //e.Request.Header
                //e.Request.Cookies
                e.Cancel = true;
            };

使用者可以根據實際情況的需要判斷對應的資料來確定是否取消當前WebSocket連線

基於流解釋WebSocket協議

網上有很多WebSocket協議解釋程式碼,之前也寫過一份,不過都是針對byte[]進行分析,以下程式碼是基於Stream的方式來分析協議,通過stream操作起來會更簡潔易懂

        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 && (ulong)stream.Length >= this.Length)
                {
                    if (this.IsMask)
                        ReadMask(stream);
                    Body = this.DataPacketSerializer.FrameDeserialize(this, stream);
                    mLoadStep = DataPacketLoadStep.Completed;
                }
            }
            return mLoadStep;
        }

 

void IDataResponse.Write(PipeStream stream)
        {
            try
            {
                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;
                        if (len > 125 && len <= UInt16.MaxValue)
                        {
                            header[1] |= (byte)126;
                            stream.Write(header, 0, 2);
                            stream.Write((UInt16)len);
                        }
                        else if (len > UInt16.MaxValue)
                        {
                            header[1] |= (byte)127;
                            stream.Write(header, 0, 2);
                            stream.Write((ulong)len);
                        }
                        else
                        {
                            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);
                }
            }
            finally
            {
                this.DataPacketSerializer = null;
                this.Body = null;
            }
        }

 

如果你對這程式碼有興趣,最直接的方法是檢視BeetleX的程式碼https://github.com/IKende/FastHttpApi

 

相關文章