protobuf, rpc, 火焰图
文章目录
- 前言
- 一、生成火焰图
- 二、protobuf
- 标量值类型
- 变长整形
- 有符号整数
- 非变长整数
- 长度限定记录
- 可选和重复元素
- 打包重复字段
- 映射
- grpc
- 2. CMakeLists.txt
- references
前言
protobuf: 一种将结构化数据进行序列化和反序列化的协议,提供IDL工具,生成接口代码,支持多种语言。
rpc: 类似http的请求相应模式,可跨设备提供远程接口调用功能,其接口可通过protobuf定义。
利用perf生成火焰图,分析程序性能
一、生成火焰图
perf record -F 99 -g --pid $PID -- sleep 30
- -F 99:设置采样频率为每秒99次(可以根据需要调整)。
- –pid $PID:指定要监控的进程ID。
- – sleep 30:表示监控30秒。
git clone https://github.com/brendangregg/FlameGraph.git
perf script | ./FlameGraph/stackcollapse-perf.pl | ./FlameGraph/flamegraph.pl > flamegraph.svg
二、protobuf
- 编译源码:
git clone https://github.com/protocolbuffers/protobuf.git
cd protobuf
git submodule update --init --recursive
cmake . -DCMAKE_CXX_STANDARD=14
cmake --build .
- 定义.proto文件
syntax = "proto2";
package tutorial;
message Person {
optional string name = 1;
optional int32 id = 2;
optional string email = 3;
enum PhoneType {
PHONE_TYPE_UNSPECIFIED = 0;
PHONE_TYPE_MOBILE = 1;
PHONE_TYPE_HOME = 2;
PHONE_TYPE_WORK = 3;
}
message PhoneNumber {
optional string number = 1;
optional PhoneType type = 2 [default = PHONE_TYPE_HOME];
}
repeated PhoneNumber phones = 4;
}
message AddressBook {
repeated Person people = 1;
}
- 编译.proto文件生成接口代码
protoc -I=$SRC_DIR --cpp_out=$DST_DIR $SRC_DIR/addressbook.proto
- CMakeLists.txt
cmake_minimum_required(VERSION 3.10)
project(protobuf-test)
find_package(protobuf CONFIG REQUIRED)
if(protobuf_VERBOSE)
message(STATUS "Using Protocol Buffers ${protobuf_VERSION}")
endif()
set(CMAKE_INCLUDE_CURRENT_DIR TRUE)
add_executable(${PROJECT_NAME} main.cpp addressbook.pb.cc)
target_include_directories(${PROJECT_NAME} PUBLIC
install/usr/local/include
)
target_link_libraries(${PROJECT_NAME}
protobuf::libprotobuf
)
#include <iostream>
#include <fstream>
#include <string>
#include "addressbook.pb.h"
using namespace std;
void list_people(const tutorial::AddressBook& address_book)
{
for (int i = 0; i< address_book.people_size(); ++i) {
const tutorial::Person& person = address_book.people(i);
cout << "Person ID: " << person.id() << endl;
cout << " Name: " << person.name() << endl;
if (person.has_email()) {
cout << " E-mail address: " << person.email() << endl;
}
for (int j = 0; j < person.phones_size(); ++j) {
const tutorial::Person::PhoneNumber& phone_number = person.phones(j);
switch (phone_number.type()) {
case tutorial::Person::PHONE_TYPE_MOBILE:
cout << " Mobile phone #: ";
break;
case tutorial::Person::PHONE_TYPE_HOME:
cout << " Home phone #: ";
break;
case tutorial::Person::PHONE_TYPE_WORK:
cout << " Work phone #: ";
break;
}
cout << phone_number.number() << endl;
}
}
}
void PromptForAddress(tutorial::Person* person)
{
cout << "enter person id number: ";
int id;
cin >> id;
person->set_id(id);
cin.ignore(256, '\n');
cout << "enter name: ";
getline(cin, *person->mutable_name());
string email;
getline(cin, email);
if (!email.empty())
{
person->set_email(email);
}
while (true)
{
cout << "enter a ph num: ";
string number;
getline(cin, number);
if (number.empty()) break;
tutorial::Person::PhoneNumber* phone_number = person->add_phones();
phone_number->set_number(number);
cout << "is this a mobile, home, or work phone?";
string type;
getline(cin, type);
if (type == "mobile") {
phone_number->set_type(tutorial::Person::PHONE_TYPE_MOBILE);
} else if (type == "home") {
phone_number->set_type(tutorial::Person::PHONE_TYPE_HOME);
} else if (type == "work") {
phone_number->set_type(tutorial::Person::PHONE_TYPE_WORK);
} else {
cout << "Unknown type. using default." << endl;
}
}
}
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;
}
tutorial::AddressBook address_book;
{
fstream input(argv[1], ios::in | ios::binary);
if (!input) {
cout << argv[1] << ": File not found. Create a new file." << endl;
} else if (!address_book.ParseFromIstream(&input)) {
cerr << "Failed to parse address book." << endl;
return -1;
}
}
PromptForAddress(address_book.add_people());
{
fstream output(argv[1], ios::out | ios::trunc | ios::binary);
std::stringstream ss;
std::vector<char> buf;
if (!address_book.SerializeToOstream(&output)) {
cerr << "Failed to write address book." << endl;
return -1;
}
string str;
if (!address_book.SerializeToString(&str))
{
cerr << "Failed to write address book." << endl;
return -1;
}
for (int i = 0; i < str.size(); ++i)
{
printf("%x ", *(unsigned char*)&str[i]);
}
cout << "\n";
}
list_people(address_book);
// Optional: Delete all global objects allocated by libprotobuf.
google::protobuf::ShutdownProtobufLibrary();
}
标量值类型
.proto 类型 | 注释 | C++ 类型 |
---|---|---|
double | double | double |
float | float | float |
int32 | 使用可变长度编码。对编码负数效率低下——如果您的字段可能包含负值,请改用 sint32。 | int32 |
int64 | 使用可变长度编码。对编码负数效率低下——如果您的字段可能包含负值,请改用 sint64。 | int64 |
uint32 | 使用可变长度编码。 | uint32 |
uint64 | 使用可变长度编码。 | uint64 |
sint32 | 使用可变长度编码。有符号整数类型。这些类型比常规 int32 更有效地编码负数。 | int32 |
sint64 | 使用可变长度编码。有符号整数类型。这些类型比常规 int64 更有效地编码负数。 | int64 |
fixed32 | 始终为四个字节。如果值通常大于 228,则比 uint32 更有效。 | uint32 |
fixed64 | 始终为八个字节。如果值通常大于 256,则比 uint64 更有效。 | uint64 |
sfixed32 | 始终为四个字节。 | int32 |
sfixed64 | 始终为八个字节。 | int64 |
bool | bool | |
string 字符串必须始终包含 UTF-8 编码或 7 位 ASCII 文本,并且长度不能超过 232。 | string | |
bytes 可以包含任何任意字节序列,长度不超过 232。 | string |
标量之类型 https://protobuf.com.cn/programming-guides/proto3/
编码 https://protobuf.com.cn/programming-guides/encoding/
变长整形
可变宽度整数,或 变长整数,是线格式的核心。它们允许使用 1 到 10 个字节对无符号 64 位整数进行编码,其中较小的值使用较少的字节。
变长整数中的每个字节都有一个 延续位,指示其后面的字节是否属于该变长整数。这是字节的 最高有效位 (MSB)(有时也称为 符号位)。较低的 7 位是有效负载;生成的整数是通过将组成字节的 7 位有效负载拼接在一起构建的。
因此,例如,以下是数字 1,编码为 01
——它是一个字节,因此 MSB 未设置
0000 0001
^ msb
以下是 150,编码为
低字节 高字节
10010110 00000001
^ msb ^ msb
10010110 00000001 // Original inputs.
低字节 高字节
0010110 0000001 // Drop continuation bits.
p[0] p[1]
uint64 = p[0]&0x7f + p[1]&0xf7 * 128UL = 22 + 128 = 150
协议缓冲区消息是一系列键值对。消息的二进制版本仅使用字段的编号作为键——每个字段的名称和声明类型只能在解码端通过引用消息类型的定义(即 .proto 文件)来确定。Protoscope 无法访问此信息,因此它只能提供字段编号。
当消息被编码时,每个键值对都会转换为一个 记录,该记录由字段编号、线类型和有效负载组成。线类型告诉解析器后面的有效负载有多大。这允许旧解析器跳过它们不理解的新字段。这种类型的方案有时称为 标记-长度-值 或 TLV。
有六种线类型:VARINT、I64、LEN、SGROUP(已弃用)、EGROUP(已弃用) 和 I32
ID | 名称 | 用于 |
---|---|---|
0 | VARINT | int32、int64、uint32、uint64、sint32、sint64、bool、enum |
1 | I64 | fixed64、sfixed64、double |
2 | LEN | string、bytes、嵌入式消息、打包重复字段 |
5 | I32 | fixed32、sfixed32、float |
记录的“标记”编码为一个变长整数,该整数由字段编号和线类型通过公式 (field_number << 3) | wire_type 形成。换句话说,在解码表示字段的变长整数后,低 3 位告诉我们线类型,其余整数告诉我们字段编号。
现在让我们再次查看我们的简单示例。您现在知道流中的第一个数字始终是变长整数键,这里它是 0x08
,或者(删除 MSB)
key的值:
0000 1000
000 1000
^^^ ^ field_number
^^^ wire_type
所以 字段编号为1
类型为 VARINT
您取最后三位以获取线类型 (0),然后右移三位以获取字段编号 (1)。Protoscope 将标记表示为整数后跟冒号和线类型,因此我们可以将上述字节写为 1:VARINT。
因为线类型是 0 或 VARINT,所以我们知道我们需要解码一个变长整数以获取有效负载。如上所述,字节 0x9601
变长整数解码为 150,从而给出我们的记录。我们可以用 Protoscope 将其写成 1:VARINT 150。
布尔值和枚举都像 int32 一样编码。特别是,布尔值始终编码为 00
或 01
。在 Protoscope 中,false 和 true 是这些字节字符串的别名。
有符号整数
正如您在上一节中看到的,与线类型 0 关联的所有协议缓冲区类型都编码为变长整数。但是,变长整数是无符号的,因此不同的有符号类型,sint32 和 sint64 与 int32 或 int64 相比,对负整数的编码方式不同。
intN 类型将负数编码为二进制补码,这意味着作为无符号 64 位整数,它们设置了最高位。结果,这意味着必须使用 所有十个字节。例如,-2 由 protoscope 转换为
11111110 11111111 11111111 11111111 11111111
11111111 11111111 11111111 11111111 00000001
这是 2 的二进制补码,在无符号算术中定义为~0 - 2 + 1,其中~0 是全为 1 的 64 位整数。理解为什么这会产生这么多 1 是一项有益的练习。
sintN 使用“ZigZag”编码而不是二进制补码来编码负整数。正整数p 编码为2 * p(偶数),而负整数n 编码为2 * |n| - 1(奇数)。因此,编码在正数和负数之间“锯齿形”变化。例如
带符号原始值 | 编码为 |
---|---|
0 | 0 |
-1 | 1 |
1 | 2 |
-2 | 3 |
… | … |
0x7fffffff | 0xfffffffe |
-0x80000000 | 0xffffffff |
“ZigZag”:
整数: 左移动一位
负数: ~(左移一位)
非变长整数
非 varint 数值类型很简单 - double 和fixed64 的线类型为I64,它告诉解析器预期一个固定的八字节数据块。我们可以通过编写5: 25.4 来指定double 记录,或通过6: 200i64 来指定fixed64 记录。在这两种情况下,省略显式线类型都意味着I64 线类型。
类似地,float 和fixed32 的线类型为I32,它告诉解析器预期四个字节。这些类型的语法包括添加i32 后缀。25.4i32 将发出四个字节,200i32 也是如此。标签类型被推断为I32。
长度限定记录
message Test2 {
optional string b = 2;
}
字段b 的记录是一个字符串,字符串是LEN 编码的。如果我们将b 设置为"testing",则将其编码为一个LEN 记录,字段号为 2,包含 ASCII 字符串"testing"。
12 07 [74 65 73 74 69 6e 67]
可选和重复元素
message Test4 {
optional string d = 4;
repeated int32 e = 5;
}
4: {"hello"}
5: 1
5: 2
5: 3
打包重复字段
映射
映射字段只是特殊类型的重复字段的简写。如果我们有
message Test6 {
map<string, int32> g = 7;
}
这实际上与以下内容相同
message Test6 {
message g_Entry {
optional string key = 1;
optional int32 value = 2;
}
repeated g_Entry g = 7;
}
因此,映射的编码方式与repeated 消息字段完全相同:作为一系列LEN 类型的记录,每个记录有两个字段。
grpc
https://grpc.io/docs/languages/cpp/basics/
https://github.com/grpc/grpc/tree/master/examples/cpp/route_guide
2. CMakeLists.txt
cmake_minimum_required(VERSION 3.8)
project(RouteGuide C CXX)
foreach(__prefix_path__ absl c-ares grpc protobuf re2 utf8_range)
list(APPEND CMAKE_PREFIX_PATH ${CMAKE_CURRENT_SOURCE_DIR}/../../thirdparty/grpc/lib/cmake)
endforeach()
include(/home/igs/Projects/grpc/examples/cpp/cmake/common.cmake)
# Find absl package
find_package(absl CONFIG REQUIRED)
# Proto file
get_filename_component(rg_proto "${CMAKE_CURRENT_SOURCE_DIR}/route_guide.proto" ABSOLUTE)
get_filename_component(rg_proto_path "${rg_proto}" PATH)
# Generated sources
set(rg_proto_srcs "${CMAKE_CURRENT_BINARY_DIR}/route_guide.pb.cc")
set(rg_proto_hdrs "${CMAKE_CURRENT_BINARY_DIR}/route_guide.pb.h")
set(rg_grpc_srcs "${CMAKE_CURRENT_BINARY_DIR}/route_guide.grpc.pb.cc")
set(rg_grpc_hdrs "${CMAKE_CURRENT_BINARY_DIR}/route_guide.grpc.pb.h")
add_custom_command(
OUTPUT "${rg_proto_srcs}" "${rg_proto_hdrs}" "${rg_grpc_srcs}" "${rg_grpc_hdrs}"
COMMAND ${_PROTOBUF_PROTOC}
ARGS --grpc_out "${CMAKE_CURRENT_BINARY_DIR}"
--cpp_out "${CMAKE_CURRENT_BINARY_DIR}"
-I "${rg_proto_path}"
--plugin=protoc-gen-grpc="${_GRPC_CPP_PLUGIN_EXECUTABLE}"
"${rg_proto}"
DEPENDS "${rg_proto}")
# Include generated *.pb.h files
include_directories("${CMAKE_CURRENT_BINARY_DIR}")
# rg_grpc_proto
add_library(rg_grpc_proto
${rg_grpc_srcs}
${rg_grpc_hdrs}
${rg_proto_srcs}
${rg_proto_hdrs})
target_link_libraries(rg_grpc_proto
absl::absl_log
${_REFLECTION}
${_GRPC_GRPCPP}
${_PROTOBUF_LIBPROTOBUF})
# Targets route_guide_(client|server)
foreach(_target route_guide_server route_guide_client route_guide_callback_server route_guide_callback_client)
add_executable(${_target}
"${_target}.cc" helper.cc)
target_link_libraries(${_target}
rg_grpc_proto
absl::flags_parse
absl::absl_log
absl::log_initialize
absl::log_globals
${_REFLECTION}
${_GRPC_GRPCPP}
${_PROTOBUF_LIBPROTOBUF})
endforeach()
references
https://protobuf.dev/programming-guides/proto3/
https://cloud.tencent.com/developer/article/2463651
https://grpc.io/docs/languages/cpp/basics/
https://github.com/grpc/grpc/tree/master/examples/cpp/route_guide