[C++] Protobuf (Google Protocol Buffer) 라이브러리
Protobuf
JSON과 같은 메시지 직렬화/역직렬화 라이브러리로 이진 데이터 직렬화로 효율적인 메모리 사용하고 빠른 메시지 파싱 및 다중 언어를 지원한다는 특징이 있다.
메시지의 프로토콜은 .proto 파일로 정의하여 protoc라는 실행 파일로 (cpp의 경우) .pb.h / pb.cc를 자동 생성한다.
message Person {
optional string name = 1;
optional int32 id = 2;
optional string email = 3;
}
Protobuf의 직렬화 원리
다음 글을 읽어보면 좋다. 기본적으로 Key-Value 쌍으로 이루어진 이진 데이터로 인코딩 되는데
https://medium.com/naver-cloud-platform/nbp-%EA%B8%B0%EC%88%A0-%EA%B2%BD%ED%97%98-%EC%8B%9C%EB%8C%80%EC%9D%98-%ED%9D%90%EB%A6%84-grpc-%EA%B9%8A%EA%B2%8C-%ED%8C%8C%EA%B3%A0%EB%93%A4%EA%B8%B0-2-b01d390a7190
- Key (1 바이트) = (MSB 1bit) | (Field Number 3bit) | (WireType 4bit) 으로 이루어져서 Field Number는 1~15의 값만 사용 가능하다. WireType은 데이터 타입에 따라 결정되는 값이다.
- Value의 경우 7비트씩 나누어 8비트를 만들고 byte를 역순으로 재배열한 다음 MSB 비트를 설정한다.
프로토콜 버퍼 사용 방식
1. .proto file을 정의한다.
2. protoc 컴파일러로 원하는 언어에서 사용가능한 메시지 정의 파일을 만들어낸다.
3. 자동 생성된 pb를 자신의 프로젝트에 포함하여 사용한다.
4. pb '메시지 클래스' 들을 사용하여 정의된 메시지를 직렬화/역직렬화할 수 있다.
.proto에서 사용되는 메시지 syntax
메시지 필드에서 자주 사용되는 키워드
- singular : 없거나 하나의 값만 갖는 필드, proto3에서는 필드 키워드가 없다면 기본으로 적용된다.
- optional : singular와 같은 의미이나 값이 실제로 설정되어 있는 지 확인 가능하다.
- repeated : 배열과 같은 타입으로 없거나 하나 이상의 값을 가질 수 있다. 데이터의 순서는 보존된다.
- map : 키와 값의 쌍으로 이루어지는 필드이다.
라이브러리 설치 및 적용
-> protobuffer 가이드
https://developers.google.com/protocol-buffers
-> Download and Install
https://github.com/protocolbuffers/protobuf#protocol-compiler-installation
https://github.com/protocolbuffers/protobuf/blob/main/src/README.md
CMake로 정적 빌드하려 하였으나 최선 버전에서 정적 라이브러리 링크를 하게되면 abseil 링커 에러가 발생하여 vcpkg를 이용하여 동적 라이브러리 빌드 및 활용을 한다.
자세한 건 다음 지시 사항을 따른다.
- 추가 포함 헤더에 include 폴더 추가
- 동적 라이브러리 링크 (bin 및 lib 폴더에 존재)
.proto 파일에서 pb 파일 추출
protoc.exe 사용법 (C++)
protoc -I=$SRC_DIR --cpp_out=$DST_DIR $SRC_DIR/addressbook.proto
- 첫번째 인자 : -I $SRC_DIR (.proto 파일이 있는 폴더)
- 두번째 인자 : --cpp_out $DST_DIR (pb 파일들을 생성할 폴더)
- 세번째 인자 : .proto (대상 .proto 파일)
protoc 실행 파일이 있는 경로로 부터 상대 경로를 사용해야한다.
addressbook.proto
syntax = "proto3";
package tutorial;
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;
}
// Our address book file is just one of these.
message AddressBook {
repeated Person people = 1;
}
메시지 생성 결과
사용 예시 코드
vcpkg로 빌드된 라이브러리를 링크하고
생성된 pb 파일들을 직접 프로젝트에 추가시켜서 사용한다.
#include <iostream>
#include <fstream>
#include <string>
#include "addressbook.pb.h"
using namespace std;
// This function fills in a Person message based on user input.
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());
cout << "Enter email address (blank for none): ";
string email;
getline(cin, email);
if (!email.empty()) {
person->set_email(email);
}
while (true) {
cout << "Enter a phone number (or leave blank to finish): ";
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::MOBILE);
}
else if (type == "home") {
phone_number->set_type(tutorial::Person::HOME);
}
else if (type == "work") {
phone_number->set_type(tutorial::Person::WORK);
}
else {
cout << "Unknown phone type. Using default." << endl;
}
}
}
// Main function: Reads the entire address book from a file,
// adds one person based on user input, then writes it back out to the same
// file.
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;
{
// Read the existing address book.
fstream input(argv[1], ios::in | ios::binary);
if (!input) {
cout << argv[1] << ": File not found. Creating a new file." << endl;
}
else if (!address_book.ParseFromIstream(&input)) {
cerr << "Failed to parse address book." << endl;
return -1;
}
}
// Add an address.
PromptForAddress(address_book.add_people());
{
// Write the new address book back to disk.
fstream output(argv[1], ios::out | ios::trunc | ios::binary);
if (!address_book.SerializeToOstream(&output)) {
cerr << "Failed to write address book." << endl;
return -1;
}
}
// Optional: Delete all global objects allocated by libprotobuf.
google::protobuf::ShutdownProtobufLibrary();
return 0;
}
다음 가이드에서 참고 가능하다.
https://developers.google.com/protocol-buffers/docs/cpptutorial