其他分享
首页 > 其他分享> > Tcp网络通讯详解二(解决分包粘包)

Tcp网络通讯详解二(解决分包粘包)

作者:互联网

解决分包粘包

系统缓冲区

要想知道为什么在Tcp通讯中会存在分包粘包的现象,首先你必须先了解Tcp网络通讯的消息传播机制,而系统缓冲区将是不得不讲的一个话题,那么什么是系统缓冲区呢?其实就是接到对端信息数据的时候,操作系统会将数据存入到Socket的接收缓冲区中,而在这一段时间,系统缓冲区完全是由操作系统进行操作,程序并不能直接操作它们,只能通过Socket.Receive();Socket.Send等方法来间接进行操作。其中,Socket.Receive()方法只是把接收缓冲区的数据提取出来, 比如调用Receive(readBuff,0,2) , 接收2个字节的数据到了用户缓冲区readbuff,当系统的接收缓冲区为空, Receive方法会被阻塞, 直到里面有数据。同样地, Socket的Send方法只是把数据写入到发送缓冲区里, 具体的发送过程由操作系统负责。当操作系统的发送缓冲区满了, Send方法将会阻塞。

粘包半包现象

粘包

半包

解决粘包问题的方法

一般有三种方法可以解决粘包和半包问题, 分别是长度信息法、固定长度法和结束符号法。一般的游戏开发会在每个数据包前面加上长度字节, 以方便解析, 本文也将详细介绍这种方法。

长度信息法

长度信息法是指在每个数据包前面加上长度信息。每次接收到数据后, 先读取表示长度的字节, 如果缓冲区的数据长度大于要取的字节数, 则取出相应的字节, 否则等待下一次数据接收。假如客户端要发送“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