一、Protobuf通信協議概述
- 關於Protobuf通信協議語法可以參閱:https://blog.csdn.net/qq_41453285/article/details/106731318
- 關於Protobuf通信協議的實現可以參閱:
- 開源地址爲:https://github.com/protocolbuffers/protobuf
二、protobuf庫的安裝
- 通過下方鏈接下載,此處我們下載3.12.1版本的。下載完解壓,進入目錄
# 如果下載失敗,那就進入網頁進行下載
wget https://distfiles.macports.org/protobuf3-cpp/protobuf-cpp-3.12.1.tar.gz
tar zxf protobuf-cpp-3.12.1.tar.gz
cd protobuf-3.12.1
- 進行配置、編譯、安裝
./configure
make
sudo make install
- 默認情況下:
- 頭文件默認安裝在/usr/local/include/google/protobuf目錄下
- 庫文件默認安裝在/usr/local/lib/目錄下
- 可執行文件protoc默認安裝在/usr/local/bin/目錄下
- 重新加載動態庫
sudo ldconfig
- 查看一下protobuf庫的版本
protoc --version
編寫帶有protobuf庫的C++程序
- 編譯時要帶上-lprotobuf選項,並且程序下要帶有與protobuf相關的.h和.cc文件
三、演示案例
編寫.protobuf文件
- 我們現在一份代碼,主目錄名爲person,建立完成之後進入目錄
- 現在我們在建立一個目錄proto,用來存儲.proto文件
- 現在我們編寫兩個.proto文件,分別名爲IM.BaseDefine.proto、IM.Login.proto,文件內容分別如下所示
syntax = "proto3"; //聲明protobuf版本, 此處爲protobuf3, 如果不寫默認爲2 package IM.BaseDefine; //導致命名空間, 生成對應的.h和.cpp中會有一個IM命名空間和一個BaseDefine命名空間, 其中BaseDefine命名空間包含在IM命名空間內 option optimize_for = LITE_RUNTIME; //設置選項 enum PhoneType{ PHONE_DEFAULT = 0x0; PHONE_HOME = 0x0001; // 家庭電話 PHONE_WORK = 0x0002; // 工作電話 }
syntax = "proto3"; package IM.Login; import "IM.BaseDefine.proto"; //import引入IM.BaseDefine.proto文件 option optimize_for = LITE_RUNTIME; // message關鍵字 代表一個對象 message Phone{ string number = 1; // = 1是什麼?默認值 IM.BaseDefine.PhoneType phone_type = 2; } message Book{ string name = 1; float price = 2; } message Person{ string name = 1; int32 age = 2; repeated string languages = 3; // repeated 重複, 可以嵌套對象, 類似數組, 會提供對應的add_languages()接口 Phone phone = 4; repeated Book books = 5; bool vip = 6; string address = 7; } //使用T開頭測試 message TInt32{ int32 int1 = 1; } message TString{ string str1 = 1; }
- 然後建立一個腳本create.sh並賦予其可執行權限,用來編譯.proto文件生成C++代碼文件,腳本內容如下:
#!/bin/sh # proto文件在哪裏 SRC_DIR=./ # .h .cc輸出到哪裏 DST_DIR=../ #C++ protoc -I=$SRC_DIR --cpp_out=$DST_DIR $SRC_DIR/*.proto
- 現在.proto目錄下有如下三個文件:
生成.h和.cc文件
- 指向上面的create.sh,就會在proto的上一級目錄自動生成對應的.h和.cc文件,如下所示:
- IM.BaseDefine.proto:會生成IM.BaseDefine.pb.h和IM.BaseDefine.pb.cc
- IM.Login.proto:會生成IM.Login.pb.h和IM.Login.pb.cc
- .h文件中會根據.proto文件生成對應的接口,.cc文件則是.h文件的實現。下面截取IM.BaseDefine.pb.h的部分代碼,關於具體實現就不詳細介紹了
編寫測試代碼①
- 有了上面對應的.h和.cc代碼之後,我們就可以自己編寫C++程序來測試上面的接口了
- 例如下面是一個code_test.cpp的文件,用來測試相關接口的使用
//code_test.cpp #include <iostream> #include <stdio.h> #include <unistd.h> #include <sys/time.h> #include <sys/wait.h> #include "IM.BaseDefine.pb.h" #include "IM.Login.pb.h" static uint64_t getNowTime() { struct timeval tval; uint64_t nowTime; gettimeofday(&tval, NULL); nowTime = tval.tv_sec * 1000L + tval.tv_usec / 1000L; return nowTime; } /* 如果proto結構體的變量是基礎變量,比如int、string等等,那麼set的時候直接調用set_xxx即可。 如果變量是自定義類型(也就是message嵌套),那麼C++的生成代碼中,就沒有set_xxx函數名,取而代之的是三個函數名: set_allocated_xxx() release_xxx() mutable_xxx() 使用set_allocated_xxx()來設置變量的時候,變量不能是普通棧內存數據, 必須是手動new出來的指針,至於何時delete,就不需要調用者關心了, protobuf內部會自動delete掉通過set_allocated_設置的內存; release_xxx()是用來取消之前由set_allocated_xxx設置的數據, 調用release_xxx以後,protobuf內部就不會自動去delete這個數據; mutable_xxx()返回分配內存後的對象,如果已經分配過則直接返回,如果沒有分配則在內部分配,建議使用mutable_xxx */ bool ProtobufEncode(std::string &strPb) { IM::Login::Person person; person.set_name("dongshao"); // 設置以set_爲前綴 person.set_age(80); person.add_languages("C++"); // 數組add person.add_languages("Java"); // 電話號碼 // mutable_ 嵌套對象時使用,並且是單個對象時使用,比如對應的Person裏面的Phone phone = 4; // 比如mutable_phone如果phone已經存在則直接返回,如果不存在則new 一個返回 IM::Login::Phone *phone = person.mutable_phone(); if(!phone) { std::cout << "mutable_phone failed." << std::endl; return false; } phone->set_number("18888888888"); phone->set_phone_type(IM::BaseDefine::PHONE_HOME); // 書籍 // add_則是針對repeated的嵌套對象,每次調用都返回一個新的對象,注意和mutable_的區別。 // 比如Person裏面的repeated Book books = 5; IM::Login::Book *book = person.add_books(); book->set_name("Linux kernel development"); book->set_price(7.7); book = person.add_books(); book->set_name("Linux server development"); book->set_price(8.0); // vip person.set_vip(true); // 地址 person.set_address("anhui"); uint32_t pbSize = person.ByteSize(); // 序列化後的大小 strPb.clear(); strPb.resize(pbSize); uint8_t *szData = (uint8_t *)strPb.c_str(); if (!person.SerializeToArray(szData, pbSize)) // 拷貝序列化後的數據 { std::cout << "person pb msg SerializeToArray failed." << std::endl; return false; } return true; } bool ProtobufDecode(std::string &strPb) { IM::Login::Person person; person.ParseFromArray(strPb.c_str(), strPb.size()); // 反序列化 // printPerson(person); 這個函數在本文件被刪除了, 實現可以參閱pb_speed.cpp return true; } void printHex(uint8_t *data, uint32_t len) { for(uint32_t i = 0; i < len; i++) { printf("%02x ", data[i]); } printf("\n\n"); } void TInt() { std::string strPb; uint8_t *szData; IM::Login::TInt32 int1; uint32_t int1Size = int1.ByteSize(); // 序列化後的大小 std::cout << "null int1Size = " << int1Size << std::endl; int1.set_int1(0x12); int1Size = int1.ByteSize(); // 序列化後的大小 std::cout << "0x12 int1Size = " << int1Size << std::endl; strPb.clear(); strPb.resize(int1Size); szData = (uint8_t *)strPb.c_str(); int1.SerializeToArray(szData, int1Size); // 拷貝序列化後的數據 printHex(szData, int1Size); int1.set_int1(-11); int1Size = int1.ByteSize(); // 序列化後的大小 std::cout << "-11 int1Size = " << int1Size << std::endl; strPb.clear(); strPb.resize(int1Size); szData = (uint8_t *)strPb.c_str(); int1.SerializeToArray(szData, int1Size); // 拷貝序列化後的數據 printHex(szData, int1Size); int1.set_int1(0x7f); int1Size = int1.ByteSize(); // 序列化後的大小 std::cout << "0xff int1Size = " << int1Size << std::endl; strPb.clear(); strPb.resize(int1Size); szData = (uint8_t *)strPb.c_str(); int1.SerializeToArray(szData, int1Size); // 拷貝序列化後的數據 printHex(szData, int1Size); int1.set_int1(0xff); int1Size = int1.ByteSize(); // 序列化後的大小 std::cout << "0xff int1Size = " << int1Size << std::endl; strPb.clear(); strPb.resize(int1Size); szData = (uint8_t *)strPb.c_str(); int1.SerializeToArray(szData, int1Size); // 拷貝序列化後的數據 printHex(szData, int1Size); int1.set_int1(0x1234); int1Size = int1.ByteSize(); // 序列化後的大小 std::cout << "0x1234 int1Size = " << int1Size << std::endl; strPb.clear(); strPb.resize(int1Size); szData = (uint8_t *)strPb.c_str(); int1.SerializeToArray(szData, int1Size); // 拷貝序列化後的數據 printHex(szData, int1Size); int1.set_int1(0x123456); int1Size = int1.ByteSize(); // 序列化後的大小 std::cout << "0x123456 int1Size = " << int1Size << std::endl; strPb.clear(); strPb.resize(int1Size); szData = (uint8_t *)strPb.c_str(); int1.SerializeToArray(szData, int1Size); // 拷貝序列化後的數據 printHex(szData, int1Size); } void TString(void) { std::string strPb; uint8_t *szData; IM::Login::TString str1; uint32_t str1Size = str1.ByteSize(); // 序列化後的大小 std::cout << "null str1Size = " << str1Size << std::endl; str1.set_str1("1"); str1Size = str1.ByteSize(); // 序列化後的大小 std::cout << "1 str1Size = " << str1Size << std::endl; strPb.clear(); strPb.resize(str1Size); szData = (uint8_t *)strPb.c_str(); str1.SerializeToArray(szData, str1Size); // 拷貝序列化後的數據 printHex(szData, str1Size); str1.set_str1("1234"); str1Size = str1.ByteSize(); // 序列化後的大小 std::cout << "1234 str1Size = " << str1Size << std::endl; strPb.clear(); strPb.resize(str1Size); szData = (uint8_t *)strPb.c_str(); str1.SerializeToArray(szData, str1Size); // 拷貝序列化後的數據 printHex(szData, str1Size); str1.set_str1("老師"); str1Size = str1.ByteSize(); // 序列化後的大小 std::cout << "老師 str1Size = " << str1Size << std::endl; strPb.clear(); strPb.resize(str1Size); szData = (uint8_t *)strPb.c_str(); str1.SerializeToArray(szData, str1Size); // 拷貝序列化後的數據 printHex(szData, str1Size); } int main(void) { TInt(); TString(); return 0; }
- 使用下面的命令進行編譯,編譯時可能會警告,可以忽略
g++ -o code_test code_test.cpp IM.BaseDefine.pb.cc IM.Login.pb.cc -lprotobuf -lpthread
- 運行程序,效果如下:
編寫測試代碼②
- 下面我們再編寫一個pb_speed.cpp,用來測試相關接口性能
//pb_speed.cpp #include <iostream> #include <stdio.h> #include <unistd.h> #include <sys/time.h> #include <sys/wait.h> #include "IM.BaseDefine.pb.h" #include "IM.Login.pb.h" static uint64_t getNowTime() { struct timeval tval; uint64_t nowTime; gettimeofday(&tval, NULL); nowTime = tval.tv_sec * 1000L + tval.tv_usec / 1000L; return nowTime; } /* 如果proto結構體的變量是基礎變量,比如int、string等等,那麼set的時候直接調用set_xxx即可。 如果變量是自定義類型(也就是message嵌套),那麼C++的生成代碼中,就沒有set_xxx函數名,取而代之的是三個函數名: set_allocated_xxx() release_xxx() mutable_xxx() 使用set_allocated_xxx()來設置變量的時候,變量不能是普通棧內存數據, 必須是手動new出來的指針,至於何時delete,就不需要調用者關心了, protobuf內部會自動delete掉通過set_allocated_設置的內存; release_xxx()是用來取消之前由set_allocated_xxx設置的數據, 調用release_xxx以後,protobuf內部就不會自動去delete這個數據; mutable_xxx()返回分配內存後的對象,如果已經分配過則直接返回,如果沒有分配則在內部分配,建議使用mutable_xxx */ bool ProtobufEncode(std::string &strPb) { IM::Login::Person person; person.set_name("dongshao"); // 設置以set_爲前綴 person.set_age(80); person.add_languages("C++"); // 數組add 自帶類型 protobuff關鍵字支持的 person.add_languages("Java"); // 電話號碼 // mutable_ 嵌套對象時使用,並且是單個對象時使用,比如對應的Person裏面的Phone phone = 4; // 比如mutable_phone如果phone已經存在則直接返回,如果不存在則new 一個返回 IM::Login::Phone *phone = person.mutable_phone(); if(!phone) { std::cout << "mutable_phone failed." << std::endl; return false; } phone->set_number("18888888888"); phone->set_phone_type(IM::BaseDefine::PHONE_HOME); // 書籍 // add_則是針對repeated的嵌套對象,每次調用都返回一個新的對象,注意和mutable_的區別。 // 比如Person裏面的repeated Book books = 5; IM::Login::Book *book = person.add_books(); book->set_name("Linux kernel development"); book->set_price(7.7); book = person.add_books(); book->set_name("Linux server development"); book->set_price(8.0); // vip person.set_vip(true); // 地址 person.set_address("anhui"); uint32_t pbSize = person.ByteSize(); // 獲取序列化後的大小 strPb.clear(); strPb.resize(pbSize); uint8_t *szData = (uint8_t *)strPb.c_str(); if (!person.SerializeToArray(szData, pbSize)) // 拷貝序列化後的數據 { std::cout << "person pb msg SerializeToArray failed." << std::endl; return false; } return true; } static void printPerson(IM::Login::Person &person) { std::cout << "name:\t" << person.name() << std::endl; std::cout << "age:\t" << person.age() << std::endl; std::string languages; for (int i = 0; i < person.languages_size(); i++) { if (i != 0) { languages += ", "; } languages += person.languages(i); } std::cout << "languages:\t" << languages << std::endl; if (person.has_phone()) // 自定義message的嵌套並且不是設置爲repeated則有has_ { // 注意引用 const IM::Login::Phone &phone = person.phone(); std::cout << "phone number:\t" << phone.number() << ", type:\t" << phone.phone_type() << std::endl; } else { std::cout << "no phone" << std::endl; } for (int i = 0; i < person.books_size(); i++) { const IM::Login::Book &book = person.books(i); std::cout << "book name:\t" << book.name() << ", price:\t" << book.price() << std::endl; } std::cout << "vip:\t" << person.vip() << std::endl; std::cout << "address:\t" << person.address() << std::endl; } bool ProtobufDecode(std::string &strPb) { IM::Login::Person person; person.ParseFromArray(strPb.c_str(), strPb.size()); // 反序列化 printPerson(person); return true; } #define TEST_COUNT 1000000 int main(void) { std::string strPb; ProtobufEncode(strPb); // 序列化後是二進制 std::cout << "ProtobufDecode, size: " << strPb.size() << std::endl; ProtobufDecode(strPb); #if 1 uint64_t startTime; uint64_t nowTime; startTime = getNowTime(); std::cout << "protobuf encode time testing" << std::endl; for (int i = 0; i < TEST_COUNT; i++) { ProtobufEncode(strPb); } nowTime = getNowTime(); std::cout << "protobuf encode " << TEST_COUNT << " time, need time: " << nowTime - startTime << "ms" << std::endl; startTime = getNowTime(); std::cout << "protobuf decode time testing" << std::endl; for (int i = 0; i < TEST_COUNT; i++) { ProtobufDecode(strPb); } nowTime = getNowTime(); std::cout << "protobuf decode " << TEST_COUNT << " time, need time: " << nowTime - startTime << "ms" << std::endl; #endif return 0; }
- 使用下面的命令進行編譯,編譯時可能會警告,可以忽略
g++ -o pb_speed pb_speed.cpp IM.BaseDefine.pb.cc IM.Login.pb.cc -lprotobuf -lpthread
- 運行程序,效果如下: