iT邦幫忙

0

解決TCP傳輸黏包的問題(以C#為例)

  • 分享至 

  • xImage
  •  

因為TCP是基於Stream的方式傳輸,所以會有封包沾黏或讀取不完全的問題
解決方式就是在封包最前面附帶資料長度的資訊
這個解決方法是不管什麼程式語言都能適用

在這邊我將以C#舉例,Python例子在這

開啟Visual Studio,建立兩個專案,分別是Server和Client,由Client不斷地傳送訊息給Server
我這邊使用.NET 6.0做示範

Server端

#pragma warning disable CS8618 //關掉編譯器的警告資訊

using System.Net;
using System.Net.Sockets;
using System.Text;

//設定封包格式。不一定要用struct,這邊選它只是為了讓資料結構更好看
public class PacketProtocol
{
	//https://learn.microsoft.com/en-us/dotnet/csharp/language-reference/builtin-types/integral-numeric-types
	public int Length;//4 Bytes
	public byte[] Data;//因為Length使用int型態,所以最大允許的資料數是2,147,483,647
}

public class Server
{
	//使用委派和事件把接收到的資料傳遞出去
	public delegate void DReceive(string Message);
	public event DReceive Receive;

	private Socket server;//建立Server端的開口
	private Socket socket;//一旦有目標連到server的開口,就分配給這個物件
	private Thread threadAccept;//因為程式會在等待連線中停住,所以需要使用Thread等非同步方式去處理
	private Thread threadReceive;
	private bool FLAG_RECEIVE = true;

	public Server(int port, int allowNumber)
	{
		//InterNetwork: IPv4
		//Stream: 因為TCP是串流方式傳輸,所以這邊要選擇Stream
		//TCP: 建立TCP連線
		server = new Socket(AddressFamily.InterNetwork, SocketType.Stream, ProtocolType.Tcp);

		server.Bind(new IPEndPoint(IPAddress.Any, port));//綁定連線參數
		server.Listen(allowNumber);//傳入的參數代表允許多少個目標連入
		threadAccept = new Thread(Accept);
		threadAccept.Start();
	}

	~Server()
	{
		Close();
	}

	private void Accept()
	{
		try
		{
			Console.WriteLine("Wait Connect...");
			socket = server.Accept();
			server.Close();//若不需要再接收其他連線則可以關掉server這個socket物件
			Console.WriteLine("Connect SUCCESS");

			//準備接收資料
			threadReceive = new Thread(ReceiveMessage);
			threadReceive.Start();
		}
		catch (SocketException se)
		{
			Console.WriteLine(se.ToString());
		}
	}

	public void Close()
	{
		server.Close();
		socket.Close();
		FLAG_RECEIVE = false;
	}

	public bool IsConnected
	{
		get
		{
			if (socket == null) return false;
			else return socket.Connected;
		}
	}

	private void ReceiveMessage()
	{
		while(FLAG_RECEIVE)
		{
			if (Receive == null) continue;//只有設定Receive事件後才開始處理接收事件
			PacketProtocol data = new PacketProtocol();

			//先接收資料長度資訊
			byte[] length = new byte[4];//因為int是4bytes大小
			socket.Receive(length);
			data.Length = BitConverter.ToInt32(length, 0);

			//接收數據資料
			data.Data = new byte[data.Length];
			socket.Receive(data.Data);

			//將接收的資料轉為字串後,透過事件發送出去
			Receive(Encoding.UTF8.GetString(data.Data));
		}
	}
}

public class Process
{
	public static void Main()
	{
		Server server = new Server(8080, 1);
		server.Receive += ReceiveMessage;
		
		Console.ReadKey();//pause
		server.Receive -= ReceiveMessage;
		server.Close();
	}

	private static void ReceiveMessage(string Message)
	{
		Console.WriteLine(Message);
	}
}

Client端

#pragma warning disable CS8618 //關掉警告資訊

using System.Net;
using System.Net.Sockets;
using System.Text;

//設定封包格式。不一定要用struct,這邊選它只是為了讓資料結構更好看
public class PacketProtocol
{
	//https://learn.microsoft.com/en-us/dotnet/csharp/language-reference/builtin-types/integral-numeric-types
	public int Length;//4 Bytes
	public byte[] Data;//因為Length使用int型態,所以最大允許的資料數是2,147,483,647
}

public class Client
{
	private Socket socket;

	public Client(string ip, int port)
	{
		//InterNetwork: IPv4
		//Stream: 因為TCP是串流方式傳輸,所以這邊要選擇Stream
		//TCP: 建立TCP連線
		socket = new Socket(AddressFamily.InterNetwork, SocketType.Stream, ProtocolType.Tcp);
		Console.WriteLine("Wait Connect...");
		socket.Connect(ip, port);//這裡只考慮Server先開啟,再開Client的狀況
		Console.WriteLine("Connect SUCCESS");
	}

	~Client()
	{
		Close();
	}

	public void Close()
	{
		socket.Close();
	}

	public bool IsConnected
	{
		get
		{
			if (socket == null) return false;
			else return socket.Connected;
		}
	}

	public void Send(string message)
	{
		//只有連線成功才開始傳遞資料
		if (socket.Connected)
		{
			PacketProtocol packet = new PacketProtocol();
			packet.Data = Encoding.UTF8.GetBytes(message);
			packet.Length = packet.Data.Length;
			byte[] data = PacketProtocolToBytes(packet);
			socket.Send(data);
		}
	}

	private byte[] PacketProtocolToBytes(PacketProtocol data)
	{
		//將封包資料轉換為Byte陣列
		byte[] bytes;
		using (MemoryStream ms = new MemoryStream())
		{
			using (BinaryWriter bw = new BinaryWriter(ms))
			{
				//先寫入資料長度,再寫入資料
				//寫入順序錯誤會導致解析失敗
				bw.Write(data.Length);
				bw.Write(data.Data);
				bytes = ms.ToArray();
			}
		}
		return bytes;
	}
}

public class Process
{
	public static void Main()
	{
		Client client = new Client("127.0.0.1", 8080);
		while(client.IsConnected)
		{
			client.Send("Hello Server!");
		}

		Console.ReadKey();//pause
		client.Close();
	}
}

圖片
  直播研討會
圖片
{{ item.channelVendor }} {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言