Socket 解决粘包半包问题及代码实现

# 解决粘包半包

1,粘包半包现象

系统缓冲区

收到对端数据时,操作系统将数据存入Socket接受缓冲区,操作系统层面上的缓冲区完全由操作系统操作,程序不能直接操作它们,只能通过socket.Receive、soket.Send等方法间接操作。

Socket Receive方法把接受缓冲区的数据提取出来,比如Receive(readBuff,0,2),接受两个字节的数据到readbuff。如果系统接受缓冲区为空,该方法会阻塞。Send方法同理。

粘包

如果发送端快速发送多条数据,接收端没有及时调用Receive,那么数据便会在接收端的缓冲区中累积

这一现象有时与功能需求不符,比如在聊天软件中,客户端依次发送“Lpy”和“_is_handsome”,期望其他客户端也展示出“Lpy”和“_is_handsome”两条信息,但由于Receive可能把两条信息当作一条信息处理,有可能只展示“Lpy_is_handsome”一条信息。Receive方法返回多少个数据,取决于操作系统接收缓冲区中存放的内容。

Socket 解决粘包半包问题及代码实现_第1张图片

半包

发送端发送的数据还有可能被拆分,如发送“HelloWorld”,但在接收端调用Recei ve时,操作系统只接收到了部分数据,如“Hel”,在等待一小段时间后再次调用Receive才接收到另一部分数据“loWorld”。

Socket 解决粘包半包问题及代码实现_第2张图片

解决粘包问题方法

1,长度信息法

长度信息法是指在每个数据包前面加上长度信息。每次接收到数据后,先读取表示长度的字节,如果缓冲区的数据长度大于要取的字节数,则取出相应的字节,否则等待下一次数据接收。

Socket 解决粘包半包问题及代码实现_第3张图片

**说人话:**服务端buff接受到10字节的长度信息,但是只收到”hel“3个字节,那么不会发送,然后收到超过10个字节的长度,那么只发送10个字节,剩下的再判断是否满足长度

准确性高,可扩展性强(指变长消息),适用性广(指各种类型消息)

前面的例子使用一个字节表示长度,最大值为255。游戏程序一般会使用16位整型数或32位整型数来存放长度信息,16位整型数的取值范围是0~65535,32位整型数的取值范围是0~4294967295。对于大部分游戏,网络消息的长度很难超过65535字节,使用16位整型数来存放长度信息较合适。

一字节代表8位,32位需要两个字节

2,固定长度法

每次都以相同的长度发送数据,假设规定每条信息的长度都为10个字符,那么发送“Hello”“Unity”两条信息可以发送成“He llo… ”“Unity… ”,其中的“. ”表示填充字符,是为凑数,没有实际意义,只为了每次发送的数据都有固定长度。接收方每次读取10个字符,作为一条消息去处理。如果读到的字符数大于10,比如第1次读到“He llo…Un”,那它只要把前10个字节“Hello… ”抽取出来,再把后面的两个字节“Un”存起来,等到再次接收数据,拼接第二条信息。

**说人话:**每段消息的长度受到限制,如果不够10个字符,则填充为10个字符,如果超过还会出现粘包现象

3,结束符号法

规定一个结束符号,作为消息间的分隔符。假设规定结束符号为“ ”,那么发送“ H e l l o ”“ U n i t y ”两条信息可以发送成“ H e l l o ”,那么发送“Hello”“Unity”两条信息可以发送成“Hello ,那么发送Hello”“Unity两条信息可以发送成Hello”“ Unity ”。接收方每次读取数据,直到“ ”。接收方每次读取数据,直到“ 。接收方每次读取数据,直到”出现为止,并且使用“ ”去分割消息。比如接收方第一次读到“ H e l l o ”去分割消息。比如接收方第一次读到“Hel lo 去分割消息。比如接收方第一次读到HelloUn”,那它把结束符前面的Hello提取出来,作为第一条消息去处理,再把“Un”保存起来。待后续读到“ity$”,再把“Un”和“ity”拼成第二条消息。

**说人话:**简单容易实现,但是可靠性差,无法处理符号本身出现在消息内容的情况,因为符号在尾端所以不适用长消息。无法处理变长消息的情况

2,代码实现长度信息法

1,发送数据
        //点击发送按钮
        public void Send(string sendStr)
        {
            //组装协议
            byte[] bodyBytes = System.Text.Encoding.Default.GetBytes(sendStr);
            Int16 len = (Int16)bodyBytes.Length;
            byte[] lenBytes = BitConverter.GetBytes(len);
            byte[] sendBytes = lenBytes.Concat(bodyBytes).ToArray();
            //为了精简代码:使用同步Send
            //不考虑抛出异常
            socket.Send( sendBytes);
        }

