Socket通信中粘包分包问题的介绍和解决(C#)

最近在做Unity局域网时,用到了Socket通信基于TCP协议,然后使用异步方式,主要用到了BeginAccept和BeginReceive方法

然而就可以实现异步通信,然而还是要解决粘包和分包问题

这里我先说明一下什么是分包和粘包,TCP提供面向连接的、可靠的数据流传输,所以当我们发送数据在短时间内比较频繁并且数据量比较小时,TCP为了优化内存资源,会将多条数据粘成几个包来进行处理,相比发送的消息条数要少了很多。比如,客户端在非常的时间内向服务器发送了100条数据,每条数据的字节长度又比较小,那么在服务器端收到的可能只有几条数据,因为粘包,这100条数据粘成了几个包,这样优化了内存资源,这其实是TCP的一种优化机制,这看似是一种好处,但是在开发网络游戏时,反而是坏处,因为在网络游戏中,客户端需要频繁向服务器发送多条数据,如果出现了粘包情况,可能会出现画面帧不同步。

接着数一下什么是分包,当发送的一条数据的数据量比较大的时候,而服务器的数据缓冲区一次却装不下那么多的数据,所以就会将这一条数据分成 几个包,也就是说把该条数据分割成几个数据,因为数据发送接收都是以字节数组的形式传输,所以可能会出现数据的丢失,这种情况在实际开发中也需要避免。

      粘包举例:假设主机A第一次发送了: ab   第二次发送了:cd    第三次发送了:ef  第四次发送了:gh      然后主机B接收了数据,主机A分别四次发送了数据,由于发送时间短,数据量小,在主机B却只收到两条数据,第一条数据是:abcd,  第二条数据是efgh,

    粘包举例:假设主机A发送了一条非常大的数据,这条数据的字节长度是2048非常庞大,超过了主机B的数据缓冲区的长度1024,所以主机B在接收数据时,因为存不下那么大的数据,会将该条数据分成两条,第一条数据的字节长度是1024,第二条数据的字节长度也是1024

解决粘包分包问题:

    由于传输的数据是字节数组,我们可以得到该条数据字节数组的长度,长度是一个整型,并且所占字节长度固定为4,所以可以将该整型数据转成字节数组,不管消息的长度多大,该整型数据的字节数组长度始终为4(因为sizeof(int) = 4),所以发送数据的时候,在该条数据的前面加上这个整型数据的字节数组,也就是说,发出去的数据,前四个字节表示实际消息的长度,后面的字节表示实际消息。比如我们要发送 HelloWorld, 实际上发送的是4HelloWorld。

代码如下:

服务器端:

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Net;
using System.Net.Sockets;

namespace TCP服务器端
{
    class Program
    {
        static void Main(string[] args)
        {
            AsyncStart();
            Console.ReadKey();
        }
        //static byte[] dataBuffer = new byte[1024];
        //异步Socket
        static void AsyncStart() {
            Socket serverSocket = new Socket(AddressFamily.InterNetwork, SocketType.Stream, ProtocolType.Tcp);
            IPEndPoint ipEndPoint = new IPEndPoint(IPAddress.Parse("192.168.137.1"), 8888);
            serverSocket.Bind(ipEndPoint);
            serverSocket.Listen(0);
            Console.WriteLine("服务器启动成功!");

            //Socket clientSocket = serverSocket.Accept();
            //实现可以连接多个客户端
            serverSocket.BeginAccept(AcceptCb, serverSocket);

            
        }
        static Message msg = new Message();
        static void AcceptCb(IAsyncResult ar) {
            try
            {
                Socket serverSocket = ar.AsyncState as Socket;
                Socket clientSocket = serverSocket.EndAccept(ar);
                //发送数据
                string msgStr = "Hello stupid 人类...";
                byte[] data = System.Text.Encoding.UTF8.GetBytes(msgStr);
                clientSocket.Send(data);

                //接收数据  实现服务端能够接收多条数据
                clientSocket.BeginReceive(msg.data, msg.startIndex, msg.remainSize, SocketFlags.None, ReceiveCb, clientSocket);

                serverSocket.BeginAccept(AcceptCb, serverSocket);  //循环调用
            }
            catch (Exception e) {
                Console.WriteLine("AcceptCb失败:" + e.Message);
            }
        }
        static void ReceiveCb(IAsyncResult ar) {
            try
            {
                Socket clientSocket = ar.AsyncState as Socket;
                int count = clientSocket.EndReceive(ar);
                Console.WriteLine("Receive回调的消息长度:" + count);
                if (count == 0)  //用来判断客户端是否正常判断,不加可能服务器端循环打印或者服务器故障
                {
                    Console.WriteLine("count<=0");
                    clientSocket.Close();
                    return;
                }
                msg.AddCount(count);
                //string msgStr = Encoding.UTF8.GetString(msg.data, 0, count);
                //Console.WriteLine("收到客户端消息:"+msg);
                msg.ReadMessage();
                clientSocket.BeginReceive(msg.data, msg.startIndex, msg.remainSize, SocketFlags.None, ReceiveCb, clientSocket);  //循环调用
            }
            catch (Exception e) {
                Console.WriteLine("ReceiveCb异常:" + e);
            }
        }

}

----服务器静态类:

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;

namespace TCP服务器端
{
    class Message
    {
        public byte[] data = new byte[1024];
        public int startIndex = 0; //data里面存了多少字节,也是下一次读取的一个索引
        public int remainSize;
        
        public Message() {
            remainSize = data.Length - startIndex;    
        }
        public void AddCount(int count) {
            startIndex += count;
            remainSize = data.Length - startIndex;
        }
        //由于客户端发送的数据比较频繁,会出现粘包,99条数据最终可能粘成了2-3个包,
        public void ReadMessage() {
            while (true) {
                if (startIndex <= 4) {    //解决粘包问题
                    Console.WriteLine("不构成一条消息");
                    return; 
                }
                int count = BitConverter.ToInt32(data, 0); //得到消息长度
                Console.WriteLine("count : " + count);
                Console.WriteLine("startIndex-4 : " + (startIndex - 4));
                if ((startIndex - 4) >= count)  //缓冲中待处理的数据个数是否大于本次发送的消息长度
                {
                    string s = Encoding.UTF8.GetString(data, 4, count);
                    Console.WriteLine("收到数据:" + s);
                    Array.Copy(data, count + 4, data, 0, startIndex-4-count);
                    startIndex -= (count + 4);
                }
                else {    //  解决分包问题
                    Console.WriteLine("消息过长不做处理");
                    break;
                }

            }
        }
    }
}
 

客户端代码

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Net;
using System.Net.Sockets;

namespace TCP客户端
{
    class Program
    {
        static void Main(string[] args)
        {
            Socket clientSocket = new Socket(AddressFamily.InterNetwork, SocketType.Stream, ProtocolType.Tcp);
            clientSocket.Connect("192.168.137.1", 8888);
            //收到消息
            byte[] data = new byte[1024];
            int count = clientSocket.Receive(data);
            string receiveStr = System.Text.Encoding.UTF8.GetString(data, 0, count);
            Console.WriteLine("收到服务器消息:" + receiveStr);
            //发送消息
            //while (true)
            //{
            //    //string msg = Console.ReadLine();
            //    //if (msg == "c") {
            //    //    clientSocket.Close();
            //    //    return;
            //    //}
            //}
            string s = @"这是一条文字消息发生看了感觉as弗兰克见鬼十法的开奖号噶水电费就考了个和接口辣鸡啊撒发圣诞节快乐发货
            撒大计科了访华sad接口里发哈圣诞节快乐这是一条文字消息发生看了感觉as弗兰克见鬼十法的开奖号噶水电费就考了个和接口
            辣鸡啊撒发圣诞节快乐发货这是一条文字消息发生看了感觉as弗兰克见鬼十法的开奖号噶水电费就考了个和接口辣鸡啊撒发圣诞
            节快乐发货这是一条文字消息发生看了感觉as弗兰克见鬼十法的开奖号噶水电费就考了个和接口辣鸡啊撒发圣诞节快乐发货
            撒大计科了访华sad接口里发哈圣诞节快乐这是一条文字消息发生看了感觉as弗兰克见鬼十法的开奖号噶水电费就考了个和接口
            辣鸡啊撒发圣诞节快乐发货这是一条文字消息发生看了感觉as弗兰克见鬼十法的开奖号噶水电费就考了个和接口辣鸡啊撒发圣诞
            节快乐发货这是一条文字消息发生看了感觉as弗兰克见鬼十法的开奖号噶水电费就考了个和接口辣鸡啊撒发圣诞节快乐发货
            撒大计科了访华sad接口里发哈圣诞节快乐这是一条文字消息发生看了感觉as弗兰克见鬼十法的开奖号噶水电费就考了个和接口
            辣鸡啊撒发圣诞节快乐发货这是一条文字消息发生看了感觉as弗兰克见鬼十法的开奖号噶水电费就考了个和接口辣鸡啊撒发圣诞
            节快乐发货这是一条文字消息发生看了感觉as弗兰克见鬼十法的开奖号噶水电费就考了个和接口辣鸡啊撒发圣诞节快乐发货
            撒大计科了访华sad接口里发哈圣诞节快乐这是一条文字消息发生看了感觉as弗兰克见鬼十法的开奖号噶水电费就考了个和接口
            辣鸡啊撒发圣诞节快乐发货这是一条文字消息发生看了感觉as弗兰克见鬼十法的开奖号噶水电费就考了个和接口辣鸡啊撒发圣诞
            节快乐发货这是一条文字消息发生看了感觉as弗兰克见鬼十法的开奖号噶水电费就考了个和接口辣鸡啊撒发圣诞节快乐发货
            撒大计科了访华sad接口里发哈圣诞节快乐这是一条文字消息发生看了感觉as弗兰克见鬼十法的开奖号噶水电费就考了个和接口
            辣鸡啊撒发圣诞节快乐发货这是一条文字消息发生看了感觉as弗兰克见鬼十法的开奖号噶水电费就考了个和接口辣鸡啊撒发圣诞
            节快乐发货这是一条文字消息发生看了感觉as弗兰克见鬼十法的开奖号噶水电费就考了个和接口辣鸡啊撒发圣诞节快乐发货
            撒大计科了访华sad接口里发哈圣诞节快乐这是一条文字消息发生看了感觉as弗兰克见鬼十法的开奖号噶水电费就考了个和接口
            辣鸡啊撒发圣诞节快乐发货这是一条文字消息发生看了感觉as弗兰克见鬼十法的开奖号噶水电费就考了个和接口辣鸡啊撒发圣诞
            节快乐发货这是一条文字消息发生看了感觉as弗兰克见鬼十法的开奖号噶水电费就考了个和接口辣鸡啊撒发圣诞节快乐发货
            撒大计科了访华sad接口里发哈圣诞节快乐这是一条文字消息发生看了感觉as弗兰克见鬼十法的开奖号噶水电费就考了个和接口
            辣鸡啊撒发圣诞节快乐发货这是一条文字消息发生看了感觉as弗兰克见鬼十法的开奖号噶水电费就考了个和接口辣鸡啊撒发圣诞
            节快乐发货";

            while (true){
                string input = Console.ReadLine();
                if (input == "for")
                {
                    for (int i = 1; i < 100; i++)
                    {
                        clientSocket.Send(Message.GetBytes(i.ToString() + "长度"));
                    }
                }
                else
                {
                    clientSocket.Send(Message.GetBytes(s));
                }
            }

            


            Console.ReadKey();
            clientSocket.Close();
        }
    }
}
// ----客户端Message类

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;

namespace TCP客户端
{
    class Message
    {
        public static byte[] GetBytes(string msg) {
            
            byte[] data = Encoding.UTF8.GetBytes(msg);
            int len = data.Length;
            byte[] lenBytes = BitConverter.GetBytes(len);
            byte[] sendBuffer = lenBytes.Concat(data).ToArray();
            return sendBuffer;
        }
    }
}

先启动服务器程序,然后启动客户端程序

然后在客户端控制台输入:for

在客户端可以看到:

如上可以看到服务器照样收到了99条消息,并没有出现粘包现象,然后在客户端控制台输入a,在服务器端控制台可以看到

如上,由于发送的数据量比较大,所以服务器端对其不做处理,直接清空缓存区

猜你喜欢

转载自blog.csdn.net/qq_36450306/article/details/85088447