cat
Shioho

# 前言

哇,感觉好久没写 Unity 相关的文章啦,这也应该是今年最后一篇文了,站点应该能破 100k 字啦,嘿嘿,为了不把 Unity 丢掉,这次来写一篇关于自定义 Protobuf 脚本生成类工具并且在 Unity 中自定义编辑器,能够更加方便的使用 Protobuf 的文章吧~

# protobuf 的介绍与使用

总所周知,protobuf 是谷歌开源的一种数据传输格式,性能高,体积小,能够有效的对自定义类进行序列化反序列化,让你的通讯时间更短,带宽消耗更小。

关于 protobuf 的格式定义可以参考这里:Protobuf3 教程

# 在 Unity 中使用 protobuf 进行序列化反序列化

  • 下载谷歌提供的 protobuf 转 c# 工具

登录 github,下载最新的 protoc-win32.zip

解压后得到 protoc.exe

# 定义一个 proto 文件

随便定义一个 proto 文件吧~,像这样:

syntax = "proto3";
package proto;
message Player {
    string id = 1;
    string name = 2;
    string head = 3;
    uint64 playerExp = 4 ; // 修为
    uint64 playerLevel = 5;// 玩家段位
    uint64 weaponLevel = 6;// 兵器段位
    uint64 weaponExp = 7;// 兵器修为
}
// 查询玩家
message GetPlayerRequest {
    string playerId = 1;// 玩家 ID
}
message GetPlayerResponse {
    // 错误码 0 为成功
    sint64 code = 1;
    // 错误信息
    string info = 2;
    message Data {
        Player player = 1;
        bool Exist = 2;
    }
    Data data = 3;
}

# 利用 protoc.exe 生成 .cs 代码

  • 单文件命令行转化
protoc.exe -I=%proto文件夹% --csharp_out=%生成的cs文件夹% %proto文件名%
//如:
// protoc.exe -I=./Protos/ --csharp_out=./Proto2CSharp/ ./Protos/player.proto

想了解更多可以看这里:Protocol Buffers Doc

  • 基于 nodejs 的多文件批量转化
const fs = require('fs');
const child = require('child_process')
var sourceFolder = __dirname + "/Protos/";
var targetFolder = __dirname + "/Proto2CSharp/";
var exePath = 'protoc.exe';
var files = fs.readdirSync(sourceFolder);
files.forEach(file => {
    if (!file.endsWith('.proto'))
        return false;
    console.log("开始转换", file);
    try {
        var args = `${exePath} -I=${sourceFolder}/ --csharp_out=${targetFolder} ${sourceFolder}${file}`;
        child.exec(args, (err, stdout, stderr) => {
            if (err) {
                console.error("转换失败", err, stderr);
                return;
            }
            console.log(stdout);
            console.log("转换完成", file);
        });
    } catch (e) {
        console.error("转换失败", e);
    }
});

好嘞,一切顺利的话就会在 Proto2CSharp 文件夹下生成对应的 cs 文件,该类包含了对 proto 内容的类声明以及序列化反序列化方法,接下来看看如何序列化反序列化吧

# 序列化与反序列化

  • dll 支持
    要使用 protobuf 的话需要在 Unity 中导入以下 dll: Google.Protobuf , System.Buffers , System.Memory , System.Runtime.CompilerServices.Unsafe ,这些 dll 会在下方的项目 gitee 上给出哈~

  • 序列化与反序列化

using Proto;
using Google.Protobuf;
......
// 序列化
var player = new Player();
player.Name = "哈哈哈";
var buf = ((IMessage)player).ToByteArray();
// 反序列化
var desPlayer = Player.Parser.ParseFrom(buf);
Debug.Log(desPlayer.Name);

(๑‾ ꇴ ‾๑) 好哒,用起来没什么问题,也很简单,但真正到项目中的时候肯定是不能这么写的,太麻烦啦,数据包都是通过网络传输而来的,如果要为每一个 proto 都写文件的话,那会累死的,那么怎么办哩,当然是我们自己先去定义传输协议,然后封装一套网络抽象,最后再通过编辑器生成 proto 文件类来快捷使用啦,来看看具体实现吧~

# 更加愉快的 protobuf 使用体验

这里先放上 gitee 项目地址:unity-protobuf,配合食用效果更加哦~

# 传输协议

总所周知,在客户端与服务端消息交互时,通常有 request-response (客户端发起请求,服务端返回消息) 以及 push (服务端主动推送) 两种方式,那么我们就需要定义一个标记位 (type) 来确认消息类型;其次为了知道对应发过来的 proto 对应哪一个消息,我们需要为 res-rep 和 push 定义一个唯一 (protoId) 来确认;为了知道 proto 序列化后的消息长度,避免粘包问题,我们还需要记录消息长度 (msglength);有时在连续发送两条相同的 req 时,为了知道服务端返回的 res 对应哪一条 req,因此需要添加一个递增的 (seqId);至于是否需要压缩 (compress),是否需要加密 (encryption) 这些,暂时先预留标志位但不实现吧,那么将会得到以下传输协议:

// 客户端请求
    request: (6+2+N)byte
    |<-                         Head                         ->|<-      Body     ->|    
    | version | type | compress | encryption | seqId | protoId | msglength | msg   |
    |   3bit  | 3bit |   1bit   |    1bit    | 3byte |  2byte  |  2byte    | Nbyte |
// 服务端回复 
    response:(6+2+N)byte
    |<-                             Head                     ->|<-      Body     ->|    
    | version | type | compress | encryption | seqId | protoID | msglength | msg   |
    |   3bit  | 3bit |   1bit   |    1bit    | 3byte |  2byte  |  2byte    | Nbyte |
// 服务端推送
    push:(3+2+N)byte    
    |<-                       Head                   ->|<-       Body      ->| 
    | version | type | compress | encryption | protoID | msglength |  msg    |
    |   3bit  | 3bit |   1bit   |    1bit    |  2byte  |  2byte    |  N byte |
public enum Type
{
    REQUEST = 0,
    RESPONSE,
    PUSH,
}

# 网络抽象

# 传输协议实现

  • 序列化反序列化接口与抽象实现
public interface IProtoMsgPaser 
    {
        int ProtoId { get; }
        byte[] Encode(IMessage data);
        IMessage Decode(byte[] buffer);
    }
    public interface IProtoResponseMsgPaser : IProtoMsgPaser
    {
        long GetCode(IMessage data);
        string GetInfo(IMessage data);
    }
public abstract class AProtoMsgPaser<T> : IProtoMsgPaser
        where T : IMessage<T>
    {
        public int ProtoId => _protoId;
        protected abstract MessageParser<T> _msgPaser { get; }
        protected abstract int _protoId { get; }
        public IMessage Decode(byte[] buffer)
        {
            return _msgPaser.ParseFrom(buffer);
        }
        public byte[] Encode(IMessage data)
        {
            return data.ToByteArray();
        }
    }
    public abstract class AProtoResponseMsgPaser<T> : AProtoMsgPaser<T>, IProtoResponseMsgPaser
        where T : IMessage<T>
    {
        public long GetCode(IMessage data)
        {
            var propertyInfo = data.GetType().GetProperty("Code");
            long code = propertyInfo == null ? (long)ProtoMsgCodeDefine.Success : (long)propertyInfo.GetValue(data);
            return code;
        }
        public string GetInfo(IMessage data)
        {
            var propertyInfo = data.GetType().GetProperty("Info");
            string info = propertyInfo == null ? "" : (string)propertyInfo.GetValue(data);
            return info;
        }
    }
  • 业务消息处理接口与抽象实现
public interface IProtoMsgController
    {
        int ProtoId { get; }
    }
    public interface IProtoPushMsgController : IProtoMsgController
    {
        void Process(byte[] buffer);
    }
    /// <summary>
    /// req-res为send
    /// </summary>
    public interface IProtoSendMsgController : IProtoMsgController
    {
        byte[] GetBuffer(int seqId, IMessage data);
        void AddContext(int seqId, object context);
        void Process(int seqId, byte[] buffer);
    }
