1 说在前面

由于看到网上都是一些零零散散的 protobuf 相关介绍,加上笔者最近由于项目的原因深入分析了一下 protobuf ,所以想做一个体系的《通晓 protobuf 原理》系列的共享:

  • 「通晓 protobuf 原理之一:为什么要运用它以及怎么运用」;
  • 「通晓 protobuf 原理之二:编码原理分析」;
  • 「通晓 protobuf 原理之三:反射原理分析」;
  • 「通晓 protobuf 原理之四:RPC 原理分析」;
  • 「通晓 protobuf 原理之五:Arena 分配器原理分析」。
  • 后续的待定……

本文是系列文章的榜首篇,阅览了本文,读者能够了解到:

  1. 为什么要运用 protobuf ,而不运用 xml、json 等其他数据结构化标准;
  2. 在 centos7 下怎么编译装置 protobuf 以及或许遇到哪些装置问题;
  3. 怎么将 proto IDL 文件生成 C++ 源代码;
  4. protobuf 一般数据接口怎么运用;
  5. protobuf 的反射是什么?以及反射接口怎么运用;
  6. 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做了以下事情:

  1. 经过 git submodule 指令下载第三方依靠;
  2. 履行 autogen.sh 脚本生成 configure`;
  3. 履行 configure 生成 Makefile`;
  4. 根据 Makefile 履行 make 编译;
  5. 编译和履行单元测验用例;
  6. 编译 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 格局存储,每个用户有成百上千个特征(一个特征能够了解成一个字段),在建模的时分能够只需求这些特征的几个或许几十个特征即可。需求如下:

  1. 挑选这些特征;
  2. 经过指定的函数对这些特征做数据转化,输出指定格局的成果。

如果运用 get/set 读写这些特征,那是不是每个模型都要写一遍完成代码(由于读写的特征字段不一样)。能够能够做到这样,给定一个装备文件,格局如下:

[榜首列]  [第二列]
特征字段  转化函数(即算子)

经过装备特征字段和其转化函数,程序自动进行字段、转化函数的挑选,并履行转化和输出成果。这个时分 protobuf 的反射功用就派上用场了。

这儿简略介绍了一下反射功用的运用布景,详细的完成原理将在后续系列文章中专门介绍。

7.5 RPC接口

rpc.cpp 源文件。

protobuf 提供了一个 rpc 接口标准,能够运用它来界说接口格局,如下办法:

//service界说
service EchoService {
	rpc Echo(EchoRequest) returns(EchoResponse);
}

EchoService是一个服务笼统,Echo是该服务的办法,也能够了解成接口。

可不能够不运用 protobuf 的rpc接口标准?当然能够。 protobuf 的rpcmessage并不是强制绑定的,开发者能够挑选运用只运用message或许运用rpc+message。这是谷歌内部沉淀的一个根据 protobuf 的rpc规划形式,笔者觉得这是一个很好的规划形式,主张运用。

精通protobuf原理之一:为什么要使用以及如何使用

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完成的便是EchoServiceEcho 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的时分实际上是调用channelCallMethod接口发送恳求,如下完成:

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

控制器提供了ResetFailedErrorTextStartCancelSetFailedIsCanceledNotifyOnCancel六个接口,主要是为了控制、操作、获取恳求的状况。

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 。

感谢阅览,如果还想了解更多的内容,请在评论区留言。

欢迎学习沟通,也欢迎纠正。