利用網絡通信中,經常會出現粘包的問題,圍繞着這個問題說原因和解決的蠻多帖子的,但是給出粘包代碼的就好少,爲了便於大家更好的理解粘包的問題,這裏對客戶端和服務器端出現的粘包問題進行模擬,以方便更好的理解這個問題的出現原因。在開始之前還是需要理解幾個基礎概念。
1、什麼是粘包?
粘包是指發送方發送的若干包數據到接收方接收時粘成一包,從接收緩衝區看,後一包數據的頭緊接着前一包數據的尾。只有TCP有粘包現象,UDP不會。
2、哪裏會發生粘包?
其實粘包主要分爲兩個方面,客戶端和服務器端都可能會出現:
a、客戶端發生:當連續發送數據時,由於tcp協議的nagle算法,會將較小的內容拼接成大的內容,一次性發送到服務器端,因此造成粘包
b、服務器端發生:當發送內容較大時,由於服務器端的recv(buffer_size)方法中的buffer_size較小,不能一次性完全接收全部內容,因此在下一次請求到達時,接收的內容依然是上一次沒有完全接收完的內容,因此造成粘包現象。
好了說了這些之後,我們簡單通過一個圖來介紹下我們要模擬的粘包的場景:簡單說就是客戶端向服務器端不停的發送兩個包,一個數據包的內容是一個整形的數:4,另一個包是一個字符串:你好!
理想情況下,我們應該是客戶端發送什麼,發送多少服務器端就接收對應數量的數據:
然而,當我們加快客戶端的發送速度時,服務器端接收的數據包就不正常了,如圖所示:
很顯然,客戶端還是正常的,不停的發送21和40兩個數據包,但是在服務器端除了21,40正常的包外還出現了101和61兩種包類型,其中101我們可以分析的出是兩個40的包和一個21的包粘到一起了,而61的包是一個21的包和一個40的包粘到一起了
這裏爲了方便大家大家復現,先把客戶端的代碼貼出來:
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using System.Net.Sockets;
using System.Net;
using System.Threading;
using LitJson;
class client
{
//聲明socket
Socket socket;
//聲明一個地址類
IPEndPoint iep;
public void connect()
{
try
{
//設定Socket的模式
socket = new Socket(AddressFamily.InterNetwork, SocketType.Stream, ProtocolType.Tcp);
//指定服務端的地址和端口
iep = new IPEndPoint(IPAddress.Parse("127.0.0.1"), 30000);
//建立連接
socket.Connect(iep);
Console.WriteLine("服務器連接成功");
//定義一個Json來存放數據
JsonData jd_int = new JsonData();
jd_int["code"] = 1;
jd_int["result"] = 4;
//定義一個byte數組存放數據
byte[] jsByte_int = Encoding.UTF8.GetBytes(jd_int.ToJson());
//發送數據
JsonData jd_string = new JsonData();
jd_string["code"] = 2;
jd_string["result"] = "你好!";
byte[] jsByte_string = Encoding.UTF8.GetBytes(jd_string.ToJson());
//發送數據
while (true)
{
Thread.Sleep(1); //設置發送消息的間隔
socket.Send(jsByte_int);
Console.WriteLine("發送了" + jsByte_int.Length + "個字節");
socket.Send(jsByte_string);
Console.WriteLine("發送了" + jsByte_string.Length + "個字節");
}
}
catch(Exception e)
{
Console.WriteLine("服務器連接失敗");
Console.WriteLine(e.ToString());
}
}
}
這裏各位可以根據自己電腦的情況設置發送的間隔,在不同的電腦裏可能出現粘包的間隔不同,性能較差的電腦可能sleep時間較長就會出現粘包現象,而筆者的電腦是設置爲1毫秒時出現了粘包的情況,具體代碼如下:
//發送數據
while (true) //循環發送數據,模擬客戶端較大的數據量
{
Thread.Sleep(1); //設置發送消息的間隔,越小越容易出現粘包現象(和電腦配置有關)
socket.Send(jsByte_int);
Console.WriteLine("發送了" + jsByte_int.Length + "個字節");
socket.Send(jsByte_string);
Console.WriteLine("發送了" + jsByte_string.Length + "個字節");
}
服務器端的代碼就相對簡單了,就是一個簡單的異步接收:
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using System.Net.Sockets;
using System.Net;
using LitJson;
public class demo
{
//需要添加System.Net.Sockets的支持
Socket socket;
private string ip;
private int port;
//定義每次接收的最大字節數和接收數組
private int maxBuffer = 1024;
private byte[] buffer;
//定義一個套接字代表客戶端
private Socket client;
public demo(string _ip,int _port)
{
this.ip = _ip;
this.port = _port;
}
//服務器連接函數
public void StartConnect()
{
try
{
socket = new Socket(AddressFamily.InterNetwork, SocketType.Stream, ProtocolType.Tcp);
IPEndPoint iep = new IPEndPoint(IPAddress.Parse(ip), port);
//綁定地址
socket.Bind(iep);
//設置客戶端連接數量
socket.Listen(1000);
Console.WriteLine("服務器開啓成功!");
//監聽客戶端連接
Console.WriteLine("監控客戶端連接!");
client = socket.Accept();
Console.WriteLine("有客戶端連接了!");
Receive();
}
catch(Exception e)
{
Console.WriteLine("服務器開啓失敗!");
//顯示出異常詳情
Console.WriteLine(e.ToString());
}
}
//接收客戶端信息
public void Receive()
{
//初始化接收的數組
buffer = new byte[maxBuffer];
try
{
//異步接收,接收的數據放到回調函數BegRece裏面去處理,無論客戶端是否發送消息都會執行這一步
client.BeginReceive(buffer, 0, maxBuffer, 0, new AsyncCallback(BegRece), client);
}
catch (Exception e)
{
Console.WriteLine("接收異常!");
Console.WriteLine(e.ToString());
}
}
private void BegRece(IAsyncResult ar)
{
Socket so = (Socket)ar.AsyncState;
//接收字節的數據長度
int length = so.EndReceive(ar);
Console.WriteLine("收到了客戶端發來的信息,長度爲:" + length);
try
{
//接收數據
string content = Encoding.UTF8.GetString(buffer, 0, length);
//Json的解析
JsonData jd = JsonMapper.ToObject<JsonData>(content);
Console.WriteLine("code: " + (int)jd["code"] + " result: " + jd["result"]);
buffer = new byte[maxBuffer];
}
catch(Exception e)
{
Console.WriteLine("解碼錯誤!");
Console.WriteLine(e.ToString());
}
//爲了實現不停地接收數據,再次調用並對buffer重新賦值即可
client.BeginReceive(buffer, 0, maxBuffer, 0, new AsyncCallback(BegRece), client);
}
}