protobuf 是用於結構化數據串行化的靈活、高效、自動化的解決方案。又如 XML,不過它更小、更快、也更簡單。你只需要按照你想要的數據存儲格式編寫一個.proto,然后使用生成器生成的代碼來讀寫這個數據結構。更重要的是,你甚至可以在無需重新部署程序的情況下更新數據結構。
在項目中使用protocol buffers需要經歷如下三步:
(1).定義消息格式文件,最好以proto作為后綴名
(2).使用Google提供的protocol buffers編譯器來生成代碼文件,一般為.h和.cc文件,主要是對消息格式以特定的語言方式描述
(3).使用protocol buffers庫提供的API來編寫應用程序
1.先來定義一個
本消息將以客戶端請求登錄和服務端返回為列
syntax = "proto3"; package pt; option optimize_for = LITE_RUNTIME; message req_login { string username = 1; string password = 2; } message obj_user_info { string nickname = 1; //昵稱 string icon = 2; //頭像 int64 coin = 3; //金幣 string location = 4; //所屬地 } //游戲數據統計 message obj_user_game_record { string time = 1; int32 kill = 2; //擊殺數 int32 dead = 3; //死亡數 int32 assist= 4; //助攻數 } message rsp_login { enum RET { SUCCESS = 0; ACCOUNT_NULL = 1; //賬號不存在 ACCOUNT_LOCK = 2; //賬號鎖定 PASSWORD_ERROR = 3; //密碼錯誤 ERROR = 10; } int32 ret = 1; obj_user_info user_info = 2; repeated obj_user_game_record record = 3; }
2.生成目標語言代碼
下面的命令幫助我們將game.proto文件中定義的Protocol Buffer格式的消息編譯成C++代碼文件。
//SRC_DIR .proto文件存放目錄 //--cpp_out 指示編譯器生成C++代碼,DST_DIR為生成文件存放目錄 //game.proto 待編譯的協議文件 protoc -I=$SRC_DIR --cpp_out=$DST_DIR $SRC_DIR/game.proto
由於我們在game.proto文件中定義選項optimize_for=LITE_RUNTIME,因此由該文件內生成的所有C++類的父類均為::google::protobuf::MessageLite,而非::google::protobuf::Message。MessageLite類是Message的父類,MessageLite中缺少對反射的支持,而此類功能均在Message類中提供了具體的實現。
對於我們的項目而言,整個系統相對比較封閉,不會和外部程序進行交互,與此同時,我們的客戶端部分又是運行在Android平台,有鑒於此,我們考慮使用LITE版本的Protocol Buffer。這樣不僅可以得到更高編碼效率,而且生成代碼編譯后所占用的資源也會更少,至於反射所能帶來的靈活性和極易擴展性,對於該項目而言完全可以忽略。
3.protobuf自動生成的API
class rsp_login : public ::google::protobuf::MessageLite { public: //每一個message類都包含以下方法供你檢測或操作 void CopyFrom(const rsp_login& from); void MergeFrom(const rsp_login& from); void Clear(); //清除所有字段內容,並置為空狀態 bool IsInitialized() const; //檢測所有required 是否初始化
int ByteSize() const; //類所占字節數 //整形變量只提供獲取、修改、清除 void clear_ret(); ::google::protobuf::int32 ret() const; void set_ret(::google::protobuf::int32 value); //自定義類類型user_info bool has_user_info() const; void clear_user_info(); const ::pt::obj_user_info& user_info() const; //自定義類型,並沒提供set方法,而是通過mutable_接口返回user_info的指針,可根據此指針進行賦值操作 ::pt::obj_user_info* mutable_user_info(); //返回user_info字段指針,將所有權移交給此指針,並將user_info字段置為empty狀態 ::pt::obj_user_info* release_user_info(); //使用set_allocated要小心,傳入的參數需要顯示allocate,設置后函數內部維護此指針 void set_allocated_user_info(::pt::obj_user_info* user_info); //record為repeated的類數組類型 int record_size() const; void clear_record(); //根據id索引,返回記錄的引用,const不可修改內容 const ::pt::obj_user_game_record& record(int index) const; //根據id索引,返回記錄的指針,以供查看、修改 ::pt::obj_user_game_record* mutable_record(int index); //repeated類型提供add接口增加一條記錄,並返回此記錄的指針,以便對其賦值 ::pt::obj_user_game_record* add_record(); //提供mutable接口,並返回record字段的容器指針,可根據此指針遍歷、修改 ::google::protobuf::RepeatedPtrField< ::pt::obj_user_game_record >* mutable_record(); //返回record字段的容器引用,const不可修改內容 const ::google::protobuf::RepeatedPtrField< ::pt::obj_user_game_record >& record() const; }
4.protobuf讀和寫
#include <iostream> #include <string> #include "game.pb.h" int main() { pt::rsp_login rsp{}; rsp.set_ret(pt::rsp_login_RET_SUCCESS); auto user_info = rsp.mutable_user_info(); user_info->set_nickname("dsw"); user_info->set_icon("345DS55GF34D774S"); user_info->set_coin(2000); user_info->set_location("zh"); for (int i = 0; i < 5; i++) { auto record = rsp.add_record(); record->set_time("2017/4/13 12:22:11"); record->set_kill(i * 4); record->set_dead(i * 2); record->set_assist(i * 5); } std::string buff{}; rsp.SerializeToString(&buff); //------------------解析---------------------- pt::rsp_login rsp2{}; if (!rsp2.ParseFromString(buff)) { std::cout << "parse error\n"; } auto temp_user_info = rsp2.user_info(); std::cout << "nickname:" << temp_user_info.nickname() << std::endl; std::cout << "coin:" << temp_user_info.coin() << std::endl; for (int m = 0; m < rsp2.record_size(); m++) { auto temp_record = rsp2.record(m); std::cout << "time:" << temp_record.time() << " kill:" << temp_record.kill() << " dead:" << temp_record.dead() << " assist:" << temp_record.assist() << std::endl; } }
輸出如下:

nickname:dsw coin:2000 time:2017/4/13 12:22:11 kill:0 dead:0 assist:0 time:2017/4/13 12:22:11 kill:4 dead:2 assist:5 time:2017/4/13 12:22:11 kill:8 dead:4 assist:10 time:2017/4/13 12:22:11 kill:12 dead:6 assist:15 time:2017/4/13 12:22:11 kill:16 dead:8 assist:20
4.擴展protoobuf
在你發布了使用Protocol-Buffers的代碼之后, 你必定會想"改進"message的定義. 如果你想讓你的新message向后兼容, 並且舊的message能夠向前兼容。你一定希望如此,那么你在新的Person 中就要遵守其他的一些規則了:
(1).對已存在的任何字段, 你都不能更改其標識tag號。
(2).你絕對不能添加或刪除任何required的字段。
(3).你可以添加新的optional或repeated的字段, 但是你必須使用新的標識tag號(此message中從未使用過的標識號,甚至連已經被刪除過標識號也不行)。
(4).有一些例外情況, 但是它們很少使用。
如果你遵守這些規則, 老代碼將能很好地解析新的消息, 並忽略掉任何新的字段. 對老代碼來說, 已經被刪除的optional字段將被賦予默認值. 已被刪除的repeated字段將是空的. 新的代碼也能夠透明地讀取舊的消息. 但是請牢記心中: 新的optional字段將不會出現在舊的消息中, 如果沒有為一個optional項指定默認值, 那么就會使用與特定類型相關的默認值: 對string(null)、boolean(false)、數值類型(0)。
還要注意: 如果你添加了一個新的repeated字段, 你的新代碼將無法告訴你它是否被留空了(被新代碼), 或者是否從未被置值(被舊代碼), 這是因為它沒有has_標志.