public abstract class AProtoSendMsgController<ReqPaser, ResPaser, Res> : IProtoSendMsgController
        where ReqPaser : IProtoMsgPaser
        where ResPaser : IProtoResponseMsgPaser
        where Res : IMessage
    {
        int IProtoMsgController.ProtoId => _protoId;
        private IProtoMsgPaser _reqPaser;
        private IProtoResponseMsgPaser _resPaser;
        private int _protoId;
        private Dictionary<int, object> _contextDic = new Dictionary<int, object>();
        protected abstract void OnProcessSuccess(Res data, object context);
        protected abstract void OnProcessFailed(Res data, object context);
        public AProtoSendMsgController()
        {
            _reqPaser = Activator.CreateInstance(typeof(ReqPaser)) as IProtoMsgPaser;
            _resPaser = Activator.CreateInstance(typeof(ResPaser)) as IProtoResponseMsgPaser;
            _protoId = _resPaser.ProtoId;
        }
        public void Process(int seqId, byte[] buffer)
        {
            try
            {
                var data = (Res)_resPaser.Decode(buffer);
                _contextDic.TryGetValue(seqId, out var context);
                _contextDic.Remove(seqId);
                if (_resPaser.GetCode(data) == (int)ProtoMsgCodeDefine.Success)
                {
                    OnProcessSuccess(data, context);
                }
                else
                {
                    Debug.LogError($"On Response ProcessFailed: {data}");
                    OnProcessFailed(data, context);
                }
            }
            catch (Exception e)
            {
                throw e;
            }
        }
        public byte[] GetBuffer(int seqId, IMessage data)
        {
            var protoBuf = _reqPaser.Encode(data);
            return ProtoUtil.GetReqBuffer(seqId, _protoId, protoBuf);
        }
        public void AddContext(int seqId, object context)
        {
            _contextDic[seqId] = context;
        }
    }
    public abstract class ATpro2PushMsgController<PushPaser, Push> : IProtoPushMsgController
        where PushPaser : IProtoMsgPaser
        where Push : IMessage
    {
        public int ProtoId => _protoId;
        private int _protoId;
        private IProtoMsgPaser _pushPaser;
        protected abstract void OnProcessSuccess(Push data);
        public ATpro2PushMsgController()
        {
            _pushPaser = Activator.CreateInstance(typeof(PushPaser)) as IProtoMsgPaser;
            _protoId = _pushPaser.ProtoId;
        }
        void IProtoPushMsgController.Process(byte[] buffer)
        {
            try
            {
                var data = (Push)_pushPaser.Decode(buffer);
                OnProcessSuccess(data);
            }
            catch (Exception e)
            {
                throw e;
            }
        }
    }
  • 自定义数据解析帮助类 ProtoUtil
public static byte[] GetReqBuffer(int seqId, int protoId, byte[] buffer, bool isCompress = false, bool isEncryption = false)
    {
        var headLen = 8;
        var len = buffer.Length + headLen;
        var bytes = new byte[len];
        //version type compress encryption
        int version = ProtoSetting.ProtoVersion << 5;
        int type = (int)ProtoMsgType.Request << 2;
        int compress = (isCompress ? 1 : 0) << 1;
        int encryption = (isEncryption ? 1 : 0) << 0;
        bytes[0] = Convert.ToByte((version + type + compress + encryption) & 0xFF);
        //seqID
        bytes[1] = Convert.ToByte(seqId >> 16 & 0xFF);
        bytes[2] = Convert.ToByte(seqId >> 8 & 0xFF);
        bytes[3] = Convert.ToByte(seqId & 0xFF);
        //protoID
        bytes[4] = Convert.ToByte(protoId >> 8 & 0xFF);
        bytes[5] = Convert.ToByte(protoId & 0xFF);
        var targetBuffer = buffer;
        if (isCompress)
        {
            //TODO 数据压缩
        }
        if (isEncryption)
        {
            //TODO 数据加密
        }
        //msgLengh
        var msgLen = targetBuffer.Length;
        bytes[6] = Convert.ToByte(msgLen >> 8 & 0xFF);
        bytes[7] = Convert.ToByte(msgLen & 0xFF);
        //body
        for (int i = 0; i < msgLen; i++)
        {
            bytes[i + headLen] = targetBuffer[i];
        }
        return bytes;
    }
    public static ProtoMsgType GetMsgType(byte buffer)
    {
        return (ProtoMsgType)((buffer << 3 & 0xff) >> 5);
    }
    public static ProtoResponseData DecodeResponseData(byte[] bytes)
    {
        var headLen = 8;
        var data = new ProtoResponseData();
        data.Type = ProtoMsgType.Response;
        data.SeqId = (bytes[1] << 16) + (bytes[2] << 8) + (bytes[3]);
        data.ProtoId = (bytes[4] << 8) + bytes[5];
        bool isCompress = bytes[0] >> 1 == 1;
        bool isEncryption = bytes[0] >> 0 == 1;
        var srcBuffer = new byte[bytes.Length - headLen];
        for (int i = 0; i < srcBuffer.Length; i++)
        {
            srcBuffer[i] = bytes[i + headLen];
        }
        if (isCompress)
        {
            //TODO 数据压缩
        }
        if (isEncryption)
        {
            //TODO 数据解密
        }
        data.MsgLength = srcBuffer.Length;
        data.Msg = srcBuffer;
        return data;
    }
    public static ProtoPushData DecodePushData(byte[] bytes)
    {
        var headLen = 5;
        var data = new ProtoPushData();
        data.Type = ProtoMsgType.Push;
        data.ProtoId = (bytes[1] << 8) + bytes[2];
        bool isCompress = bytes[0] >> 1 == 1;
        bool isEncryption = bytes[0] >> 0 == 1;
        var srcBuffer = new byte[bytes.Length - headLen];
        for (int i = 0; i < srcBuffer.Length; i++)
        {
            srcBuffer[i] = bytes[i + headLen];
        }
        if (isCompress)
        {
            //TODO 数据压缩
        }
        if (isEncryption)
        {
            //TODO 数据解密
        }
        data.MsgLength = srcBuffer.Length;
        data.Msg = srcBuffer;
        return data;
    }
  • 业务接口注册查询类
public abstract class AProtoMsgCtrlProvider
    {
        private Dictionary<int, IProtoMsgController> _msgCtrlDic = new Dictionary<int, IProtoMsgController>();
        protected abstract void OnInit();
        public AProtoMsgCtrlProvider()
        {
            OnInit();
        }
        protected void AddMsgCtrl(IProtoMsgController msgCtrl)
        {
            if (HasMsgCtrl(msgCtrl.ProtoId))
                return;
            _msgCtrlDic.Add(msgCtrl.ProtoId, msgCtrl);
        }
        public void Clear()
        {
            _msgCtrlDic.Clear();
        }
        public bool HasMsgCtrl(int protoId)
        {
            return _msgCtrlDic.ContainsKey(protoId);
        }
        public IProtoPushMsgController GetPushMsgCtrl(int protoId)
        {
            return (IProtoPushMsgController)GetMsgCtrl(protoId);
        }
        public IProtoSendMsgController GetSendMsgCtrl(int protoId)
        {
            return (IProtoSendMsgController)GetMsgCtrl(protoId);
        }
        private IProtoMsgController GetMsgCtrl(int protoId)
        {
            if (!HasMsgCtrl(protoId))
            {
                Debug.LogError($"{GetType()}未找到当前protoId对应的msgCtrl,请确认是否绑定,protoId: ${protoId}");
                return null;
            }
            _msgCtrlDic.TryGetValue(protoId, out var ctrl);
            return ctrl;
        }
    }

# Websocket 抽象

