Unity开发

Unity网络编程(四)Protobuf

2019-12-15  本文已影响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
上一篇下一篇

猜你喜欢

热点阅读