Protocol Buffers는 구조화 된 데이터의 직렬화를위한 언어 중립적, 플랫폼 중립적, 확장 가능한 메커니즘을 제공하며 Google에서 개발 한 BSD 3-Clause 라이선스 오픈 소스 프로젝트이다.
Protobuf에서 사용하는 중립 언어를 사용하면 .proto 파일을 통해 구조화 된 형식으로 메시지를 모델링 할 수 있다.
.proto 파일에 구조화된 데이터를 정의하고 프로토콜 버퍼 컴파일러(protoc)를 통해 .proto 파일을 다양한 언어(C++, Java등)들에서 사용할 수 있는 소스코드(클래스)를 사용하여 데이터 스트림의 자동 인코딩 및 구문 분석을 처리해 주므로 구조화된 데이터를 쉽게 읽고 쓸 수 있다.
생성 된 클래스는 프로토콜 버퍼를 구성하는 필드에 대해 getter 및 setter를 제공하고 프로토콜 버퍼를 하나의 단위로 읽고 쓰는 세부 사항을 처리한다.
이 글은 C++언어 및 python으로 conan package manager를 사용하여 protobuf를 사용해 본것을 정리한 것이다.
프로토콜 포맷 정의
프로토콜 버퍼 언어의 구문은 proto2 버전과 proto3 버전이 있다. 다음은 구조화된 센서 정보를 정의하는 sensor.proto 파일이다.
syntax = "proto2";
message Sensor {
required string name = 1;
required double temperature = 2;
required int32 humidity = 3;
enum SwitchLevel {
CLOSED = 0;
OPEN = 1;
}
required SwitchLevel door = 4;
}
.proto 파일은 message 정의가 있다. message는 유형이 지정된 필드 집합을 포함하는 구조체와 같다. bool, int32, float, double 및 string을 포함한 많은 표준 데이터 유형을 필드 유형으로 사용할 수 있다. 필드 중 하나에 미리 정의 된 열거형 유형을 정의 할 수도 있다. 예제 에서는 door가 SwitchLevel 유형을 사용하도록 지정한다.
각 요소의 "= 1", "= 2"마커는 필드가 이진 인코딩에서 사용하는 고유 한 "태그"로 사용된다. 태그 번호 1-15는 인코딩하는 데 더 적은 바이트가 필요하므로 최적화를 위해 일반적으로 사용되거나 반복되는 요소에 대해 부여할 수 있으며 자주 사용되지 않는 요소에는 태그 16 이상을 사용한다.
각 필드는 다음 예약어 중 하나로 정의해야 한다.
optional: 이 필드는 설정되거나 설정되지 않을 수 있다. 선택적 필드 값이 설정되지 않은 경우 기본 값이 사용된다. 간단한 유형의 경우 예제에서 전화 번호 유형에 대해 수행 한 것처럼 고유 한 기본 값을 지정할 수 있다. 그렇지 않으면 시스템 기본 값이 사용된다.(숫자 유형의 경우 0, 문자열의 경우 빈 문자열, 부울의 경우 false이다.) embedded messages 의 경우 기본 값은 항상 해당 필드가 설정되지 않은 메시지의 "기본 인스턴스"또는 "프로토 타입"이다. 접근자를 호출하여 명시 적으로 설정되지 않은 선택적 (또는 필수) 필드의 값을 가져 오면 항상 해당 필드의 기본 값이 반환 된다.
repeated: 필드는 여러 번 반복 될 수 있음을 나타낸다 (0 포함). 반복되는 값의 순서는 프로토콜 버퍼에 보존된다. 반복 필드를 동적 크기의 배열이라고 생각하자.
required: 필드 값을 제공해야 한다. 그렇지 않으면 메시지가 "초기화되지 않은"것으로 간주된다. libprotobuf 가 디버그 모드에서 컴파일 된 경우 초기화되지 않은 메시지를 직렬화하면 assertion 오류가 발생한다. 최적화 된 빌드에서는 검사를 건너 뛰고 메시지가 작성될 수 있지만 초기화되지 않은 메시지를 구문 분석하는 것은 항상 실패다 (parse 메서드에서 false를 반환함으로써). 이 외에 필수 필드는 선택 필드와 똑같이 작동한다.
required 필드의 경우 몇 가지 문제로 인하여 Google 에서는 권장하지 않으며 proto2 구문에 정의 된 대부분의 메시지는 optional 및 repeated 만 사용한다. (Proto3는 required 필드를 지원조차 하지 않는다.)
프로토콜 버퍼 컴파일
이제 .proto가 있으므로 다음으로 해야 할 일은 메시지를 읽고 쓰는 데 필요한 클래스를 생성하는 것이다. 프로토콜 버퍼 컴파일러 protoc을 실행해야 한다.
우선 conan을 통해 필요한 패키지를 설치하려면 conanfile.txt 을 작성해야한다. protobuf 패키지를 정의한다.
[build_requires]
protoc_installer/3.6.1@bincrafters/stable
[requires]
protobuf/3.6.1@bincrafters/stable
[generators]
cmake
conan 패키지 매니저와 cmake를 연동하여 빌드할 수 있으므로 CMakeLists.txt파일에 protoc 컴파일 명령을 실행할 수 있다. 다음 CMakeLists.txt 는 .proto 파일을 컴파일 하도록 하는 방법을 보여준다.
cmake_minimum_required(VERSION 3.1.2)
project(sensor CXX)
set(CMAKE_VERBOSE_MAKEFILE ON)
include(${CMAKE_BINARY_DIR}/conanbuildinfo.cmake)
conan_basic_setup(TARGETS)
find_package(Protobuf REQUIRED)
protobuf_generate_cpp(PROTO_SRCS PROTO_HDRS sensor.proto)
protobuf_generate_python(PROTO_PYS sensor.proto)
add_executable(${PROJECT_NAME} main.cc ${PROTO_SRCS} ${PROTO_HDRS})
target_link_libraries(${PROJECT_NAME} PUBLIC CONAN_PKG::protobuf)
target_include_directories(${PROJECT_NAME} PRIVATE ${CMAKE_BINARY_DIR})
set_target_properties(${PROJECT_NAME} PROPERTIES CXX_STANDARD 11)
add_custom_target(proto_python ALL DEPENDS ${PROTO_PYS})
protobuf_generate_cpp 와 protobuf_generate_python 는 C++와 Python에서 사용할 수 있는 파일을 생성하도록 하는 것이다.
직렬화 및 파싱
각 프로토콜 버퍼 클래스에는 프로토콜 버퍼 바이너리 형식을 사용하여 선택한 유형의 메시지를 읽고 쓰는 메서드가 있다. 다음의 매소드들을 포함하고 있다.
bool SerializeToString(string* output) const; : 메시지를 직렬화하고 지정된 문자열에 바이트를 저장한다. 저장된 데이터들은 텍스트가 아니라 바이너리이다. string 클래스를 편리한 컨테이너 용도로 사용할 뿐이다.
bool ParseFromString(const string& data); : 주어진 string으로부터 메시지를 파싱한다.
bool SerializeToOstream(ostream* output) const; : 주어진 C++ ostream 에 메시지를 쓴다.
bool ParseFromIstream(istream* input); : 주어진 C++ istream에서 메시지를 파싱한다.
다음은 생성된 파일(sensor.pb.h)을 사용하는 C++ 예제 소스코드이다. SerializeToOstream 을 사용하여 데이터를 직렬화하여 파일에 쓰도록 되어있다.
#include <fstream>
#include "sensor.pb.h"
int main() {
Sensor sensor;
sensor.set_name("Laboratory");
sensor.set_temperature(23.4);
sensor.set_humidity(68);
sensor.set_door(Sensor_SwitchLevel_OPEN);
std::ofstream ofs("sensor.data", std::ios_base::out | std::ios_base::binary);
sensor.SerializeToOstream(&ofs);
}
다음의 Python 코드는 직렬화 되어 저장된 파일(sensor.data)을 열어서 필드의 값을 출력하도록한다.
import os
from build.sensor_pb2 import Sensor
if __name__ == "__main__":
with open("sensor.data", 'rb') as file:
content = file.read()
print("Retrieve Sensor object from sensor.data")
sensor = Sensor()
sensor.ParseFromString(content)
door = "Open" if sensor.door else "Closed"
print("Sensor name: {}".format(sensor.name))
print("Sensor temperature: {}".format(sensor.temperature))
print("Sensor humidity: {}".format(sensor.humidity))
print("Sensor door: {}".format(door))
빌드 및 실행
mkdir build
cd build
pip install protobuf
conan install .. —build=missing
cmake .. -G"MinGW Makefiles"
cmake --build . --config Release
예제 소스코드는 다음 위치에서 찾을 수 있다:
https://github.com/conan-io/examples/tree/master/libraries/protobuf/serialization