ABOUT ME

-

Today
-
Yesterday
-
Total
-
  • [Protocol Buffer] 프로토콜 버퍼 소개 및 Go에서의 사용법
    OpenSource 2017. 5. 18. 15:14


    Google Protocol Buffer

    프로토콜 버퍼는 구글에서 내놓은 오픈소스 직렬화 라이브러리이다. 메시지를 연속된 비트로 만들고(직렬화-Serialize) 반대로 비트들에서 원래의 메시지로 만들어 낼 수 있다. (역직렬화-Deserialize) 네트워크 통신과 데이터 저장에 사용된다. 데이터 구조(Message)와 서비스를 Proto 스키마 파일에 저장하고,  다양한 언어(Java, C++, Python, Ruby, C, Go, Erlang, Javascript, Lua, Perl, PHP, R, Rust, Scala, Swift )로 인/디코딩하여 멀티 플랫폼 대응을 가능하게 한다. 즉, Big endian과 Little endian을 사용하는 언어사이에 이기종간 통신을 할 수 있다. 웹 애플리케이션에서는 보통 JSON을 사용한다. 네이티브 클라이언트라면 JSON 보다 Protobuf가 데이터의 크기를 줄이고 전반적인 성능 향상을 기대할 수 있다. 특히 전력, 네트워크 컴퓨팅 파워가 부족한 임베디드 기기의 경우 도움이 된다. 유사한 기술로는 Apache thrift와 Avro가 있다.


    JSON, XML, Protocol buf에 대한 비교

    JSON
    • 인간이 읽고 쓸 수 있다. : 개발과 디버깅이 편하다.
    • 스키마에 대한 정보 없이, 사용할 수 있다.
    • 웹 브라우저에서 사용하기 좋다.
    • XML에 비해서 가볍다.
    • 딱히 표준이라 할 만한 것들이 없다.
    XML
    • 인간이 읽고 쓸 수 있다. : JSON 보다는 복잡하긴 하지만..
    • 스키마에 대한 정보 없이, 사용할 수 있다.
    • SOAP등의 표준이 있다.
    • xsd, xslt, sax, dom 과 같은 좋은 툴들이 많다.
    • JSON에 비해서 좀 장황하다.
    Protocol Buffer
    • 데이터 압축률이 좋다.
    • 정보를 얻기 위해서는 스키마에 대한 정확한 지식이 필요하다.
    • 처리속도가 빠르다.
    • 이진 데이터라서 사람의 눈으로 확인할 수 없다.


    Protocol Buffer Language (Proto2)

    (google의 language guide : https://developers.google.com/protocol-buffers/docs/proto)

    : 프로토콜 버퍼는 Proto파일에 메세지 타입을 정의함으로써 직렬화할 데이터의 구조를 지정한다.

    protocol buffer 언어를 사용하여 확장자가 " .proto "인 Proto 파일(데이터 스키마)을 작성한다.


    • Message : 하나 이상의 필드, enum, nested message로 구성
    • Field : 고유 번호, 이름, 데이터 타입을 가지며, 한정자 조건(required, optional, repeated) 명시

    -       required : 1개만 존재 가능
    -       optional : 0개 혹은 1개 존재 가능
    -       repeated : 여러 개 존재 가능 (0개 포함)


    고유번호

    : 고유번호는 각각의 {}로 묶여진 범위내에서 고유한 번호를 가져야 하며, 1~15까지는 1바이트를 사용하기 때문에 최적화를 위해서는 required, repeated 필드는 1~15의 번호를 사용하고, 16이상 부터 optional 필드에게 할당하는 것이 좋다.





    Golang에서 사용하기 (Proto2)


    필드 데이터 타입 (Scalar Value Types)

    .proto Type Notes Go Type
    double *float64
    float *float32
    int32 Uses variable-length encoding. Inefficient for encoding negative numbers – if your field is likely to have negative values, use sint32 instead. *int32
    int64 Uses variable-length encoding. Inefficient for encoding negative numbers – if your field is likely to have negative values, use sint64 instead. *int64
    uint32 Uses variable-length encoding. *uint32
    uint64 Uses variable-length encoding. *uint64
    sint32 Uses variable-length encoding. Signed int value. These more efficiently encode negative numbers than regular int32s. *int32
    sint64 Uses variable-length encoding. Signed int value. These more efficiently encode negative numbers than regular int64s. *int64
    fixed32 Always four bytes. More efficient than uint32 if values are often greater than 228. *uint32
    fixed64 Always eight bytes. More efficient than uint64 if values are often greater than 256. *uint64
    sfixed32 Always four bytes. *int32
    sfixed64 Always eight bytes. *int64
    bool *bool
    string A string must always contain UTF-8 encoded or 7-bit ASCII text. *string
    bytes May contain any arbitrary sequence of bytes. []byte




    컴파일링


    1. C++로 구현된 Protocol Buffer 컴파일러 다운로드 -> "src/README.md"에 따라서 설치

    (https://github.com/google/protobuf/releases)

    $ https://github.com/google/protobuf/releases/download/v3.3.0/protobuf-cpp-3.3.0.tar.gz

    $ tar xvf protobuf-cpp-3.3.0.tar.gz

    $ cd protobuf-3.3.0

        $ ./configure
        $ make
        $ make check
        $ sudo make install
        $ sudo ldconfig


    2. Go protocol buffer 플러그인 설치 

    $ go get -u github.com/golang/protobuf/protoc-gen-go


    3. 컴파일링

    $ protoc -I=$SRC_DIR --go_out=$DST_DIR $SRC_DIR/[.proto 파일 이름]





    Go API

    addressbook.proto

    package tutorial;

    message Person {
      required
    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;
    }


    위와 같은 proto 파일을 컴파일 했을때, (protoc -I=. --go_out=. addressbook.proto) 

    go 코드가 생성된다. 


     addressbook.pb.go

     ....

    type Person_PhoneType int32

    const (
            Person_MOBILE Person_PhoneType = 0
            Person_HOME   Person_PhoneType = 1
            Person_WORK   Person_PhoneType = 2
    )

    var Person_PhoneType_name = map[int32]string{
            0: "MOBILE",
            1: "HOME",
            2: "WORK",
    }
    var Person_PhoneType_value = map[string]int32{
            "MOBILE": 0,
            "HOME":   1,
            "WORK":   2,
    }

    ....

    type Person struct {

            Name             *string               `protobuf:"bytes,1,req,name=name" json:"name,omitempty"`
            Id               *int32                `protobuf:"varint,2,req,name=id" json:"id,omitempty"`
            Email            *string               `protobuf:"bytes,3,opt,name=email" json:"email,omitempty"`
            Phone            []*Person_PhoneNumber `protobuf:"bytes,4,rep,name=phone" json:"phone,omitempty"`
            XXX_unrecognized []byte                `json:"-"`
    }

    .....


    위와 같이 생성된 golang 코드를 바탕으로 인스턴스 생성, 직렬화, 역직렬화 하기


    example.go

    package main

    import (
            pb "beji/proto2/tutorial"
            "fmt"
            "io/ioutil"
            "log"

            "github.com/golang/protobuf/proto"
    )

    func NewPerson(id *int32, name, email, num *string) *pb.Person {

            mobile := pb.Person_MOBILE

            p := &pb.Person{
                    Id:    id,
                    Name:  name,
                    Email: email,
                    Phone: []*pb.Person_PhoneNumber{
                            {Number: num, Type: &mobile},
                    },
            }
            return p
    }

    func Write(ab *pb.AddressBook, fname string) {

            out, err := proto.Marshal(ab)
            if err != nil {
                    log.Fatalln("Failed to encode address book:", err)
            }

            if err := ioutil.WriteFile(fname, out, 0633); err != nil {
                    log.Fatalln("Failed to write address book:", err)
            }
    }
    func Read(fname string) *pb.AddressBook {
            in, err := ioutil.ReadFile(fname)
            if err != nil {
                    log.Fatalln("Error reading file:", err)
            }

            book := &pb.AddressBook{}
            if err := proto.Unmarshal(in, book); err != nil {
                    log.Fatalln("Failed to parse address book:", err)
            }

            return book
    }

    func main() {

            id := int32(1234)
            name := "beji"
            email := "beji@example.com"
            number := "010-123-4567"

            book := &pb.AddressBook{
                    Person: []*pb.Person{
                            NewPerson(&id, &name, &email, &number),
                    },
            }

            fmt.Println("book:", book)
            Write(book, "output.txt")

            book2 := Read("output.txt")
            fmt.Println("book2:", book2.String())
            fmt.Println("book2 Person Email:", book2.GetPerson()[0].GetName())
            fmt.Println("book2 Person Phone:", book2.GetPerson()[0].GetPhone())
    }


    output : 

    book: person:<name:"beji" id:1234 email:"beji@example.com" phone:<number:"010-123-4567" type:MOBILE > >
    book2: person:<name:"beji" id:1234 email:"beji@example.com" phone:<number:"010-123-4567" type:MOBILE > >
    book2 Person Email: beji
    book2 Person Phone: [number:"010-123-4567" type:MOBILE ]






    proto2 버전이 필요해서 이 버전으로 공부를 해봤는데,
    이 예제에서는 proto3 버전이 사용하기 쉬웠을 것이다. 데이터 타입이 포인터가 아니기 때문에 !


Designed by Tistory.