为了方便,网络通信就采用 ws 长连接啦,可以参考之前写过的这篇文章:Unity 之 Websocket 解决方案

public sealed class ProtoWebsocket : AWebsocket
{
    public Action OnProtoOpen;
    public Action OnProtoClose;
    public Action OnProtoError;
    public Action OnProtoSilentReconnect;
    public Action<ProtoResponseData> OnTrpo2ResponseMsg;
    public Action<ProtoPushData> OnTrpo2PushMsg;
    public Action<string> OnTrpo2RecivedStringMsg;
    public bool IsReconnectWhenServerError = true;
    public int SeqId => _seqId;
    private int _seqId;
    public ProtoWebsocket(string url) : base(url)
    {
    }
    public void ProtoRequest(byte[] buffer)
    {
        SendBinary(buffer);
        _seqId++;
    }
    protected override void OnOpen()
    {
        ResetSeqId();
        OnProtoOpen?.Invoke();
    }
    protected override void OnError()
    {
        OnProtoError?.Invoke();
    }
    protected override void OnSilentReconnect()
    {
        base.OnSilentReconnect();
        OnProtoSilentReconnect?.Invoke();
    }
    protected override void OnClosed()
    {
        OnProtoClose?.Invoke();
    }
    protected override void OnReceivedBinary(byte[] buffer)
    {
        try
        {
            var type = ProtoUtil.GetMsgType(buffer[0]);
            switch (type)
            {
                case ProtoMsgType.Response:
                    var response = ProtoUtil.DecodeResponseData(buffer);
                    OnTrpo2ResponseMsg?.Invoke(response);
                    break;
                case ProtoMsgType.Push:
                    var push = ProtoUtil.DecodePushData(buffer);
                    OnTrpo2PushMsg?.Invoke(push);
                    break;
                default: break;
            }
        }
        catch (Exception e)
        {
            Debug.LogError(e);
        }
    }
    protected override void OnReceivedString(string message)
    {
        OnTrpo2RecivedStringMsg?.Invoke(message);
    }
    private void ResetSeqId()
    {
        _seqId = 0;
    }
}

# 自定义 protobuf 生成器

由于重新把模块封装了嘛,但原先的 protoc 只会生成序列化反序列化方法,为了能同时将 Paser , Controller , Provider 一起生成了,避免我们重复劳动,那么我们就需要自定义一个 protoc 生成器啦~

# protobuf 生成器

  • 下载 protoc 项目源码

  • src->google->protobuf->stubs 下添加 c++ 智能指针 scoped_ptr

#ifndef GOOGLE_PROTOBUF_STUBS_SCOPED_PTR_H_
#define GOOGLE_PROTOBUF_STUBS_SCOPED_PTR_H_
#include <google/protobuf/stubs/port.h>
namespace google {
namespace protobuf {
// ===================================================================
// from google3/base/scoped_ptr.h
namespace internal {
//  This is an implementation designed to match the anticipated future TR2
//  implementation of the scoped_ptr class, and its closely-related brethren,
//  scoped_array, scoped_ptr_malloc, and make_scoped_ptr.
template <class C> class scoped_ptr;
template <class C> class scoped_array;
// A scoped_ptr<T> is like a T*, except that the destructor of scoped_ptr<T>
// automatically deletes the pointer it holds (if any).
// That is, scoped_ptr<T> owns the T object that it points to.
// Like a T*, a scoped_ptr<T> may hold either NULL or a pointer to a T object.
//
// The size of a scoped_ptr is small:
// sizeof(scoped_ptr<C>) == sizeof(C*)
template <class C>
class scoped_ptr {
 public:
  // The element type
  typedef C element_type;
  // Constructor.  Defaults to initializing with NULL.
  // There is no way to create an uninitialized scoped_ptr.
  // The input parameter must be allocated with new.
  explicit scoped_ptr(C* p = NULL) : ptr_(p) { }
  // Destructor.  If there is a C object, delete it.
  // We don't need to test ptr_ == NULL because C++ does that for us.
  ~scoped_ptr() {
    enum { type_must_be_complete = sizeof(C) };
    delete ptr_;
  }
  // Reset.  Deletes the current owned object, if any.
  // Then takes ownership of a new object, if given.
  // this->reset(this->get()) works.
  void reset(C* p = NULL) {
    if (p != ptr_) {
      enum { type_must_be_complete = sizeof(C) };
      delete ptr_;
      ptr_ = p;
    }
  }
  // Accessors to get the owned object.
  // operator* and operator-> will assert() if there is no current object.
  C& operator*() const {
    assert(ptr_ != NULL);
    return *ptr_;
  }
  C* operator->() const  {
    assert(ptr_ != NULL);
    return ptr_;
  }
  C* get() const { return ptr_; }
  // Comparison operators.
  // These return whether two scoped_ptr refer to the same object, not just to
  // two different but equal objects.
  bool operator==(C* p) const { return ptr_ == p; }
  bool operator!=(C* p) const { return ptr_ != p; }
  // Swap two scoped pointers.
  void swap(scoped_ptr& p2) {
    C* tmp = ptr_;
    ptr_ = p2.ptr_;
    p2.ptr_ = tmp;
  }
  // Release a pointer.
  // The return value is the current pointer held by this object.
  // If this object holds a NULL pointer, the return value is NULL.
  // After this operation, this object will hold a NULL pointer,
  // and will not own the object any more.
  C* release() {
    C* retVal = ptr_;
    ptr_ = NULL;
    return retVal;
  }
 private:
  C* ptr_;
  // Forbid comparison of scoped_ptr types.  If C2 != C, it totally doesn't
  // make sense, and if C2 == C, it still doesn't make sense because you should
  // never have the same object owned by two different scoped_ptrs.
  template <class C2> bool operator==(scoped_ptr<C2> const& p2) const;
  template <class C2> bool operator!=(scoped_ptr<C2> const& p2) const;
  // Disallow evil constructors
  scoped_ptr(const scoped_ptr&);
  void operator=(const scoped_ptr&);
};
// scoped_array<C> is like scoped_ptr<C>, except that the caller must allocate
// with new [] and the destructor deletes objects with delete [].
//
// As with scoped_ptr<C>, a scoped_array<C> either points to an object
// or is NULL.  A scoped_array<C> owns the object that it points to.
//
// Size: sizeof(scoped_array<C>) == sizeof(C*)
template <class C>
class scoped_array {
 public:
  // The element type
  typedef C element_type;
  // Constructor.  Defaults to initializing with NULL.
  // There is no way to create an uninitialized scoped_array.
  // The input parameter must be allocated with new [].
  explicit scoped_array(C* p = NULL) : array_(p) { }
  // Destructor.  If there is a C object, delete it.
  // We don't need to test ptr_ == NULL because C++ does that for us.
  ~scoped_array() {
    enum { type_must_be_complete = sizeof(C) };
    delete[] array_;
  }
  // Reset.  Deletes the current owned object, if any.
  // Then takes ownership of a new object, if given.
  // this->reset(this->get()) works.
  void reset(C* p = NULL) {
    if (p != array_) {
      enum { type_must_be_complete = sizeof(C) };
      delete[] array_;
      array_ = p;
    }
  }
  // Get one element of the current object.
  // Will assert() if there is no current object, or index i is negative.
  C& operator[](std::ptrdiff_t i) const {
    assert(i >= 0);
    assert(array_ != NULL);
    return array_[i];
  }
  // Get a pointer to the zeroth element of the current object.
  // If there is no current object, return NULL.
  C* get() const {
    return array_;
  }
  // Comparison operators.
  // These return whether two scoped_array refer to the same object, not just to
  // two different but equal objects.
  bool operator==(C* p) const { return array_ == p; }
  bool operator!=(C* p) const { return array_ != p; }
  // Swap two scoped arrays.
  void swap(scoped_array& p2) {
    C* tmp = array_;
    array_ = p2.array_;
    p2.array_ = tmp;
  }
  // Release an array.
  // The return value is the current pointer held by this object.
  // If this object holds a NULL pointer, the return value is NULL.
  // After this operation, this object will hold a NULL pointer,
  // and will not own the object any more.
  C* release() {
    C* retVal = array_;
    array_ = NULL;
    return retVal;
  }
 private:
  C* array_;
  // Forbid comparison of different scoped_array types.
  template <class C2> bool operator==(scoped_array<C2> const& p2) const;
  template <class C2> bool operator!=(scoped_array<C2> const& p2) const;
  // Disallow evil constructors
  scoped_array(const scoped_array&);
  void operator=(const scoped_array&);
};
}  // namespace internal
// We made these internal so that they would show up as such in the docs,
// but we don't want to stick "internal::" in front of them everywhere.
using internal::scoped_ptr;
using internal::scoped_array;
}  // namespace protobuf
}  // namespace google
#endif  // GOOGLE_PROTOBUF_STUBS_SCOPED_PTR_H_
  • src->google->protobuf->compiler 下创建自定义文件夹,生成如下脚本模板
