当前位置:   article > 正文

粘包/半包及解决方案

粘包/半包及解决方案

一、粘包/半包介绍

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:发送数据
  1. byte[] bodyBytes = System.Text.Encoding.Default.GetBytes(SendStr);
  2. Int16 len = (Int16)bodyBytes.Length;
  3. //把长度转化为Int16
  4. byte[] lenBytes = BitConverter.GetBytes(len);
  5. //此时SendBytes包含长度字符串和内容字符串
  6. byte[] sendBytes = lenBytes.Concat(bodyBytes).ToArray();

2:接收数据

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

  1. //接收缓冲区
  2. byte[] readBuff = new byte[1024];
  3. //接收缓冲区长度
  4. 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:处理数据

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

Copy的原型函数如下:

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

四、完整示例

1:客户端
  1. using System;
  2. using System.Linq;
  3. using System.Net.Sockets;
  4. using UnityEngine;
  5. using UnityEngine.UI;
  6. public class SendScr : MonoBehaviour
  7. {
  8. Socket socket;
  9. public InputField inputField;
  10. public Text text;
  11. byte[] readBuff = new byte[1024];
  12. int buffCount = 0;//缓冲区数据长度
  13. string recvStr = "";
  14. public void Connection()
  15. {
  16. socket = new Socket(AddressFamily.InterNetwork, SocketType.Stream, ProtocolType.Tcp);
  17. socket.Connect("127.0.0.1", 8888);
  18. socket.BeginReceive(readBuff, buffCount, 1024 - buffCount, 0, ReceiveCallback, socket);
  19. }
  20. private void ReceiveCallback(IAsyncResult ar)
  21. {
  22. try
  23. {
  24. Socket socket = (Socket)ar.AsyncState;
  25. //获取接收数据长度
  26. int count = socket.EndReceive(ar);
  27. buffCount += count;
  28. //处理二进制消息
  29. OnReceiveData();
  30. //继续接收数据
  31. socket.BeginReceive(readBuff, buffCount, 1024 - buffCount, 0, ReceiveCallback, socket);
  32. }
  33. catch (SocketException ex)
  34. {
  35. Debug.Log("Socket Receive fail" + ex.ToString());
  36. }
  37. }
  38. private void OnReceiveData()
  39. {
  40. Debug.Log("[Recv 1] buffCount = " + buffCount);
  41. Debug.Log("[Recv 2] readbuff = " + BitConverter.ToString(readBuff));
  42. if (buffCount <= 2) return;
  43. Int16 bodyLength = BitConverter.ToInt16(readBuff, 0);
  44. Debug.Log("[Recv 3] bodyLength=" + bodyLength);
  45. //消息体
  46. if (buffCount < 2 + bodyLength) return;
  47. string s = System.Text.Encoding.UTF8.GetString(readBuff, 2, buffCount);
  48. Debug.Log("[Recv 4] s=" + s);
  49. //更新缓冲区
  50. int start = 2 + bodyLength;
  51. int count = buffCount - start;
  52. Array.Copy(readBuff, start, readBuff, 0, count);
  53. buffCount -= start;
  54. Debug.Log("[Recv 5] buffCount=" + buffCount);
  55. //消息处理
  56. recvStr = s + "\n" + recvStr;
  57. //继续读取消息
  58. OnReceiveData();
  59. }
  60. public void Send()
  61. {
  62. string sendStr = inputField.text;
  63. byte[] bodyBytes = System.Text.Encoding.Default.GetBytes(sendStr);
  64. Int16 len = (Int16)bodyBytes.Length;
  65. byte[] lenBytes = BitConverter.GetBytes(len);
  66. //此时SendBytes包含长度字符串和内容字符串
  67. byte[] sendBytes = lenBytes.Concat(bodyBytes).ToArray();
  68. socket.Send(sendBytes);
  69. Debug.Log("[Send]" + BitConverter.ToString(sendBytes));
  70. }
  71. private void Update()
  72. {
  73. text.text = recvStr;
  74. }
  75. }

捋一下客户端的功能:

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

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

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

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

