• 设为首页
  • 点击收藏
  • 手机版
    手机扫一扫访问
    迪恩网络手机版
  • 关注官方公众号
    微信扫一扫关注
    公众号

UnityC#自定义TCP传输协议以及封包拆包、解决粘包问题

原作者: [db:作者] 来自: [db:来源] 收藏 邀请

本文只是初步实现了一个简单的TCP自定协议,更为复杂的协议可以根据这种方式去扩展。


TCP协议,通俗一点的讲,它是一种基于socket传输的由发送方和接收方事先协商好的一种消息包组成结构,主要由消息头和消息体组成。 


众所周知,基于socket的信息交互有两个问题: 


第一、接收方不能主动识别发送方发送的信息类型,例如A方(客户端)向B方(服务器)发送了一条信息:123,没有事先经过协议规定的话,B方不可能知道这条信息123到底是一个int型123还是一个string型123,甚至他根本就不知道这条信息解析出来是123,所以B方找不到处理这条信息的方式; 


第二、接收方不能主动拆分发送方发送的多条信息,例如A方连续向B方发送了多条信息:123、456、789,由于网络延迟或B方接收缓冲区大小的不同设置,B方收到的信息可能是:1234、5678、9,也可能是123456789,也可能是1、2、3、4、5、6、7、8、9,还可能是更多意想不到的情况...... 


所以TCP协议就是为了解决这两个问题而存在的,当然为消息包加密也是它的另一个主要目的。 


TCP协议的格式一般都是:消息头+消息体,消息头的长度是固定的,A方和B方都事先知道消息头长度,以及消息头中各个部位的值所代表的意义,其中包含了对消息体的描述,包括消息体长度,消息体里的消息类型,消息体的加密方式等。 


B方在收到A方消息后,先按协议中规定的方式解析消息头,获取到里面对消息体的描述信息,他就可以知道消息体的长度是多少,以便于跟这条消息后面所紧跟的下一条消息进行拆分,他也可以从描述信息中得知消息体中的消息类型,并按正确的解析方式进行解析,从而完成信息的交互。 


这里以一个简单的TCP协议作为例子:


第一步:定义协议(我们将协议定义如下)

消息头(28字节):(int)消息校验码4字节 + (int)消息体长度4字节 + (long)身份ID8字节 + (int)主命令4字节 + (int)子命令4字节 + (int)加密方式4字节

消息体:(int)消息1长度4字节 + (string)消息1 + (int)消息2长度4字节 + (string)消息2 + (int)消息3长度4字节 + (string)消息3 + ......  


第二步:服务器建立监听

    //SocketTCPServer.cs
    private static string ip = "127.0.0.1";
    private static int port = 5690;
    private static Socket socketServer;
    public static List<Socket> listPlayer = new List<Socket>();
    private static Socket sTemp;
    ///<summary>
    ///绑定地址并监听
    ///</summary>
    ///ip地址 端口 类型默认为TCP
    public static void init(string ipStr, int iPort)
    {
        try
        {
            ip = ipStr;
            port = iPort;
            socketServer = new Socket(AddressFamily.InterNetwork, SocketType.Stream, ProtocolType.Tcp);
            socketServer.Bind(new IPEndPoint(IPAddress.Parse(ip), port)); 
            Thread threadListenAccept = new Thread(new ThreadStart(ListenAccept));
            threadListenAccept.Start();
        }
        catch (ArgumentNullException e)
        {
            Debug.Log(e.ToString());
        }
        catch (SocketException e)
        {
            Debug.Log(e.ToString());
        }
    }
    ///<summary>
    ///监听用户连接
    ///</summary>
    private static void ListenAccept()
    {
        socketServer.Listen(0);                       //对于socketServer绑定的IP和端口开启监听
        sTemp = socketServer.Accept();                //如果在socketServer上有新的socket连接,则将其存入sTemp,并添加到链表
        listPlayer.Add(sTemp);
        Thread threadReceiveMessage = new Thread(new ThreadStart(ReceiveMessage));
        threadReceiveMessage.Start();
        while (true)
        {
            sTemp = socketServer.Accept();
            listPlayer.Add(sTemp);
        }
    }



第三步:客户端连接服务器

 //SocketTCPClient.cs
    private static string ip = "127.0.0.1";
    private static int port = 5690;
    private static Socket socketClient;
    public static List<string> listMessage = new List<string>();
    ///<summary>
    ///创建一个SocketClient实例
    ///</summary>
    ///ip地址 端口 类型默认为TCP
    public static void CreateInstance(string ipStr, int iPort)
    {
        ip = ipStr;
        port = iPort;
        socketClient = new Socket(AddressFamily.InterNetwork, SocketType.Stream, ProtocolType.Tcp);
        ConnectServer();
    }
    /// <summary>
    ///连接服务器
    /// </summary>
    private static void ConnectServer()
    {
        try
        {
            socketClient.Connect(IPAddress.Parse(ip), port);
            Thread threadConnect = new Thread(new ThreadStart(ReceiveMessage));
            threadConnect.Start();
        }
        catch (ArgumentNullException e)
        {
            Debug.Log(e.ToString());
        }
        catch (SocketException e)
        {
            Debug.Log(e.ToString());
        }
    }


