Socket通訊中粘包分包問題的介紹和解決(C#)
最近在做Unity區域網時,用到了Socket通訊基於TCP協議,然後使用非同步方式,主要用到了BeginAccept和BeginReceive方法
然而就可以實現非同步通訊,然而還是要解決粘包和分包問題
這裡我先說明一下什麼是分包和粘包,TCP提供面向連線的、可靠的資料流傳輸,所以當我們傳送資料在短時間內比較頻繁並且資料量比較小時,TCP為了優化記憶體資源,會將多條資料粘成幾個包來進行處理,相比傳送的訊息條數要少了很多。比如,客戶端在非常的時間內向伺服器傳送了100條資料,每條資料的位元組長度又比較小,那麼在伺服器端收到的可能只有幾條資料,因為粘包,這100條資料粘成了幾個包,這樣優化了記憶體資源,這其實是TCP的一種優化機制,這看似是一種好處,但是在開發網路遊戲時,反而是壞處,因為在網路遊戲中,客戶端需要頻繁向伺服器傳送多條資料,如果出現了粘包情況,可能會出現畫面幀不同步。
接著數一下什麼是分包,當傳送的一條資料的資料量比較大的時候,而伺服器的資料緩衝區一次卻裝不下那麼多的資料,所以就會將這一條資料分成 幾個包,也就是說把該條資料分割成幾個資料,因為資料傳送接收都是以位元組陣列的形式傳輸,所以可能會出現資料的丟失,這種情況在實際開發中也需要避免。
粘包舉例:假設主機A第一次傳送了: ab 第二次傳送了:cd 第三次傳送了:ef 第四次傳送了:gh 然後主機B接收了資料,主機A分別四次傳送了資料,由於傳送時間短,資料量小,在主機B卻只收到兩條資料,第一條資料是:abcd, 第二條資料是efgh,
粘包舉例:假設主機A傳送了一條非常大的資料,這條資料的位元組長度是2048非常龐大,超過了主機B的資料緩衝區的長度1024,所以主機B在接收資料時,因為存不下那麼大的資料,會將該條資料分成兩條,第一條資料的位元組長度是1024,第二條資料的位元組長度也是1024
解決粘包分包問題:
由於傳輸的資料是位元組陣列,我們可以得到該條資料位元組陣列的長度,長度是一個整型,並且所佔位元組長度固定為4,所以可以將該整型資料轉成位元組陣列,不管訊息的長度多大,該整型資料的位元組陣列長度始終為4(因為sizeof(int) = 4),所以傳送資料的時候,在該條資料的前面加上這個整型資料的位元組陣列,也就是說,發出去的資料,前四個位元組表示實際訊息的長度,後面的位元組表示實際訊息。比如我們要傳送 HelloWorld, 實際上傳送的是4HelloWorld。
程式碼如下:
伺服器端:
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Net;
using System.Net.Sockets;
namespace TCP伺服器端
{
class Program
{
static void Main(string[] args)
{
AsyncStart();
Console.ReadKey();
}
//static byte[] dataBuffer = new byte[1024];
//非同步Socket
static void AsyncStart() {
Socket serverSocket = new Socket(AddressFamily.InterNetwork, SocketType.Stream, ProtocolType.Tcp);
IPEndPoint ipEndPoint = new IPEndPoint(IPAddress.Parse("192.168.137.1"), 8888);
serverSocket.Bind(ipEndPoint);
serverSocket.Listen(0);
Console.WriteLine("伺服器啟動成功!");
//Socket clientSocket = serverSocket.Accept();
//實現可以連線多個客戶端
serverSocket.BeginAccept(AcceptCb, serverSocket);
}
static Message msg = new Message();
static void AcceptCb(IAsyncResult ar) {
try
{
Socket serverSocket = ar.AsyncState as Socket;
Socket clientSocket = serverSocket.EndAccept(ar);
//傳送資料
string msgStr = "Hello stupid 人類...";
byte[] data = System.Text.Encoding.UTF8.GetBytes(msgStr);
clientSocket.Send(data);
//接收資料 實現服務端能夠接收多條資料
clientSocket.BeginReceive(msg.data, msg.startIndex, msg.remainSize, SocketFlags.None, ReceiveCb, clientSocket);
serverSocket.BeginAccept(AcceptCb, serverSocket); //迴圈呼叫
}
catch (Exception e) {
Console.WriteLine("AcceptCb失敗:" + e.Message);
}
}
static void ReceiveCb(IAsyncResult ar) {
try
{
Socket clientSocket = ar.AsyncState as Socket;
int count = clientSocket.EndReceive(ar);
Console.WriteLine("Receive回撥的訊息長度:" + count);
if (count == 0) //用來判斷客戶端是否正常判斷,不加可能伺服器端迴圈列印或者伺服器故障
{
Console.WriteLine("count<=0");
clientSocket.Close();
return;
}
msg.AddCount(count);
//string msgStr = Encoding.UTF8.GetString(msg.data, 0, count);
//Console.WriteLine("收到客戶端訊息:"+msg);
msg.ReadMessage();
clientSocket.BeginReceive(msg.data, msg.startIndex, msg.remainSize, SocketFlags.None, ReceiveCb, clientSocket); //迴圈呼叫
}
catch (Exception e) {
Console.WriteLine("ReceiveCb異常:" + e);
}
}
}
----伺服器靜態類:
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
namespace TCP伺服器端
{
class Message
{
public byte[] data = new byte[1024];
public int startIndex = 0; //data裡面存了多少位元組,也是下一次讀取的一個索引
public int remainSize;
public Message() {
remainSize = data.Length - startIndex;
}
public void AddCount(int count) {
startIndex += count;
remainSize = data.Length - startIndex;
}
//由於客戶端傳送的資料比較頻繁,會出現粘包,99條資料最終可能粘成了2-3個包,
public void ReadMessage() {
while (true) {
if (startIndex <= 4) { //解決粘包問題
Console.WriteLine("不構成一條訊息");
return;
}
int count = BitConverter.ToInt32(data, 0); //得到訊息長度
Console.WriteLine("count : " + count);
Console.WriteLine("startIndex-4 : " + (startIndex - 4));
if ((startIndex - 4) >= count) //緩衝中待處理的資料個數是否大於本次傳送的訊息長度
{
string s = Encoding.UTF8.GetString(data, 4, count);
Console.WriteLine("收到資料:" + s);
Array.Copy(data, count + 4, data, 0, startIndex-4-count);
startIndex -= (count + 4);
}
else { // 解決分包問題
Console.WriteLine("訊息過長不做處理");
break;
}
}
}
}
}
客戶端程式碼
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Net;
using System.Net.Sockets;
namespace TCP客戶端
{
class Program
{
static void Main(string[] args)
{
Socket clientSocket = new Socket(AddressFamily.InterNetwork, SocketType.Stream, ProtocolType.Tcp);
clientSocket.Connect("192.168.137.1", 8888);
//收到訊息
byte[] data = new byte[1024];
int count = clientSocket.Receive(data);
string receiveStr = System.Text.Encoding.UTF8.GetString(data, 0, count);
Console.WriteLine("收到伺服器訊息:" + receiveStr);
//傳送訊息
//while (true)
//{
// //string msg = Console.ReadLine();
// //if (msg == "c") {
// // clientSocket.Close();
// // return;
// //}
//}
string s = @"這是一條文字訊息發生看了感覺as弗蘭克見鬼十法的開獎號噶水電費就考了個和介面辣雞啊撒發聖誕節快樂發貨
撒大計科了訪華sad接口裡發哈聖誕節快樂這是一條文字訊息發生看了感覺as弗蘭克見鬼十法的開獎號噶水電費就考了個和介面
辣雞啊撒發聖誕節快樂發貨這是一條文字訊息發生看了感覺as弗蘭克見鬼十法的開獎號噶水電費就考了個和介面辣雞啊撒發聖誕
節快樂發貨這是一條文字訊息發生看了感覺as弗蘭克見鬼十法的開獎號噶水電費就考了個和介面辣雞啊撒發聖誕節快樂發貨
撒大計科了訪華sad接口裡發哈聖誕節快樂這是一條文字訊息發生看了感覺as弗蘭克見鬼十法的開獎號噶水電費就考了個和介面
辣雞啊撒發聖誕節快樂發貨這是一條文字訊息發生看了感覺as弗蘭克見鬼十法的開獎號噶水電費就考了個和介面辣雞啊撒發聖誕
節快樂發貨這是一條文字訊息發生看了感覺as弗蘭克見鬼十法的開獎號噶水電費就考了個和介面辣雞啊撒發聖誕節快樂發貨
撒大計科了訪華sad接口裡發哈聖誕節快樂這是一條文字訊息發生看了感覺as弗蘭克見鬼十法的開獎號噶水電費就考了個和介面
辣雞啊撒發聖誕節快樂發貨這是一條文字訊息發生看了感覺as弗蘭克見鬼十法的開獎號噶水電費就考了個和介面辣雞啊撒發聖誕
節快樂發貨這是一條文字訊息發生看了感覺as弗蘭克見鬼十法的開獎號噶水電費就考了個和介面辣雞啊撒發聖誕節快樂發貨
撒大計科了訪華sad接口裡發哈聖誕節快樂這是一條文字訊息發生看了感覺as弗蘭克見鬼十法的開獎號噶水電費就考了個和介面
辣雞啊撒發聖誕節快樂發貨這是一條文字訊息發生看了感覺as弗蘭克見鬼十法的開獎號噶水電費就考了個和介面辣雞啊撒發聖誕
節快樂發貨這是一條文字訊息發生看了感覺as弗蘭克見鬼十法的開獎號噶水電費就考了個和介面辣雞啊撒發聖誕節快樂發貨
撒大計科了訪華sad接口裡發哈聖誕節快樂這是一條文字訊息發生看了感覺as弗蘭克見鬼十法的開獎號噶水電費就考了個和介面
辣雞啊撒發聖誕節快樂發貨這是一條文字訊息發生看了感覺as弗蘭克見鬼十法的開獎號噶水電費就考了個和介面辣雞啊撒發聖誕
節快樂發貨這是一條文字訊息發生看了感覺as弗蘭克見鬼十法的開獎號噶水電費就考了個和介面辣雞啊撒發聖誕節快樂發貨
撒大計科了訪華sad接口裡發哈聖誕節快樂這是一條文字訊息發生看了感覺as弗蘭克見鬼十法的開獎號噶水電費就考了個和介面
辣雞啊撒發聖誕節快樂發貨這是一條文字訊息發生看了感覺as弗蘭克見鬼十法的開獎號噶水電費就考了個和介面辣雞啊撒發聖誕
節快樂發貨這是一條文字訊息發生看了感覺as弗蘭克見鬼十法的開獎號噶水電費就考了個和介面辣雞啊撒發聖誕節快樂發貨
撒大計科了訪華sad接口裡發哈聖誕節快樂這是一條文字訊息發生看了感覺as弗蘭克見鬼十法的開獎號噶水電費就考了個和介面
辣雞啊撒發聖誕節快樂發貨這是一條文字訊息發生看了感覺as弗蘭克見鬼十法的開獎號噶水電費就考了個和介面辣雞啊撒發聖誕
節快樂發貨";
while (true){
string input = Console.ReadLine();
if (input == "for")
{
for (int i = 1; i < 100; i++)
{
clientSocket.Send(Message.GetBytes(i.ToString() + "長度"));
}
}
else
{
clientSocket.Send(Message.GetBytes(s));
}
}
Console.ReadKey();
clientSocket.Close();
}
}
}
// ----客戶端Message類
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
namespace TCP客戶端
{
class Message
{
public static byte[] GetBytes(string msg) {
byte[] data = Encoding.UTF8.GetBytes(msg);
int len = data.Length;
byte[] lenBytes = BitConverter.GetBytes(len);
byte[] sendBuffer = lenBytes.Concat(data).ToArray();
return sendBuffer;
}
}
}
先啟動伺服器程式,然後啟動客戶端程式
然後在客戶端控制檯輸入:for
在客戶端可以看到:
如上可以看到伺服器照樣收到了99條訊息,並沒有出現粘包現象,然後在客戶端控制檯輸入a,在伺服器端控制檯可以看到
如上,由於傳送的資料量比較大,所以伺服器端對其不做處理,直接清空快取區