遊戲開發-協議設計-protobuf

本篇是遊戲開發系列第二篇,如若你有興趣,請持續關注,後期會持續更新。其餘文章列表以下:javascript

遊戲開發—協議設計java

遊戲開發—協議-protobufpython

遊戲開發-協議-protobuf原理詳解git

WHAT 

簡介

咱們看官方文檔是如此介紹的:github

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

Protocol buffers 是一個跨語言,跨平臺以及支持可擴展的序列化結構數據的格式。ruby

簡單來講,Protocol Buffers就是一種google定義的結構化數據格式,用於數據的序列化和反序列化。因爲它直接對二進制源數據進行操做,因此它相對於xml來講,足夠的小,快以及簡單,並且又與語言、平臺無關,因此兼容性也有不錯的表現。目前很適合作數據存儲或 網絡通信間的數據傳輸。網絡

當前官方顯示的已支持的開發語言多達10種,分別有:C++、Java、Python、Objective-C、C#、JavaNano、JavaScript、Ruby、Go、PHP,基本上主流的語言都已支持。固然也有非官方(好比Lua)的支持語言,具體也是增長一個解析lib,有特殊需求的能夠參考官方文檔本身編寫。目前支持的語言以下(有source地址):jvm

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

 

性能如何:

官方介紹的它性能足夠強悍,具體有多好?咱們看下性能測試對比。socket

以上是基於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---------------------------------------------------

掃描關注更多,關注我的成長和技術學習,期待用本身的一點點改變,帶給你一些啓發及感悟。

相關文章
相關標籤/搜索