粘包/半包及解决方案

粘包/半包及解决方案本文详细介绍了在网络编程中遇到的粘包和半包问题 包括其原因 以及长度信息法 固定长度法和结束符号法等解决方案 并给出了 C 代码示例

大家好,欢迎来到IT知识分享网。

一、粘包/半包介绍

1:粘包

粘包(Packet Concatenation)通常发生在基于流式传输协议(如 TCP)的通信中,因为 TCP 是面向流的传输协议,它不保证数据包的边界,而是将数据视为连续的字节流,它表示客户端发送多条消息,服务端只收到了一条消息

2:半包

半包(Half Packet)与粘包问题相反。在半包问题中,接收端接收到的数据包不完整,即接收到的数据包只是完整数据包的一部分,无法完整地解析和处理。

3:原因

①网络延迟/阻塞

②发送方连续发送数据

③接收端缓冲区大小限制

④数据包丢失

二、粘包/半包解决方案

1:长度信息法

在每个数据包前面加上长度信息,每次接收数据后,先读取长度,如果缓冲区数据长度大于要取的字节数,则取出相应字节,否则等待下一次接收,举个例子

①客户端第一次发送包含长度信息的内容

粘包/半包及解决方案

②客户端第二次发送包含长度信息的内容

粘包/半包及解决方案

③服务端第一次接收到了4个字节,存入缓冲区,但是这时候并不处理,因为收到了10,所以要等到11个字节完整再处理

粘包/半包及解决方案

④服务端等到客户端发送剩下的7个字节,但是第二次接收到了9个字节,服务端把之前的6个字节再读取,然后拼接,把10helloworld进行处理,读取到标志长度4,等待下次处理

粘包/半包及解决方案

⑤服务端最后一次收到ove,就把之前的l一起拼接,返回完整的4love

*一般的游戏是16位的整型数来存放长度信息

粘包/半包及解决方案

2:固定长度法

每次都发送相同长度的数据,一次不足的数据用.来补充,.位补充字符,没有实际意义

如果接收到的字符数大于10,就只提取前10个字符

3:结束符号法

规定一个结束符号作为消息的分隔符,比如Hello$World就是两条信息

三、代码示例

1:发送数据
 byte[] bodyBytes = System.Text.Encoding.Default.GetBytes(SendStr); Int16 len = (Int16)bodyBytes.Length; //把长度转化为Int16 byte[] lenBytes = BitConverter.GetBytes(len); //此时SendBytes包含长度字符串和内容字符串 byte[] sendBytes = lenBytes.Concat(bodyBytes).ToArray();

2:接收数据

定义一个接收缓冲区和接收缓冲区长度。缓冲区会保存尚未处理的数据

 //接收缓冲区 byte[] readBuff = new byte[1024]; //接收缓冲区长度 int buffCount = 0;

之前的BeginReceive函数原型如下:

public IAsyncResult BeginReceive ( byte[] buffer, int offset, int size, SocketFlags socketFlags, AsyncCallback callback, object state )

现在的参数应该写成这个样子:

socket.BeginReceive(readBuff, buffCount 1024-buffCount, 0, ReceiveCallback, socket); 

readBuff 是缓冲区

buffCount 是开始读取的位置

1024 – buffCount 是剩余多少可读取的大小

3:处理数据

