文档章节

游戏开发-协议设计-protobuf

wier
 wier
发布于 2017/03/06 07:32
字数 2340
阅读 2.1K
收藏 43

本篇是游戏开发系列第二篇,如若你有兴趣,请持续关注,后期会持续更新。其他文章列表如下:

游戏开发—协议设计

游戏开发—协议-protobuf

游戏开发-协议-protobuf原理详解

WHAT 

简介

我们看官方文档是如此介绍的:

Protocol buffers are a language-neutral, platform-neutral extensible mechanism for serializing structured data.

Protocol buffers 是一个跨语言,跨平台以及支持可扩展的序列化结构数据的格式。

简单来说,Protocol Buffers就是一种google定义的结构化数据格式,用于数据的序列化和反序列化。由于它直接对二进制源数据进行操作,所以它相对于xml来说,足够的小,快以及简单,而且又与语言、平台无关,所以兼容性也有不错的表现。目前很适合做数据存储或 网络通讯间的数据传输。

当前官方显示的已支持的开发语言多达10种,分别有:C++、Java、Python、Objective-C、C#、JavaNano、JavaScript、Ruby、Go、PHP,基本上主流的语言都已支持。当然也有非官方(比如Lua)的支持语言,具体也是增加一个解析lib,有特殊需求的可以参考官方文档自己编写。目前支持的语言如下(有source地址):

Language Source
C++ (include C++ runtime and protoc) src
Java java
Python python
Objective-C objectivec
C# csharp
JavaNano javanano
JavaScript js
Ruby ruby
Go golang/protobuf
PHP

 

性能如何:

官方介绍的它性能足够强悍,具体有多好?我们看下性能测试对比。

以上是基于Full Object Graph Serializers,包括创建对象,将对象序列化为内存中的字节序列,然后再反序列化的整个过程。图一是(序列化+反序列化)总共耗时,图二是压缩后的大小。我们可以看出protocolBuffer无论是序列化速度,还是数据大小,都有有明显优势。具体测试数据点此.

HOW

具体如何用,官方guide已经有很详细的介绍了,我们基于官方demo对package进行一次分解,了解其序列化过程以及soruce结构,以便对整个机制有一个大概的了解(以下语言基于java)。

demo

此demo假定你已经拥有当前平台的compiler(.proto生成目标语言代码的编译器),如若没有,请参照官网编译C++ runtime and protoc,如若window平台,也可以点击此处下载一个,无需自己编译。

step1:引入maven 

<dependency>
    <groupId>com.google.protobuf</groupId>
    <artifactId>protobuf-java</artifactId>
    <version>3.2.0</version>
</dependency>

step2:定义.proto文件

syntax = "proto3";
package msg;

option java_package = "com.example.msg";
option java_outer_classname = "LoginMsg";

message Login {
  string useranme = 1;
  int32  pw=2;
}

可支持的数据类型:

官网吧

step3:compiler生产代码

//--java_out是目标语言代码目录 紧跟着空格之后是.proto文件目录,生成多个可用-I
protoc --java_out=java resources/protoc/login.proto

最终生成的文件以及目录:

Reader&Writer

上述通过.proto定义生成的LoginMsg.java,已经整合了对LoginMsg的序列化和反序列化相关代码,我们对login这个消息的reader和writer时只需要通过对该class进行操作即可。比如要把loginMsg写入到流里面发送出去,只需要对loginMsg进行赋值然后writer,对象就被序列化为二进制数据写出,或者接收端读取LoginMsg时,调用其ParserbyReader,就可以基于二进制流反序列化为LoginMsg对象。

Write:

   public void write() throws Exception{
        //构建Login消息对象
        LoginMsg.Login.Builder builder = LoginMsg.Login.newBuilder();
        builder.setUseranme("wier");
        builder.setPwd(111);

        //序列化并写出到磁盘
        FileOutputStream output = new FileOutputStream("/Users/wier/login_msg");
        builder.build().writeTo(output);
        output.close();
    }