说人话:

 byte[] bodyBytes = System.Text.Encoding.Default.GetBytes(sendStr);

通过编码,将发送消息sendStr编码成字节数组,也就是消息体bodyBytes

Int16 len = (Int16)bodyBytes.Length;

长度信息法需要获得消息体的长度len

byte[] lenBytes = BitConverter.GetBytes(len);

将整型长度转换为字节长度lenBytes

byte[] sendBytes = lenBytes.Concat(bodyBytes).ToArray();

lenBytes和bodyBytes合并成sendBytes

 socket.Send( sendBytes);

发送合并后的消息

2,接受数据/完整代码

游戏程序一般会使用“长度信息法”处理粘包问题,核心思想是定义一个缓冲区(readBuff)和一个指示缓冲区有效数据长度变量(b uffCount)。接受有缓冲区,发送没有缓冲区

        using System.Collections;
        using System.Collections.Generic;
        using UnityEngine;
        using System.Net.Sockets;
        using UnityEngine.UI;
        using System;
        using System.Linq;

        public class Echo : MonoBehaviour {

            //定义套接字
            Socket socket;
            //UGUI
            public InputField InputFeld;
            public Text text;
            //接收缓冲区
            byte[] readBuff = new byte[1024];
       		 //接收缓冲区的数据长度
       		 int buffCount = 0;
        	//显示文字
        	string recvStr = "";

        //点击连接按钮
        public void Connection()
        {
            //Socket
            socket = new Socket(AddressFamily.InterNetwork,
            SocketType.Stream, ProtocolType.Tcp);
            //为了精简代码:使用同步Connect
            //不考虑抛出异常
            socket.Connect("127.0.0.1", 8888);
            socket.BeginReceive( readBuff, buffCount, 1024-buffCount, 0,
            ReceiveCallback, socket);
        }

        //Receive回调
        public void ReceiveCallback(IAsyncResult ar){
            try {
                Socket socket = (Socket) ar.AsyncState;
                //获取接收数据长度
                int count = socket.EndReceive(ar);
                buffCount+=count;
                //处理二进制消息
                OnReceiveData();
                //继续接收数据
                socket.BeginReceive( readBuff, buffCount, 1024-buffCount, 0,
                    ReceiveCallback, socket);
            }
            catch (SocketException ex){
                Debug.Log("Socket Receive fail" + ex.ToString());
            }
        }

        public void OnReceiveData(){
            Debug.Log("[Recv 1] buffCount=" +buffCount);
            Debug.Log("[Recv 2] readbuff=" + BitConverter.ToString(readBuff));
            //消息长度
            if(buffCount <= 2)
                return;
            Int16 bodyLength = BitConverter.ToInt16(readBuff, 0);
            Debug.Log("[Recv 3] bodyLength=" +bodyLength);
            //消息体
            if(buffCount < 2+bodyLength)
                return;
            string s = System.Text.Encoding.UTF8.GetString(readBuff, 2, buffCount);
            Debug.Log("[Recv 4] s=" +s);
            //更新缓冲区
            int start = 2 + bodyLength;
            int count = buffCount - start;
            Array.Copy(readBuff, start, readBuff, 0, count);
            buffCount -= start;
            Debug.Log("[Recv 5] buffCount=" +buffCount);
            //消息处理
            recvStr = s + "\n" + recvStr;
            //继续读取消息
            OnReceiveData();
        }

        //点击发送按钮
        public void Send()
        {
            string sendStr = InputFeld.text;
            //组装协议
            byte[] bodyBytes = System.Text.Encoding.Default.GetBytes(sendStr);
            Int16 len = (Int16)bodyBytes.Length;
            byte[] lenBytes = BitConverter.GetBytes(len);
            byte[] sendBytes = lenBytes.Concat(bodyBytes).ToArray();
            //为了精简代码:使用同步Send
            //不考虑抛出异常
            socket.Send(sendBytes);
            Debug.Log("[Send]" + BitConverter.ToString(sendBytes));

        }

        public void Update(){
            text.text = recvStr;
        }
    }

3,代码解析:

       		//定义套接字
            Socket socket;
            //UGUI
            public InputField InputFeld;
            public Text text;
            //接收缓冲区
            byte[] readBuff = new byte[1024];
       		 //接收缓冲区的数据长度
       		 int buffCount = 0;
        	//显示文字
        	string recvStr = "";
