TCP粘包處理經驗分享
近日有幸使用Socket實作一Protocol,該Protocol使用的是TCP/IP的傳輸方式,當Protocol實作完成後進行測試發現當Client端每次提交數據的間隔很短或是Server端接收延遲時,TCP/IP就會將多個封包合併成一個後一次拋向Server,當被合併起來的封包大小大於Server接收數據的緩衝區時可能會造成Server服務異常,就這次實作的經驗與之前接觸過的,輕則數據接收異常,重則CPU使用率衝破天際線,系統Crash
什麼是「粘包」?
我們先來看第一張圖,在一切正常美好的情況下數據的傳送與回應會是下圖這樣:
Client向Server發送數據,Server向數據流讀取128Byte的資料,這樣是能完整的拿到封包資料的,當然Server就能完成收完封包後續的流程
接著來看看發生粘包時數據傳送的情況:
當Server發生接收延遲或Client每次提交的數據不大且間隔很短,封包就會被合成一個封包一次送至Server,這時候Server就會數據流讀取128Byte的資料,可想而知的是Server會開始進行封包拆解得到2個完整的封包,然後進行接下來的邏輯流程
接著我們再看看另一張發生粘包的圖:
這次有3個封包被合起來一次提交至Server,然後Server就會數據流讀取128Byte的資料接續進行封包拆解,但這次送過來共有150個Byte,後面32Byte的封包沒有完整接收,這樣就會導致封包接收不完全的問題
為什麼會發生粘包?
1. 發送端需要等緩沖區滿才發送出去,造成粘包 (Nalge算法造成的粘包現象)
日常生活的Nalge算法的概念可以想像成飲料店外送服務;假設飲料店有200個外送員,負責製作飲料的店員每做完1杯飲料就請外送員開車去外送給顧客,但20年前的路比較小條,所以一下子整個馬路就被外送車給塞爆了,結果經過了半天飲料也還在路上 ; 每一杯飲料都要一台車子去外送,這樣的傳輸效率是非常低落的,為了解決這樣的問題,所以發明了Nagle算法
如果將每30分鐘內所有訂單做完後由一台車外送或是30分鐘內接滿20杯飲料就立馬外送,這樣就不用1杯飲料跑一趟了,飲料的獲利也就很快地回到了飲料店裡 :D
※Nagle算法是時代的產物 ->因當時網路頻寬有限,而現在網路頻寬寬裕很多,所以目前的TCP/IP協議Deafult是將Nagle算法關閉
2. 接收方不及時接收緩沖區的包,造成多個包接收
我們該怎麼處理粘包的問題呢?
Google了很久大致上找到2種「解決方案」
1. 接收數據時給定超大緩衝區:
接受數據時給定一個超大緩衝區就是把上面圖片的128Byte加大! 例如我給定409600Byte.....
好處是只需要一次將數據流的資料讀出來,接著交由程式的遞迴方法將大封包拆解成一個個完整的小封包,但這是有缺點的:
- 無法100%肯定緩衝區一定完整接收粘包數據,因為粘包數據會有多大是不可知的,還是有機會大於緩衝取
- 接收端因為給定非常大的緩衝區,付出的代價就是接收端要承載大量資源的耗
2. 給每個封包加上一固定長度的Header,Header內容包含整個完整的封包長度:
這種方式的好處就是假定Header固定16Byte,接收端每次只需要先讀取16Byte,接著先解析這16Byte的標頭來知道接下來該讀取多少數據為一個完整封包,好處即接收端不需要配置非常大的緩衝區、每次讀取的數據都是已知的長度,可以避免緩衝區不夠大的問題,缺點還是有的:在數據接收的效能上比上一種方式慢了一點,但多少則見仁見智這要看使用情境了
接著來看一段簡易的Sample Code
程式採非同步發送與接收,在IAsyncResult 參數會特別帶一個變數來區分現在讀取的數據是否為Header,如果是Header則解析完Header取的完整封包長度,接著再將剩餘的Body數據讀取出來,讀取完之後就能開始進行邏輯處理了
using System;
using System.Collections.Generic;
using System.Linq;
using System.Net;
using System.Net.Sockets;
using System.Text;
namespace Socke.Test
{
class Program
{
static void Main(string[] args)
{
TcpClient tcpClient = new TcpClient();
tcpClient.NoDelay = true; //關閉Nagle演算法
SocketTempMode state = new SocketTempMode();
state.TcpClientObj = tcpClient;
tcpClient.BeginConnect(IPAddress.Parse("192.168.1.103"), 6666, new AsyncCallback(endConnectCallback), state);
}
private static void endConnectCallback(IAsyncResult ar)
{
SocketTempMode state = (SocketTempMode)ar.AsyncState;
TcpClient tcpClient = state.TcpClientObj;
state.RecvData = new PacketModel();
tcpClient.EndConnect(ar);
if (tcpClient.Connected){
NetworkStream networkStream = tcpClient.GetStream();
byte[] tempRecvBuffer = new byte[16];
if (networkStream.CanRead){
//接收Server的回應數據
networkStream.BeginRead(tempRecvBuffer, 0, 16, new AsyncCallback(endReadCallback), new object[] { state, tempRecvBuffer, true });
}
if (networkStream.CanWrite){
//發送數據至Server
byte[] buffer = Encoding.UTF8.GetBytes("你好Server");
networkStream.BeginWrite(buffer, 0, buffer.Length, new AsyncCallback(endWriteCallback), state);
}
}
}
private static void endWriteCallback(IAsyncResult ar)
{
SocketTempMode state = (SocketTempMode)ar.AsyncState;
TcpClient tcpClient = state.TcpClientObj;
NetworkStream networkStream = tcpClient.GetStream();
networkStream.EndWrite(ar);
}
/// <summary>
/// 這邊讀取數據流數據
/// </summary>
/// <param name="ar"></param>
private static void endReadCallback(IAsyncResult ar)
{
object[] objArr = (object[])ar.AsyncState;
SocketTempMode state = (SocketTempMode)objArr[0];
byte[] recvBuffer = (byte[])objArr[1];
bool isHeader = (bool)objArr[2];
TcpClient tcpClient = state.TcpClientObj;
int packetLength = 0;
NetworkStream networkStream = tcpClient.GetStream();
packetLength = networkStream.EndRead(ar);
if (packetLength > 0){
if (isHeader){
#region 假定這邊已完成解析Header數據,Header長度的值為85
state.RecvData.Header.Length = 85;
#endregion
Array.Resize(ref recvBuffer, state.RecvData.Header.Length);
networkStream.BeginRead(recvBuffer, 16, (recvBuffer.Length - 16), new AsyncCallback(endReadCallback), new object[] { state, recvBuffer, false });
}else{
#region 這邊已拿到完整的封包數據(是包含Header的),可以進行邏輯處理了!
#endregion
//拿完Body後接著拿Header
Array.Resize(ref recvBuffer, 16);
networkStream.BeginRead(recvBuffer, 0, 16, new AsyncCallback(endReadCallback), new object[] { state, recvBuffer, true });
}
}
}
}
class SocketTempMode
{
public TcpClient TcpClientObj { get; set; }
public PacketModel RecvData { get; set; }
}
class PacketModel
{
public Header Header { get; set; }
public string Body { get; set; }
}
class Header
{
public int Length { get; set; }
}
}
Reference :
https://tools.ietf.org/html/rfc896
https://msdn.microsoft.com/zh-tw/library/system.net.sockets.tcpclient.nodelay(v=vs.110).aspx
如果您是實作某一項Protocol,Protocol可能已經幫您定義好Header,那就可以直接使用Header的Length屬性
假設是自行規劃的介接功能,則可自行定義Header 來處理粘包的問題
如果您也剛好第一次處理Socket採TCP/IP的方式傳輸資料,希望這一篇可以對您有所幫助 ,如有錯誤也歡迎指導
謝謝 :D
egan2608@gmail.com