問題產生
一個完整的業務可能會被TCP拆分成多個包進行發送,也有可能把多個小的包封裝成一個大的數據包發送,這個就是TCP的拆包和封包問題。
下面可以看一張圖,是客戶端向服務端發送包:
1. 第一種情況,Data1和Data2都分開發送到了Server端,沒有產生粘包和拆包的情況。
2. 第二種情況,Data1和Data2數據粘在了一起,打成了一個大的包發送到Server端,這個情況就是粘包。
3. 第三種情況,Data2被分離成Data2_1和Data2_2,並且Data2_1在Data1之前到達了服務端,這種情況就產生了拆包。
由於網絡的複雜性,可能數據會被分離成N多個複雜的拆包/粘包的情況,所以在做TCP服務器的時候就需要首先解決拆包/粘包的問題。
TCP粘包和拆包產生的原因
1. 應用程序寫入數據的字節大小大於套接字發送緩衝區的大小
2. 進行MSS大小的TCP分段。MSS是最大報文段長度的縮寫。MSS是TCP報文段中的數據字段的最大長度。數據字段加上TCP首部纔等於整個的TCP報文段。所以MSS並不是TCP報文段的最大長度,而是:MSS=TCP報文段長度-TCP首部長度
3. 以太網的payload大於MTU進行IP分片。MTU指:一種通信協議的某一層上面所能通過的最大數據包大小。如果IP層有一個數據包要傳,而且數據的長度比鏈路層的MTU大,那麼IP層就會進行分片,把數據包分成若干片,讓每一片都不超過MTU。注意,IP分片可以發生在原始發送端主機上,也可以發生在中間路由器上。
TCP粘包和拆包的解決策略
1. 消息定長。例如100字節。
2. 在包尾部增加特殊字符進行分割。
3. 將消息分爲消息頭和消息尾。(最常用)
先把我看了大神的代碼後,寫的代碼放出來,方便以後不懂得時候回來看:
服務器端(在Unity中實現):
using UnityEngine;
using System.Collections;
//引入庫
using System.Net;
using System.Net.Sockets;
using System.Text;
using System.Threading;
using System;
using LuaInterface;
using System.IO;
public class TcpServer : MonoBehaviour
{
//以下預設都是私有的成員
Socket serverSocket; //伺服器端socket
Socket clientSocket; //客戶端socket
IPEndPoint ipEnd; //偵聽埠
string recvStr; //接收的字串
string sendStr; //傳送的字串
byte[] recvData = new byte[1024]; //接收的資料,必須為位元組
byte[] sendData = new byte[1024]; //傳送的資料,必須為位元組
int recvLen; //接收的資料長度
Thread connectThread; //連線執行緒
private int contentSize = 0;
//初始化
void InitSocket()
{
//定義偵聽埠,偵聽任何IP
ipEnd = new IPEndPoint(IPAddress.Any, 5566);
//定義套接字型別,在主執行緒中定義
serverSocket = new Socket(AddressFamily.InterNetwork, SocketType.Stream, ProtocolType.Tcp);
//連線
serverSocket.Bind(ipEnd);
//開始偵聽,最大10個連線
serverSocket.Listen(10);
//開啟一個執行緒連線,必須的,否則主執行緒卡死
connectThread = new Thread(new ThreadStart(SocketReceive));
connectThread.Start();
}
//連線
void SocketConnet()
{
if (clientSocket != null)
clientSocket.Close();
//控制檯輸出偵聽狀態
print("Waiting for a client");
//一旦接受連線,建立一個客戶端
clientSocket = serverSocket.Accept();
//獲取客戶端的IP和埠
IPEndPoint ipEndClient = (IPEndPoint)clientSocket.RemoteEndPoint;
//輸出客戶端的IP和埠
print("Connect with " + ipEndClient.Address.ToString() + ":" + ipEndClient.Port.ToString());
//連線成功則傳送資料
sendStr = "Welcome to my server";
SocketSend(sendStr);
}
void SocketSend(string sendStr)
{
//清空傳送快取
sendData = new byte[1024];
//資料型別轉換
sendData = Encoding.ASCII.GetBytes(sendStr);
//傳送
clientSocket.Send(sendData, sendData.Length, SocketFlags.None);
}
private NetworkStream stream;
//伺服器接收
void SocketReceive()
{
//連線
SocketConnet();
//進入接收迴圈
while (true)
{
//對data清零
recvData = new byte[1024];
//獲取收到的資料的長度
recvLen = clientSocket.Receive(recvData);
contentSize += recvLen;
while (true)
{
//判斷接收到的字符長度,如果連包頭的長度都不夠就不進行了
if (contentSize <= 4)
{
return;
}
//得到包體的長度
int receiveCount = BitConverter.ToInt32(recvData, 0);
//接收的數據不到一個完整的數據
if (contentSize - 4 < receiveCount)
{
return;
}
//去除包頭後的數據
string receiveStr = Encoding.UTF8.GetString(recvData, 4, receiveCount);
Debug.Log(receiveStr);
//將剩餘數據存儲到緩衝區
Array.Copy(recvData, 4 + receiveCount, recvData, 0, contentSize - 4 - receiveCount);
contentSize = contentSize - 4 - receiveCount;
}
////Debug.Log("獲取資源的長度: " + recvLen);
//////如果收到的資料長度為0,則重連並進入下一個迴圈
//if (recvLen == 0)
//{
// SocketConnet();
// continue;
//}
////輸出接收到的資料
//recvStr = Encoding.ASCII.GetString(recvData, 4, recvLen-4);
//Debug.Log("接收到的所有數據: " + recvStr);
}
}
//連線關閉
void SocketQuit()
{
//先關閉客戶端
if (clientSocket != null)
clientSocket.Close();
//再關閉執行緒
if (connectThread != null)
{
connectThread.Interrupt();
connectThread.Abort();
}
//關閉服務器
serverSocket.Close();
print("diconnect");
}
// Use this for initialization
void Start()
{
InitSocket(); //在這裡初始化server
}
void OnApplicationQuit()
{
SocketQuit();
}
}
客戶端:
using UnityEngine;
using System.Collections;
//引入庫
using System.Net;
using System.Net.Sockets;
using System.Text;
using System.Threading;
using System.IO;
using System;
public class TcpClient : MonoBehaviour
{
string editString = "hello wolrd"; //編輯框文字
Socket serverSocket; //伺服器端socket
IPAddress ip; //主機ip
IPEndPoint ipEnd;
string recvStr; //接收的字串
string sendStr; //傳送的字串
byte[] recvData = new byte[1024]; //接收的資料,必須為位元組
byte[] sendData = new byte[1024]; //傳送的資料,必須為位元組
int recvLen; //接收的資料長度
Thread connectThread; //連線執行緒
//初始化
void InitSocket()
{
//定義伺服器的IP和埠,埠與伺服器對應
ip = IPAddress.Parse("127.0.0.1"); //可以是區域網或網際網路ip,此處是本機
ipEnd = new IPEndPoint(ip, 5566);
//開啟一個執行緒連線,必須的,否則主執行緒卡死
connectThread = new Thread(new ThreadStart(SocketReceive));
connectThread.Start();
}
void SocketConnet()
{
if (serverSocket != null)
serverSocket.Close();
//定義套接字型別,必須在子執行緒中定義
serverSocket = new Socket(AddressFamily.InterNetwork, SocketType.Stream, ProtocolType.Tcp);
print("ready to connect");
//連線
serverSocket.Connect(ipEnd);
//輸出初次連線收到的字串
recvLen = serverSocket.Receive(recvData);
recvStr = Encoding.ASCII.GetString(recvData, 0, recvLen);
print(recvStr);
}
void SocketSend(string sendStr)
{
//清空傳送快取
sendData = new byte[1024];
//資料型別轉換
sendData = Encoding.ASCII.GetBytes(sendStr);
sendData = BuildDataPackage(sendData);
Debug.Log("輸出的字節總長度:" + sendData.Length);
Debug.Log("字節體的長度 : " + BitConverter.ToInt32(sendData, 0));
Debug.Log("字節體: " + Encoding.ASCII.GetString(sendData ,4, sendData.Length-4));
//傳送
serverSocket.Send(sendData, sendData.Length, SocketFlags.None);
}
/// <summary>
/// 構建數據
/// </summary>
/// <param name="data"></param>
/// <param name="dataLength"></param>
public byte[] BuildDataPackage(byte[] data)
{
using (MemoryStream ms = new MemoryStream())
{
using (BinaryWriter bw = new BinaryWriter(ms))
{
bw.Write(data.Length);
bw.Write(data);
byte[] byteArray = new byte[(int)ms.Length];
Buffer.BlockCopy(ms.GetBuffer(), 0, byteArray, 0, (int)ms.Length);
return byteArray;
}
}
}
void SocketReceive()
{
SocketConnet();
//不斷接收伺服器發來的資料
while (true)
{
recvData = new byte[1024];
recvLen = serverSocket.Receive(recvData);
if (recvLen == 0)
{
SocketConnet();
continue;
}
recvStr = Encoding.ASCII.GetString(recvData, 0, recvLen);
print(recvStr);
}
}
void SocketQuit()
{
//關閉執行緒
if (connectThread != null)
{
connectThread.Interrupt();
connectThread.Abort();
}
//最後關閉伺服器
if (serverSocket != null)
serverSocket.Close();
print("diconnect");
}
// Use this for initialization
void Start()
{
InitSocket();
}
void OnGUI()
{
editString = GUI.TextField(new Rect(10, 10, 100, 20), editString);
if (GUI.Button(new Rect(10, 30, 60, 20), "send"))
{
SocketSend(editString);
}
}
// Update is called once per frame
void Update()
{
}
//程式退出則關閉連線
void OnApplicationQuit()
{
SocketQuit();
}
}
-----------------------下面是大神的腳本用來加深學習--------------------------------
class Program
{
static void Main(string[] args)
{
///客戶端代碼
Socket clientSocket = new Socket(AddressFamily.InterNetwork, SocketType.Stream, ProtocolType.Tcp);
IPEndPoint ipEndPoint = new IPEndPoint(IPAddress.Parse("192.168.1.102"), 3344);
clientSocket.Connect(ipEndPoint);
byte[] receiveBuffer = new byte[1024];
int count = clientSocket.Receive(receiveBuffer);
string msg = Encoding.UTF8.GetString(receiveBuffer, 0, count);
Console.WriteLine("接收到服務端的消息:" + msg);
for (int i = 0; i < 100; i++) ///客戶端啓動向服務端發送250條數據
{
clientSocket.Send(SendMsg(i.ToString()));
}
Console.ReadKey();
}
/// <summary>
/// 構造發送數據
/// </summary>
/// <param name="msg"></param>
/// <returns></returns>
public static byte[] SendMsg(string msg)
{
int length = msg.Length;
//構造表頭數據,固定4個字節的長度,表示內容的長度
byte[] headerBytes = BitConverter.GetBytes(length);
//構造內容
byte[] bodyBytes = Encoding.UTF8.GetBytes(msg);
byte[] tempBytes = new byte[headerBytes.Length + bodyBytes.Length];
///拷貝到同一個byte[]數組中,發送出去..
Buffer.BlockCopy(headerBytes, 0, tempBytes, 0, headerBytes.Length);
Buffer.BlockCopy(bodyBytes, 0, tempBytes, headerBytes.Length, bodyBytes.Length);
return tempBytes;
}
}
/////服務端代碼
class Program
{
static void Main(string[] args)
{
///服務端代碼
Socket serverSocket = new Socket(AddressFamily.InterNetwork, SocketType.Stream, ProtocolType.Tcp);
IPAddress ip = IPAddress.Parse("192.168.1.102");
IPEndPoint ipEndPoint = new IPEndPoint(ip, 3344);
serverSocket.Bind(ipEndPoint);
serverSocket.Listen(0);//開啓監聽
Console.WriteLine("服務器啓動");
//開始異步接收客戶端
serverSocket.BeginAccept(AcceptAsyncCallBack, serverSocket);
Console.ReadKey();
}
/// <summary>
/// 異步等待客戶端回調方法
/// </summary>
/// <param name="ar"></param>
private static void AcceptAsyncCallBack(IAsyncResult ar)
{
Socket serverSocket = ar.AsyncState as Socket;//傳遞過來的參數
Socket clientSokcet = serverSocket.EndAccept(ar); //一個客戶端連接過來了
string msg = ":Hello client! 你好......";
byte[] dataBytes = Encoding.UTF8.GetBytes(msg); //網絡連接收發數據,只能發送byte[] 字節數組
clientSokcet.Send(dataBytes);
///messageHandle.DataBuffer緩存區,messageHandle.ContentSize(緩存區中已經存在的內容長度開始存)
/// messageHandle.remainSize 緩存區中剩餘可以存儲的空間
clientSokcet.BeginReceive(messageHandle.DataBuffer, messageHandle.ContentSize, messageHandle.remainSize, SocketFlags.None, ReceiveCallBack, clientSokcet); //開始異步接收數據
serverSocket.BeginAccept(AcceptAsyncCallBack, serverSocket);//循環等待客戶端接收....
}
static MessageHandle messageHandle = new MessageHandle();
/// <summary>
/// 異步接收數據回調方法
/// </summary>
/// <param name="ar"></param>
private static void ReceiveCallBack(IAsyncResult ar)
{
Socket clientSocket = null;
try
{
clientSocket = ar.AsyncState as Socket;
int count = clientSocket.EndReceive(ar); //接收到的數據量
if (count == 0) ///說明客戶端已經已經斷開連接了
{
if (clientSocket != null)
{
clientSocket.Close();
}
return;
}
//j解析數據(把新接收的數據傳入)
messageHandle.ReadMessage(count);
//開始異步接收數據
clientSocket.BeginReceive(messageHandle.DataBuffer, messageHandle.ContentSize, messageHandle.remainSize, SocketFlags.None, ReceiveCallBack, clientSocket);
}
catch (Exception e)///說明客戶端已經已經斷開連接了,異常斷開
{
Console.WriteLine(e);
if (clientSocket != null)
{
clientSocket.Close();
}
}
}
}
public class MessageHandle
{
//表頭的數據長度爲4個個字節,表示後面的數據的長度
//保證能夠每次接收發送的消息小於1024bit大小,否則無法完整接收整條數據
private byte[] dataBuffer = new byte[1024];
//從dataBuffer已經存了多少個字節數據
private int contentSize = 0;
public int ContentSize {
get { return contentSize; }
}
/// <summary>
/// 剩餘多少存儲空間
/// </summary>
public int remainSize {
get { return dataBuffer.Length - contentSize; }
}
public byte[] DataBuffer {
get { return dataBuffer; }
}
/// <summary>
/// 解析數據 ,count 新讀取到的數據長度
/// </summary>
public void ReadMessage(int count)
{
contentSize += count;
//用while表示緩存區,可能有多條數據
while (true)
{
//緩存區小於4個字節,表示連表頭都無法解析
if (contentSize <= 4) return;
//讀取四個字節數據,代表這條數據的內容長度(不包括表頭的4個數據)
int receiveCount = BitConverter.ToInt32(dataBuffer, 0);
//緩存區中的數據,不夠解析一條完整的數據
if (contentSize - 4 < receiveCount) return;
//2、解析數據
//從除去表頭4個字節開始解析內容,解析的數據長度爲(表頭數據表示的長度)
string receiveStr = Encoding.UTF8.GetString(dataBuffer, 4, receiveCount);
Console.WriteLine("接收的客戶端數據:" + receiveStr);
//把剩餘的數據Copy到緩存區頭部位置
Array.Copy(dataBuffer, 4 + receiveCount, dataBuffer, 0, contentSize - 4 - receiveCount);
contentSize = contentSize - 4 - receiveCount;
}
}
/// <summary>
/// 構造發送數據
/// </summary>
/// <param name="msg"></param>
/// <returns></returns>
public byte[] SendMsg(string msg)
{
int length = msg.Length;
//構造表頭數據,固定4個字節的長度,表示內容的長度
byte[] headerBytes = BitConverter.GetBytes(length);
//構造內容
byte[] bodyBytes = Encoding.UTF8.GetBytes(msg);
byte[] tempBytes = new byte[headerBytes.Length + bodyBytes.Length];
///拷貝到同一個byte[]數組中,發送出去..
Buffer.BlockCopy(headerBytes, 0, tempBytes, 0, headerBytes.Length);
Buffer.BlockCopy(bodyBytes, 0, tempBytes, headerBytes.Length, bodyBytes.Length);
return tempBytes;
}
}
參考:https://blog.csdn.net/qq_33537945/article/details/79180502