# 前言
哇,感觉好久没写 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 ¶meter, | |
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 ¶meter, | |
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 ¶meter, | |
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
, 将其设为启动项目
右键解决方案protoc
和libprotoc
,选择属性,将 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 消息除去后缀的前缀不能重复,可能会导致重复写入问题
- 多文件批量转化
将上方基于 nodejs 的多文件批量转化中的批处理参数改为如下参数:
... | |
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.cs
和ReceivePlayerMsgController.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 + 了,兔年还请继续努力呀,加油,打工人ヾ (◍°∇°◍)ノ゙