•声明需要使用的变量
  //点击连接按钮
        public void Connection()
        {
            //Socket
            socket = new Socket(AddressFamily.InterNetwork,
            SocketType.Stream, ProtocolType.Tcp);
            //为了精简代码:使用同步Connect
            //不考虑抛出异常
            socket.Connect("127.0.0.1", 8888);
            socket.BeginReceive( readBuff, buffCount, 1024-buffCount, 0,
            ReceiveCallback, socket);
        }
•连接服务端

•实例化socket,参数为地址族v4、字节流类型socket、Tcp协议类型

•socket连接,参数为IP地址、端口

•调用开始接受函数,参数为接收缓存区、缓存区数据长度、最大接受长度、接受操作标志位、回调方法、socket

 //Receive回调
        public void ReceiveCallback(IAsyncResult ar){
            try {
                Socket socket = (Socket) ar.AsyncState;
                //获取接收数据长度
                int count = socket.EndReceive(ar);
                buffCount+=count;
                //处理二进制消息
                OnReceiveData();
                //继续接收数据
                socket.BeginReceive( readBuff, buffCount, 1024-buffCount, 0,
                    ReceiveCallback, socket);
            }
            catch (SocketException ex){
                Debug.Log("Socket Receive fail" + ex.ToString());
            }
        }
•接收回调函数。

•通过IAsyncResult类型接口的AsyncState属性获得socket

•获取socket的接受数据的长度(因为这是接受的回调所以包含接受数据的长度信息),增加接收缓存区的长度。

•在接收缓冲区中处理二进制消息。

•继续接受数据。(客户端会不断检测缓冲区并且接受数据)

public void OnReceiveData(){
            Debug.Log("[Recv 1] buffCount=" +buffCount);
            Debug.Log("[Recv 2] readbuff=" + BitConverter.ToString(readBuff));
            //消息长度
            if(buffCount <= 2)
                return;
            Int16 bodyLength = BitConverter.ToInt16(readBuff, 0);
            Debug.Log("[Recv 3] bodyLength=" +bodyLength);
            //消息体
            if(buffCount < 2+bodyLength)
                return;
            string s = System.Text.Encoding.UTF8.GetString(readBuff, 2, buffCount);
            Debug.Log("[Recv 4] s=" +s);
            //更新缓冲区
            int start = 2 + bodyLength;
            int count = buffCount - start;
            Array.Copy(readBuff, start, readBuff, 0, count);
            buffCount -= start;
            Debug.Log("[Recv 5] buffCount=" +buffCount);
            //消息处理
            recvStr = s + "\n" + recvStr;
            //继续读取消息
            OnReceiveData();
        }

•接受数据处理函数。

•如果接受缓存区的消息长度小于2,也就是没受到任何消息体(消息长度字节为2)那么不接收消息。

•否则,获得接收缓存区的消息体长度(BitConverter.ToInt16表示取缓冲区readBuff某个字节开始(这里是0,表示从第1个字节开始)的2个字节(因为Int16需要用2个字节表示)数据,再把它转换成数字。)

•如果接收缓存区数据长度小于2+消息体的长度,也就是长度信息法中,消息体的长度小于长度信息(位于消息的头部)中的数量,那么不接受消息。

•获得接收缓存区中一条消息的字符串形式。

•该消息的长度为2+消息体的长度,用接受缓存区的总消息长度减去该消息长度,实现更新缓存区。

•将消息字符串和换行符以及接受字符串赋值给接受字符串用于换行更新文本。

//点击发送按钮
        public void Send()
        {
            string sendStr = InputFeld.text;
            //组装协议
            byte[] bodyBytes = System.Text.Encoding.Default.GetBytes(sendStr);
            Int16 len = (Int16)bodyBytes.Length;
            byte[] lenBytes = BitConverter.GetBytes(len);
            byte[] sendBytes = lenBytes.Concat(bodyBytes).ToArray();
            //为了精简代码:使用同步Send
            //不考虑抛出异常
            socket.Send(sendBytes);
            Debug.Log("[Send]" + BitConverter.ToString(sendBytes));

        }
•发送消息函数

•点击发送,获得ugui输入框中的文本,把文本通过编码转化为消息体字节数组,然后再通过长度信息法组装成一个新的字节数组,该字节数组由消息体长度(2个字节)和消息体组成。

•发送这个组装后的消息数组。

 public void Update(){
            text.text = recvStr;
        }
•更新UI

更新UI面板信息为接受的信息

你可能感兴趣的:(Unity3D网络游戏实战,网络,服务器)