2:服务端
  1. using System.Net.Sockets;
  2. using System.Net;
  3. internal class ClientState
  4. {
  5. public Socket socket;
  6. public byte[] readBuff = new byte[1024];
  7. }
  8. class Class
  9. {
  10. static Socket listenfd;
  11. static Dictionary<Socket, ClientState> clients = new Dictionary<Socket, ClientState>();
  12. public static void Main(string[] args)
  13. {
  14. listenfd = new Socket(AddressFamily.InterNetwork, SocketType.Stream, ProtocolType.Tcp);
  15. IPAddress ipAdr = IPAddress.Parse("127.0.0.1");
  16. IPEndPoint ipEp = new IPEndPoint(ipAdr, 8888);
  17. listenfd.Bind(ipEp);
  18. listenfd.Listen(0);
  19. Console.WriteLine("[服务器]启动成功");
  20. List<Socket> checkRead = new List<Socket>();
  21. while (true)
  22. {
  23. //填充checkRead列表
  24. checkRead.Clear();
  25. checkRead.Add(listenfd);
  26. foreach (ClientState s in clients.Values)
  27. {
  28. checkRead.Add(s.socket);
  29. }
  30. //select
  31. Socket.Select(checkRead, null, null, 1000);
  32. //检查可读对象
  33. foreach (Socket s in checkRead)
  34. {
  35. if (s == listenfd)
  36. {
  37. ReadListenfd(s);
  38. }
  39. else
  40. {
  41. ReadClientfd(s);
  42. }
  43. }
  44. }
  45. }
  46. public static void ReadListenfd(Socket listenfd)
  47. {
  48. Console.WriteLine("Accept");
  49. Socket clientfd = listenfd.Accept();
  50. ClientState state = new ClientState();
  51. state.socket = clientfd;
  52. clients.Add(clientfd, state);
  53. }
  54. public static bool ReadClientfd(Socket clientfd)
  55. {
  56. ClientState state = clients[clientfd];
  57. //接收
  58. int count = clientfd.Receive(state.readBuff);
  59. if (count == 0)
  60. {
  61. clientfd.Close();
  62. clients.Remove(clientfd);
  63. Console.WriteLine("Socket Close");
  64. return false;
  65. }
  66. //广播
  67. string recvStr = System.Text.Encoding.Default.GetString(state.readBuff, 2, count - 2);
  68. Console.WriteLine("Receive" + recvStr);
  69. byte[] sendBytes = new byte[count];
  70. Array.Copy(state.readBuff, 0, sendBytes, 0, count);
  71. foreach (ClientState cs in clients.Values)
  72. {
  73. cs.socket.Send(sendBytes);
  74. }
  75. return true;
  76. }
  77. }

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

3:测试

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

①客户端反馈

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

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

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

Recv4是完整的消息内容

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

②服务端反馈

服务端接受到的消息

四、模拟粘包

客户端修改

  1. using System;
  2. using System.Linq;
  3. using System.Net.Sockets;
  4. using UnityEngine;
  5. using UnityEngine.UI;
  6. public class SendScr : MonoBehaviour
  7. {
  8. Socket socket;
  9. public InputField inputField;
  10. public Text text;
  11. byte[] readBuff = new byte[1024];
  12. int buffCount = 0;//缓冲区数据长度
  13. string recvStr = "";
  14. public void Connection()
  15. {
  16. socket = new Socket(AddressFamily.InterNetwork, SocketType.Stream, ProtocolType.Tcp);
  17. socket.Connect("127.0.0.1", 8888);
  18. socket.BeginReceive(readBuff, buffCount, 1024 - buffCount, 0, ReceiveCallback, socket);
  19. }
  20. private void ReceiveCallback(IAsyncResult ar)
  21. {
  22. try
  23. {
  24. Socket socket = (Socket)ar.AsyncState;
  25. //获取接收数据长度
  26. int count = socket.EndReceive(ar);
  27. buffCount += count;
  28. //处理二进制消息
  29. OnReceiveData();
  30. System.Threading.Thread.Sleep(1000*30);
  31. //继续接收数据
  32. socket.BeginReceive(readBuff, buffCount, 1024 - buffCount, 0, ReceiveCallback, socket);
  33. }
  34. catch (SocketException ex)
  35. {
  36. Debug.Log("Socket Receive fail" + ex.ToString());
  37. }
  38. }
  39. private void OnReceiveData()
  40. {
  41. Debug.Log("[Recv 1] buffCount = " + buffCount);
  42. Debug.Log("[Recv 2] readbuff = " + BitConverter.ToString(readBuff));
  43. if (buffCount <= 2) return;
  44. Int16 bodyLength = BitConverter.ToInt16(readBuff, 0);
  45. Debug.Log("[Recv 3] bodyLength=" + bodyLength);
  46. //消息体
  47. if (buffCount < 2 + bodyLength) return;
  48. string s = System.Text.Encoding.UTF8.GetString(readBuff, 2, buffCount);
  49. Debug.Log("[Recv 4] s=" + s);
  50. //更新缓冲区
  51. int start = 2 + bodyLength;
  52. int count = buffCount - start;
  53. Array.Copy(readBuff, start, readBuff, 0, count);
  54. buffCount -= start;
  55. Debug.Log("[Recv 5] buffCount=" + buffCount);
  56. //消息处理
  57. recvStr = s + "\n" + recvStr;
  58. //继续读取消息
  59. OnReceiveData();
  60. }
  61. public void Send()
  62. {
  63. string sendStr = inputField.text;
  64. byte[] bodyBytes = System.Text.Encoding.Default.GetBytes(sendStr);
  65. Int16 len = (Int16)bodyBytes.Length;
  66. byte[] lenBytes = BitConverter.GetBytes(len);
  67. //此时SendBytes包含长度字符串和内容字符串
  68. byte[] sendBytes = lenBytes.Concat(bodyBytes).ToArray();
  69. socket.Send(sendBytes);
  70. Debug.Log("[Send]" + BitConverter.ToString(sendBytes));
  71. }
  72. private void Update()
  73. {
  74. text.text = recvStr;
  75. }
  76. }

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

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

声明:本文内容由网友自发贡献,不代表【wpsshop博客】立场,版权归原作者所有,本站不承担相应法律责任。如您发现有侵权的内容,请联系我们。转载请注明出处:https://www.wpsshop.cn/w/凡人多烦事01/article/detail/332659
推荐阅读
相关标签
  

闽ICP备14008679号