本文參考於CSDN博客wxy941011
1、疑問
我們使用第四個博客中的項目。
修改客戶端為:連接成功后循環向服務器發送從1-100的數字。看看服務器會不會正常的接收100次數據。

可是我們發現服務器只接收了兩次數據,為什么和期望的不一樣呢,這就觸發了粘包問題。
2、什么是粘包和拆包
當客戶端不斷向服務器發送數據包時,服務器就可能出現兩個數據包粘在一起的情況。
而和Tcp同為傳輸層的Udp則不會發生粘包和拆包問題。因為Udp是基於報文發送的,從Udp幀結構可以看出,Udp首部采用了16bit來顯示Udp數據報文長度,因此在應用層可以很好的把不同數據報文分開,避免了粘包和拆包問題。
TCP是基於字節流的,雖然應用層和TCP傳輸層之間的數據交互是大小不等的數據塊,但是TCP把這些數據塊僅僅看成一連串無結構的字節流,沒有邊界;另外從TCP的幀結構也可以看出,在TCP的首部沒有表示數據長度的字段。所以只有Tcp會發生粘包、拆包現象
3、粘包拆包的表現形式
客戶端向服務器連續發送兩個數據包,packet1、packet2,服務器收到有三種情況。
-
正常收到了兩個數據包

-
只收到一個數據包,由於Tcp不會出現丟包現象,所以這一個數據包包含了兩個數據包的信息,稱為粘包。

-
接收到兩個錯誤的數據報,發生了粘包+拆包

4、粘包拆包的發生原因
- 要發送的數據大於TCP發送緩沖區剩余空間大小,將會發生拆包。
- 待發送數據大於MSS(最大報文長度),TCP在傳輸前將進行拆包。
- 要發送的數據小於TCP發送緩沖區的大小,TCP將多次寫入緩沖區的數據一次發送出去,將會發生粘包。
- 接收數據端的應用層沒有及時讀取接收緩沖區中的數據,將發生粘包。
等等。
5、粘包拆包解決方法
1、發送端給每個數據包添加包首部,首部中應該至少包含數據包的長度,這樣接收端在接收到數據后,通過讀取包首部的長度字段,便知道每一個數據包的實際長度了。
2、發送端將每個數據包封裝為固定長度(不夠的可以通過補0填充),這樣接收端每次從接收緩沖區中讀取固定長度的數據就自然而然的把每個數據包拆分開來。
3、可以在數據包之間設置邊界,如添加特殊符號,這樣,接收端通過這個邊界就可以將不同的數據包拆分開。
等等。
6、構造包頭包尾方法
class EncodeTool
{
/// <summary>
/// 構造包 包頭+包尾
/// </summary>
public static byte[] EncodePacket(byte[] data)
{
//內存流用完需要close釋放,using自動釋放。
using (MemoryStream ms = new MemoryStream())
{
//bw用於向ms流中寫入內容
using (BinaryWriter bw = new BinaryWriter(ms))
{
//寫入包頭(數據的長度),把字節數組寫入流
bw.Write(data.Length);
//寫入包尾(數據)
bw.Write(data);
//拿到寫入的數據
byte[] packet = new byte[ms.Length];
Buffer.BlockCopy(ms.GetBuffer(), 0, packet, 0, (int) ms.Length); //把內存流中的數據復制到packet中
return packet;
}
}
}
/// <summary>
/// 解析包,從緩沖區里取出一個完成的包
/// </summary>
public static byte[] DecodePacket(ref List<byte> cache)
{
if (cache.Count < 4)
{
return null;
}
//這種構造實例根據byte類型的字節數組進行初始化
//並且實例的容量大小固定為字節數組的長度
using (MemoryStream ms = new MemoryStream(cache.ToArray()))
{
//從流中讀取數據
using (BinaryReader br = new BinaryReader(ms))
{
int length = br.ReadInt32(); //讀取前四個字節(包頭-數據的長度)
//讀取的長度和緩沖區剩余的長度進行比較
int remainLength = (int) ms.Length - (int) ms.Position; //流中剩余的長度
if (length > remainLength)
{
return null;
}
byte[] data = br.ReadBytes(length);
//更新數據緩存
cache.Clear();
cache.AddRange(br.ReadBytes(remainLength)); //把剩余的再填進去???
return data;
}
}
}
}