shioho_msg.cc
#include <string>
using namespace std;
class MsgInfo
{
public:
	int protocolID;
	bool isPush;
	std::string className;
	std::string controllerName;
};
shioho_controller.cc
......
namespace google
{
    namespace protobuf
    {
        namespace compiler
        {
            namespace shioho_controller
            {
                void GenerateFile(const google::protobuf::FileDescriptor *file,
                                  io::Printer *printer,
                                  const gpcc::Options *options,
                                  const ShiohoOption *opt, vector<MsgInfo>::iterator msgInfo)
                {
                    printer->Print(
                        "// \n"
                        "//			Generated by the Shioho Msg Compiler.\n"
                        "// \n"
                        "// \n");
                    // using
                    printer->Print("using Shioho.Net;\n");
                    // namespace
                    printer->Print("namespace $namespace_name$\n", "namespace_name", shioho_namespace);
                    printer->Print("{\n");
                    printer->Indent();
                    // class
                    string className = msgInfo->className;
                    if (msgInfo->isPush)
                    {
                        printer->Print("public class $controller_name$ : ATpro2PushMsgController<$class_name$PushMsgPaser,$class_name$Push>\n", "controller_name", msgInfo->controllerName, "class_name", className);
                        printer->Print("{\n");
                        printer->Indent();
                        printer->Print("protected override void OnProcessSuccess($class_name$Push data)\n", "class_name", className);
                        printer->Print("{\n");
                        printer->Print("\n");
                        printer->Print("}\n");
                        printer->Outdent();
                        printer->Print("}\n");
                    }
                    else
                    {
                        printer->Print("public class $controller_name$ : AProtoSendMsgController<$class_name$RequestMsgPaser,$class_name$ResponseMsgPaser,$class_name$Response>\n", "controller_name", msgInfo->controllerName, "class_name", className);
                        printer->Print("{\n");
                        printer->Indent();
                        printer->Print("protected override void OnProcessFailed($class_name$Response data, object context)\n", "class_name", className);
                        printer->Print("{\n");
                        printer->Print("\n");
                        printer->Print("}\n");
                        printer->Print("protected override void OnProcessSuccess($class_name$Response data, object context)\n", "class_name", className);
                        printer->Print("{\n");
                        printer->Print("\n");
                        printer->Print("}\n");
                        printer->Outdent();
                        printer->Print("}\n");
                    }
                    printer->Outdent();
                    printer->Print("}\n");
                }
                bool Generator::Generate(
                    const FileDescriptor *file,
                    const string &parameter,
                    GeneratorContext *generator_context,
                    string *error) const
                {
                    vector<pair<string, string>> options;
                    ParseGeneratorParameter(parameter, &options);
                    struct gpcc::Options cli_options;
                    ShiohoOption opt;
                    for (int i = 0; i < options.size(); i++)
                    {
                        if (options[i].first == "file_extension")
                        {
                            cli_options.file_extension = options[i].second;
                        }
                        else if (options[i].first == "base_namespace")
                        {
                            cli_options.base_namespace = options[i].second;
                            cli_options.base_namespace_specified = true;
                        }
                        else if (options[i].first == "internal_access")
                        {
                            cli_options.internal_access = true;
                        }
                        else
                        {
                            *error = "Unknown generator option: " + options[i].first;
                            return false;
                        }
                    }
                    vector<MsgInfo>::iterator msgInfo;
                    msgInfo = vec.begin();
                    while (msgInfo != vec.end())
                    {
                        string filename = msgInfo->className + "MsgController.cs";
                        fs::path fullpath(controller_out + filename);
                        bool exist = fs::exists(fullpath);
                        if (!exist)
                        {
                            scoped_ptr<io::ZeroCopyOutputStream> output(
                                generator_context->Open(filename));
                            io::Printer printer(output.get(), '$');
                            GenerateFile(file, &printer, &cli_options, &opt, msgInfo);
                            cout << "[MsgController Generated Successfully]" << filename << endl;
                        }
                        msgInfo++;
                    }
                }
            } // namespace shioho_provider
        }     // namespace compiler
    }         // namespace protobuf
} // namespace google
shioho_paser.cc
......
namespace google
{
	namespace protobuf
	{
		namespace compiler
		{
			namespace shioho_paser
			{
				void AddPaserInfo(io::Printer *printer, string className, int protoId, string extendClassName)
				{
					printer->Print("public class $class_name$MsgPaser : $extend_class_name$<$class_name$>\n", "class_name", className, "extend_class_name", extendClassName);
					printer->Print("{\n");
					printer->Indent();
					printer->Print("protected override MessageParser<$class_name$> _msgPaser => $class_name$.Parser;\n", "class_name", className);
					printer->Print("protected override int _protoId => $proto_id$;\n", "proto_id", std::to_string(protoId));
					printer->Outdent();
					printer->Print("}\n");
				}
				void GenerateFile(const google::protobuf::FileDescriptor *file,
								  io::Printer *printer,
								  const gpcc::Options *options,
								  const ShiohoOption *opt, vector<MsgInfo>::iterator msgInfo)
				{
					printer->Print(
						"// \n"
						"//			Generated by the Shioho Msg Compiler.\n"
						"// \n"
						"//			   DO NOT EDIT!  DO NOT EDIT!\n"
						"// \n");
					// using
					printer->Print("using Google.Protobuf;\n");
					printer->Print("using Shioho.Net;\n");
					// namespace
					printer->Print("namespace $namespace_name$\n", "namespace_name", shioho_namespace);
					printer->Print("{\n");
					printer->Indent();
					// class
					string className;
					if (msgInfo->isPush)
					{
						className = msgInfo->className + "Push";
						AddPaserInfo(printer, className, msgInfo->protocolID, "AProtoMsgPaser");
					}
					else
					{
						className = msgInfo->className + "Request";
						AddPaserInfo(printer, className, msgInfo->protocolID, "AProtoMsgPaser");
						className = msgInfo->className + "Response";
						AddPaserInfo(printer, className, msgInfo->protocolID, "AProtoResponseMsgPaser");
					}
					printer->Outdent();
					printer->Print("}\n");
				}
				bool Generator::Generate(
					const FileDescriptor *file,
					const string &parameter,
					GeneratorContext *generator_context,
					string *error) const
				{
					vector<pair<string, string>> options;
					ParseGeneratorParameter(parameter, &options);
					struct gpcc::Options cli_options;
					ShiohoOption opt;
					for (int i = 0; i < options.size(); i++)
					{
						if (options[i].first == "file_extension")
						{
							cli_options.file_extension = options[i].second;
						}
						else if (options[i].first == "base_namespace")
						{
							cli_options.base_namespace = options[i].second;
							cli_options.base_namespace_specified = true;
						}
						else if (options[i].first == "internal_access")
						{
							cli_options.internal_access = true;
						}
						else
						{
							*error = "Unknown generator option: " + options[i].first;
							return false;
						}
					}
					vector<MsgInfo>::iterator msgInfo;
					msgInfo = vec.begin();
					while (msgInfo != vec.end())
					{
						string filename = msgInfo->className + "MsgPaser.cs";
						scoped_ptr<io::ZeroCopyOutputStream> output(
							generator_context->Open(filename));
						io::Printer printer(output.get(), '$');
						GenerateFile(file, &printer, &cli_options, &opt, msgInfo);
						cout << "[MsgController Generated Successfully]" << filename << endl;
						msgInfo++;
					}
				}
			} // namespace shioho_provider
		}	  // namespace compiler
	}		  // namespace protobuf
} // namespace google
shioho_provider.cc
......
namespace google
{
	namespace protobuf
	{
		namespace compiler
		{
			namespace shioho_provider
			{
				void GenerateFile(const google::protobuf::FileDescriptor *file,
								  io::Printer *printer,
								  const gpcc::Options *options,
								  const ShiohoOption *opt)
				{
					printer->Print(
						"// \n"
						"//			Generated by the Shioho Msg Compiler.\n"
						"// \n"
						"//			   DO NOT EDIT!  DO NOT EDIT!\n"
						"// \n");
					// using
					printer->Print("using Shioho.Net;\n");
					// namespace
					printer->Print("namespace $namespace_name$\n", "namespace_name", shioho_namespace);
					printer->Print("{\n");
					printer->Indent();
					// class
					printer->Print("public class $namespace$MsgProvider : AProtoMsgCtrlProvider\n", "namespace", shioho_namespace);
					printer->Print("{\n");
					printer->Indent();
					// MsgProvider
					printer->Print("protected override void OnInit()\n");
					printer->Print("{\n");
					printer->Indent();
					vector<MsgInfo>::iterator msgInfo;
					msgInfo = vec.begin();
					while (msgInfo != vec.end())
					{
						printer->Print("AddMsgCtrl(new $ctrl$());\n", "ctrl", msgInfo->controllerName);
						msgInfo++;
					}
					printer->Outdent();
					printer->Print("}\n");
					// enum
					printer->Print("public enum E$namespace$ProtoId\n", "namespace", shioho_namespace);
					printer->Print("{\n");
					printer->Indent();
					msgInfo = vec.begin();
					while (msgInfo != vec.end())
					{
						string protoName = msgInfo->isPush ? msgInfo->className + "Push" : msgInfo->className;
						printer->Print("$protoName$ = $protoId$,\n", "protoName", protoName, "protoId", std::to_string(msgInfo->protocolID));
						msgInfo++;
					}
					printer->Outdent();
					printer->Print("}\n");
					printer->Outdent();
					printer->Print("}\n");
					printer->Outdent();
					printer->Print("}\n");
				}
				bool Generator::Generate(
					const FileDescriptor *file,
					const string &parameter,
					GeneratorContext *generator_context,
					string *error) const
				{
					vector<pair<string, string>> options;
					ParseGeneratorParameter(parameter, &options);
					struct gpcc::Options cli_options;
					ShiohoOption opt;
					for (int i = 0; i < options.size(); i++)
					{
						if (options[i].first == "file_extension")
						{
							cli_options.file_extension = options[i].second;
						}
						else if (options[i].first == "base_namespace")
						{
							cli_options.base_namespace = options[i].second;
							cli_options.base_namespace_specified = true;
						}
						else if (options[i].first == "internal_access")
						{
							cli_options.internal_access = true;
						}
						else
						{
							*error = "Unknown generator option: " + options[i].first;
							return false;
						}
					}
					string filename = shioho_namespace + "MsgProvider.cs";
					scoped_ptr<io::ZeroCopyOutputStream> output(generator_context->Open(filename));
					io::Printer printer(output.get(), '$');
					GenerateFile(file, &printer, &cli_options, &opt);
				}
			} // namespace shioho_provider
		}	  // namespace compiler
	}		  // namespace protobuf
} // namespace google
csharp_message.cc

