1 说在前面
由于看到网上都是一些零零散散的 protobuf 相关介绍,加上笔者最近由于项目的原因深入分析了一下 protobuf ,所以想做一个体系的《通晓 protobuf 原理》系列的共享:
- 「通晓 protobuf 原理之一:为什么要运用它以及怎么运用」;
- 「通晓 protobuf 原理之二:编码原理分析」;
- 「通晓 protobuf 原理之三:反射原理分析」;
- 「通晓 protobuf 原理之四:RPC 原理分析」;
- 「通晓 protobuf 原理之五:Arena 分配器原理分析」。
- 后续的待定……
本文是系列文章的榜首篇,阅览了本文,读者能够了解到:
- 为什么要运用 protobuf ,而不运用 xml、json 等其他数据结构化标准;
- 在 centos7 下怎么编译装置 protobuf 以及或许遇到哪些装置问题;
- 怎么将 proto IDL 文件生成 C++ 源代码;
- protobuf 一般数据接口怎么运用;
- protobuf 的反射是什么?以及反射接口怎么运用;
- protobuf 的 RPC 接口有什么用处?以及怎么运用。
阅览本文大约需求十分钟左右。主张读者先阅览目录,先大约了解有哪些内容,然后在挑选悉数阅览仍是挑选性阅览,以提高阅览功率。
2 为什么运用protobuf
为什么要运用 protobuf ?先说说 protobuf 面世的目的是处理什么问题。
protobuf (protocol buffer) 是谷歌内部的混合言语数据标准。经过将结构化的数据进行序列化,用于通讯协议、数据存储等领域的言语无关、平台无关、可扩展的序列化结构数据格局。其实便是和 xml、json 做的类似的事情。那么问题又来了,为什么不挑选运用 xml、json,而要挑选 protobuf 呢?先经过以下表格做一个比较:
特性 \ 类型 | xml | json | protobuf |
---|---|---|---|
数据结构支撑 | 简略结构 | 简略结构 | 杂乱结构 |
数据保存办法 | 文本 | 文本 | 二进制 |
数据保存大小 | 大 | 大 | 小 |
编解码功率 | 慢 | 慢 | 快 |
言语支撑程度 | 掩盖干流言语 | 掩盖干流言语 | 掩盖干流言语 |
总结下来便是,运用 protobuf 能够多(数据结构支撑、言语支撑程度)、快(编解码功率)、好(数据保存办法)、省(数据保存大小)。
- [多]:业务场景中,不免或许有比较杂乱的数据结构,关于扩展性没有后顾之虑;掩盖了干流的编程言语,在一定程度上减少了自研成本,开发者能够轻松上手;
- [快]:快是一个非常重要的体系功用指标;
- [好]:运用二进制对数字类型更节约空间、读取转化时间,由于数字转化成文件占用的字节数比较多,字符串和数字之间的转化也比较耗时;
- [省]:当海量数据都需求存储在
redis
内存中的时分,节约空间又多重要;当网络带宽有限的情况下,节约带宽有多重要。
3 编译环境
操作体系:CentOS Linux release 7.9.2009 (Core)
编译器版别:gcc version 4.8.5 20150623 (Red Hat 4.8.5-44) (GCC)
protobuf 版别:3.17.3
4 体系依靠包
$ yum install gcc-c++ make autoconf automake
5 下载源码
$ git clone https://github.com/protocolbuffers/protobuf.git
$ cd protobuf/
$ git checkout v3.17.3
6 编译装置
6.1 编译办法
# 生成 confiure 文件
$ ./autogen.sh
# 履行 confiure 文件,prefix 默许也是 /usr/local
$ ./configure CXXFLAGS="-fPIC -std=c++11" --prefix=/usr/local
# 履行 make
$ make -j 4
6.2 装置办法
$ make install
# bin装置目录
$ ls /usr/local/bin/
protoc
# lib装置目录
$ ls /usr/local/lib
libprotobuf-lite.a libprotobuf-lite.so.28 libprotobuf.la libprotobuf.so.28.0.3 libprotoc.so pkgconfig
libprotobuf-lite.la libprotobuf-lite.so.28.0.3 libprotobuf.so libprotoc.a libprotoc.so.28
libprotobuf-lite.so libprotobuf.a libprotobuf.so.28 libprotoc.la libprotoc.so.28.0.3
# 头文件装置目录
$ ls /usr/local/include/
google
6.3 submodule依靠
以上的编译装置,没有用到 submodule。可是如果需求履行单元测验和功用测验,就会用到(见 tests.sh
)。C++ 版别只需求履行如下指令:
$ ./tests.sh cpp
看看这个指令做了什么(见build_cpp
函数):
...
internal_build_cpp() {
if [ -f src/protoc ]; then
# Already built.
return
fi
# Initialize any submodules.
git submodule update --init --recursive
./autogen.sh
./configure CXXFLAGS="-fPIC -std=c++11" # -fPIC is needed for python cpp test.
# See python/setup.py for more details
make -j$(nproc)
}
build_cpp() {
internal_build_cpp
make check -j$(nproc) || (cat src/test-suite.log; false)
cd conformance && make test_cpp && cd ..
# The benchmark code depends on cmake, so test if it is installed before
# trying to do the build.
if [[ $(type cmake 2>/dev/null) ]]; then
# Verify benchmarking code can build successfully.
cd benchmarks && make cpp-benchmark && cd ..
else
echo ""
echo "WARNING: Skipping validation of the bench marking code, cmake isn't installed."
echo ""
fi
}
...
build_cpp
做了以下事情:
- 经过 git submodule 指令下载第三方依靠;
- 履行 autogen.sh 脚本生成 configure`;
- 履行 configure 生成 Makefile`;
- 根据 Makefile 履行 make 编译;
- 编译和履行单元测验用例;
- 编译 benchmarks。
经过.gitmodules
文件能够看到 protobuf 依靠 benchmark(功用测验结构) 和googletest(单元测验结构)两个第三方模块。
$ cat .gitmodules
[submodule "third_party/benchmark"]
path = third_party/benchmark
url = https://github.com/google/benchmark.git
[submodule "third_party/googletest"]
path = third_party/googletest
url = https://github.com/google/googletest.git
ignore = dirty
6.4 常见编译问题
6.4.1 没有装置 autoconf 包
+ test -d third_party/googletest
+ mkdir -p third_party/googletest/m4
+ autoreconf -f -i -Wall,no-obsolete
autogen.sh: line 41: autoreconf: command not found
6.4.2 没有装置 automake包
+ test -d third_party/googletest
+ mkdir -p third_party/googletest/m4
+ autoreconf -f -i -Wall,no-obsolete
Can't exec "aclocal": No such file or directory at /usr/share/autoconf/Autom4te/FileUtils.pm line 326.
autoreconf: failed to run aclocal: No such file or directory
7 开发中运用
7.1 生成源代码
根据 proto IDL 文件生成 C++ 源代码。
$ tree proto/
proto/
├── Makefile
└── echo.proto
界说一个 proto IDL 文件:
//指定proto版别
syntax = "proto3";
//拟定命名空间
package self;
//告知proto编译器生成service接口
option cc_generic_services = true;
//枚举界说
enum QueryType {
PRIMMARY = 0;
SECONDARY = 1;
};
//message界说
message EchoRequest {
QueryType querytype = 1;
string payload = 2;
}
message EchoResponse {
int32 code = 1;
string msg = 2;
}
//service界说
service EchoService {
rpc Echo(EchoRequest) returns(EchoResponse);
}
Makefile
源文件:
CC = g++
CXXFLAGS = -std=c++11
TARGET = libproto.a
SOURCE = $(wildcard *.cc)
OBJS = $(patsubst %.cc, %.o, $(SOURCE))
INCLUDE = -I./
$(TARGET): $(OBJS)
ar rcv $(TARGET) $(OBJS)
%.o: %.c
protoc -I=./ --cpp_out=./ ./echo.proto
$(CC) $(CXXFLAGS) $(INCLUDE) -o $@ -c $^
.PHONY:clean
clean:
rm *.o $(TARGET)
履行 make 生成 C++ 源代码
$ make -C proto/
$ tree proto/
proto/
├── Makefile
├── echo.pb.cc
├── echo.pb.h
└── echo.proto
7.2 运用源代码
$ tree test_echo
test_echo
├── Makefile
|── general.cpp
├── reflection.cpp
├── rpc.cpp
└── test_echo.cpp
test_echo.cpp
源文件
#include <iostream>
#include <string>
#include "../proto/echo.pb.h"
extern void test_general();
extern void test_relection();
extern void test_rpc();
int main() {
test_general();
test_relection();
test_rpc();
return 0;
}
Makefile
源文件
CC = g++
CXXFLAGS = -std=c++11
TARGET = test_echo
SOURCE = $(wildcard *.cpp)
OBJS = $(patsubst %.cpp, %.o, $(SOURCE))
INCLUDE = -I./
LIBS = -lproto -lprotobuf
LIBPATH = -L../proto
$(TARGET): $(OBJS)
$(CC) $(CXXFLAGS) -o $@ $^ $(LIBPATH) $(LIBS)
%.o: %.c
protoc -I=./ --cpp_out=./ ./echo.proto
$(CC) $(CXXFLAGS) $(INCLUDE) -o $@ -c $^
.PHONY:clean
clean:
rm -f *.o $(TARGET)
编译和履行
$ make
$ ./test_echo
=== START TEST GENERAL ===
req.querytype[1], req_rcv.querytype[1]
req.payload[this is a payload], req_rcv.payload[this is a payload]
=== END TEST GENERAL ===
=== START TEST REFLECTION ===
type_name: self.EchoRequest
ref_req_msg_payload:
ref_req_msg_payload: my payload
=== END TEST REFLECTION ===
=== END TEST RPC ===
=== START RPC SERVER ===
MyEchoServiceImpl::recieve request|I have received <querytype:{1}, payload:{rpc_server::request::payload}
MyEchoServiceImpl::OnCallbak: response|<code:0, msg:I have received <querytype:{1}, payload:{rpc_server::request::payload}>
=== END RPC SERVER ===
=== START RPC CLIENT ===
rpc_client::response<code:0,msg:I have sent <querytype:{1}, payload:{rpc_client::request::payload}>
=== END RPC CLIENT ===
=== END TEST RPC ===
或许遇到问题:
$ ./test_echo
./test_echo: error while loading shared libraries: libprotobuf.so.28: cannot open shared object file: No such file or directory
由于 protobuf 装置目录为/usr/local/lib
,不在操作体系默许lib
目录中(操作体系默许/usr/lib
、/usr/lib64
、/lib
、/lib64
)。所以怎么告知操作体系呢?如下在/etc/ld.so.conf.d/
中新增一个文件usr_local_lib.conf
,内容为/usr/local/lib
,然后履行ldconfig
,再履行test_echo
就没有问题了。
[root@af82601d9d63 test_echo]# cat /etc/ld.so.conf.d/usr_local_lib.conf
/usr/local/lib
还有一种办法是export LD_LIBRARY_PATH=${LD_LIBRARY_PATH}:/usr/local/lib
,写在~/.bash_profile
或许~/.bashrc
装备文件中,每次登录用户即时生效。
7.3 一般接口
见 general.cpp
源文件。
extern void test_general() {
std::cout << "=== START TEST GENERAL ===" << std::endl;
self::EchoRequest req;
req.set_querytype(self::SECONDARY);
req.set_payload("this is a payload");
std::string req_body;
req.SerializeToString(&req_body);
self::EchoRequest req_rcv;
req_rcv.ParseFromString(req_body);
std::cout << "req.querytype[" << req.querytype() << "], "
<< "req_rcv.querytype[" << req_rcv.querytype() << "]" << std::endl
<< "req.payload[" << req.payload() << "], "
<< "req_rcv.payload[" << req_rcv.payload() << "]"<< std::endl;
std::cout << "=== END TEST GENERAL ===" << std::endl << std::endl;
}
开发中常常运用的是读写field(即get/set),序列化(SerializeToString)和反序列化(ParseFromString)。
7.4 反射接口
见reflection.cpp
源文件。
#include "../proto/echo.pb.h"
void test_relection() {
std::cout << "=== START TEST REFLECTION ===" << std::endl;
std::string type_name = self::EchoRequest::descriptor()->full_name();
std::cout << "type_name: " << type_name << std::endl;
const google::protobuf::Descriptor* descriptor
= google::protobuf::DescriptorPool::generated_pool()->FindMessageTypeByName(type_name);
const google::protobuf::Message* prototype
= google::protobuf::MessageFactory::generated_factory()->GetPrototype(descriptor);
google::protobuf::Message* req_msg = prototype->New();
const google::protobuf::Reflection* req_msg_ref
= req_msg->GetReflection();
const google::protobuf::FieldDescriptor *req_msg_ref_field_payload
= descriptor->FindFieldByName("payload");
std::cout << "ref_req_msg_payload: "
<< req_msg_ref->GetString(*req_msg, req_msg_ref_field_payload)
<< std::endl;
req_msg_ref->SetString(req_msg, req_msg_ref_field_payload, "my payload");
std::cout << "ref_req_msg_payload: "
<< req_msg_ref->GetString(*req_msg, req_msg_ref_field_payload)
<< std::endl;
std::cout << "=== END TEST REFLECTION ===" << std::endl << std::endl;
}
既然已经有了 get/set 的读写 API,为什么还需求反射呢?
运用场景比如:在推荐体系中,用户特征运用 protobuf 格局存储,每个用户有成百上千个特征(一个特征能够了解成一个字段),在建模的时分能够只需求这些特征的几个或许几十个特征即可。需求如下:
- 挑选这些特征;
- 经过指定的函数对这些特征做数据转化,输出指定格局的成果。
如果运用 get/set 读写这些特征,那是不是每个模型都要写一遍完成代码(由于读写的特征字段不一样)。能够能够做到这样,给定一个装备文件,格局如下:
[榜首列] [第二列]
特征字段 转化函数(即算子)
经过装备特征字段和其转化函数,程序自动进行字段、转化函数的挑选,并履行转化和输出成果。这个时分 protobuf 的反射功用就派上用场了。
这儿简略介绍了一下反射功用的运用布景,详细的完成原理将在后续系列文章中专门介绍。
7.5 RPC接口
见rpc.cpp
源文件。
protobuf 提供了一个 rpc 接口标准,能够运用它来界说接口格局,如下办法:
//service界说
service EchoService {
rpc Echo(EchoRequest) returns(EchoResponse);
}
EchoService
是一个服务笼统,Echo
是该服务的办法,也能够了解成接口。
可不能够不运用 protobuf 的rpc
接口标准?当然能够。 protobuf 的rpc
和message
并不是强制绑定的,开发者能够挑选运用只运用message
或许运用rpc+message
。这是谷歌内部沉淀的一个根据 protobuf 的rpc
规划形式,笔者觉得这是一个很好的规划形式,主张运用。
7.5.1 服务端接口完成
static void rpc_server() {
std::cout << " === START RPC SERVER ===" << std::endl;
MyEchoServiceImpl svc;
MyRpcControllerImpl cntl;
self::EchoRequest request;
self::EchoResponse response;
request.set_querytype(self::SECONDARY);
request.set_payload("rpc_server::request::payload");
auto req_msg = dynamic_cast<google::protobuf::Message*>(&request);
auto rsp_msg = dynamic_cast<google::protobuf::Message*>(&response);
google::protobuf::Closure* done
= google::protobuf::NewCallback(&svc,
&MyEchoServiceImpl::OnCallbak, //指定回调函数,履行done->Run()的时分触发回调
req_msg, //回调函数的榜首个参数
rsp_msg); //回调函数的第二个参数
svc.Echo(&cntl, &request, &response, done); //调用处理逻辑
std::cout << " === END RPC SERVER ===" << std::endl;
}
当服务端收到恳求之后,会经过svc.Echo
调用处理流程。是的,svc
完成的便是EchoService
的Echo rpc
接口,如下完成:
class MyEchoServiceImpl: public self::EchoService {
public:
virtual void Echo(google::protobuf::RpcController* cntl,
const self::EchoRequest* request,
self::EchoResponse* response,
google::protobuf::Closure* done) override {
std::ostringstream oss;
oss << "I have received <querytype:{" << request->querytype()
<< "}, payload:{" << request->payload() << "}";
std::string rcv = oss.str();
std::cout << "MyEchoServiceImpl::recieve request|" << rcv << std::endl;
response->set_code(0);
response->set_msg(rcv);
done->Run(); //记住调用 Run 才能触发 OnCallback 操作。
}
void OnCallbak(google::protobuf::Message* request,
google::protobuf::Message* response) {
std::cout << "MyEchoServiceImpl::OnCallbak: response|<code:"
<< dynamic_cast<self::EchoResponse*>(response)->code() << ", msg:"
<< dynamic_cast<self::EchoResponse*>(response)->msg() << ">"
<< std::endl;
}
};
7.5.2 客户端接口完成
static void rpc_client() {
std::cout << " === START RPC CLIENT ===" << std::endl;
MyRpcControllerImpl cntl;
self::EchoRequest request;
self::EchoResponse response;
request.set_querytype(self::SECONDARY);
request.set_payload("rpc_client::request::payload");
MyRpcChannelImpl channel;
channel.init();
self::EchoService_Stub stub(&channel);
stub.Echo(&cntl, &request, &response, nullptr);
std::cout << "rpc_client::response<code:" << response.code()
<< ",msg:" << response.msg() << ">" << std::endl;
std::cout << " === END RPC CLIENT ===" << std::endl;
}
stub
接收了channel
参数,在履行stub.Echo
的时分实际上是调用channel
的CallMethod
接口发送恳求,如下完成:
class MyRpcChannelImpl: public google::protobuf::RpcChannel {
public:
void init() {}
virtual void CallMethod(const google::protobuf::MethodDescriptor* method,
google::protobuf::RpcController* controller,
const google::protobuf::Message* request,
google::protobuf::Message* response,
google::protobuf::Closure* done) override {
auto req = dynamic_cast<self::EchoRequest*>(
const_cast<google::protobuf::Message*>(request));
auto rsp = dynamic_cast<self::EchoResponse*>(response);
std::ostringstream oss;
oss << "I have sent <querytype:{" << req->querytype()
<< "}, payload:{" << req->payload() << "}";
std::string rcv = oss.str();
rsp->set_code(0);
rsp->set_msg(rcv);
}
};
7.5.3 控制器 Controller
控制器提供了Reset
、Failed
、ErrorText
、StartCancel
、SetFailed
、IsCanceled
、NotifyOnCancel
六个接口,主要是为了控制、操作、获取恳求的状况。
class MyRpcControllerImpl: public google::protobuf::RpcController {
public:
virtual void Reset() override {
std::cout << "MyRpcController::Reset" << std::endl;
}
virtual bool Failed() const override {
std::cout << "MyRpcController::Failed" << std::endl;
return false;
}
virtual std::string ErrorText() const override {
std::cout << "MyRpcController::ErrorText" << std::endl;
return "";
}
virtual void StartCancel() override {
std::cout << "MyRpcController::StartCancel" << std::endl;
}
virtual void SetFailed(const std::string& reason) override {
std::cout << "MyRpcController::SetFailed" << std::endl;
}
virtual bool IsCanceled() const override {
std::cout << "MyRpcController::IsCanceled" << std::endl;
return false;
}
virtual void NotifyOnCancel(google::protobuf::Closure* callback) override {
std::cout << "MyRpcController::NotifyOnCancel" << std::endl;
}
//private:
//bool concel_ = false;
//std::string err_reason_;
};
8 参考文献
测验相关源码位置:github.com/sullivan120…
9 说在最后
以上便是系列文章榜首篇的所有内容。经过本文,读者应该已经了解在日常项目开发中怎么运用 protobuf ,也对能够运用 protobuf 做什么有了一个开始的了解。从下一篇开始,将是对 protobuf 原理方面的一些介绍,如果说本文是告知读者怎么运用 protobuf ,那么后面的系列文章将会帮助读者怎么用好 protobuf 。
感谢阅览,如果还想了解更多的内容,请在评论区留言。
欢迎学习沟通,也欢迎纠正。