Socket編程(網絡編程)中TCP粘包的實例,含代碼演示

利用網絡通信中,經常會出現粘包的問題,圍繞着這個問題說原因和解決的蠻多帖子的,但是給出粘包代碼的就好少,爲了便於大家更好的理解粘包的問題,這裏對客戶端和服務器端出現的粘包問題進行模擬,以方便更好的理解這個問題的出現原因。在開始之前還是需要理解幾個基礎概念。

如果需要源代碼請點擊此處下載

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);

    }
}

 

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章