Read

    public void read() throws Exception{
        FileInputStream inputStream = new FileInputStream("/Users/wier/login_msg");
        LoginMsg.Login login = LoginMsg.Login.parseFrom(inputStream);
        System.out.print("login.username:"+login.getUseranme());
        System.out.print("login.pwd:"+login.getPwd());

    }

我们看到上述代码对消息的read和write都很简单,你只需要对上述的stream改造为为socket就可以基于tcp进行消息传输了。

Message类结构

我们基于LoginMsg来看下整个消息对象主要包含的信息。

一个message类主要包含以下信息:

Login  消息结构对象的主体,主要存储数据,同时继承GeneratedMessageV3,内部封装对象的序列化和反序列化,writeTo序列化,paser反序列化。

LoginOrBuilder 用来连接Login和Builder,提供类型信息以及对外提供field get方法。

Builder 消息对象构建器,对外封装field set方法。

Descriptor 消息对象元数据的描述信息,一般用不到,如果你有动态解析的需求可以通过此来处理

Parser  解析器,为消息反序列号提供服务

我们看下class的层次关系

MessageLite/Message接口是所有message的抽象接口,message可以基于Parser从字节流数据中构建对象,也可以通过Builder创建的对象序列化后写入字节流数据到IO管道,MessageLite和Message内部都定义了自己的Builder类,继承自MessageLiteOrBuilder以及MessageOrBuiler,并定义了MessageLite/Message和它们各自Builder类的共同接口。

调用时序

write

上面write的过程,我们可以看到,数据的封装主要通过build来处理,GeneratedMessageV3封装了一些基础字段读取的操作,最终的字段的写入主要依靠CodedOutputStream来进行,CodedOutputStream封装的所有(定义类型)字段转二进制的方式,比如int,String 等,你只需基于定义字段传入即可。OutputStreamEncoder是CodedOutputStream是一个子类。

read

read的过程也是一个解包的过程,Parser主要来做解析管理,比如可以基于二进制数据或者基于IO来解析,或者一些扩展字段调用预注册的ExtensionRegister来自己定义解析。最终的字段读取调用CodedInputStream来读取,CodedInputStream和上面的CodedOutputStream一样,也是基于一些定义字段进行读取操作,将二进制数据转换为指定字段类型。消息的构造函数有基于CodedInputStream读取的,读取顺序基于tag来进行。具体每个field的tag是做什么的后续讲解。

message二进制结构

通过上面的read和write过程,我们可以看到每个消息字段读取的时候,都会先调用一次readTag或者writeTag,那么这个tag是做什么的,我们先看一个message的二进制组成结构。

一个二进制流,都是一队有序的byte数据组成,上述图中每个field都是有一个tag和value组成,tag等于就是这个value信息的描述或者定义,告知解析器当前fields是什么类型字段,以及读取的顺序,有了这个信息,解析器就知道一个field在流中的开始位置和结束位置,如此一个field解码成功,并且与字段顺序无关。

tag的构成:

(fieldNumber << 3) | wireType;

为何需要fieldNumber,一个是它可以告知解析器当前field在字节流中解析的顺序,另外也可以做到对协议的扩展,比如你在已经用到的协议消息中,需要增加一个字段或者更改一个字段,可以 fieldNumber+1,这样即便是同样一个消息,无论client是否更新协议(比如依然采用old message),依然不影响server端的解析。这样的机制,保证了即使该消息添加了新的字段,也不会影响旧的编/解码程序正常工作。

Descriptor

Descriptor 是消息对象的元数据描述信息,在compilerss生成消息对象class的时候,会为每个message定义一个Descriptor静态字段、同时还会定义一个FieldAccessorTable静态字段用于使用反射读取/设置某个字段的值。

