前情提要
為什麼會談到封包切割時做這個議題呢,底層元件基本上會有一個超大buffer去裝載收到的封包,並觸發OnReceive
事件,到筆者這邊的底層元件則是最原始的封包,不過該套件也會順便丟出offset
及size
,讓筆者知道要怎麼取得這次的有意義的封包。接著可以想一個情境,試想你是一個Server,會有多個Client跟你進行連線,連線必會發生封包傳輸,底層元件使用一個超大buffer裝載封包,試想同時收到多個Client端的情求,如何辨別有效的每一段封包,這時封包切割邏輯就顯得重要了。
內容
以筆者剛出社會時也實作過Socket
相關程式,基本上都會將封包設計成知道該byte
內容為多少,表示該封包已結束,可以進行處理了,這只是一種方式,有千千萬萬種的設計。筆者目前的公司會設計封包開頭幾個byte
位置的內容即整段封包的長度
,取得這幾個byte
位置的內容,讓筆者有一個切割封包的依據。
封包切割方法
筆者就照這樣設計一個封包切割的方法,但是要注意一點是筆者撰寫的底層元件是要給各個需要Socket
功能Client端使用,基本上不一應該寫死封包切割方法,這時Func
就派上用場了,若外部有傳入Func
實作方法就使用該方法,若無則使用底層元件已寫好的切割方法。先貼上底層元件的封包切割方法﹔
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42
| private Tuple<List<byte>, List<byte[]>> CommSlicingByteFunc(List<byte> source) { var result = new Tuple<List<byte>, List<byte[]>>(new List<byte>(), new List<byte[]>()); try { if (source.Count > 4) { var totalLength = int.Parse(CoreHelper.Encoding.GetString(ByteExtensions.GetSubByte(source.ToArray(), 0, 4))); _logger.Info($"[Total Length]: {totalLength}"); while (totalLength <= source.Count) { var subBytes = new byte[totalLength]; System.Buffer.BlockCopy(source.ToArray(), 0, subBytes, 0, totalLength); _logger.Info($"[Sub byte]: {CoreHelper.Encoding.GetString(subBytes)}"); var newQueue = ByteExtensions.GetSubByte(source.ToArray(), totalLength, source.Count - totalLength); _logger.Info($"[newQueue]: {CoreHelper.Encoding.GetString(newQueue)}"); source = new List<byte>(); for (int i = 0; i < newQueue.Length; i++) { source.Add(newQueue[i]); } result.Item2.Add(subBytes); if (newQueue.Length > 4) { totalLength = int.Parse(CoreHelper.Encoding.GetString(ByteExtensions.GetSubByte(newQueue, 0, 4))); } } } } catch (Exception ex) { _logger.Error($"[CommSlicingByteFunc]: {ex}"); result.Item2.Add(source.ToArray()); source = new List<byte>(); } foreach (var item in source) { result.Item1.Add(item); } return result; }
|
簡單解釋一下上述程式碼,我會先把前四碼封包內容解讀出來當作是切割的依據,切割完的封包加入至回傳封包資料集中,剩下的封包中繼續使用該方法切割到不能切割為止,若切割有錯誤當作byte內容有誤,整包原封不動往外拋出。
套用外部傳入之封包切割方法
筆者當初設計時有些參數是從外部傳入,類似像Socket
設定,IP、Port
等等資訊,將包裝成IOptions
項目,筆者於SocketServiceRegistration
中設定,這樣任何物件中只要注入對應的IOptions
項目即可使用外部傳入之參數,筆者這邊使用的物件為
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23
| public class SocketSettingOption { public string MainSocketSetting { get; set; } public string SubSocketSetting { get; set; } public string HubUrlSetting { get; set; } public Func<List<byte>, Tuple<List<byte>, List<Byte[]>>> ClientSliceByteFunc { get; set; } public Func<List<byte>, Tuple<List<byte>, List<Byte[]>>> ServerSliceByteFunc { get; set; }
public bool DisconnectAfterClientReceive { get; set; } = false;
public string AppId { get; set; } public string AppName { get; set; } public string ServiceName { get; set; } public bool MonitorService { get; set; } = true;
#region ---ForLogging--- public Func<byte[], Type> GetHeaderResponseModelType { get; set; } = (data) => { return typeof(HeaderResponseModel); }; public string ConnectionStringKey { get; set; } = "FMTR"; #endregion }
|
使用方式為建構式中注入,並判斷是否為null,若為非null
則使用外部串入之切割方法
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37
| private readonly MyTcpClient _client;
public BaseClient(IAnalyzeService analyzeService , IOptions<SocketSettingOption> socketSettingOption , IConfiguration config , SocketSettingType socketSettingType) { if (options.ClientSliceByteFunc != null) { _client.SliceByteFunc = options.ClientSliceByteFunc; } }
public class MyTcpClient : NetCoreServer.TcpClient {
public Func<List<byte>, Tuple<List<byte>, List<Byte[]>>> SliceByteFunc; public MyTcpClient(IPAddress address, int port , IOptions<SocketSettingOption> options) : base(address, port) { if (SliceByteFunc == null) { SliceByteFunc = ByteExtensions.CommSlicingByteFunc; } _socketSetting = options.Value; } }
|
Client端使用方式
Client端使用方式參考,若自己切割方法與底層元件不符則自己宣告切割邏輯。
1 2 3 4 5 6 7 8 9 10 11
| services.AddMySocket(option => { option.ClientSliceByteFunc = ((source) => { var result = new Tuple<List<byte>, List<byte[]>>(new List<byte>(), new List<byte[]>()); result.Item2.Add(source.ToArray()); return result; }); });
|
結論
筆者自從會使用Func、Event、Action
這三個資料型別後,真的是有一種挖到寶的感覺,尤其是筆者偏向撰寫底層元件為主,為底層元件的邏輯處理更有彈性,且具有擴充性。