public void OnReceiveData(){ //消息长度小于2,直接返回等待下一次接收 if(buffCount <= 2) return; //消息的长度 Int16 bodyLength = BitConverter.ToInt16(readBuff, 0); //消息体 //如果消息长度小于我消息内容和长度的字节,就返回继续读取 if(buffCount < 2+bodyLength) return; //如果长度够用,就转化为string类型 string s = System.Text.Encoding.UTF8.GetString(readBuff, 2,buffCount); //更新缓冲区 int start = 2 + bodyLength; int count = buffCount - start; //Copy函数把缓冲区后面的内容提到前面 Array.Copy(readBuff, start, readBuff, 0, count); buffCount -= start; //继续读取消息 if(readBuff.length > 2){ OnReceiveData(); } }

Copy的原型函数如下:

public static void Copy( Array sourceArray,//源数组 long sourceIndex,//目标数据 Array destinationArray,//目标数组 long destinationIndex,//目标数组起始位置 long length//复制消息的长度 )

四、完整示例

1:客户端
using System; using System.Linq; using System.Net.Sockets; using UnityEngine; using UnityEngine.UI; public class SendScr : MonoBehaviour { Socket socket; public InputField inputField; public Text text; byte[] readBuff = new byte[1024]; int buffCount = 0;//缓冲区数据长度 string recvStr = ""; public void Connection() { socket = new Socket(AddressFamily.InterNetwork, SocketType.Stream, ProtocolType.Tcp); socket.Connect("127.0.0.1", 8888); socket.BeginReceive(readBuff, buffCount, 1024 - buffCount, 0, ReceiveCallback, socket); } private void ReceiveCallback(IAsyncResult ar) { try { Socket socket = (Socket)ar.AsyncState; //获取接收数据长度 int count = socket.EndReceive(ar); buffCount += count; //处理二进制消息 OnReceiveData(); //继续接收数据 socket.BeginReceive(readBuff, buffCount, 1024 - buffCount, 0, ReceiveCallback, socket); } catch (SocketException ex) { Debug.Log("Socket Receive fail" + ex.ToString()); } } private void OnReceiveData() { Debug.Log("[Recv 1] buffCount = " + buffCount); Debug.Log("[Recv 2] readbuff = " + BitConverter.ToString(readBuff)); if (buffCount <= 2) return; Int16 bodyLength = BitConverter.ToInt16(readBuff, 0); Debug.Log("[Recv 3] bodyLength=" + bodyLength); //消息体 if (buffCount < 2 + bodyLength) return; string s = System.Text.Encoding.UTF8.GetString(readBuff, 2, buffCount); Debug.Log("[Recv 4] s=" + s); //更新缓冲区 int start = 2 + bodyLength; int count = buffCount - start; Array.Copy(readBuff, start, readBuff, 0, count); buffCount -= start; Debug.Log("[Recv 5] buffCount=" + buffCount); //消息处理 recvStr = s + "\n" + recvStr; //继续读取消息 OnReceiveData(); } public void Send() { string sendStr = inputField.text; byte[] bodyBytes = System.Text.Encoding.Default.GetBytes(sendStr); Int16 len = (Int16)bodyBytes.Length; byte[] lenBytes = BitConverter.GetBytes(len); //此时SendBytes包含长度字符串和内容字符串 byte[] sendBytes = lenBytes.Concat(bodyBytes).ToArray(); socket.Send(sendBytes); Debug.Log("[Send]" + BitConverter.ToString(sendBytes)); } private void Update() { text.text = recvStr; } }

捋一下客户端的功能:

当进入场景点击Connection,创建新的Socket连接,绑定服务端口,开始接收信息

这里的BeginReceive就是我们接受消息的函数,它传入我们的缓冲区readBuff,缓冲区长度buffCount,1024 – buffCount表示我们还剩多少字节数据,然后进入回调函数

Receive回调函数中创建新的Socket对象来解析获取到的socket对象,count用于跟踪每次异步接收操作实际接收到的数据长度,然后把count加到buffCount上,这样更新了缓冲区的长度

然后使用OnReceiveData处理数据,继续读取数据,点击发送就调用Send()

粘包/半包及解决方案

2:服务端
using System.Net.Sockets; using System.Net; internal class ClientState { public Socket socket; public byte[] readBuff = new byte[1024]; } class Class { static Socket listenfd; static Dictionary<Socket, ClientState> clients = new Dictionary<Socket, ClientState>(); public static void Main(string[] args) { listenfd = new Socket(AddressFamily.InterNetwork, SocketType.Stream, ProtocolType.Tcp); IPAddress ipAdr = IPAddress.Parse("127.0.0.1"); IPEndPoint ipEp = new IPEndPoint(ipAdr, 8888); listenfd.Bind(ipEp); listenfd.Listen(0); Console.WriteLine("[服务器]启动成功"); List<Socket> checkRead = new List<Socket>(); while (true) { //填充checkRead列表 checkRead.Clear(); checkRead.Add(listenfd); foreach (ClientState s in clients.Values) { checkRead.Add(s.socket); } //select Socket.Select(checkRead, null, null, 1000); //检查可读对象 foreach (Socket s in checkRead) { if (s == listenfd) { ReadListenfd(s); } else { ReadClientfd(s); } } } } public static void ReadListenfd(Socket listenfd) { Console.WriteLine("Accept"); Socket clientfd = listenfd.Accept(); ClientState state = new ClientState(); state.socket = clientfd; clients.Add(clientfd, state); } public static bool ReadClientfd(Socket clientfd) { ClientState state = clients[clientfd]; //接收 int count = clientfd.Receive(state.readBuff); if (count == 0) { clientfd.Close(); clients.Remove(clientfd); Console.WriteLine("Socket Close"); return false; } //广播 string recvStr = System.Text.Encoding.Default.GetString(state.readBuff, 2, count - 2); Console.WriteLine("Receive" + recvStr); byte[] sendBytes = new byte[count]; Array.Copy(state.readBuff, 0, sendBytes, 0, count); foreach (ClientState cs in clients.Values) { cs.socket.Send(sendBytes); } return true; } }

这里的服务端代码和Select部分的代码基本相同

3:测试

打开服务器,客户端连接后输入UNITY

粘包/半包及解决方案

①客户端反馈

粘包/半包及解决方案

Recv1的buffCount就是接收到服务端所返回的消息,服务端所返回的信息是客户端发送的拼接数组,就是UNITY+lenBytes,就是7

Rec2的readbuff就是缓冲区存储的内容

Recv3是当消息没有接受完整的时候的消息本体长度

Recv4是完整的消息内容

Recv5是接受完成UNITY后更新后的新buffCount新长度,因为没有后续的,所以是0

②服务端反馈

粘包/半包及解决方案

服务端接受到的消息

四、模拟粘包

客户端修改

using System; using System.Linq; using System.Net.Sockets; using UnityEngine; using UnityEngine.UI; public class SendScr : MonoBehaviour { Socket socket; public InputField inputField; public Text text; byte[] readBuff = new byte[1024]; int buffCount = 0;//缓冲区数据长度 string recvStr = ""; public void Connection() { socket = new Socket(AddressFamily.InterNetwork, SocketType.Stream, ProtocolType.Tcp); socket.Connect("127.0.0.1", 8888); socket.BeginReceive(readBuff, buffCount, 1024 - buffCount, 0, ReceiveCallback, socket); } private void ReceiveCallback(IAsyncResult ar) { try { Socket socket = (Socket)ar.AsyncState; //获取接收数据长度 int count = socket.EndReceive(ar); buffCount += count; //处理二进制消息 OnReceiveData(); System.Threading.Thread.Sleep(1000*30); //继续接收数据 socket.BeginReceive(readBuff, buffCount, 1024 - buffCount, 0, ReceiveCallback, socket); } catch (SocketException ex) { Debug.Log("Socket Receive fail" + ex.ToString()); } } private void OnReceiveData() { Debug.Log("[Recv 1] buffCount = " + buffCount); Debug.Log("[Recv 2] readbuff = " + BitConverter.ToString(readBuff)); if (buffCount <= 2) return; Int16 bodyLength = BitConverter.ToInt16(readBuff, 0); Debug.Log("[Recv 3] bodyLength=" + bodyLength); //消息体 if (buffCount < 2 + bodyLength) return; string s = System.Text.Encoding.UTF8.GetString(readBuff, 2, buffCount); Debug.Log("[Recv 4] s=" + s); //更新缓冲区 int start = 2 + bodyLength; int count = buffCount - start; Array.Copy(readBuff, start, readBuff, 0, count); buffCount -= start; Debug.Log("[Recv 5] buffCount=" + buffCount); //消息处理 recvStr = s + "\n" + recvStr; //继续读取消息 OnReceiveData(); } public void Send() { string sendStr = inputField.text; byte[] bodyBytes = System.Text.Encoding.Default.GetBytes(sendStr); Int16 len = (Int16)bodyBytes.Length; byte[] lenBytes = BitConverter.GetBytes(len); //此时SendBytes包含长度字符串和内容字符串 byte[] sendBytes = lenBytes.Concat(bodyBytes).ToArray(); socket.Send(sendBytes); Debug.Log("[Send]" + BitConverter.ToString(sendBytes)); } private void Update() { text.text = recvStr; } }

*在接收数据的时候,强制等待30s再进行下一次接收,ReceiveCallback是在子线程执行,调用Sleep函数并不会卡住主线程,客户端不会被卡住,在30s内多次发送数据,经由服务端转发,再次调用BeginReceive的时候,缓冲区有很多数据,会产生粘包。

客户端快速发送三条消息,发送不会堵塞,每次接收都会等待30s,但是接收到的消息不是在一起的,而是分开的

粘包/半包及解决方案

免责声明:本站所有文章内容,图片,视频等均是来源于用户投稿和互联网及文摘转载整编而成,不代表本站观点,不承担相关法律责任。其著作权各归其原作者或其出版社所有。如发现本站有涉嫌抄袭侵权/违法违规的内容,侵犯到您的权益,请在线联系站长,一经查实,本站将立刻删除。 本文来自网络,若有侵权,请联系删除,如若转载,请注明出处:https://haidsoft.com/120522.html

(0)
上一篇 2025-10-29 10:10
下一篇 2025-10-29 10:20

相关推荐

发表回复

您的邮箱地址不会被公开。 必填项已用 * 标注

关注微信