把pbc移植到项目引擎中

射击游戏项目开发笔记 (一)

目前我使用的引擎对代码的编码方式有了非常严格的控制,一般的C++写法已经无法在引擎中运行了,由于目前项目准备使用protobuf来作为通讯数据,所以我必须实现一个动态解析protobuf的库,考虑到已经有人做了一个事情,而且做得很好(https://github.com/cloudwu/pbc),所以我就把云风大侠的c版本移植到目前使用的引擎上来.这篇文章主要记录的是一些修改事项和protobuf的一些基本内容,并非使用教程.

简要说明

protocol buffers是google提供的一种将结构化数据进行序列化和反序列化的方法,其优点是语言中立,平台中立,可扩展性好,目前在google内部大量用于数据存储,通讯协议等方面。PB在功能上类似XML,但是序列化后的数据更小,解析更快,使用上更简单。用户只要按照proto语法在.proto文件中定义好数据的结构,就可以使用PB提供的工具(protoc)自动生成处理数据的代码,使用这些代码就能在程序中方便的通过各种数据流读写数据。PB目前支持Java, C++和Python3种语言。另外,PB还提供了很好的向后兼容,即旧版本的程序可以正常处理新版本的数据,新版本的程序也能正常处理旧版本的数据。

ProtocolBuffer是用于结构化数据串行化的灵活、高效、自动的方法,有如XML,不过它更小、更快、也更简单。你可以定义自己的数据结构,然后使用代码生成器生成的代码来读写这个数据结构。你甚至可以在无需重新部署程序的情况下更新数据结构。

protoc 工具的使用

基本样式: protoc.exe [OPTION] PROTO_FILES
载入文件PROTO_FILES,然后根据以下选项生成文件

** -IPATH, –proto_path=PATH**

指定目录搜索proto文件,可以指定多个,然后按照输入顺序搜索,如果搜索完毕没找到文件,则搜索当前目录.

protoc.exe --version

–version

打印工具版本号

protoc.exe --version

-h, –help

输出帮助信息

protoc.exe --help or protoc.exe -h

–encode=MESSAGE_TYPE

指定编码方式,读取一个这个编码方式的文件,然后写入一个二进制文件,这种编码方式必须定义在输入的文件 或者其他导入的文件里.

–decode=MESSAGE_TYPE

读取一个二进制文件,然后根据这种解码方式解码.生成一个这种解码方式的文件,这种解码方式必须定义在输入的文件 或者其他导入的文件里.

–decode_raw

读取输入内容,然后使用 tag/value 的解析格式输出,不需要输入文件信息

-oFILE,–descriptor_set_out=FILE

根据输入的proto文件 生成一种二进制文件,FILE 为指定的输出文件名, 这种方式输出的文件用于protobuf直接编解码数据.(大家都知道protobuf能用代理类去解编码数据)

–include_imports

当使用-descriptor_set_out生成编码数据的时候使用,意思是导入一些需要的依赖文件.

protoc.exe --descriptor_set_out=Output.file --include_imports Input.proto

–include_source_info

当使用-descriptor_set_out生成编码数据的时候使用,意思是包含源文件信息

–error_format=FORMAT

指定打印的错误格式,默认是’gcc’,也可以是 ‘msvs'(vc的错误格式)

–plugin=EXECUTABLE

指定一个可执行插件,默认是在当前目录寻找,当然也可以指定其他目录下的文件.

–cpp_out=OUT_DIR

生成c++代理类

–java_out=OUT_DIR

生成java代理类

–python_out=OUT_DIR

生成python代理类

proto语法说明

支持的类型

.proto类型 C++中的类型
double double
float float
int32 int32
int64 int64
uint32 uint32
uint64 uint64
sint32 int32
sint64 int64
fixed32 uint32
fixed64 uint64
sfixed32 int32
sfixed64 int64
bool bool
string string

举例如下

message Person {
  required string name = 1;
  required int32 id = 2;
  optional string email = 3;
 
  enum PhoneType {
    MOBILE = 0;
    HOME = 1;
    WORK = 2;
  }
 
  message PhoneNumber {
    required string number = 1;
    optional PhoneType type = 2 [default = HOME];
  }
 
  repeated PhoneNumber phone = 4;
}
 
message AddressBook {
  repeated Person person = 1;
}

关键字

message 编写最小的数据结构的关键字
required 必需的,字段加了这个关键字之后代表字段是必须的.不可缺少的.
optional 可选的,代表数据结构里的该字段是可以选择性添加的.
repeated 可重复的

Package:

命名空间,影响java的包名及生成的类名;

如:packagecom.example.message

Import:

导入其它文件中的message

如:import “myfile/message1.proto”

顺序标识号

等于号与数字 = N 代表字段在数据结构中的顺序.(官方推荐1-15)

修饰符

[default = value] -> 为该域设置一个默认值 (默认值是不需编码的)

如:optional uint32 ad_bid_count = 4[default = 2];

[packed =false / true]->采用更紧凑的编码方式

如:repeated int32 samples = 4[packed=true];

[deprecated =false/true]->标识该域是否已经被弃用

如:optional int32 old_field = 6[deprecated=true];

[optimize_for= SPEED/CODE/LITE_RUNTIME]:影响代码生成

扩展 extensions 关键字

通过扩展,可以将一个范围内的字段标识号声明为可被第三方扩展所用。然后,其他人就可以在他们自己的.proto文件中为该消息类型声明新的字段,而不必去编辑原始文件了。看个具体例子:

message Foo {
  // …
  extensions 100 to 199;
}

这个例子表明:在消息Foo中,范围[100,199]之内的字段标识号被保留为扩展用。现在,其他人就可以在他们自己的.proto文件中添加新字段到Foo里了,但是添加的字段标识号要在指定的范围内——例如:

extend Foo {
  optional int32 bar = 126;
}

这个例子表明:消息Foo现在有一个名为bar的optional int32字段。

当用户的Foo消息被编码的时候,数据的传输格式与用户在Foo里定义新字段的效果是完全一样的。

然而,要在程序代码中访问扩展字段的方法与访问普通的字段稍有不同——生成的数据访问代码为扩展准备了特殊的访问函数来访问它。例如,下面是如何在C++中设置bar的值:

Foo foo;
foo.SetExtension(bar, 15);

RPC (Remote Procedure Call)

RPC在proto中的定义样本

service SearchService {
  rpc Search (SearchRequest) returns (SearchResponse);
}

数据解析方式

生产者:产生消息,填充内容,并序列化保存
消费者:读取数据,反序列化得到消息,使用消息

  • 静态消息(也是大众的使用方式,step1.编写proto,step2.生成代理类,step3.完了)
  • 自描述消息 – 自描述消息解放了消费者
  • 动态消息,动态编译 – 动态消息解放生产者
  • 动态自描述消息 – 解放生产者和消费者

过程中遇到的名词解释

RPC

远程过程调用(RPC,Remote Procedure Call)是这样一种协议:无需了解网络细节,某一程序即可使用该协议请求来自网络内另一台计算机程序的服务。(过程调用有时也称为函数调用或子程序调用。)远程过程调用采用客户机/服务器模式。请求程序就是一个客户机,而服务提供程序就是一台服务器。和常规或本地过程调用一样,远程过程调用是同步操作,在远程过程结果返回之前,需要暂时中止请求程序。使用相同地址空间的低权进程或低权线程允许同时运行多个远程过程调用。
  当使用远程过程调用(RPC)的程序语句被编译到一个可执行程序中时,代表远程过程代码的编译代码中包含一个存根。程序运行和过程调用发布时,存根接受请求并将其转发给本地计算机的一个客户运行时间程序。客户运行时间程序能够知道如何记录远程计算机和服务器应用地址,并可在请求远程过程的网络间发送信息。同样,服务器包括同远程过程本身相连接的一个运行时间程序和存根,结果会以同样的方式被返回。
  有几种RPC模式。一个很流行的模式是开放软件基金会的分布式计算环境(DCE)。1991年11月,美国电气和电子工程师协会在它的ISO远程过程调用规范(ISO/IEC CD 11578 N6561, ISO/IEC)中定义了远程过程调用。
  远程过程调用(RPC)跨越了开放系统互连(OSI)网络通信模型中的传输层和应用层。远程过程调用使得开发应用程序更容易。
  客户端/服务器通信的替代方法包括信息队列和IBM的高级程序间通信(APPC)。

性能结论

速度 动态消息(不生成代理类)使用的时间 为 静态消息(生成代理类) 的 4倍左右
大小 动态消息 都比 静态消息 的大小大,但是多出的大小固定不变

移植pbc过程中遇到的问题

几种传输统计:

protobuf protobuf 官方库(protobuf-2.5.0)
pbc 云风(吴云洋)分享的动态解析库
pbc_Ex 移植pbc到引擎的版本

Socket 最裸奔的普通socket
RConnection 引擎中封装的网络通讯类,解放了握手规则.封装了引擎自带的Socket
(引擎原本的TCP连接器Connection 客户端服务端都须使用几个引擎类,所以弃之~)

Server Client net State
protobuf protobuf Socket 2 Socket
pbc pbc Socket 2 Socket
protobuf pbc Socket 2 Socket
pbc protobuf Socket 2 Socket
pbc_Ex pbc_Ex Connection 2 Connection
pbc_Ex pbc_Ex RConnection 2 RConnection
pbc pbc_Ex Socket 2 RConnection
protobuf pbc_Ex Socket 2 RConnection
pbc_Ex pbc RConnection 2 Socket ×
pbc_Ex protobuf RConnection 2 Socket ×

引擎中的类型大小与替代pbc中的类型统计

引擎中类型 字节大小 pbc中的类型
U64 8 uint64_t,unsigned long
I64 8 long
Flt 4 float
Dbl 8 double
U32 4 uint32_t,unsigned int,size_t,
I32,Int 4 int
I16 2 int16_t
U16 2 uint16_t
U8 1 uint8_t,unsigned byte
I8 1 int8_t,byte
Char 2 wchar_t
Char8 1 char 引擎中全部使用char8

C++一般定义

typedef signed char        int8_t;
typedef short              int16_t;
typedef int                int32_t;
typedef long long          int64_t;
typedef unsigned char      uint8_t;
typedef unsigned short     uint16_t;
typedef unsigned int       uint32_t;
typedef unsigned long long uint64_t;

typedef signed char        int_least8_t;
typedef short              int_least16_t;
typedef int                int_least32_t;
typedef long long          int_least64_t;
typedef unsigned char      uint_least8_t;
typedef unsigned short     uint_least16_t;
typedef unsigned int       uint_least32_t;
typedef unsigned long long uint_least64_t;

typedef signed char        int_fast8_t;
typedef int                int_fast16_t;
typedef int                int_fast32_t;
typedef long long          int_fast64_t;
typedef unsigned char      uint_fast8_t;
typedef unsigned int       uint_fast16_t;
typedef unsigned int       uint_fast32_t;
typedef unsigned long long uint_fast64_t;
  1. memset memcpy alloca 函数的替代可能出问题AllocZero,CopyN,_pbcM_Malloc
  2. char EE中2字节,pbc中1字节,可能出问题
  3. register.c中strtoul,strtol,strtoll,strtod 使用的是EE中TextDbl,TextULong,TextInt,替代的 可能出问题
  4. offsetof 的替代方式是 直接 声明一个结构 然后 -((int)&v – (int)&v.v)
  5. 去掉所有static函数标识
  6. 去掉所有 类型 struce Type 参数,变量的声明格式
  7. pbc_wmessage 结构ptr 改名ptrW
  8. memmove 使用CopyN处理(CopyN A to Temp,CopyN Temp to B)
  9. 字符串使用Str8格式,需要显示打印再转换成Str宽字符
  10. google官方protobuf传输数据把\0去掉了.pbc则默认加上\0的大小 也就是+1

protobuf序列化储存数据大小变化测试(退化规则)

对于int(包括任何需要int储存的数据,string长度,int本身长度,float长度等都符合int的退化规则).

类型 标识位(字节) 长度位(字节) 实际值大小 数据占用规则(字节)
uint/8,16,32,64 1 0 < 0x80 1+1
uint/8,16,32,64 1 0 < 0x4000 1+2
uint/8,16,32,64 1 0 < 0x200000 1+3
uint/8,16,32,64 1 0 < 0x10000000 1+4
uint/8,16,32,64 1 0 other 1+5
float 1 0 32位 1+4
double 1 0 64位 1+8
string 1 int(len = strlen(string) + 1) 可任意长度 1+ int(len) + len

备注:string的+1 是’\0′

资料连接

http://www.cnblogs.com/jacksu-tencent/p/3447310.html
http://www.ibm.com/developerworks/cn/linux/l-cn-gpb/
http://www.searchtb.com/2012/09/protocol-buffers.html
https://developers.google.com/protocol-buffers/docs/overview?hl=zh-CN&csw=1
http://gashero.yeax.com/?p=108 (08年翻译的文档,推荐认真看完)
http://code.google.com/apis/protocolbuffers/docs/proto.html
http://blog.csdn.net/zhaozheng7758/article/details/6749047
http://blog.codingnow.com/2011/12/protocol_buffers_for_c.html

2 评论

发表评论

电子邮件地址不会被公开。 必填项已用*标注