csharp_message.cc 添加 shioho_msg.cc 所需要的全局变量引用,在 Generate 方法内添加 proto 收集,csharp_message.h 添加 std::string GetProtocolID(); 引用

...
#include <regex>
...
#include <google/protobuf/compiler/csharp_shioho/shioho_msg.cc>
extern std::vector<MsgInfo> vec;
using namespace std;
void CheckRepeateProtocolID(vector<MsgInfo> &v, MsgInfo info)
{
  for (size_t i = 0, length = v.size(); i < length; i++)
  {
    if (v[i].protocolID == info.protocolID)
    {
      std::cout << "\nError: Message protocolID = " << info.protocolID << " repeat!!\n"
                << "	[Message old]:" << v[i].className << "\n"
                << "	[Message new]:" << info.className << "\n"
                << std::endl;
      system("pause");
      exit(0);
    }
  }
  v.push_back(info);
string MessageGenerator::GetProtocolID()
{
  SourceLocation location;
  if (descriptor_->GetSourceLocation(&location))
  {
    std::regex pattern("^\\s*id\\s*=\\s*(\\d+)\\s*$", std::regex::icase);
    std::match_results<string::const_iterator> result
    if (std::regex_search(location.leading_comments, result, pattern))
    {
      return result[1];
    }
  }
  return "";
}
void MessageGenerator::Generate(io::Printer *printer)
{
    ...
          bool isResponse = false;
          bool isRequest = false;
          bool isPush = false;
          //proto 收集
          string protocolID = GetProtocolID();
          if (protocolID != "")
          {
            string responseName = "Response";
            string requestName = "Request";
            string pushName = "Push";
            string protoName = vars["class_name"];
            string::size_type respPos = protoName.find(responseName);
            string::size_type reqPos = protoName.find(requestName);
            string::size_type pushPos = protoName.find(pushName);
            isResponse = respPos != string::npos;
            isRequest = reqPos != string::npos;
            isPush = pushPos != string::npos;
            if (!isResponse && !isRequest && !isPush)
            {
              std::cout << "\nError: proto name is not equal Request or Response or Push :" << protoName << "\n"
                        << std::endl;
              system("pause");
              exit(0);
            }
            MsgInfo info;
            info.protocolID = atoi(protocolID.c_str());
            info.isPush = isPush;
            if (isRequest)
            {
              info.className = protoName.replace(reqPos, requestName.length(), "");
            }
            else if (isResponse)
            {
              info.className = protoName.replace(respPos, responseName.length(), "");
            }
            else if (isPush)
            {
              info.className = protoName.replace(pushPos, pushName.length(), "");
            }
            info.controllerName = info.className + "MsgController";
            CheckRepeateProtocolID(vec, info);
          }
    ...
}
csharp_helpers.cc

修改原生工具生成的序列化反序列化类的命名空间为自定义

...
using namespace std;
extern string shioho_namespace;
...
std::string GetFileNamespace(const FileDescriptor* descriptor) {
  return shioho_namespace;
}
main.cc

src->google->protobuf->compiler 下的 main.cc 改名为 main_old.cc 进行备份,新建一个 main.cc 为自定义启动文件,内容如下:

......
void AddProtoInfo(vector<ProtoInfo>& files, ProtoInfo file)
{
	for (size_t i = 0, length = files.size(); i < length; i++)
	{
		ProtoInfo old = files[i];
		if (old.name == file.name)
		{
			cout << "\nError:  proto filename repeat!!\n"
				<< "	[FileName]:" << file.name << "\n\n"
				<< "	[OldPath]: " + old.path + "\n"
				<< "	[NewPath]: " + file.path + "\n"
				<< endl;
			system("pause");
			exit(0);
		}
	}
	files.push_back(file);
}
void CollectAllProto(vector<ProtoInfo>& files, string dir)
{
	for (auto& p : fs::recursive_directory_iterator(dir))
	{
		if (p.is_directory())
			continue;
		auto filepath = p.path();
		auto extension = filepath.extension();
		if (extension.generic_string() != ".proto")
			continue;
		auto filename = filepath.filename();
		ProtoInfo proto;
		proto.path = filepath.generic_string();
		proto.name = filename.generic_string();
		AddProtoInfo(files, proto);
	}
}
bool comp(const MsgInfo& a, const MsgInfo& b)
{
	return a.protocolID < b.protocolID;
}
void ProtobufMain(int argc, char* argv[])
{
	//tmp 为具体 proto 路径
	// argv = "-I=${sourceFolder}/ tmp --csharp_out=${targetFolder} --csharp_opt=namespace_name=XXX --provider_out=${targetFolder} --paser_out=${targetFolder} --controller_out=${targetFolder}";
	string dir = argv[1];
	//-I = 替换为空
	dir = dir.replace(0, 3, "");
	vector<ProtoInfo> proto_files;
	CollectAllProto(proto_files, dir);
	// 初始化 namespace
	string opt = argv[4];
	opt = opt.substr(13);
	string namespaceName = "namespace_name=";
	string::size_type respPos = opt.find(namespaceName);
	shioho_namespace = opt.substr(respPos + namespaceName.length());
	// 去除 --provider_out=
	provider_out = argv[5];
	provider_out = provider_out.substr(15);
	// 去除 --paser_out=
	paser_out = argv[6];
	paser_out = paser_out.substr(12);
	// 去除 --controller_out=
	controller_out = argv[7];
	controller_out = controller_out.substr(17);
	for (size_t i = 0, length = proto_files.size(); i < length; i++)
	{
		//proto 文件名
		argv[2] = proto_files[i].path.data();
		google::protobuf::compiler::CommandLineInterface cli;
		cli.AllowPlugins("protoc-");
		google::protobuf::compiler::csharp::Generator csharp_generator;
		cli.RegisterGenerator("--csharp_out", "--csharp_opt", &csharp_generator,
			"Generate C# source file.");
		int result = cli.Run(argc - 4, argv);
		if (result == 0)
		{
			cout << "[Proto Generated Successfully] " << argv[2] << endl;
		}
		else
		{
			cout << "Error: Proto Generated Failed!!  File: " << argv[2] << endl;
		}
	}
	cout << endl;
	// 排序
	sort(vec.begin(), vec.end(), comp);
	// MsgProvider.cs
	string providerDir = ("--csharp_out=" + provider_out);
	argv[3] = providerDir.data();
	google::protobuf::compiler::CommandLineInterface cli_provider;
	cli_provider.AllowPlugins("protoc-");
	google::protobuf::compiler::shioho_provider::Generator shioho_provider;
	cli_provider.RegisterGenerator("--csharp_out", "--csharp_opt", &shioho_provider,
		"Generate Shioho Provider source file.");
	int result = cli_provider.Run(argc - 4, argv);
	if (result == 0)
	{
		cout << "[MsgProvider Generated Successfully] " << endl;
	}
	else
	{
		cout << "Error: MsgProvider Generated Failed!!" << endl;
	}
	// MsgPaser.cs
	string paserDir = ("--csharp_out=" + paser_out);
	argv[3] = paserDir.data();
	google::protobuf::compiler::CommandLineInterface cli_paser;
	cli_paser.AllowPlugins("protoc-");
	google::protobuf::compiler::shioho_paser::Generator shioho_paser;
	cli_paser.RegisterGenerator("--csharp_out", "--csharp_opt", &shioho_paser,
		"Generate Shioho Paser source file.");
	result = cli_paser.Run(argc - 4, argv);
	if (result == 0)
	{
		cout << "[Shioho Paser All Files Generated Successfully] " << endl;
	}
	else
	{
		cout << "Error: Shioho Paser File Gnerated Failed!!" << endl;
	}
	// MsgController.cs
	string controllerDir = ("--csharp_out=" + controller_out);
	argv[3] = controllerDir.data();
	google::protobuf::compiler::CommandLineInterface cli_controller;
	cli_controller.AllowPlugins("protoc-");
	google::protobuf::compiler::shioho_controller::Generator shioho_controller;
	cli_controller.RegisterGenerator("--csharp_out", "--csharp_opt", &shioho_controller,
		"Generate Shioho Paser source file.");
	result = cli_controller.Run(argc - 4, argv);
	if (result == 0)
	{
		cout << "[Shioho Controller All Files Generated Successfully] " << endl;
	}
	else
	{
		cout << "Error: Shioho Controller File Gnerated Failed!!" << endl;
	}
}
void test(char* argv[])
{
	int argc = 8;
	char* arr[8];
	arr[0] = argv[0];
	arr[1] = "-I=F://ShiohoGit/unity-protobuf/MyProtoGen/Protos/";
	arr[2] = "tmp";
	arr[3] = "--csharp_out=F://ShiohoGit/unity-protobuf/MyProtoGen/Proto2CSharp/";
	arr[4] = "--csharp_opt=namespace_name=ShiohoNet";
	arr[5] = "--provider_out=F://ShiohoGit/unity-protobuf/MyProtoGen/Proto2CSharp/";
	arr[6] = "--paser_out=F://ShiohoGit/unity-protobuf/MyProtoGen/Proto2CSharp/";
	arr[7] = "--controller_out=F://ShiohoGit/unity-protobuf/MyProtoGen/Proto2CSharp/";
	ProtobufMain(argc, arr);
}
int main(int argc, char* argv[])
{
	//test(argv);
	ProtobufMain(argc, argv);
	return 0;
}

可以看到,我们将命令行格式改为了这样: protoc.exe -I=${sourceFolder}/ tmp --csharp_out=${targetFolder} --csharp_opt=namespace_name=XXX --provider_out=${targetFolder} --paser_out=${targetFolder} --controller_out=${targetFolder}
-I :proto 文件夹路径
tmp :proto 文件名
--csharp_opt :配置参数,现在只配置了 namespace_name ,即命名空间名称
--xxx_out :生成文件的路径

  • cmake->libprotoc.cmake 添加自定义的.h 和.cpp 文件

  • 生成 vs 解决方案
    以 vs2019 为例,在 cmake 文件夹下新建 build 文件夹,添加 vs2019_solution.bat 批处理,内容如下:

cmake -G "Visual Studio 16 2019" -DCMAKE_BUILD_TYPE=Release -Dprotobuf_BUILD_TESTS=OFF ../
pause

双击批处理文件,即可生成 vs2019 解决方案

  • 双击 protobuf.sln ,用 vs2019 打开解决方案
    右键 protoc , 将其设为启动项目
    右键解决方案 protoclibprotoc ,选择属性,将 c 语言标准改为 c17,然后应用

    打开 protoc->Source Files->main.cc 文件,此为启动文件

如果需要进行断点调试,将 main 函数下的 test(argv); 注释取消,然后项目内断点即可

  • 生成 protoc.exe

点击 vs->生成->生成解决方案 即可在 cmake->build->Debug 下生成 protoc.exe

# 使用自定义 protoc

  • 将生成的 protoc.exe 和之前的 proto 文件拷贝一份出来,由于使用的自定义 protoc,所以 proto 文件需要多定义一个 protoId,格式如下:
syntax = "proto3";
package proto;
message Player {
    string id = 1;
    string name = 2;
    string head = 3;
    uint64 playerExp = 4 ; // 修为
    uint64 playerLevel = 5;// 玩家段位
    uint64 weaponLevel = 6;// 兵器段位
    uint64 weaponExp = 7;// 兵器修为
}
// 查询玩家
//id=1001
message GetPlayerRequest {
    string playerId = 1;// 玩家 ID
}
message GetPlayerResponse {
    sint64 code = 1;
    string info = 2;
    message Data {
        Player player = 1;
        bool Exist = 2;
    }
    Data data = 3;
}
//id=1002
message ReceivePlayerPush{
    Player player = 1;
}

注意,自定义代码生成严格要求 proto 文件格式,否则会生成失败,Request 消息必须以 Request 结尾,Response 消息必须以 Response 结尾,Push 消息必须以 Push 结尾;Request 和 Push 消息上方必须定义一个不重复的 id //id=xxx 用于设置 protoId,Requset 和 Push 消息除去后缀的前缀不能重复,可能会导致重复写入问题

...
    try {
        var args = `${exePath} -I=${sourceFolder}/ tmp --csharp_out=${targetFolder} --csharp_opt=namespace_name=ShiohoNet --provider_out=${targetFolder} --paser_out=${targetFolder} --controller_out=${targetFolder}`;
        ...
    } catch (e) {
        console.error("转换失败", e);
    }

点击批处理文件,看见生成以下文件就说明自定义 protoc 成功啦!!!

# 在 Unity 中使用自定义 protoc 程序

啊,终于到 Unity 相关了,虽然题目叫《Unity 之自定义 Protobuf 代码生成工具》,但其实也就这里相挂钩吧!!!我这个标题党!!!

  • Unity 下新建 Editor 文件夹
    将生成的 protoc.exe 放入该文件夹中

  • 创建 Proto2CSharpWindow 编辑器脚本

using System.IO;
using UnityEditor;
using UnityEngine;
namespace Shioho.Editor 
{
    class Proto2CSharpConfig
    {
        public string Namespace;
        public string ProtoExe;
        public string ProtoFilePath;
        public string Proto2CSharpPath;
    }
    public class Proto2CSharpWindow : EditorWindow
    {
        [MenuItem("Tools/Proto转C#工具 &6")]
        static void OpenWindow()
        {
            var window = GetWindow<Proto2CSharpWindow>("Proto转C#工具", true);
            window.minSize = new Vector2(700, 300);
            window.Show();
        }
        private bool _isInit = false;
        private string _exePath = string.Empty;
        private string _protoPath = string.Empty;
        private string _namespace = string.Empty;
        private string _proto2CSharpPath = string.Empty;
        private string _protoJsonPath = string.Empty;
        private void OnGUI()
        {
            if (!_isInit)
            {
                InitCfg();
                _isInit = true;
            }
            EditorGUILayout.BeginVertical();
            OnExeExeGUI();
            EditorGUILayout.Space();
            OnProtoFolderGUI();
            EditorGUILayout.Space();
            OnProto2CSharpFolderGUI();
            EditorGUILayout.Space();
            OnNamespaceGUI();
            EditorGUILayout.Space();
            OnGenerateGUI();
            EditorGUILayout.EndVertical();
        }
        void InitCfg()
        {
            string libPath = Application.dataPath.Replace("Assets", "Library");
            _protoJsonPath = Path.Combine(libPath, "proto2CSharpConfig.json");
            var fileExist = File.Exists(_protoJsonPath);
            // 有文件 就读文件
            if (fileExist)
            {
                var json = File.ReadAllText(_protoJsonPath);
                var cfg = new Proto2CSharpConfig();
                EditorJsonUtility.FromJsonOverwrite(json, cfg);
                _exePath = cfg.ProtoExe;
                _namespace = cfg.Namespace;
                _protoPath = cfg.ProtoFilePath;
                _proto2CSharpPath = cfg.Proto2CSharpPath;
            }
        }
        void OnExeExeGUI()
        {
            GUILayout.Label("protoc.exe地址:");
            GUILayout.BeginHorizontal();
            string preValue = _exePath;
            if (string.IsNullOrEmpty(preValue))
            {
                preValue = Path.GetDirectoryName(Application.dataPath);
            }
            // 输出浏览按钮
            if (GUILayout.Button("Browser", GUILayout.Width(100), GUILayout.Height(25)))
            {
                // 弹出窗口选择文件夹
                string selectFile = EditorUtility.OpenFilePanel("Select File", preValue, "");
                preValue = selectFile;
            }
            GUI.skin.textField.alignment = TextAnchor.MiddleLeft;
            // 输出文件夹路径
            using (new EditorGUI.DisabledGroupScope(true))
            {
                preValue = GUILayout.TextField(preValue, GUILayout.Height(25));
            }
            if (!_exePath.Equals(preValue))
            {
                _exePath = preValue;
                SaveFile();
            }
            GUILayout.EndHorizontal();
        }
        void SaveFile()
        {
            var info = new Proto2CSharpConfig()
            {
                Namespace = _namespace,
                ProtoExe = _exePath,
                ProtoFilePath = _protoPath,
                Proto2CSharpPath = _proto2CSharpPath,
            };
            var jsoninfo = EditorJsonUtility.ToJson(info);
            File.WriteAllText(_protoJsonPath, jsoninfo);
        }
        void OnProtoFolderGUI()
        {
            GUILayout.Label(new GUIContent("proto文件夹:", "包括所有.proto文件"));
            GUILayout.BeginHorizontal();
            string preValue = _protoPath;
            if (string.IsNullOrEmpty(preValue))
            {
                preValue = Path.GetDirectoryName(Application.dataPath);
            }
            // 输出浏览按钮
            if (GUILayout.Button("Browser", GUILayout.Width(100), GUILayout.Height(25)))
            {
                // 弹出窗口选择文件夹
                string selectFolder = EditorUtility.OpenFolderPanel("Select Folder", preValue, "");
                preValue = selectFolder;
            }
            GUI.skin.textField.alignment = TextAnchor.MiddleLeft;
            // 输出文件夹路径
            using (new EditorGUI.DisabledGroupScope(true))
            {
                preValue = GUILayout.TextField(preValue, GUILayout.Height(25));
            }
            if (!_protoPath.Equals(preValue))
            {
                _protoPath = preValue;
                // 保存数据
                SaveFile();
            }
            GUILayout.EndHorizontal();
        }
        void OnProto2CSharpFolderGUI()
        {
            GUILayout.Label(new GUIContent("proto转C#文件夹:"));
            GUILayout.BeginHorizontal();
            string preValue = _proto2CSharpPath;
            if (string.IsNullOrEmpty(preValue))
            {
                preValue = Path.GetDirectoryName(Application.dataPath);
            }
            // 输出浏览按钮
            if (GUILayout.Button("Browser", GUILayout.Width(100), GUILayout.Height(25)))
            {
                // 弹出窗口选择文件夹
                string selectFolder = EditorUtility.OpenFolderPanel("Select Folder", preValue, "");
                preValue = selectFolder;
            }
            GUI.skin.textField.alignment = TextAnchor.MiddleLeft;
            // 输出文件夹路径
            using (new EditorGUI.DisabledGroupScope(true))
            {
                preValue = GUILayout.TextField(preValue, GUILayout.Height(25));
            }
            if (!_proto2CSharpPath.Equals(preValue))
            {
                _proto2CSharpPath = preValue;
                // 保存数据
                SaveFile();
            }
            GUILayout.EndHorizontal();
        }
        void OnNamespaceGUI()
        {
            GUILayout.BeginHorizontal();
            GUILayout.Label("生成C#文件的命名空间: ", GUILayout.Width(135));
            string nameSpace = _namespace;
            if (string.IsNullOrEmpty(nameSpace))
            {
                nameSpace = "Game";
            }
            nameSpace = GUILayout.TextField(nameSpace, GUILayout.Height(20), GUILayout.Width(200));
            if (!_namespace.Equals(nameSpace))
            {
                _namespace = nameSpace;
                SaveFile();
            }
            GUILayout.EndHorizontal();
        }
        void OnGenerateGUI()
        {
            if (GUILayout.Button("生成"))
            {
                if (string.IsNullOrEmpty(_exePath))
                {
                    Debug.LogError("请设置protoc.exe路径");
                    return;
                }
                if (string.IsNullOrEmpty(_protoPath))
                {
                    Debug.LogError("请设置要生成脚本的proto文件夹路径");
                    return;
                }
                if (string.IsNullOrEmpty(_proto2CSharpPath))
                {
                    Debug.LogError("请设置proto对应生成的C#文件夹路径");
                    return;
                }
                if (string.IsNullOrEmpty(_namespace))
                {
                    Debug.LogError("请设置生成C#文件的命名空间");
                    return;
                }
                Generate();
            }
        }
        void Generate()
        {
            //MsgPaserPath
            var paserPath = _proto2CSharpPath + "/MsgPaser";
            if (!Directory.Exists(paserPath))
            {
                Directory.CreateDirectory(paserPath);
            }
            //ControllerPath
            var ctrlPath = _proto2CSharpPath + "/MsgController/";
            if (!Directory.Exists(ctrlPath))
            {
                Directory.CreateDirectory(ctrlPath);
            }
            var args = $"-I={_protoPath}/ tmp --csharp_out={_proto2CSharpPath} --csharp_opt=namespace_name={_namespace} --provider_out={_proto2CSharpPath} --paser_out={paserPath} --controller_out={ctrlPath}";
            var p = new System.Diagnostics.Process();
            try
            {
                p.StartInfo.UseShellExecute = false;
                p.StartInfo.FileName = _exePath;
                p.StartInfo.Arguments = args;
                p.StartInfo.CreateNoWindow = true;
                p.StartInfo.RedirectStandardOutput = true;
                p.Start();
                string output = p.StandardOutput.ReadToEnd();
                Debug.Log(output);
                p.WaitForExit();
                p.Close();
                Debug.Log("proto相关文件生成完毕");
            }
            catch (System.Exception e)
            {
                Debug.LogError(e.Message);
            }
            AssetDatabase.Refresh();
        }
    }
}
  • 编辑器面板设置

然后点击生成即可生成对应代码啦~

# 简单易用的小 Demo

让俺们看看到底方便了些什么吧

# 随便写写服务端

  • 引入 nodejs 端 protobuf 解析库
npm install protobufjs --save
  • .proto 转化为.js
pbjs -t static-module -w commonjs -o player.js player.proto
  • 服务端测试代码
var ws = require("nodejs-websocket");
var playerProto = require('./player')
function main() {
    createWsServer(4396)
}
function createWsServer(port) {
    console.log(`创建本地websocket,port:  ${port}`);
    //websocket
    wsServer = ws.createServer(conn => {
        console.log("一个客户端连接了,当前连接数:", wsServer.connections.length);
        conn.on('error', err => {
            console.log("client error:", err);
        });
        conn.on('close', msg => {
            console.log("一个客户端断开了连接,当前连接数:", wsServer.connections.length);
        });
        conn.on('binary', inStream => {
            var data = Buffer.alloc(0)
            inStream.on("readable", function () {
                var newData = inStream.read()
                if (newData)
                    data = Buffer.concat([data, newData], data.length + newData.length)
            })
            inStream.on("end", function () {
                // 输出一下收到的 req
                var recBuf = data.slice(8, data.length);
                var req = playerProto.proto.GetPlayerRequest.decode(recBuf);
                console.log("==========>playerId:", req.playerId);
                // 随便回一个 response
                var player = randomPlayer();
                var resObj = playerProto.proto.GetPlayerResponse.create();
                resObj.code = 0
                resObj.info = ""
                resObj.data = {}
                resObj.data.player = player;
                resObj.data.Exist = true;
                var encode = playerProto.proto.GetPlayerResponse.encode(resObj).finish();
                // 转化为自定义格式 resp
                //00100100 = 36
                //seqid = 1 = [0,0,1]
                //protoid 1001 = 1111101001 = [3,233]
                var buffer = [36, 0, 0, 1, 1001 >> 8, 1001 & 0XFF, encode.length >> 8, encode.length & 0XFF]
                encode.forEach(element => {
                    buffer.push(element);
                });
                // 发送
                var u8Arr = new Uint8Array(buffer);
                conn.sendBinary(u8Arr);
            })
        });
        // 连接成功 5s 后随便发一条 push 吧
        setTimeout(() => {
            var player = randomPlayer();
            var pushObj = playerProto.proto.ReceivePlayerPush.create();
            pushObj.player = player
            var encode = playerProto.proto.ReceivePlayerPush.encode(pushObj).finish();
            // 转化为自定义格式 resp
            //00101000 = 40
            var buffer = [40, 1002 >> 8, 1002 & 0XFF, encode.length >> 8, encode.length & 0XFF]
            encode.forEach(element => {
                buffer.push(element);
            });
            // 发送
            var u8Arr = new Uint8Array(buffer);
            conn.sendBinary(u8Arr);
        }, 3000)
    }).listen(port);
    wsServer.on('error', err => {
        console.log("server error: ", err);
    });
}
function randomPlayer() {
    var obj = playerProto.proto.Player.create();
    obj.name = "Name_" + Math.round(Math.random() * 1000);
    return obj;
}
main();

# 随便写写客户端

  • DemoWs
private ProtoWebsocket _ws;
   private ShiohoNetMsgProvider _provider;
   void Start()
   {
       _provider = new ShiohoNetMsgProvider();
       _ws = new ProtoWebsocket(@"ws://127.0.0.1:4396");
       _ws.OnProtoResponseMsg = OnResponseMsg;
       _ws.OnProtoPushMsg = OnPushMsg;
       _ws.Connect();
   }
   void Update()
   {
       if (Input.GetKeyDown(KeyCode.A)) 
       {
           var req = new GetPlayerRequest();
           req.PlayerId = "10086";
           SendRequest(EShiohoNetProtoId.GetPlayer, req);
       }
   }
   private void OnDestroy()
   {
       _ws.Disconnect();
   }
   private void OnResponseMsg(ProtoResponseData response)
   {
       var msgCtrl = _provider.GetSendMsgCtrl(response.ProtoId);
       msgCtrl?.Process(response.SeqId, response.Msg);
   }
   private void OnPushMsg(ProtoPushData push)
   {
       var msgCtrl = _provider.GetPushMsgCtrl(push.ProtoId);
       msgCtrl?.Process(push.Msg);
   }
   private void SendRequest(EShiohoNetProtoId protoId, IMessage data, object context = null)
   {
       var msgCtrl = _provider.GetSendMsgCtrl((int)protoId);
       if (msgCtrl == null)
           return;
       msgCtrl.AddContext(_ws.SeqId, context);
       var buffer = msgCtrl.GetBuffer(_ws.SeqId, data);
       _ws?.SendBinary(buffer);
   }
  • 添加消息输出日志
    GetPlayerMsgController.csReceivePlayerMsgController.cs 中添加消息日志
protected override void OnProcessSuccess(GetPlayerResponse data, object context)
    {
        Debug.Log("成功收到Response消息:" + data.Data.Player.Name);
    }
    protected override void OnProcessSuccess(ReceivePlayerPush data)
    {
        Debug.Log("成功收到Push消息:" + data.Player.Name);
    }

# 来运行吧~

运行服务端,运行客户端,5s 后客户端成功收到 push 消息

在客户端按下 A 键,服务端成功解析 request,客户端成功收到服务端返回的 response

好耶,一切都正常运行啦,这下我们可以不用关心序列化反序列化过程啦,只需要在对应的 response 或 push Controller 中处理对应的业务逻辑就好啦,是不是简单很多!!!(并没有 x (* ̄︶ ̄)

# 总结

哇,本来就想稍微写一写的,没想到写了 40k+,今年最后一篇文了,真的写了好久啊。站点字数也终于 100k + 了,兔年还请继续努力呀,加油,打工人ヾ (◍°∇°◍)ノ゙

更新于 阅读次数

请我喝[茶]~( ̄▽ ̄)~*

汘帆 微信

微信

汘帆 支付宝

支付宝