C# TCP网络编程5(分包和粘包二)

一:TCP粘包产生的原理
1,TCP粘包是指发送方发送的若干包数据到接收方接收时粘成一包,从接收缓冲区看,后一包数据的头紧接着前一包数据的尾。出现粘包现象的原因是多方面的,它既可能由发送方造成,也可能由接收方造成。

2,发送方引起的粘包是由TCP协议本身造成的,TCP为提高传输效率,发送方往往要收集到足够多的数据后才发送一包数据。若连续几次发送的数据都很少,通常TCP会根据优化算法把这些数据合成一包后一次发送出去,这样接收方就收到了粘包数据。接收方引起的粘包是由于接收方用户进程不及时接收数据,从而导致粘包现象。

3,这是因为接收方先把收到的数据放在系统接收缓冲区,用户进程从该缓冲区取数据,若下一包数据到达时前一包数据尚未被用户进程取走,则下一包数据放到系统接收缓冲区时就接到前一包数据之后,而用户进程根据预先设定的缓冲区大小从系统接收缓冲区取数据,这样就一次取到了多包数据。
三:解决原理及代码实现
1,采用包头(固定长度,里面存着包体的长度,发送时动态获取)+包体的传输机制。如图
C# TCP网络编程5(分包和粘包二)
HeaderSize 存放着包体的长度,其HeaderSize本身是定长4字节;
一个完整的数据包(L)=HeaderSize+BodySize;、
2,分包算法

其基本思路是首先将待处理的接收数据流即系统缓冲区数据(长度设为M)强行转换成预定的结构数据形式,并从中取出结构数据长度字段L,而后根据包头计算得到第一包数据长度。

   M=系统缓冲区大小;L=用户发送的数据包=HeaderSize+BodySize;
   发送数据的时候,先构造固定长度的表头数据(比如4个字节)+需要发送的数据

C# TCP网络编程5(分包和粘包二)

///上述内容引用https://www.cnblogs.com/wangjun8868/p/7160661.html

  class Program
    {
        static void Main(string[] args)
        {
            ///客户端代码
            Socket clientSocket = new Socket(AddressFamily.InterNetwork, SocketType.Stream, ProtocolType.Tcp);
            IPEndPoint ipEndPoint = new IPEndPoint(IPAddress.Parse("192.168.1.102"), 3344);
            clientSocket.Connect(ipEndPoint);

            byte[] receiveBuffer = new byte[1024];
            int count = clientSocket.Receive(receiveBuffer);

            string msg = Encoding.UTF8.GetString(receiveBuffer, 0, count);
            Console.WriteLine("接收到服务端的消息:" + msg);
            for (int i = 0; i < 100; i++) ///客户端启动向服务端发送250条数据
            {
                clientSocket.Send(SendMsg(i.ToString()));
            }
            Console.ReadKey();
        }

        /// <summary>
        /// 构造发送数据
        /// </summary>
        /// <param name="msg"></param>
        /// <returns></returns>
        public static byte[] SendMsg(string msg)
        {
            int length = msg.Length;
            //构造表头数据,固定4个字节的长度,表示内容的长度
            byte[] headerBytes = BitConverter.GetBytes(length);
            //构造内容
            byte[] bodyBytes = Encoding.UTF8.GetBytes(msg);
            byte[] tempBytes = new byte[headerBytes.Length + bodyBytes.Length];
            ///拷贝到同一个byte[]数组中,发送出去..
            Buffer.BlockCopy(headerBytes, 0, tempBytes, 0, headerBytes.Length);
            Buffer.BlockCopy(bodyBytes, 0, tempBytes, headerBytes.Length, bodyBytes.Length);
            return tempBytes;
        }
    }

