建议继续看下一篇 grpc 的使用介绍,在编译安装 grpc 时会自动编译安装依赖项(包括 protobuf);可以直接安装 grpc,这里就不用单独安装 protobuf;
介绍 protobuf
Protocol Buffers,是 Google 公司开发的一种数据描述语言,类似于 XML 能够将结构化数据序列化,可用于数据存储、通信协议等方面。
官网 | 下载 |
安装 protobuf
-
安装依赖 zlib
1
sudo apt install zlib1g-dev
-
下载安装
安装完成后,会生成 protoc 执行文件,这是 protobuf 的编译器;
头文件目录:/usr/local/include/google/protobuf
库文件目录:/usr/local/lib
可执行文件目录:/usr/local/bin
1 2 3 4 5 6 7 8 9 10 11 12 13 14
git clone https://github.com/protocolbuffers/protobuf.git cd protobuf/cmake mkdir build cd build # 编译为静态库(默认,和下面的方式选一种) cmake .. -Dprotobuf_BUILD_TESTS=OFF # 编译为动态库 cmake .. -Dprotobuf_BUILD_TESTS=OFF -DBUILD_SHARED_LIBS=ON make sudo make install
-
将 /usr/local/bin 放到环境变量,编辑
vim ~/.zshrc
文件最后行1
export PATH=$PATH:/usr/local/bin
接着刷新
source ~/.zshrc
; -
如果执行报错,就刷新下 ldconfig
报错:
解决:
1
sudo ldconfig
语法介绍
基础描述
使用版本
现在一般都是使用 proto3 语法,将如下语句写在 proto 文件首行,编译时会按照 proto3 语法进行解析;
1
syntax = "proto3";
声明包名
包名相当于 cpp 上 namespace。编译 proto 为 cpp 源,生成的类、方法会放到 namespace pb_demo 中,其他语言类似;
1
package pb_demo;
proto 选项
放在全局(在声明包名 package xxx
的后一行):
1
option go_package = "./xxx"; // 指定 go 模块相对 go_out=dir 的生成路径和包名,路径会自动创建,go 模块名为 xxx,仅 golang 使用
放在 message 描述、enum 描述的第一行:
1
option allow_alias = true; // 允许将同一个 field number 赋值给多个枚举变量
导入 proto
放在声明包名 package xxx
的后面,且放在全局 option 前面:
1
import "other.proto";
消息描述
1
2
3
message <messageName>{
[field rules] <field type> <field name> = <field number>;
}
-
[可选项] field rules 可取值:
- optional 表示此字段可选;
- required 表示此字段必须存在;
- repeated 表示此字段可以被重复任何次数(包含 0),相当于数组;
-
[必选项] field type 可取值:
-
基础类型:double、float、int32、int64、uint32、uint64、sint32、sint64、fixed32、fixed64、sfixed32、sfixed64、bool、string、bytes;
-
枚举类型:enum(可放在全局或某个 message 中)
1 2 3 4 5
enum PhoneType{ MOBILE = 0; HOME = 1; WORK = 2; }
示例使用如下:
第一字段是必选电话号码;
第二字段是可选电话号码类型,如未选默认值为 HOME;
1 2 3 4
message PhoneNumber{ required string number = 1; optional PhoneType = 2 [default = HOME]; }
-
自定义类型:你定义的 messageName;
-
-
[必选项] field number 字段号可取值:
- 范围 0< n <2^30;
- 字段号 1-15 编码后字段长度为 1 字节,而字段号 >=16 的编码后字段长度为 2 字节;
使用 protobuf
定义第一个 .proto 协议文件
-
新建目录和文件
1 2
mkdir -p ~/vscode/protobuf/proto touch ~/vscode/protobuf/proto/addressbook.proto
目录看起来像这样:
proto 目录存放 .proto 协议文件;
proto_out_cpp 存放生成的 cpp 源文件;
proto_out_go 存放生成的 go 源文件;
-
编辑 addressbook.proto 文件
syntax = "proto3"
表示使用 proto3 语法;package addressbook
表示生成的方法放在 namespace addressbook 中;option go_package = "./addressbook"
仅在生成 go 源码有效,可指定一个生成路径(相对 go_out=dir 的路径)和 go 模块名;1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48
// [START declaration] syntax = "proto3"; package addressbook; import "google/protobuf/timestamp.proto"; // [END declaration] // [START java_declaration] option java_multiple_files = true; option java_package = "com.example.addressbook.protos"; option java_outer_classname = "AddressBookProtos"; // [END java_declaration] // [START csharp_declaration] option csharp_namespace = "Google.Protobuf.Examples.AddressBook"; // [END csharp_declaration] // [START go_declaration] option go_package = "./addressbook"; // [END go_declaration] // [START messages] message Person { string name = 1; int32 id = 2; // Unique ID number for this person. string email = 3; enum PhoneType { MOBILE = 0; HOME = 1; WORK = 2; } message PhoneNumber { string number = 1; PhoneType type = 2; } repeated PhoneNumber phones = 4; google.protobuf.Timestamp last_updated = 5; } // Our address book file is just one of these. message AddressBook { repeated Person people = 1; } // [END messages]
编译协议文件为 c++ 源
-
使用 protoc 进行编译
1
protoc --cpp_out=../proto_out_cpp addressbook.proto
-
编译后生成的文件如下:
其中命名空间和协议类如下图:
编译协议文件为 go 源
-
安装系列 go 插件
下载安装有关 protoc、grpc 的插件,默认放到 ~/go/bin 目录;
1 2 3 4
go get -u google.golang.org/protobuf/cmd/protoc-gen-go go install google.golang.org/protobuf/cmd/protoc-gen-go@latest go get -u google.golang.org/grpc/cmd/protoc-gen-go-grpc go install google.golang.org/grpc/cmd/protoc-gen-go-grpc@latest
-
编辑
vim ~/.zshrc
文件最后行环境变量包括:protoc、go、go 插件目录;
1
export PATH=$PATH:/usr/local/bin:/usr/local/go/bin:~/go/bin
接着刷新
source ~/.zshrc
; -
使用 protoc 进行编译
1
protoc --go_out=../proto_out_go addressbook.proto
编译后生成的文件如下:
-
在 proto_out_go/frist 目录中执行如下指令,生成模块
1 2
go mod init example.com/addressbook go mod tidy
接着生成的模块信息文件如下:
c++ 使用生成的源文件
-
新建主模块 list_people.cpp 文件
1
touch proto_out_cpp/list_people.cpp
-
编辑 proto_out_cpp/list_people.cpp 文件
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77
#include <fstream> #include <google/protobuf/util/time_util.h> #include <iostream> #include <string> #include "addressbook.pb.h" using namespace std; using google::protobuf::util::TimeUtil; // Iterates though all people in the AddressBook and prints info about them. void ListPeople(const addressbook::AddressBook& address_book) { for (int i = 0; i < address_book.people_size(); i++) { const addressbook::Person& person = address_book.people(i); cout << "Person ID: " << person.id() << endl; cout << " Name: " << person.name() << endl; if (person.email() != "") { cout << " E-mail address: " << person.email() << endl; } for (int j = 0; j < person.phones_size(); j++) { const addressbook::Person::PhoneNumber& phone_number = person.phones(j); switch (phone_number.type()) { case addressbook::Person::MOBILE: cout << " Mobile phone #: "; break; case addressbook::Person::HOME: cout << " Home phone #: "; break; case addressbook::Person::WORK: cout << " Work phone #: "; break; default: cout << " Unknown phone #: "; break; } cout << phone_number.number() << endl; } if (person.has_last_updated()) { cout << " Updated: " << TimeUtil::ToString(person.last_updated()) << endl; } } } // Main function: Reads the entire address book from a file and prints all // the information inside. int main(int argc, char* argv[]) { // Verify that the version of the library that we linked against is // compatible with the version of the headers we compiled against. GOOGLE_PROTOBUF_VERIFY_VERSION; if (argc != 2) { cerr << "Usage: " << argv[0] << " ADDRESS_BOOK_FILE" << endl; return -1; } addressbook::AddressBook address_book; { // Read the existing address book. fstream input(argv[1], ios::in | ios::binary); if (!address_book.ParseFromIstream(&input)) { cerr << "Failed to parse address book." << endl; return -1; } } ListPeople(address_book); // Optional: Delete all global objects allocated by libprotobuf. google::protobuf::ShutdownProtobufLibrary(); return 0; }
-
安装 pkg-config
pkg-config 用于检索系统中安装库文件的信息,一般用于库的编译和连接;
1
sudo apt-get install pkg-config
-
编译成 elf
1
g++ list_people.cpp addressbook.pb.cc -o list_people `pkg-config --cflags --libs protobuf`
-
执行程序
1
./list_people
使用 CMakeLists.txt 方式编译
自定义命令 add_custom_command 中有
DEPENDS ${msg}
,实现了当 ${msg} 发生变化时重新进行 protoc 编译;
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
cmake_minimum_required (VERSION 3.14)
project (list_people)
# 查找 Protobuf 3 库
find_package(Protobuf 3 REQUIRED)
if(Protobuf_FOUND)
message(STATUS "protobuf library found")
else()
message(FATAL_ERROR "protobuf library is needed but cant be found")
endif()
# 设置 proto 目录
set(MSG_PROTO_DIR ${CMAKE_SOURCE_DIR}/../proto)
# 设置需要编译的 proto 文件
file(GLOB_RECURSE MSG_PROTOS ${MSG_PROTO_DIR}/*.proto)
# 循环执行 protoc
set(MESSAGE_SRC "")
set(MESSAGE_HDRS "")
foreach(msg ${MSG_PROTOS})
get_filename_component(FIL_WE ${msg} NAME_WE)
list(APPEND MESSAGE_SRC "${CMAKE_SOURCE_DIR}/${FIL_WE}.pb.cc")
list(APPEND MESSAGE_HDRS "${CMAKE_SOURCE_DIR}/${FIL_WE}.pb.h")
add_custom_command(
OUTPUT "${CMAKE_SOURCE_DIR}/${FIL_WE}.pb.cc"
"${CMAKE_SOURCE_DIR}/${FIL_WE}.pb.h"
COMMAND protoc
ARGS --proto_path=${MSG_PROTO_DIR} --cpp_out=${CMAKE_SOURCE_DIR} ${msg}
DEPENDS ${msg}
COMMENT "Running C++ protocol buffer compiler on ${msg}"
VERBATIM
)
endforeach()
# 设置文件属性为 GENERATED
set_source_files_properties(${MESSAGE_SRC} ${MESSAGE_HDRS} PROPERTIES GENERATED TRUE)
include_directories(${PROTOBUF_INCLUDE_DIRS})
include_directories(${CMAKE_SOURCE_DIR})
add_executable(list_people list_people.cpp ${MESSAGE_SRC} ${MESSAGE_HDRS})
target_link_libraries(list_people ${PROTOBUF_LIBRARIES})
go 使用生成的源文件
编译的配置太痛苦了,后续请使用 GoLand ide ;
-
新建主模块 list_people.go 文件
1 2
mkdir -p proto_out_go/main touch proto_out_go/main/list_people.go
-
编辑 proto_out_go/main/list_people.go 文件
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61
package main import ( "fmt" "io" "io/ioutil" "log" "os" "github.com/golang/protobuf/proto" pb "example.com/addressbook" ) func writePerson(w io.Writer, p *pb.Person) { fmt.Fprintln(w, "Person ID:", p.Id) fmt.Fprintln(w, " Name:", p.Name) if p.Email != "" { fmt.Fprintln(w, " E-mail address:", p.Email) } for _, pn := range p.Phones { switch pn.Type { case pb.Person_MOBILE: fmt.Fprint(w, " Mobile phone #: ") case pb.Person_HOME: fmt.Fprint(w, " Home phone #: ") case pb.Person_WORK: fmt.Fprint(w, " Work phone #: ") } fmt.Fprintln(w, pn.Number) } } func listPeople(w io.Writer, book *pb.AddressBook) { for _, p := range book.People { writePerson(w, p) } } // Main reads the entire address book from a file and prints all the // information inside. func main() { if len(os.Args) != 2 { log.Fatalf("Usage: %s ADDRESS_BOOK_FILE\n", os.Args[0]) } fname := os.Args[1] // [START unmarshal_proto] // Read the existing address book. in, err := ioutil.ReadFile(fname) if err != nil { log.Fatalln("Error reading file:", err) } book := &pb.AddressBook{} if err := proto.Unmarshal(in, book); err != nil { log.Fatalln("Failed to parse address book:", err) } // [END unmarshal_proto] listPeople(os.Stdout, book) }
-
主模块初始化并补充依赖
1 2 3 4 5 6 7 8
# 初始化模块 go mod init example.com/list_people.go # 重定向依赖,addressbook 在 ../addressbook 目录 go mod edit -replace example.com/addressbook=../addressbook # 添加缺少模块以及移除多余模块 go mod tidy
-
执行程序
1
go run .