Tcp网络通讯详解二(解决分包粘包)
作者:互联网
解决分包粘包
系统缓冲区
要想知道为什么在Tcp通讯中会存在分包粘包的现象,首先你必须先了解Tcp网络通讯的消息传播机制,而系统缓冲区将是不得不讲的一个话题,那么什么是系统缓冲区呢?其实就是接到对端信息数据的时候,操作系统会将数据存入到Socket的接收缓冲区中,而在这一段时间,系统缓冲区完全是由操作系统进行操作,程序并不能直接操作它们,只能通过Socket.Receive();Socket.Send等方法来间接进行操作。其中,Socket.Receive()方法只是把接收缓冲区的数据提取出来, 比如调用Receive(readBuff,0,2) , 接收2个字节的数据到了用户缓冲区readbuff,当系统的接收缓冲区为空, Receive方法会被阻塞, 直到里面有数据。同样地, Socket的Send方法只是把数据写入到发送缓冲区里, 具体的发送过程由操作系统负责。当操作系统的发送缓冲区满了, Send方法将会阻塞。
粘包半包现象
粘包
- 如果发送端快速发送多条数据,接收端没有及时调用Receive,那么数据便会在接收端的缓冲区中累积。客户端先发送“ 1、2、3、4"四个字节的数据,紧接看又发送“ 5、6、7、8"四个字节的数据。等到服务端调用Receive时,服务端操作系统巳经将接收到的数据全部写入缓冲区,共接收到8个数据。这样一来,明明对方发送的是两条消息,但却当成了一条数据进行处理,明显与功能不符。Receive方法返回多少个数据,取决于操作系统接收缓冲区中存放的内容。
半包
- 发送端发送的数据还有可能被拆分,如发送“ HelloWorld",但在接收端调用Receive时,操作系统只接收到了部分数据,如“ Hel ” ,在等待一小段时间后再次调用Receive才接收到另一部分数据“ loWorld"。这样一来对方明明发送的是一条消息,但却被当成了两条进行处理,肯定也是不能符合规则。
解决粘包问题的方法
一般有三种方法可以解决粘包和半包问题, 分别是长度信息法、固定长度法和结束符号法。一般的游戏开发会在每个数据包前面加上长度字节, 以方便解析, 本文也将详细介绍这种方法。
长度信息法
长度信息法是指在每个数据包前面加上长度信息。每次接收到数据后, 先读取表示长度的字节, 如果缓冲区的数据长度大于要取的字节数, 则取出相应的字节, 否则等待下一次数据接收。假如客户端要发送“HelloWorld”,那么为了让服务端判断是不是接收到了完整的消息,通常在“HelloWorld”前面加上长度即“10HelloWorld”。加入服务端第一次Receive接收到的是“10Hello”,先读取第一个字节“10”,这时候服务端知道了完整消息的长度,而显然此时消息的长度并不能达到要求,所以服务端不进行任何处理,等待下一次接收。这样就可以保证每次接收到的消息都是完整的。
结束符号法
规定一个结束符号,作为消息间的分隔符假设规定结束符号为"@",那么发送" Hello" “Unity"两条信息可以发送成"Hello@“和“Unity@”接收方每次读取数据,直到”@ ” 出现为止,并且使用“ @ “ 去分割消息。比如接收方第一次读到“Hello@Un”,那它把结束符前面的Hello提取出来,作为第一条消息去处理,再把“Un"保存起来。待后续读到“ ity@". 再把“ Un"和“ ity"拼成第二条消息。
固定长度法
每次都以相同的长度发送数据,假设规定每条信息的长度都为10个字符,那么发送"Hello" “Unity"两条信息可以发送成“ Hello…” “Unity… “,其中的”.”表示填充字符,是为凑数,没有实际意义,只为了每次发送的数据都有固定长度,接收方每次读取10个字符,作为一条消息去处理。如果读到的字符数大于10,比如第1次读到“Hello…Un” , 那它只要把前10个字节“Hello "抽取出来,再把后面的两个字节”Un"存起来,等到再次接收数据,拼接第二条信息。
代码实现
本文会展示在异步客户端上,实现带有32字节长度信息的协议,来解决粘包问题。用Vs创建控制台应用进行测试长度信息发解决分包粘包问题。
客户端代码:
using System;
using System.Collections.Generic;
using System.Linq;
using System.Net;
using System.Net.Sockets;
using System.Text;
using System.Threading.Tasks;
namespace Socket_Package_Test
{
/// <summary>
/// 客户端
/// </summary>
class _Client
{
Socket socket;
int BUFFER_SIZE = 1024;//缓冲区的长度
byte[] readBuff;//接收消息的缓冲区
int nowBuffLength = 0;//缓冲区现在的字节长度
byte[] fontBuff=new byte[sizeof(Int32)];//接收到消息数组的长度的数组
public _Client() {
readBuff = new byte[BUFFER_SIZE];
StartClient();
}
//开始启动客户端
void StartClient() {
Console.WriteLine("开始启动客户端");
socket = new Socket(AddressFamily.InterNetwork, SocketType.Stream, ProtocolType.Tcp);
IPAddress ipAdr = IPAddress.Parse("127.0.0.1");
IPEndPoint end = new IPEndPoint(ipAdr, 1234);
socket.Connect(end);
try
{
socket.BeginReceive(readBuff,nowBuffLength,BUFFER_SIZE-nowBuffLength,SocketFlags.None,ReceiveCb,socket);
}
catch (Exception)
{
//throw;
}
}
/// <summary>
/// 接收消息的回调函数
/// </summary>
/// <param name="ar"></param>
void ReceiveCb(IAsyncResult ar) {
Socket listen = (Socket)ar.AsyncState;
try
{
//本次接收到数据的字节长度
int count = listen.EndReceive(ar);
//现在缓存区中数据的字节长度
nowBuffLength += count;
//处理接收到的数据
HandleDate(listen);
//继续回调接收消息
listen.BeginReceive(readBuff, nowBuffLength, BUFFER_SIZE-nowBuffLength, SocketFlags.None, ReceiveCb, listen);
}
catch (Exception e)
{
// throw;
}
}
/// <summary>
/// 处理接收到的数据
/// </summary>
/// <param name="listen"></param>
void HandleDate(Socket listen) {
if (nowBuffLength<sizeof(Int32))//缓冲区中的数据长度小于四个字节
{
return;
}
//消息头长度的数组更新
Array.Copy(readBuff,fontBuff,sizeof(Int32));
//消息头(一段消息的长度)
int receiveLength = BitConverter.ToInt32(fontBuff,0);
if (nowBuffLength<sizeof(Int32)+receiveLength)//如果缓冲区小于4字节+有效消息的长度(消息还没有接收完)
{
//一段话没有接收完整,等待下次一块处理
return;
}
//解析出一条消息
string str = UTF8Encoding.UTF8.GetString(readBuff,sizeof(Int32),receiveLength);
Console.WriteLine("接收到服务端的消息:"+str);
Console.WriteLine("客户端回复:");
string s = Console.ReadLine();
if (s != "")
{
//服务器发送消息
SendDate(listen, s);
}
//清除掉已经处理过的数据
int remainCount = nowBuffLength - sizeof(Int32) - receiveLength;
//把剩余没有处理的本次接收到的数据重新拷贝到缓存区
Array.Copy(readBuff,receiveLength+sizeof(Int32),readBuff,0,remainCount);
//缓冲区现在的数据长度
nowBuffLength = remainCount;
if (nowBuffLength>0)
{
HandleDate(listen);
}
}
/// <summary>
/// 发送消息
/// </summary>
/// <param name="listen"></param>
/// <param name="str"></param>
void SendDate(Socket listen,string str) {
//消息的内容数组(消息体)
byte[] sendDate = UTF8Encoding.UTF8.GetBytes(str);
//消息体的字节长度
int dateLength = sendDate.Length;
//要发送消息的长度的数组(消息头)
byte[] length = BitConverter.GetBytes(dateLength);
//消息头和消息体进行拼接
byte[] bytes = length.Concat(sendDate).ToArray();
listen.Send(bytes);
}
}
}
服务端代码:
using System;
using System.Collections.Generic;
using System.Linq;
using System.Net;
using System.Net.Sockets;
using System.Text;
using System.Threading.Tasks;
namespace Socket_Package_Test
{
/// <summary>
/// 服务端
/// </summary>
class _Socket
{
Socket socket;
int BUFFER_SIZE = 1024;//缓冲区的长度
byte[] readBuff ;//接收消息的缓冲区
int nowBuffLength=0;//缓冲区现在的字节长度
byte[] fontBuff=new byte[sizeof(Int32)];//接收到消息数组的长度的数组
int fontLenth = sizeof(Int32);//消息头占用的字节长度
int receiveDateLength;//接收到消息的长度
public _Socket() {
readBuff = new byte[BUFFER_SIZE];
StartServer();
}
int maxListen = 50;
/// <summary>
/// 开启服务器
/// </summary>
void StartServer()
{
Console.WriteLine("开始启动服务器");
socket = new Socket(AddressFamily.InterNetwork, SocketType.Stream, ProtocolType.Tcp);
IPAddress ipAdr = IPAddress.Parse("127.0.0.1");
IPEndPoint end = new IPEndPoint(ipAdr, 1234);
socket.Bind(end);
socket.Listen(maxListen);
try
{
socket.BeginAccept(AcceptCb, null);
}
catch (Exception)
{
// throw;
}
}
/// <summary>
/// BeginAccept的回调
/// </summary>
/// <param name="ar"></param>
void AcceptCb(IAsyncResult ar)
{
Socket listen = socket.EndAccept(ar);//直到有人连接服务器 返回连接者的Socket
Console.WriteLine(listen.RemoteEndPoint+"进入房间");
SendDate(listen,"欢迎您进入房间");
//开始接收消息
listen.BeginReceive(readBuff,nowBuffLength,BUFFER_SIZE-nowBuffLength,SocketFlags.None,ReceiveCb,listen);
socket.BeginAccept(AcceptCb, null);
}
/// <summary>
/// 接收消息的回调
/// </summary>
/// <param name="ar"></param>
void ReceiveCb(IAsyncResult ar) {
Socket listen = ar.AsyncState as Socket;
try
{
//本次接收到的数据长度
int count = listen.EndReceive(ar);
//现在的缓冲区长度增加新接收的长度
nowBuffLength += count;
//处理接收的数据
HandleDate(listen);
//循环接收消息
listen.BeginReceive(readBuff, nowBuffLength, BUFFER_SIZE-nowBuffLength, SocketFlags.None, ReceiveCb, listen);
}
catch (Exception)
{
// throw;
}
}
/// <summary>
/// 处理服务器接收的消息
/// </summary>
void HandleDate(Socket listen) {
if (nowBuffLength<=fontLenth)//缓冲区中的数据长度小于四个字节
{
return;
}
//消息头长度的数组更新
Array.Copy(readBuff,fontBuff,fontLenth);
//消息头(一段消息的长度)
receiveDateLength = BitConverter.ToInt32(fontBuff,0);
if (nowBuffLength<sizeof(Int32)+receiveDateLength)//如果缓冲区小于4字节+有效消息的长度(消息还没有接收完)
{
return;
}
//解析出一条消息
string str = UTF8Encoding.UTF8.GetString(readBuff,sizeof(Int32),receiveDateLength);
Console.WriteLine("收到客户端消息:"+str);
Console.WriteLine("服务端回复:");
string s = Console.ReadLine();
if (s!="")
{
//服务器发送消息
SendDate(listen, s);
}
int remainCount = nowBuffLength-sizeof(Int32)-receiveDateLength;
//把剩余没有处理的本次接收到的数据重新拷贝到缓存区
Array.Copy(readBuff,sizeof(Int32)+ receiveDateLength, readBuff,0,remainCount);
//缓冲区现在的数据长度
nowBuffLength = remainCount;
if (nowBuffLength>0)//如果缓冲区还有数据
{
HandleDate(listen);
}
}
/// <summary>
/// 发送消息
/// </summary>
/// <param name="listen"></param>
/// <param name="str"></param>
void SendDate(Socket listen, string str) {
//消息的内容数组(消息体)
byte[] Date = UTF8Encoding.UTF8.GetBytes(str);
//消息体的字节长度
int sendLength = Date.Length;
//要发送消息的长度的数组(消息头)
byte[] sendDateLength = BitConverter.GetBytes(sendLength);
//消息头和消息体进行拼接
byte[] sendBytes = sendDateLength.Concat(Date).ToArray();
//发送给客户端
listen.Send(sendBytes);
}
}
}
上面的处理方式基本上解决了分包粘包的问题,但是还是存在一些问题,例如:大端小端问题、线程冲突问题。这些问题本片文章先不进行处理。除了这些问题之外还存在一些其他的不足之处,例如:在Copy操作的时候,每次成功接收一条完整的数据后,程序会调用Array.Copy,将缓冲区的数据往前移动。但Array.Copy是个时间复杂度为o(n)的操作,假如缓冲区中的数据很多,那移动全部数据将会花费较长的时间。一个可行的办法是,使用ByteArray结构作为缓冲区,使用readldx指向的数据作为缓冲区的第一个数据,当接收完数据后,只移动readldx,时间复杂度为o(l),当然,肯定还有一些其它的问题,这里就不一一列举了。
军礼 发布了12 篇原创文章 · 获赞 2 · 访问量 1476 私信 关注标签:Socket,Tcp,粘包,消息,网络通讯,缓冲区,长度,接收,listen 来源: https://blog.csdn.net/weixin_42498461/article/details/104625780