第四步:封包以及发送消息包 

    /// <summary>
    /// 构建消息数据包
    /// </summary>
    /// <param name="Crccode">消息校验码,判断消息开始</param>
    /// <param name="sessionid">用户登录成功之后获得的身份ID</param>
    /// <param name="command">主命令</param>
    /// <param name="subcommand">子命令</param>
    /// <param name="encrypt">加密方式</param>
    /// <param name="MessageBody">消息内容(string数组)</param>
    /// <returns>返回构建完整的数据包</returns>
    public static byte[] BuildDataPackage(int Crccode,long sessionid, int command,int subcommand, int encrypt, string[] MessageBody)
    {
        //消息校验码默认值为0x99FF
        Crccode = 65433;
        //消息头各个分类数据转换为字节数组(非字符型数据需先转换为网络序  HostToNetworkOrder:主机序转网络序)
        byte[] CrccodeByte = BitConverter.GetBytes(IPAddress.HostToNetworkOrder(Crccode));
        byte[] sessionidByte = BitConverter.GetBytes(IPAddress.HostToNetworkOrder(sessionid));
        byte[] commandByte = BitConverter.GetBytes(IPAddress.HostToNetworkOrder(command));
        byte[] subcommandByte = BitConverter.GetBytes(IPAddress.HostToNetworkOrder(subcommand));
        byte[] encryptByte = BitConverter.GetBytes(IPAddress.HostToNetworkOrder(encrypt));
        //计算消息体的长度
        int MessageBodyLength = 0;
        for (int i = 0; i < MessageBody.Length; i++)
        {
            if (MessageBody[i] == "")
                break;
            MessageBodyLength += Encoding.UTF8.GetBytes(MessageBody[i]).Length;
        }
        //定义消息体的字节数组(消息体长度MessageBodyLength + 每个消息前面有一个int变量记录该消息字节长度)
        byte[] MessageBodyByte = new byte[MessageBodyLength + MessageBody.Length*4];
        //记录已经存入消息体数组的字节数,用于下一个消息存入时检索位置
        int CopyIndex = 0;
        for (int i = 0; i < MessageBody.Length; i++)
        {
            //单个消息
            byte[] bytes = Encoding.UTF8.GetBytes(MessageBody[i]);
            //先存入单个消息的长度
            BitConverter.GetBytes(IPAddress.HostToNetworkOrder(bytes.Length)).CopyTo(MessageBodyByte, CopyIndex);
            CopyIndex += 4;
            bytes.CopyTo(MessageBodyByte, CopyIndex);
            CopyIndex += bytes.Length;
        }
        //定义总数据包(消息校验码4字节 + 消息长度4字节 + 身份ID8字节 + 主命令4字节 + 子命令4字节 + 加密方式4字节 + 消息体)
        byte[] totalByte = new byte[28 + MessageBodyByte.Length];
        //组合数据包头部(消息校验码4字节 + 消息长度4字节 + 身份ID8字节 + 主命令4字节 + 子命令4字节 + 加密方式4字节)
        CrccodeByte.CopyTo(totalByte,0);
        BitConverter.GetBytes(IPAddress.HostToNetworkOrder(MessageBodyByte.Length)).CopyTo(totalByte,4);
        sessionidByte.CopyTo(totalByte, 8);
        commandByte.CopyTo(totalByte, 16);
        subcommandByte.CopyTo(totalByte, 20);
        encryptByte.CopyTo(totalByte, 24);
        //组合数据包体
        MessageBodyByte.CopyTo(totalByte,28);
        Debug.Log("发送数据包的总长度为:"+ totalByte.Length);
        return totalByte;
    }
    ///<summary>
    ///发送信息
    ///</summary>
    public static void SendMessage(byte[] sendBytes)
    {
        //确定是否连接
        if (socketClient.Connected)
        {
            //获取远程终结点的IP和端口信息
            IPEndPoint ipe = (IPEndPoint)socketClient.RemoteEndPoint;
            socketClient.Send(sendBytes, sendBytes.Length, 0);
        }
    }


