BeetleX之WebSocket详解
对于BeetleX
来说编写WebSocket
服务是一件非常简单的事情,当你实现一个Web Api
应用的同时这些API方法也是WebSocket
服务方法。接下来主要讲解如何通过JavaScript调用BeetleX
的WebSocket
服务方法或定义一个适合自己数据格式的WebSocket
服务。
引用组件
通过Nuget引用最新版本的BeetleX.FastHttpApi
或通过下载源码编译组件
实现服务
由于组件支持基于类方法的方式来制定服务,所以定义一个服务非常简单,以下是一个基于Websocket
的hello 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