简易游戏服务器—分包与粘包问题

6.1、什么是粘包和分包

粘包和分包是利用Socket在TCP协议下内部的优化机制。

1、什么是粘包

只有TCP有粘包现象,UDP永远不会粘包,为何,且听我娓娓道来。发送数据时间间隔很短,数据了很小,也就是发送数据比较频繁,会合到一起,产生粘包;

2、什么是分包

当我们发送的数据量很大的时候,可能是几千字节,TCP就会自动分开发送,其实说通俗点,就是你去拿快递,一看20个,一次拿不完,分几次拿!

3、总结

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

发送方引起的粘包是由TCP协议本身造成的,TCP为提高传输效率,发送方往往要收集到足够多的数据后才发送一包数据。

若连续几次发送的数据都很少,通常TCP会根据优化算法把这些数据合成一包后一次发送出去,这样接收方就收到了粘包数据。接收方引起的粘包是由于接收方用户进程不及时接收数据,从而导致粘包现象。

这是因为接收方先把收到的数据放在系统接收缓冲区,用户进程从该缓冲区取数据,若下一包数据到达时前一包数据尚未被用户进程取走,则下一包数据放到系统接收缓冲区时就接到前一包数据之后,而用户进程根据预先设定的缓冲区大小从系统接收缓冲区取数据,这样就一次取到了多包数据。分包是指在出现粘包的时候我们的接收方要进行分包处理。

在前面的测试程序中,是没有粘包问题的,这时候你可能有疑惑,我为啥数据会发送的特别快,我们以游戏服务器举例,比如游戏有联机对战功能,这时候肯定是需要同步位置信息的,这个频率是很快的,大约每秒就要40~80次,这个时候就会出现粘包问题。

6.2、刻意制造粘包与分包

其实很简单只要简单修改一下客户端即可。

1、程序测试 — 粘包问题

客户端:

for (int i = 0; i < 100; i++)

{

    clientSocket.Send(System.Text.Encoding.UTF8.GetBytes(i.ToString()));

}

服务器查看调试信息:

服务器已启动......

有一个客户端进行连接成功......

从客户端接收到的数据:0

从客户端接收到的数据:123

从客户端接收到的数据:4567

从客户端接收到的数据:8910

从客户端接收到的数据:1112131415

从客户端接收到的数据:161718

从客户端接收到的数据:192021222324

从客户端接收到的数据:25262728

从客户端接收到的数据:2930313233

从客户端接收到的数据:34353637

从客户端接收到的数据:38394041

从客户端接收到的数据:42434445

从客户端接收到的数据:46474849

从客户端接收到的数据:50515253

从客户端接收到的数据:5455565758

从客户端接收到的数据:59606162636465666768

从客户端接收到的数据:6970717273

从客户端接收到的数据:74757677

从客户端接收到的数据:78798081

从客户端接收到的数据:82838485

从客户端接收到的数据:86878889

从客户端接收到的数据:90919293

从客户端接收到的数据:9495969798

从客户端接收到的数据:99

(很明显数据没有发送100次)

1、程序测试 — 粘包问题

客户端:

string s = "000000000000000000000000000000000000000000000000000000000000000" +

            "0000000000000000000000000000000000000000000000000000000000000000" +

            "撒大声地所多所多所多所多所多所多所多所多所多所多所多所多所多所多" +

            "0000000000000000000000000000000000000000000000000000000000000000" +

            "0000000000000000000000000000000000000000000000000000000000000000" +

            "0000000000000000000000000000000000000000000000000000000000000000" +

            "撒大声地所多所多所多所多所多所多所多所多所多所多所多所多所多所多" +

            "撒大声地所多所多所多所多所多所多所多所多所多所多所多所多所多所多" +

            "撒大声地所多所多所多所多所多所多所多所多所多所多所多所多所多所多" +

            "0000000000000000000000000000000000000000000000000000000000000000" +

            "0000000000000000000000000000000000000000000000000000000000000000" +

            "0000000000000000000000000000000000000000000000000000000000000000" +

            "----------------------------------------------------------------" +

            "撒大声地所多所多所多所多所多所多所多所多所多所多所多所多所多所多" +

            "77777777777777777777777777777777777777777777777777777777777777777" +

            "90909090909090909090909090909090909090000000000000000000000000099";

clientSocket.Send(System.Text.Encoding.UTF8.GetBytes(s));

服务器查看调试信息:

服务器已启动......

有一个客户端进行连接成功......

从客户端接收到的数据:

0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000撒大声地所多所多所多所多所多所多所多所多所多所000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000撒大声地所多所多所多所多所多所多所多所多所多所多所多所多所多所多撒大声地所 多所多所多所多所多所多所多所多所多所多所多所多所多所多撒大声地所多所多所多所多所多所多所多所多所多所多所多所多所多所多000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000----------------------------------------------------------------撒大声地所多所多所多所多所多所多所多所多所?