第五步:接收消息以及解析消息包 

  ///<summary>
    ///接收消息
    ///</summary>
    private static void ReceiveMessage()
    {
        while (true)
        {
            //接受消息头(消息校验码4字节 + 消息长度4字节 + 身份ID8字节 + 主命令4字节 + 子命令4字节 + 加密方式4字节 = 28字节)
            int HeadLength = 28;
            //存储消息头的所有字节数
            byte[] recvBytesHead = new byte[HeadLength];
            //如果当前需要接收的字节数大于0,则循环接收
            while (HeadLength > 0)
            {
                byte[] recvBytes1 = new byte[28];
                //将本次传输已经接收到的字节数置0
                int iBytesHead = 0;
                //如果当前需要接收的字节数大于缓存区大小,则按缓存区大小进行接收,相反则按剩余需要接收的字节数进行接收
                if (HeadLength >= recvBytes1.Length)
                {
                    iBytesHead = socketClient.Receive(recvBytes1, recvBytes1.Length, 0);
                }
                else
                {
                    iBytesHead = socketClient.Receive(recvBytes1, HeadLength, 0);
                }
                //将接收到的字节数保存
                recvBytes1.CopyTo(recvBytesHead, recvBytesHead.Length - HeadLength);
                //减去已经接收到的字节数
                HeadLength -= iBytesHead;
            }
            //接收消息体(消息体的长度存储在消息头的4至8索引位置的字节里)
            byte[] bytes = new byte[4];
            Array.Copy(recvBytesHead, 4, bytes, 0, 4);
            int BodyLength = IPAddress.NetworkToHostOrder(BitConverter.ToInt32(bytes, 0));
            //存储消息体的所有字节数
            byte[] recvBytesBody = new byte[BodyLength];
            //如果当前需要接收的字节数大于0,则循环接收
            while (BodyLength > 0)
            {
                byte[] recvBytes2 = new byte[BodyLength < 1024 ? BodyLength : 1024];
                //将本次传输已经接收到的字节数置0
                int iBytesBody = 0;
                //如果当前需要接收的字节数大于缓存区大小,则按缓存区大小进行接收,相反则按剩余需要接收的字节数进行接收
                if (BodyLength >= recvBytes2.Length)
                {
                    iBytesBody = socketClient.Receive(recvBytes2, recvBytes2.Length, 0);
                }
                else
                {
                    iBytesBody = socketClient.Receive(recvBytes2, BodyLength, 0);
                }
                //将接收到的字节数保存
                recvBytes2.CopyTo(recvBytesBody, recvBytesBody.Length - BodyLength);
                //减去已经接收到的字节数
                BodyLength -= iBytesBody;
            }
            //一个消息包接收完毕,解析消息包
            UnpackData(recvBytesHead,recvBytesBody);
        }
    }
    /// <summary>
    /// 解析消息包
    /// </summary>
    /// <param name="Head">消息头</param>
    /// <param name="Body">消息体</param>
    public static void UnpackData(byte[] Head, byte[] Body)
    {
        byte[] bytes = new byte[4];
        Array.Copy(Head, 0, bytes, 0, 4);
        Debug.Log("接收到数据包中的校验码为:" + IPAddress.NetworkToHostOrder(BitConverter.ToInt32(bytes, 0)));
 
        bytes = new byte[8];
        Array.Copy(Head, 8, bytes, 0, 8);
        Debug.Log("接收到数据包中的身份ID为:" + IPAddress.NetworkToHostOrder(BitConverter.ToInt64(bytes, 0)));
 
        bytes = new byte[4];
        Array.Copy(Head, 16, bytes, 0, 4);
        Debug.Log("接收到数据包中的数据主命令为:" + IPAddress.NetworkToHostOrder(BitConverter.ToInt32(bytes, 0)));
 
        bytes = new byte[4];
        Array.Copy(Head, 20, bytes, 0, 4);
        Debug.Log("接收到数据包中的数据子命令为:" + IPAddress.NetworkToHostOrder(BitConverter.ToInt32(bytes, 0)));
 
        bytes = new byte[4];
        Array.Copy(Head, 24, bytes, 0, 4);
        Debug.Log("接收到数据包中的数据加密方式为:" + IPAddress.NetworkToHostOrder(BitConverter.ToInt32(bytes, 0)));
 
        bytes = new byte[Body.Length];
        for (int i = 0; i < Body.Length;)
        {
            byte[] _byte = new byte[4];
            Array.Copy(Body, i, _byte, 0, 4);
            i += 4;
            int num = IPAddress.NetworkToHostOrder(BitConverter.ToInt32(_byte, 0));
 
            _byte = new byte[num];
            Array.Copy(Body, i, _byte, 0, num);
            i += num;
            Debug.Log("接收到数据包中的数据有:" + Encoding.UTF8.GetString(_byte, 0, _byte.Length));
        }
    }



第六步:测试,同时发送两个包到服务器 


输出结果如下,可见粘包问题已得到解决:






鲜花

握手

雷人

路过

鸡蛋
该文章已有0人参与评论

请发表评论

全部评论

专题导读
上一篇:
指针转换(C#编程指南)发布时间:2022-07-18
下一篇:
C#ini文件操作【源码下载】发布时间:2022-07-18
热门推荐
热门话题
阅读排行榜

扫描微信二维码

查看手机版网站

随时了解更新最新资讯

139-2527-9053

在线客服(服务时间 9:00~18:00)

在线QQ客服
地址:深圳市南山区西丽大学城创智工业园
电邮:jeky_zhao#qq.com
移动电话:139-2527-9053

Powered by 互联科技 X3.4© 2001-2213 极客世界.|Sitemap