0%

[DotnetCore]Socket程式實作-切割封包

前情提要

為什麼會談到封包切割時做這個議題呢,底層元件基本上會有一個超大buffer去裝載收到的封包,並觸發OnReceive事件,到筆者這邊的底層元件則是最原始的封包,不過該套件也會順便丟出offsetsize,讓筆者知道要怎麼取得這次的有意義的封包。接著可以想一個情境,試想你是一個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}");
//throw;
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
// BaseClient.cs
private readonly MyTcpClient _client;

public BaseClient(IAnalyzeService analyzeService
, IOptions<SocketSettingOption> socketSettingOption
, IConfiguration config
, SocketSettingType socketSettingType)
{
// ...以上省略
if (options.ClientSliceByteFunc != null)
{
// 若為非null則指定該切割方法
_client.SliceByteFunc = options.ClientSliceByteFunc;
}
// ...以下省略
}
// MyTcpClient.cs
public class MyTcpClient : NetCoreServer.TcpClient
{
// ...以上省略

/// <summary>
/// 切割byte內容
/// </summary>
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)
{
// 若為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這三個資料型別後,真的是有一種挖到寶的感覺,尤其是筆者偏向撰寫底層元件為主,為底層元件的邏輯處理更有彈性,且具有擴充性。