从客户端接收到的数据:

77777777777777777777777777777777777777777777777777777777790909090909090909090909090909090909090000000000000000000000000099

(可以看出服务器是分两次接收的,但其实只要static byte[] dataBuffer = new byte[1024];给的空间足够大,分包问题就可解决)

6.3、解决粘包问题

6.3.1、理论图解

其实也很好解决,我们在发送数据的时候事先存储数据的长度,不过用来存储数据长度的内存大小需要指定好,否则就没法判断了。

假设我们现在的数据出现了粘包,如下图所示:

这里只是演示一下,如果只有连续发送4次数据,一般是不会出现粘包的,看上图橙色部分表示我们用一个int32类型储存数据的长度,蓝色部分为我们实际要发送的数据,现在发生了粘包,也就是这四条数据合在一起发送给了服务器,

此时这条数据的总大小为 4字节 * 4 + 5 + 7 + 10 + 4 = 42字节

我们通过读取4字节数据可以知道数据的实际长度,以第一个数据为例,我们读取4字节数据,知道了这个数据有5个字节,程序如下:

int data_length = BitConverter.ToInt32(_data, 0);

此时的data_length = 5;此时我们就读取这5个字节的数据即可!

string s = Encoding.UTF8.GetString(_data, 4, 5);

然后我们截取数据,从源数据的第4 + 5的位置开始截取到一个新数组,新字节数组索引从零开始,此时新字节数据的长度为42 - (5 + 4);(下图为新字节数组)

Array.Copy(_data, 5 + 4, _data, 0, 42 - (5 + 4));

依次循环下去,粘包就被成功的分包了。当然这个不要忘记每次更新一下当前数据长度。

_curLength = _curLength - (data_length + 4); // _curLength = 42 - (5 + 4)

6.3.2、代码演示

1、客户端

创建Message类,用于发送数据前做处理,使得首4字节储存数据长度。

Message:

using System;

using System.Linq;

using System.Text;

namespace TCPClient

{

    class Message

    {

        public static byte[] GetBytes(string data)

        {

            byte[] dataBytes = Encoding.UTF8.GetBytes(data);

            int dataLength = dataBytes.Length;

            // lengBytes只有四个字节。

            byte[] lengBytes = BitConverter.GetBytes(dataLength);

            // 数组拼接,得到真正的数据(前四个字节为数据长度)

            byte[] TrueData = lengBytes.Concat(dataBytes).ToArray();

            return TrueData;

        }

    }

}

Main:

using System;

using System.Net.Sockets;

using System.Net;

namespace TCPClient

{

    class Program

    {

        static void Main(string[] args)

        {

            Socket clientSocket = new Socket(AddressFamily.InterNetwork, SocketType.Stream, ProtocolType.Tcp);

            clientSocket.Connect(new IPEndPoint(IPAddress.Parse("127.0.0.1"), 89));

            for (int i = 0; i < 100; i++)

            {

                clientSocket.Send(Message.GetBytes(i.ToString() + "米"));

            }

            clientSocket.Close();

        }

    }

}

2、服务端

创建Message类,解决粘包问题!

using System;

using System.Text;

namespace TCPServer

{

    class Message

    {

        private byte[] _data = new byte[1024];

        // 存取了多少个字节的数据在数组里面,也是每次读取时候的位置。

        private int _curLength = 0;         

        public byte[] Data { get { return _data; } set { _data = value; } }

        public int CurLength { get { return _curLength; }}

        /// <summary>

        /// 得到当前数据长度

        /// </summary>

        /// <param name="count">本次接收的数据长度</param>

        public void AddCount(int count)

        {

            _curLength = count;

        }

        /// <summary>

        /// 解析数据

        /// </summary>

        public void ReadMessage()

        {

            while (true)

            {

                if (_curLength <= 4) return;

                // 从0位置开始读取,int32是4个字节,所以这里默认偏移4个字节。

                // 这里的data_length是真实的数据数据长度。

                int data_length = BitConverter.ToInt32(_data, 0);

                // _curLength - 4 是当前接收到的数据长度。

                // 当时接收的数据长度 > 真实的数据长度,说明出现了粘包。

                if (_curLength - 4 >= data_length)

                {

                    // 每次读取都从第4个数据开始读取。

                    string s = Encoding.UTF8.GetString(_data, 4, data_length);

                    Console.WriteLine("解析到一条数据:" + s);

                    Console.WriteLine(data_length);

                    Console.WriteLine(_curLength);

                    // 移动数据

      // Array.Copy(Array源数组, 源数组起始拷贝位置索引, 目标数组, 目标数组起始索引, 数据长度);

                    // _curLength - (data_length + 4)还剩多少个字节的数据。

                     Array.Copy(_data, data_length + 4, _data, 0,

                                         _curLength - (data_length + 4));

                    // 当前长度偏移。

                    _curLength = _curLength - (data_length + 4);

                }

                else

                {

                    break;

                }

            }// while end     

        }// ReadMessage end

    }

}

