美文网首页Unity开发
Unity网络编程(四)Protobuf

Unity网络编程(四)Protobuf

作者: 罗卡恩 | 来源:发表于2019-12-15 17:38 被阅读0次

    如果服务器客户端是两种语言写的 然后编码和解码工作量比较大
    就有了protobuf来救我们
    官方git
    https://github.com/protocolbuffers/protobuf

    为啥要用protobuf

    1.比xml json 小 解析快
    2..NET自带的二进制序列化 改变数据结构麻烦
    3.自己写的 还要做不同语言的适配

    缺点
    基本没有可读性
    数据脱离.proto文件就没有意义

    protobuf流程

    原来用的都是proto2 现在用最新版proto3试一试
    1.定义proto结构 就类似于序列化结构 但要用proto格式去写
    2.用proto提供的工具编译为对应语言的代码
    3.然后就是序列化 反序列化

    proto语法

    //没有定义这个标志,默认认为你使用proto2版本
    syntax = "proto3";
    //避免不同工程间的名称冲突
    package bigtalkunity;
    //可以引用别的proto文件的一些结构 
    import "google/protobuf/timestamp.proto";
    //不写就默认生成的类的命名空间就是package的名称 
    option csharp_namespace = "BigTalkUnity.AddressBook";
    
    //把他想成class
    message Person {
      //等于号后的数字是字段的唯一编号 1-15编号使用一个字节
      //16-2047的会使用2个字节 
      //19000-19999之间的编号不能使用,因为是protobuf内部保留使用。
      string name = 1;
      int32 id = 2;  //C#int 在这里是int32 
      string email = 3;
     //枚举第一个值必须为0 也可以单独定义
      enum PhoneType {
        MOBILE = 0;
        HOME = 1;
        WORK = 2;
      }
     //也可以套
      message PhoneNumber {
        string number = 1;
        PhoneType type = 2;
      }
      //当成list
      repeated PhoneNumber phones = 4;
    
      google.protobuf.Timestamp last_updated = 5;
    }
    
    //表示可以嵌套
    message AddressBook {
      repeated Person people = 1;
    }
    

    对应关系


    image.png

    ,单行注释使用//,多行注释使用/* ... */
    proto修饰符 Required 发送必须有值 接收必须识别该字段
    optional 可以不设置值 无法识别就忽略 可以做到按需升级和平滑过渡
    Repeated 可以传0-N的相同的元素

    编译proto

    从这里下载protoc
    https://github.com/protocolbuffers/protobuf/releases
    找到对应自己电脑的版本下载

    image.png
    然后在cmd控制台输入
    -I等价于--protopath --csharp_out是输出语言种类 别的语言是别的指令 protoc -h 查看 然后面的就是生成位置
    protoc -I=$SRC_DIR --csharp_out=$DST_DIR $SRC_DIR/addressbook.proto
    

    当前路径cmd可以直接打开当前目录下的命令行


    image.png

    这样就是写入当前目录

    protoc.exe --csharp_out=. Person.proto
    
    image.png
    image.png

    然后之前的网站下载C#版的库


    image.png
    需要unity.NET版本在4.X以上才能用

    在PlayerSetting里可以设置


    image.png
    这个东西解压到unity里
    image.png

    解压到unity里
    然后就可以使用了
    先看看protobuf生成的.cs代码
    然后Person.cs放到unity里

    /**
     *Copyright(C) 2019 by #COMPANY#
     *All rights reserved.
     *FileName:     #SCRIPTFULLNAME#
     *Author:       #AUTHOR#
     *Version:      #VERSION#
     *UnityVersion:#UNITYVERSION#
     *Date:         #DATE#
     *Description:   
     *History:
    */
    using BigTalkUnity.AddressBook;
    using UnityEngine;
    //引用静态类型
    using static BigTalkUnity.AddressBook.Person.Types;
    public class ProtoTest : MonoBehaviour
    {
        // Start is called before the first frame update
        void Start()
        {
            Person john = new Person { 
                 Id=1234,
                 Name="john",
                 Email="132@qq.com",
                 Phones = { new PhoneNumber {Number="4564", Type= PhoneType.Home } }                          
            };
        }
    
    }
    
    

    数据就这样做好了
    然后就差序列化反序列化了
    这样可以储存一些数据配置表啥的

    /**
     *Copyright(C) 2019 by #COMPANY#
     *All rights reserved.
     *FileName:     #SCRIPTFULLNAME#
     *Author:       #AUTHOR#
     *Version:      #VERSION#
     *UnityVersion:#UNITYVERSION#
     *Date:         #DATE#
     *Description:   
     *History:
    */
    using BigTalkUnity.AddressBook;
    using Google.Protobuf;
    using System.IO;
    using UnityEngine;
    //引用静态类型
    using static BigTalkUnity.AddressBook.Person.Types;
    public class ProtoTest : MonoBehaviour
    {
        // Start is called before the first frame update
        void Start()
        {
            Person john = new Person { 
                 Id=1234,
                 Name="john",
                 Email="132@qq.com",
                 Phones = { new PhoneNumber {Number="4564", Type= PhoneType.Home } }                          
            };
            //序列化
            // 写入stream
            using (var output = File.Create("john.dat"))
            {
                john.WriteTo(output);
            }
          
            // 转为json字符串
            var jsonStr = john.ToString();
    
            // 转为bytestring
            var byteStr = john.ToByteString();
    
            // 转为byte数组
            var byteArray = john.ToByteArray();
    
    
            //反序列化
            // 1. 从stream中解析
            using (var input = File.OpenRead("john.dat"))
            {
                john = Person.Parser.ParseFrom(input);
            }
    
            // 2. 从字节串中解析
            john = Person.Parser.ParseFrom(byteStr);
    
            // 3. 从字节数组中解析
            john = Person.Parser.ParseFrom(byteArray);
    
            // 4. 从json字符串解析
            john = Person.Parser.ParseJson(jsonStr);
        }
    
    }
    

    运行脚本可以在这里看到我们的数据


    image.png
    image.png

    改造聊天室

    proto只是序列化反序列化 不能代表整个包体就不用我们管了

    syntax = "proto3";
    package ChatSystem;
    
    import "google/protobuf/timestamp.proto";
    
    message ChatMessage{
        string name = 1;
        string content = 2;
        //用来代表发送消息的时间
        google.protobuf.Timestamp send_time = 16;
    }
    
    image.png

    cmd编译一下

    protoc.exe --csharp_out=. chatMsg.proto
    

    然后粘贴到unity里

    /**
     *Copyright(C) 2019 by #COMPANY#
     *All rights reserved.
     *FileName:     #SCRIPTFULLNAME#
     *Author:       #AUTHOR#
     *Version:      #VERSION#
     *UnityVersion:#UNITYVERSION#
     *Date:         #DATE#
     *Description:   
     *History:
    */
    using UnityEngine;
    using System.Net;
    using System.Net.Sockets;
    using System.Text;
    using UnityEngine.UI;
    using System;
    using System.Collections.Generic;
    using System.Linq;
    using Google.Protobuf;
    // Timestamp类型需要
    using Google.Protobuf.WellKnownTypes;
    using ChatSystem;
    
    public class TalkClient : MonoBehaviour
    {
        public InputField input;
        public InputField inputName;
        public Text text;
        public Button btn;
        byte[] buffer = new byte[1024];
        Socket socket;
    
        //是msg的名字 不是proto生成C#的名字
        List<ChatMessage> msg = new List<ChatMessage>();
    
        // 用来存储接收到的数据
        List<byte> DataCache = new List<byte>();
        // Start is called before the first frame update
        void Start()
        {
            socket = new Socket(SocketType.Stream, ProtocolType.Tcp);
            //连接服务器
            socket.Connect("127.0.0.1", 9999);
    
            socket.BeginReceive(buffer, 0, buffer.Length, SocketFlags.None, ReceiveCallback, null);
            btn.onClick.AddListener(() =>
            {
                var chatMsg = new ChatMessage()
                {
                    Name = inputName.text,
                    Content = input.text,
                    // protobuf的Timestamp要求使用Utc时间传输
                    SendTime = Timestamp.FromDateTime(DateTime.UtcNow)
                };
                byte[] msg = Encode(chatMsg.ToByteArray());
                //发送数据         
                    socket.Send(msg);
            });
        }
    
        private void Update()
        {
            if (msg.Count > 0)
            {
                foreach (var item in msg)
                {
                    //因为unity不能在子线程调用unity大部分API Debug.Log可以 socket内部异步为我们开了线程
                    //UniRx插件中有一个MainThreadDispatcher类,可以很方便地用来处理子线程到主线程的转换
                    //// protobuf的Timestamp要求使用Utc时间传输,在这转为本地时间
                    text.text += item.Name + ":" + item.Content+ "" + item.SendTime.ToDateTime().ToLocalTime()+ "\n";
                }
                //清除处理过的消息
                msg.Clear();
            }
        }
        void ReceiveCallback(IAsyncResult ar)
        {
            try
            {
                //接受数据
                int length = socket.EndReceive(ar);
                if (length > 0)
                {
                    //取接收长度个加入缓存
                    DataCache.AddRange(buffer.Take(length));
                    ProcessData();
    
                    //重新开始接受
                    socket.BeginReceive(buffer, 0, buffer.Length, SocketFlags.None, ReceiveCallback, null);
                }
                else
                {
                    OnClientDisconnect();
                }
            }
            catch (SocketException ex)
            {
                if (ex.SocketErrorCode == SocketError.ConnectionReset)
                    OnClientDisconnect();
            }
        }
    
        void ProcessData()
        {
            while (true)
            {
                var data = Decode();
                if (data==null)
                    break;
                //转json
                var chatMsg = ChatMessage.Parser.ParseFrom(data);
    
                Debug.Log($"接收到服务端的消息:{chatMsg.Content}");
                msg.Add(chatMsg);
            }
        }
        void OnClientDisconnect()
        {
            Debug.Log("与服务端断开连接");
            socket.Close();
        }
        //封包
        byte[] Encode(byte[] data)
        {
            byte[] jsonBytes = data;
            var length = BitConverter.GetBytes((ushort)jsonBytes.Length);
            byte[] bytes = new byte[jsonBytes.Length + 2];
    
            //拷贝字头到前两位 然后把数据拷贝到后两位
            Buffer.BlockCopy(length, 0, bytes, 0, 2);
            Buffer.BlockCopy(jsonBytes, 0, bytes, 2, jsonBytes.Length);
    
            return bytes;
        }
    
        // 解包
        byte[] Decode()
        {
            if (DataCache.Count < 2) return null;
    
            var length = BitConverter.ToUInt16(DataCache.Take(2).ToArray(), 0);
            // 判断数据是否足够,如果不够可能原因是发生分包,下次再解析
            if (DataCache.Count - 2 >= length)
            {
                //跳过前两位 
                var bytes = DataCache.Skip(2).Take(length).ToArray();
                DataCache.RemoveRange(0, length + 2);
                return bytes;
            }
    
            return null;
        }
        void OnDestroy()
        {
            socket.Close();
        }
    }
    
    

    跑通了


    image.png

    相关文章

      网友评论

        本文标题:Unity网络编程(四)Protobuf

        本文链接:https://www.haomeiwen.com/subject/laoknctx.html