当然了这些在一般的序列化和反序列化的时候用不到,因为消息的解析顺序以及类型已经在生成的时候基于配置文件生成好了,无需再来解析标签含义。

如果你有动态解析的需求,比如:新增或者更新一个 Message 时候,不需要更代码,重启进程,基于接收到 数据和配置文件,自动创建具体的 Protobuf Message 对象,再做的反序列化。此时Descriptor对你有很大的帮助意义。我们看下Descriptor下类层结构。

最后

extensions

在protocol2期间,还支持extensions字段定义,通过extend 用来解决消息复用的方式,目前在protocol3已经废弃了,采用Any来支持。

Unknown Fields

在protocol2期间,如果有无法解析的字段(如消息升级之后,client采用old message 传送),默认的解决方式如下:

        default: 
        if (!parseUnknownField(input, unknownFields, extensionRegistry, tag)) {
          done = true;
        }
 

如今protocol3已经对这一方案进行更新了,遇到没有定义的字段,直接skipField。

 default: 
       if (!input.skipField(tag)) {
           done = true;
           }
       break;
本节只针对protocol buffer 的是什么,以及如何用进行了介绍,并没有针对protocol为何会有占用空间小,解析速度快以及兼容性等优点进行梳理,如果你对这部分有兴趣,请关注下一篇相关文字,我会尝试梳理一下关于why问题。

 

---------------------------------------------------end---------------------------------------------------

扫描关注更多,关注个人成长和技术学习,期待用自己的一点点改变,带给你一些启发及感悟。

 

© 著作权归作者所有

wier
粉丝 781
博文 50
码字总数 134184
作品 0
东城
技术主管
私信 提问
加载中

评论(5)

wier
wier 博主

引用来自“公孙二狗”的评论

如果我不按你的格式向你的 Socket 发送数据呢?例如用 Telnet 等,服务器收到数据怎么处理?Protobuf 只能保存 payload 的数据,还没有实现通讯协议呢。

回复@公孙二狗 : Protobuf 是一种数据格式,属于应用层,通信的是通信层的事
公孙二狗
公孙二狗
如果我不按你的格式向你的 Socket 发送数据呢?例如用 Telnet 等,服务器收到数据怎么处理?Protobuf 只能保存 payload 的数据,还没有实现通讯协议呢。
wier
wier 博主

引用来自“BoXuan”的评论

还是习惯直接write、read有序的数据类型方便,无额外的序列化和反序列化,而且能达到数据包的最小化,我敢说比protobuf更小,运行效率更高,不过开发效率有所降低
仔细想了下,刚才的说法有个漏洞,protobuf 编码string 的时候,其实还是节省空间的,因为字符串的leg也是采用Varint,如此1-2个字节就可以解决leg空间的问题,若是再加上tag1个字节,整体比直接readString 还是节省1个字节的,如此来说即便是string,protobuf依然比较节省空间,这个我会做个test详细与你解释
wier
wier 博主

引用来自“BoXuan”的评论

还是习惯直接write、read有序的数据类型方便,无额外的序列化和反序列化,而且能达到数据包的最小化,我敢说比protobuf更小,运行效率更高,不过开发效率有所降低
占用空间上面,有前提的情况下,同意你的说法。这个前提是,单个消息的所有字段都被赋值而且是string的。
1、字段,protobuf的string的编码方式和普通的编码方式一致,没有太大区别,再加上tag的元素,会比直接readString多1个字节。但若是采用Varint ,protobuf会更节省空间,小于 128 的数字都可以用一个 byte 表示,大于 128 的数字,会用两个字节来表示等。而自定义readInt32默认都是4个字节。protobuff即便加上每个字段的tag(一个字节),占用空间不会比直接读写大,可能还会小。
2、没赋值的字段,tag可以有效避免没赋值字段的传送,保证最小空间的占用,而直接读写的话,必须得有默认值,比如int 字段,即便是当前字段没有值,依然会占用4个字节,不然你的顺序读取就会出问题。