Main:

using System;

using System.Net.Sockets;

using System.Net;

namespace TCPServer

{

    class Program

    {

        static void Main(string[] args)

        {

            StartServerAsync();

            Console.ReadKey();

        }

        static Message message = new Message();

        // 异步

        static void StartServerAsync()

        {

            // 创建服务器

            Socket serverSocket = new Socket(AddressFamily.InterNetwork, SocketType.Stream,

                                                ProtocolType.Tcp);

            IPAddress ipAddress = IPAddress.Parse("127.0.0.1");

            IPEndPoint ipEndPoint = new IPEndPoint(ipAddress, 89);

            // 绑定IP和端口号。

            serverSocket.Bind(ipEndPoint);

            serverSocket.Listen(0);

            Console.WriteLine("服务器已启动......");

            // 采用异步的方式处理用户连接

            serverSocket.BeginAccept(AcceptCallBack, serverSocket);

        }

        /// <summary>

        /// 接收客户端的连接(异步操作)

        /// </summary>

        /// <param name="ar"></param>

        static void AcceptCallBack(IAsyncResult ar)

        {

            // 通过传递的参数,找到客户端Socket对象。

            Socket serverSocket = ar.AsyncState as Socket;

            Socket clientSocket = serverSocket.EndAccept(ar);

            Console.WriteLine("有一个客户端进行连接成功......");

            // 给客户发送消息

            string msgStr = "Hello client! 你好....";

            byte[] data = System.Text.Encoding.UTF8.GetBytes(msgStr);

            clientSocket.Send(data);

            // 开始异步接收数据。

            clientSocket.BeginReceive(message.Data, 0, 1024,

                            SocketFlags.None, ReceiveCallBack, clientSocket);

            // 重新调用中断,起到循环等待客户端连接的目的

            serverSocket.BeginAccept(AcceptCallBack, serverSocket);

        }

        // 准备一下数据存放的空间。

        // static byte[] dataBuffer = new byte[1024];

        /// <summary>

        ///  异步接收客户端发来的数据

        /// </summary>

        /// <param name="ar">这个参数必须有,异步结果</param>

        static void ReceiveCallBack(IAsyncResult ar)

        {

            // 通过传递的参数,找到客户端Socket对象。

            Socket clientSocket = null;

            try

            {

                clientSocket = ar.AsyncState as Socket;

                int count = clientSocket.EndReceive(ar);

                // 只要客户端是正常关闭,服务器就会接收到空的数据。这时关闭客户端即可!

                // 这里注意哈,如果客户端发送的是空消息,但其实是空字符串,客户端的连接不会关闭!

                if (count == 0)

                {

                    clientSocket.Close();

                    Console.WriteLine("有客户端退出.....");

                    return;

                }

                // 数据接收完毕。

                message.AddCount(count);

                // string msg = System.Text.Encoding.UTF8.GetString(dataBuffer, 0, count);

                // Console.WriteLine("从客户端接收到的数据:" + msg);

                message.ReadMessage();

                // 开始异步接收数据。

                clientSocket.BeginReceive(message.Data, 0, 1024,

                                SocketFlags.None, ReceiveCallBack, clientSocket);

            }

            catch (Exception e)

            {

                Console.WriteLine(e);

                // 出现异常就关闭与客户端的连接,这个异常一般是客户端强行退出。

                if (clientSocket != null)

                {

                    clientSocket.Close();

                }

            }

        }

    }// end class

}

服务器查看调试信息:

服务器已启动......

有一个客户端进行连接成功......

解析到一条数据:0米

4

8

解析到一条数据:1米

4

630

解析到一条数据:2米

4

622

解析到一条数据:3米

4

614

解析到一条数据:4米

4

606

解析到一条数据:5米

4

598

解析到一条数据:6米

4

590

解析到一条数据:7米

4

582

解析到一条数据:8米

4

574

解析到一条数据:9米

4

566

解析到一条数据:10米

5

558

..............................................................................

解析到一条数据:98米

5

18

解析到一条数据:99米

5

9

有客户端退出.....

6.4、解决分包问题

你解决个屁,异步接收的情况下,把_data数组调大点就完了,傻逼,咱们是做游戏!一般不会有分包问题!!

private byte[] _data = new byte[10240];

clientSocket.BeginReceive(message.Data, 0, 10240,

SocketFlags.None, ReceiveCallBack, clientSocket);
最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
平台声明:文章内容(如有图片或视频亦包括在内)由作者上传并发布,文章内容仅代表作者本人观点,简书系信息发布平台,仅提供信息存储服务。

推荐阅读更多精彩内容