/////服务端代码

    class Program
    {
        static void Main(string[] args)
        {
            ///服务端代码
            Socket serverSocket = new Socket(AddressFamily.InterNetwork, SocketType.Stream, ProtocolType.Tcp);
            IPAddress ip = IPAddress.Parse("192.168.1.102");
            IPEndPoint ipEndPoint = new IPEndPoint(ip, 3344);

            serverSocket.Bind(ipEndPoint);
            serverSocket.Listen(0);//开启监听

            Console.WriteLine("服务器启动");
            //开始异步接收客户端
            serverSocket.BeginAccept(AcceptAsyncCallBack, serverSocket);
            Console.ReadKey();
        }
        /// <summary>
        /// 异步等待客户端回调方法
        /// </summary>
        /// <param name="ar"></param>
        private static void AcceptAsyncCallBack(IAsyncResult ar)
        {

            Socket serverSocket = ar.AsyncState as Socket;//传递过来的参数
            Socket clientSokcet = serverSocket.EndAccept(ar); //一个客户端连接过来了

            string msg = ":Hello client! 你好......";
            byte[] dataBytes = Encoding.UTF8.GetBytes(msg);  //网络连接收发数据,只能发送byte[] 字节数组
            clientSokcet.Send(dataBytes);
            ///messageHandle.DataBuffer缓存区,messageHandle.ContentSize(缓存区中已经存在的内容长度开始存)
            ///  messageHandle.remainSize 缓存区中剩余可以存储的空间
            clientSokcet.BeginReceive(messageHandle.DataBuffer, messageHandle.ContentSize, messageHandle.remainSize, SocketFlags.None, ReceiveCallBack, clientSokcet); //开始异步接收数据


            serverSocket.BeginAccept(AcceptAsyncCallBack, serverSocket);//循环等待客户端接收....
        }

        static MessageHandle messageHandle = new MessageHandle();
        /// <summary>
        /// 异步接收数据回调方法
        /// </summary>
        /// <param name="ar"></param>
        private static void ReceiveCallBack(IAsyncResult ar)
        {
            Socket clientSocket = null;
            try
            {
                clientSocket = ar.AsyncState as Socket;
                int count = clientSocket.EndReceive(ar);    //接收到的数据量
                if (count == 0) ///说明客户端已经已经断开连接了
                {
                    if (clientSocket != null)
                    {
                        clientSocket.Close();
                    }
                    return;
                }
                //j解析数据(把新接收的数据传入)
                messageHandle.ReadMessage(count);
                //开始异步接收数据
                clientSocket.BeginReceive(messageHandle.DataBuffer, messageHandle.ContentSize, messageHandle.remainSize, SocketFlags.None, ReceiveCallBack, clientSocket); 

            }
            catch (Exception e)///说明客户端已经已经断开连接了,异常断开
            {
                Console.WriteLine(e);
                if (clientSocket != null)
                {
                    clientSocket.Close();
                }
            }
        }
    }
服务午安数据发送,或者接收数据解析类
  public class MessageHandle
    {
        //表头的数据长度为4个个字节,表示后面的数据的长度
        //保证能够每次接收发送的消息小于1024bit大小,否则无法完整接收整条数据
        private byte[] dataBuffer = new byte[1024];
        //从dataBuffer已经存了多少个字节数据
        private int contentSize = 0;

        public int ContentSize {
            get { return contentSize; }
        }
        /// <summary>
        /// 剩余多少存储空间
        /// </summary>
        public int remainSize {
            get { return dataBuffer.Length - contentSize; }
        }

        public byte[] DataBuffer {
            get { return dataBuffer; }
        }

        /// <summary>
        /// 解析数据 ,count 新读取到的数据长度
        /// </summary>
        public void ReadMessage(int count)
        {
            contentSize += count;
            //用while表示缓存区,可能有多条数据
            while (true)
            {
                //缓存区小于4个字节,表示连表头都无法解析
                if (contentSize <= 4) return;
                //读取四个字节数据,代表这条数据的内容长度(不包括表头的4个数据)
                int receiveCount = BitConverter.ToInt32(dataBuffer, 0);
                //缓存区中的数据,不够解析一条完整的数据
                if (contentSize - 4 < receiveCount) return;

                //2、解析数据
                //从除去表头4个字节开始解析内容,解析的数据长度为(表头数据表示的长度)
                string receiveStr = Encoding.UTF8.GetString(dataBuffer, 4, receiveCount);

                Console.WriteLine("接收的客户端数据:" + receiveStr);
                //把剩余的数据Copy到缓存区头部位置
                Array.Copy(dataBuffer, 4 + receiveCount, dataBuffer, 0, contentSize - 4 - receiveCount);

                contentSize = contentSize - 4 - receiveCount;
            }
        }

        /// <summary>
        /// 构造发送数据
        /// </summary>
        /// <param name="msg"></param>
        /// <returns></returns>
        public byte[] SendMsg(string msg)
        {
            int length = msg.Length;
            //构造表头数据,固定4个字节的长度,表示内容的长度
            byte[] headerBytes = BitConverter.GetBytes(length);
            //构造内容
            byte[] bodyBytes = Encoding.UTF8.GetBytes(msg);
            byte[] tempBytes = new byte[headerBytes.Length + bodyBytes.Length];
            ///拷贝到同一个byte[]数组中,发送出去..
            Buffer.BlockCopy(headerBytes, 0, tempBytes, 0, headerBytes.Length);
            Buffer.BlockCopy(bodyBytes, 0, tempBytes, headerBytes.Length, bodyBytes.Length);
            return tempBytes;
        }

    }
for循环中向服务端发送100条数据实验,服务端正确解析100条数据

C# TCP网络编程5(分包和粘包二)

for循环中向服务端发送1000条数据实验,服务端正确解析1000条数据

C# TCP网络编程5(分包和粘包二)