解封码效率上面,protobuf的生成代码,也是直接通过字节流刘操作,并没有标签转换之类以及损耗性的地方,速度上和直接写没什么区别
kakai
kakai
还是习惯直接write、read有序的数据类型方便,无额外的序列化和反序列化,而且能达到数据包的最小化,我敢说比protobuf更小,运行效率更高,不过开发效率有所降低
【专栏精选】网络封包神器protobuf简介

版权声明:本文为博主原创文章,遵循 CC 4.0 BY-SA 版权协议,转载请附上原文出处链接和本声明。 https://blog.csdn.net/zhenghongzhi6/article/details/94589294 本文节选自洪流学堂公众号技...

关尔Manic
2019/07/03
0
0
我的Protobuf消息设计原则(续)--实践

1.首先为 聊天服务器(Chat)定义google protobuf的协议接口文件 接口主要遵循 Request、Response、Notification(Indication),Command(本文未出现)四大消息分类,并且使用Message顶层消...

newzai
2014/07/19
1W
19
Dubbo 在跨语言和协议穿透性方向的探索:支持 HTTP/2 gRPC

Dubbo 在跨语言和协议穿透性方向上的探索:支持 HTTP/2 gRPC 和 Protobuf 本文整理自刘军在 Dubbo 成都 meetup 上分享的《Dubbo 在多语言和协议穿透性方向上的探索》。 本文总体上可分为基础...

中间件小哥
2019/11/25
0
0
如何使用 Protobuf 做数据交换

在以不同语言编写并在不同平台上运行的应用程序之间交换数据时,Protobuf 编码可提高效率。 协议缓冲区Protocol Buffers(Protobufs)像 XML 和 JSON 一样,可以让用不同语言编写并在不同平台...

作者: Marty Kalin
2019/11/22
0
0
开源点评:Protocol Buffers介绍

今天来介绍一下“Protocol Buffers”(以下简称protobuf)这个玩意儿。本来俺在构思“生产者/消费者模式 ”系列的下一个帖子:关于生产者和消费者之间的数据传输格式。由于里面扯到了protobu...

彭苏云
2014/09/26
205
0

没有更多内容

加载失败,请刷新页面

加载更多

Spring Batch 配置一个步骤(Step)

如我们在 域语言(Domain Language)章节中讨论的内容一致,一个 步骤(Step)是一个独立封装了执行顺序的批量作业(Job),并且包含有用于定义和控制一个批量作业的所有独立信息。 针对这个...

honeymoose
52分钟前
34
0
郑州哪里可以开五金工具发票-郑州新闻网

郑州哪里可以开五金工具发票【1.3.2 - 2.9.3.0 - 0.5.6.8.】李生,adb的全称为Android Debug Bridge,是Android手机通用的一个USB端口。百度CarLife的部分车机采用...

提供格
今天
38
0
郑州哪里可以开五金材料发票-郑州新闻网

郑州哪里可以开五金材料发票【1.3.2 - 2.9.3.0 - 0.5.6.8.】李生,adb的全称为Android Debug Bridge,是Android手机通用的一个USB端口。百度CarLife的部分车机采用...

法放饭
今天
43
0
郑州哪里可以开劳保用品发票-郑州新闻网

郑州哪里可以开劳保用品发票【1.3.2 - 2.9.3.0 - 0.5.6.8.】李生,adb的全称为Android Debug Bridge,是Android手机通用的一个USB端口。百度CarLife的部分车机采用...

多徐重
今天
31
0
centos php ppt转图片

参考:https://blog.csdn.net/aituochang1886/article/details/101167564 安装 Unoconv 参考: https://www.licongying.cn/2018/10/linux-centos-install-unoconv-liboffice/ https://blog.c......

四季变幻
今天
29
0

没有更多内容

加载失败,请刷新页面

加载更多

返回顶部
顶部