위 링크의 내용을 기반으로 작성했습니다.
이 문서는 proto2 버전을 사용합니다.
proto3 도 곧 다뤄보겠습니다. (아마도...)
Protocol Buffers 를 사용하는 방법은 간단합니다. 다음 방법만 따르면 됩니다.
- .proto file 에 message 구조를 정의하라
- protocol buffer compiler 를 이용하라
- 메시지를 읽고 쓰기 위해서 python protocol buffer api 를 이용하라
이전까지 서로 다른 플랫폼, 언어에서 데이터를 주고 받기 위해서는 이런 방법들을 썼었습니다.
- 간단한 방법은 여러 데이터를 하나의 문자열로 구분하는 것입니다. "chope:123-4567-8901:ABC:12" 이렇게 구분자로 합쳐서 encode, decode 하는 것입니다.
- XML 을 많이 이용했습니다. 이미 수많은 언어들로 라이브러리가 있어서 쉽게 접근할 수 있습니다. 하지만 공백을 많이 사용하고 encode/decode 포퍼먼스가 좋지 못합니다.
protocol buffer 는 이러한 문제를 해결하기 위한 유연하고 효과적이고 자동화된 설루션입니다.
compiler 는 효율적인 바이너리 포맷의 protocol buffer data 를 파싱하고 자동 encoding 을 구현한 클래스를 생성한다.
protocol buffer compiler 는 결과 바이너리 파일로 encode 하는 코드와 decode 하는 코드를 생성합니다. 그리고 그 값들을 가지고 있는 data type 또한 생성합니다.
생성된 class 는 각 필드의 getter, setter 를 제공합니다.
중요한 것은 포맷이 확장됨에도 여전히 예전 포맷으로부터 읽을 수 있다는 것입니다. 호환성이 좋다는 얘기겠네요.
Protocol buffers 를 사용하려면 가장 먼저 해야 할 것은 .proto 파일을 작성하는 것입니다.
proto 파일의 시작은 package 로 시작한다. package 는 namespace 를 지정할 수 있게 해 줍니다.
message 는 type 이 정해진 여러 필드의 모음입니다. struct 와 유사합니다.
bool, int32, float, double, string 등의 기본 type 이 사용 가능합니다.
다른 message 를 type 으로 사용 가능합니다.
message 는 nested 선언이 가능합니다.
enum type 도 지원합니다. 정의 시 지정하는 값은 tag 이자 유니크한 식별자로 바이너리 인코딩 때 사용합니다. tag 는 한 바이트 값으로 해야 합니다. 1-15
만약 한 바이트로 하지 않으면 최적화 관련 경고가 뜬다고 합니다.
공식 문서와 같이 아래와 같이 .proto 파일을 작성합니다.
syntax = "proto2";
package tutorial;
message Person {
optional string name = 1;
optional int32 id = 2;
optional string email = 3;
enum PhoneType {
MOBILE = 0;
HOME = 1;
WORK = 2;
}
message PhoneNumber {
optional string number = 1;
optional PhoneType type = 2 [default = HOME];
}
repeated PhoneNumber phones = 4;
}
message AddressBook {
repeated Person people = 1;
}
모든 필드는 3가지 수식어 중 하나를 표시해야 합니다.
optional
- 값이 있거나 없을 수 있습니다.
- 값이 설정되지 않으면 기본값을 사용하고 정의된 기본값이 없으면 시스템 기본값을 사용합니다.
- 숫자형은 0, 문자열은 빈 문자열, boolean 은 false
repeated
- 필드가 0번 이상 반복될 수 있습니다.
- 순서는 protocol buffer 에서 보존됩니다.
- repeated fields 는 동적 크기 배열입니다.
required
- 값이 필수로 존재해야 합니다. 그렇지 않으면 초기화되지 않고 exception 이 발생합니다.
required 는 조심해서 사용해야 합니다. required 로 사용하다가 optional 로 변경할 때 문제가 발생합니다. old reader 는 이러한 변화를 거부하거나 무시할 수 있습니다.
protocol buffer 가 아닌 application 에서 validation 을 추가하는 것을 고려하는 것이 좋습니다.
구글에서도 required 를 탐탁지 않게 여기며 대부분의 메시지를 optional, repeated 를 사용하고 proto3 에서는 require 를 지원하지 않습니다.
proto 파일을 작성하면 다음은 class 를 생성해야 합니다.
protoc -I=$SRC_DIR --python_out=$DST_DIR $SRC_DIR/data.proto
실행하면 다음과 같이 data_pb2.py 파일이 하나 생성됩니다.
data_pb2.py 파일을 열어 보면 다음과 같이 코드가 생성되어 있습니다.
# -*- coding: utf-8 -*-
# Generated by the protocol buffer compiler. DO NOT EDIT!
# source: data.proto
"""Generated protocol buffer code."""
from google.protobuf import descriptor as _descriptor
from google.protobuf import message as _message
from google.protobuf import reflection as _reflection
from google.protobuf import symbol_database as _symbol_database
# @@protoc_insertion_point(imports)
_sym_db = _symbol_database.Default()
DESCRIPTOR = _descriptor.FileDescriptor(
name='data.proto',
package='tutorial',
syntax='proto2',
serialized_options=None,
create_key=_descriptor._internal_create_key,
serialized_pb=b'\n\ndata.proto\x12\x08tutorial\"\xdb\x01\n\x06Person\x12\x0c\n\x04name\x18\x01 \x01(\t\x12\n\n\x02id\x18\x02 \x01(\x05\x12\r\n\x05\x65mail\x18\x03 \x01(\t\x12,\n\x06phones\x18\x04 \x03(\x0b\x32\x1c.tutorial.Person.PhoneNumber\x1aM\n\x0bPhoneNumber\x12\x0e\n\x06number\x18\x01 \x01(\t\x12.\n\x04type\x18\x02 \x01(\x0e\x32\x1a.tutorial.Person.PhoneType:\x04HOME\"+\n\tPhoneType\x12\n\n\x06MOBILE\x10\x00\x12\x08\n\x04HOME\x10\x01\x12\x08\n\x04WORK\x10\x02\"/\n\x0b\x41\x64\x64ressBook\x12 \n\x06people\x18\x01 \x03(\x0b\x32\x10.tutorial.Person'
)
_PERSON_PHONETYPE = _descriptor.EnumDescriptor(
name='PhoneType',
full_name='tutorial.Person.PhoneType',
filename=None,
file=DESCRIPTOR,
create_key=_descriptor._internal_create_key,
values=[
_descriptor.EnumValueDescriptor(
name='MOBILE', index=0, number=0,
serialized_options=None,
type=None,
create_key=_descriptor._internal_create_key),
_descriptor.EnumValueDescriptor(
name='HOME', index=1, number=1,
serialized_options=None,
type=None,
create_key=_descriptor._internal_create_key),
_descriptor.EnumValueDescriptor(
name='WORK', index=2, number=2,
serialized_options=None,
type=None,
create_key=_descriptor._internal_create_key),
],
containing_type=None,
serialized_options=None,
serialized_start=201,
serialized_end=244,
)
_sym_db.RegisterEnumDescriptor(_PERSON_PHONETYPE)
_PERSON_PHONENUMBER = _descriptor.Descriptor(
name='PhoneNumber',
full_name='tutorial.Person.PhoneNumber',
filename=None,
file=DESCRIPTOR,
containing_type=None,
create_key=_descriptor._internal_create_key,
fields=[
_descriptor.FieldDescriptor(
name='number', full_name='tutorial.Person.PhoneNumber.number', index=0,
number=1, type=9, cpp_type=9, label=1,
has_default_value=False, default_value=b"".decode('utf-8'),
message_type=None, enum_type=None, containing_type=None,
is_extension=False, extension_scope=None,
serialized_options=None, file=DESCRIPTOR, create_key=_descriptor._internal_create_key),
_descriptor.FieldDescriptor(
name='type', full_name='tutorial.Person.PhoneNumber.type', index=1,
number=2, type=14, cpp_type=8, label=1,
has_default_value=True, default_value=1,
message_type=None, enum_type=None, containing_type=None,
is_extension=False, extension_scope=None,
serialized_options=None, file=DESCRIPTOR, create_key=_descriptor._internal_create_key),
],
extensions=[
],
nested_types=[],
enum_types=[
],
serialized_options=None,
is_extendable=False,
syntax='proto2',
extension_ranges=[],
oneofs=[
],
serialized_start=122,
serialized_end=199,
)
_PERSON = _descriptor.Descriptor(
name='Person',
full_name='tutorial.Person',
filename=None,
file=DESCRIPTOR,
containing_type=None,
create_key=_descriptor._internal_create_key,
fields=[
_descriptor.FieldDescriptor(
name='name', full_name='tutorial.Person.name', index=0,
number=1, type=9, cpp_type=9, label=1,
has_default_value=False, default_value=b"".decode('utf-8'),
message_type=None, enum_type=None, containing_type=None,
is_extension=False, extension_scope=None,
serialized_options=None, file=DESCRIPTOR, create_key=_descriptor._internal_create_key),
_descriptor.FieldDescriptor(
name='id', full_name='tutorial.Person.id', index=1,
number=2, type=5, cpp_type=1, label=1,
has_default_value=False, default_value=0,
message_type=None, enum_type=None, containing_type=None,
is_extension=False, extension_scope=None,
serialized_options=None, file=DESCRIPTOR, create_key=_descriptor._internal_create_key),
_descriptor.FieldDescriptor(
name='email', full_name='tutorial.Person.email', index=2,
number=3, type=9, cpp_type=9, label=1,
has_default_value=False, default_value=b"".decode('utf-8'),
message_type=None, enum_type=None, containing_type=None,
is_extension=False, extension_scope=None,
serialized_options=None, file=DESCRIPTOR, create_key=_descriptor._internal_create_key),
_descriptor.FieldDescriptor(
name='phones', full_name='tutorial.Person.phones', index=3,
number=4, type=11, cpp_type=10, label=3,
has_default_value=False, default_value=[],
message_type=None, enum_type=None, containing_type=None,
is_extension=False, extension_scope=None,
serialized_options=None, file=DESCRIPTOR, create_key=_descriptor._internal_create_key),
],
extensions=[
],
nested_types=[_PERSON_PHONENUMBER, ],
enum_types=[
_PERSON_PHONETYPE,
],
serialized_options=None,
is_extendable=False,
syntax='proto2',
extension_ranges=[],
oneofs=[
],
serialized_start=25,
serialized_end=244,
)
_ADDRESSBOOK = _descriptor.Descriptor(
name='AddressBook',
full_name='tutorial.AddressBook',
filename=None,
file=DESCRIPTOR,
containing_type=None,
create_key=_descriptor._internal_create_key,
fields=[
_descriptor.FieldDescriptor(
name='people', full_name='tutorial.AddressBook.people', index=0,
number=1, type=11, cpp_type=10, label=3,
has_default_value=False, default_value=[],
message_type=None, enum_type=None, containing_type=None,
is_extension=False, extension_scope=None,
serialized_options=None, file=DESCRIPTOR, create_key=_descriptor._internal_create_key),
],
extensions=[
],
nested_types=[],
enum_types=[
],
serialized_options=None,
is_extendable=False,
syntax='proto2',
extension_ranges=[],
oneofs=[
],
serialized_start=246,
serialized_end=293,
)
_PERSON_PHONENUMBER.fields_by_name['type'].enum_type = _PERSON_PHONETYPE
_PERSON_PHONENUMBER.containing_type = _PERSON
_PERSON.fields_by_name['phones'].message_type = _PERSON_PHONENUMBER
_PERSON_PHONETYPE.containing_type = _PERSON
_ADDRESSBOOK.fields_by_name['people'].message_type = _PERSON
DESCRIPTOR.message_types_by_name['Person'] = _PERSON
DESCRIPTOR.message_types_by_name['AddressBook'] = _ADDRESSBOOK
_sym_db.RegisterFileDescriptor(DESCRIPTOR)
Person = _reflection.GeneratedProtocolMessageType('Person', (_message.Message,), {
'PhoneNumber' : _reflection.GeneratedProtocolMessageType('PhoneNumber', (_message.Message,), {
'DESCRIPTOR' : _PERSON_PHONENUMBER,
'__module__' : 'data_pb2'
# @@protoc_insertion_point(class_scope:tutorial.Person.PhoneNumber)
})
,
'DESCRIPTOR' : _PERSON,
'__module__' : 'data_pb2'
# @@protoc_insertion_point(class_scope:tutorial.Person)
})
_sym_db.RegisterMessage(Person)
_sym_db.RegisterMessage(Person.PhoneNumber)
AddressBook = _reflection.GeneratedProtocolMessageType('AddressBook', (_message.Message,), {
'DESCRIPTOR' : _ADDRESSBOOK,
'__module__' : 'data_pb2'
# @@protoc_insertion_point(class_scope:tutorial.AddressBook)
})
_sym_db.RegisterMessage(AddressBook)
# @@protoc_insertion_point(module_scope)
Python 에 익숙하지 않은 저는 자세히 이해하기가 어려웠습니다.
역시 그냥 이용 방법이나 알아봐야겠네요.
Write 하는 방법입니다.
공식 문서는 python2 여서 python3 로 변경했습니다.
#! /Users/chope/.pyenv/shims/python
import addressbook_pb2
import sys
# This function fills in a Person message based on user input.
def PromptForAddress(person):
person.id = int(input("Enter person ID number: "))
person.name = input("Enter name: ")
email = input("Enter email address (blank for none): ")
if email != "":
person.email = email
while True:
number = input("Enter a phone number (or leave blank to finish): ")
if number == "":
break
phone_number = person.phones.add()
phone_number.number = number
type = input("Is this a mobile, home, or work phone? ")
if type == "mobile":
phone_number.type = addressbook_pb2.Person.PhoneType.MOBILE
elif type == "home":
phone_number.type = addressbook_pb2.Person.PhoneType.HOME
elif type == "work":
phone_number.type = addressbook_pb2.Person.PhoneType.WORK
else:
print("Unknown phone type; leaving as default value.")
# Main procedure: Reads the entire address book from a file,
# adds one person based on user input, then writes it back out to the same
# file.
if len(sys.argv) != 2:
print("Usage:", sys.argv[0], "ADDRESS_BOOK_FILE")
sys.exit(-1)
address_book = addressbook_pb2.AddressBook()
# Read the existing address book.
try:
f = open(sys.argv[1], "rb")
address_book.ParseFromString(f.read())
f.close()
except IOError:
print(sys.argv[1] + ": Could not open file. Creating a new one.")
# Add an address.
PromptForAddress(address_book.people.add())
# Write the new address book back to disk.
f = open(sys.argv[1], "wb")
f.write(address_book.SerializeToString())
f.close()
실제 동작해서 2개의 전화번호를 입력했습니다.
위 결과를 저장한 address_book 을 vscode 로 열어 보았습니다.
text file 이 아니어서 경고가 한번 뜨네요.
그래도 열어 보자를 선택해 보면 아래와 같습니다.
저장한 파일을 읽기 위해 문서에 나온 예제를 이용합니다.
#! /Users/chope/.pyenv/shims/python
import addressbook_pb2
import sys
# Iterates though all people in the AddressBook and prints info about them.
def ListPeople(address_book):
for person in address_book.people:
print("Person ID:", person.id)
print(" Name:", person.name)
if person.HasField('email'):
print(" E-mail address:", person.email)
for phone_number in person.phones:
if phone_number.type == addressbook_pb2.Person.PhoneType.MOBILE:
print(" Mobile phone #: ", end='')
elif phone_number.type == addressbook_pb2.Person.PhoneType.HOME:
print(" Home phone #: ", end='')
elif phone_number.type == addressbook_pb2.Person.PhoneType.WORK:
print(" Work phone #: ", end='')
print(phone_number.number)
# Main procedure: Reads the entire address book from a file and prints all
# the information inside.
if len(sys.argv) != 2:
print("Usage:", sys.argv[0], "ADDRESS_BOOK_FILE")
sys.exit(-1)
address_book = addressbook_pb2.AddressBook()
# Read the existing address book.
f = open(sys.argv[1], "rb")
address_book.ParseFromString(f.read())
f.close()
ListPeople(address_book)
여러 데이터가 나오는 것을 보기 위해서 위에서 언급하지 않은 데이터를 3개 더 추가하고 실행했습니다.
정말 쉽게 serialization/deserialization 이 가능하다.
python 으로 생성된 코드는 java, c++ 과 조금 다르다고 합니다. 자세히 알고 싶은 분은 공식문서를 더 참고하세요.
'Programming' 카테고리의 다른 글
Protocol Buffers - protobuf 는 무엇인가? (1/3) (0) | 2020.12.01 |
---|---|
무료 SSL 인증서 발급 적용 (0) | 2020.11.27 |
GitHub Free vs Pro (0) | 2020.07.10 |
[Perfect] Dependency 추가 (2) | 2016.10.18 |
[Perfect] 프로젝트 생성하기 (1) | 2016.10.15 |