본문 바로가기

Android

protocol buffers

http://aploit.egloos.com/5233561


 

직렬화란 메모리안의 구조적인 데이타를 IO를 통해 타 프로세스로 보내거나 저장하기 위해 연속된 bit로 만드는 과정이다. c의 구조체나 클래스의 인스턴스와 같이 구조적인 혹은 계층적인 데이타를 iO를 통해 내보내려면 한줄로 쭉 정렬해야 합니다. 이런 한 줄로 쭉 세우는 과정이 직렬화이고, 반대의 작업이 역직렬화이다. 외부의 프로세스와 통신을 하거나 데이타를 파일로 저장을 한다고 하려면 어떤 방식이건 간에 직렬화를 해야 한다. 소켓 통신에서 메시지의 전문 포멧을 정하는 것이나, xml로 변환하거나, java의 Serializable이나, c의 structure 메모리 매핑을 그대로 보내는 것들 전부가 직렬화 방법들이다. 각 방법마다 장단점이 있다. 속도, 다른 OS간 통신, 다른 언어간 통신, 직렬화된 데이타의 사이즈, 개발 생산성, 개발 편의성, 유지보수성 정도가 고려되는 항목이다. xml의 경우 언어에 관계없고, 프로세스가 동작하는 OS에도 관계없지만, 직렬화/역직렬화의 속도가 떨어지고 직렬화된 데이터 사이즈가 크다는 단점이 있다. 데이타 전문의 경우 속도는 빠르지만 직렬화/역직렬화 구현을 전부 손으로 해야하기 때문에 개발편의성이 좋지 않고, 유지보수가 쉽지 않고, 버그를 찾기가 어려운 단점이 있다. c structure의 메모리 매핑은 메모리의 내용을 그대로 사용하는 것인데, OS만 달라도 사용이 불가하고, 물론 타 언어간의 통신은 불가하다. 대신 별도의 직렬화작업이 없는 이유로 속도는 가장 빠르다.

Protocol Buffers는 구글에서 사용하는 직렬화 방법이다. 구글 내에 많은 시스템이 있고, 당연히 다양한 OS들이 있고, 다양한 언어가 사용되는 환경에서 서로 통신하기 위한 직렬화 방법으로 개발한 것을 공개한 것이다. 라이센스는 new BSD이다. c structure 정의와 비슷한 형태로 데이타 구조를 정의하고, 컴파일러를 사용하여 c++과 java의 소스코드를 자동생성한다. 어플이케이션에서는 이 코드를 사용하여 값을 설정하고 직렬화하고 조회한다. 다음과 같은 장점을 가지고 있다.

    - 속도 빠르고(xml에 비해 10~100배)
    - 데이타 사이즈 작고(xml에 비해 3~10배)
    - 코드 더 간단하고
    - 개발 쉽고


Subtype.proto 파일에 다음과 같이 메시지를 정의하고

package Subtype; 
message Root {
    optional sint64 id = 1;
    optional string name = 2;
    optional Some some = 3;
}
message Some {
    optional sint64 someId = 1;
    optional string someName = 2;
    optional Some some = 3;
}



콘솔에서 다음과 같이 컴파일 한다.

protoc -cpp_out=. Subtype.proto    // cpp 코드 생성
protoc -java_out=. Subtype.proto    // java 코드 생성

컴파일 하면 Subtype.pb.cc, Subtype.pb.h, Subtype.java 파일이 생성된다.



다음은 c++ 어플리케이션에서 생성된 코드를 사용하는 코드이다.

Subtype::Root* root = new Subtype::Root();    // 자동 생성된 코드에 정의된 클래스.

root->set_id(1);
root->set_name("Tom");

Subtype::Some* sub1 = root->mutable_some();
root->set_someid(2);
root->set_somename((xc8*)"Jerry"); 

Subtype::Some* sub2 = sub1->mutable_some();
root->set_someid(3);
root->set_somename((xc8*)"Brute"); 

// 인코딩된 메시지 사이즈를 얻는다. (인코딩은 하기 전인데도.)
int encodedSize = root->ByteSize();

// 인코딩된 데이터가 담길 버퍼
char* encoded = (xuc8*)malloc(encodedSize);

// 인코딩 하고
int rtn = root->SerializeToArray(encoded, encodedSize);
if(!rtn) { print(“encoding failed\n”); }

delete message; message = NULL;

// 디코딩해서 담을 빈 메시지를 하나 만들고
Subtype::Root* newRoot = new Subtype::Root();

// 디코딩
rtn = newRoot ->ParseFromArray(encoded, encodedSize);
if(!rtn) { printf(“decoding failed\n”); }

free(encoded);


printf("Root.id = %d\n", newRoot ->id());
printf("Root.name = %s\n", newRoot ->name());

printf("Root.Some.someId = %d\n", newRoot ->some().someid());
printf("Root.Some.someName = %s\n", newRoot ->some().somename());

printf("Root.Some.Some.someId = %d\n", newRoot ->some().some().someid());
printf("Root.Some.Some.someName = %s\n", newRoot ->some().some().somename());

delete newRoot ; newRoot = NULL;



다음은 java 어플리케이션에서의 사용 코드이다.
 

Subtype.Root.Builder root = Subtype.Root.newBuilder();    // 자동 생성된 코드에 정의된 클래스

root.setId(1);
root.setName("Tom");

Some.Builder sub1 = Some.newBuilder();
sub1.setSomeId(2);
sub1.setSomeName("Jerry");

Some.Builder sub2 = Some.newBuilder();
sub2.setSomeId(3);
sub2.setSomeName("Brute");

sub1.setSome(sub2);

root.setSome(sub1);


// 인코딩된 데이터가 담길 버퍼
byte[] encoded = null;


// 인코딩 하고
try {
    encoded = root.build().toByteArray();
} catch(UninitializedMessageException e) {
    System.out.println(“encoding failed. e=“+e);
}


// 디코딩해서 담을 빈 메시지를 하나 만들고
Subtype.Root newRoot = null;

// 디코딩
try {
    newRoot = sample.Subtype.Message.Root.parseFrom(encoded);
} catch(InvalidProtocolBufferException e) {
    System.out.println("decoding failed. e="+e);
}


println("Root.id = "+newRoot .getId());
println("Root.name = "+newRoot .getName());

println("Root.Some.someId = "+newRoot .getSome().getSomeId());
println("Root.Some.someName = "+newRoot .getSome().getSomeName());

println("Root.Some.Some.someId = "+newRoot .getSome().getSome().getSomeId());
println("Root.Some.Some.someName = "+newRoot .getSome().getSome().getSomeName());


기타 사항은 다음과 같다.

    - 기본적으로 c++, java, python을 지원한다.

    - AddOn에 의해 다음언어가 가능하다. Action Script, c, c#, Java ME, Javascript, Objective C, Perl, Php, Ruby, Visual Basic, Clojure, Common Lisp, D, Erlang, Haskell, Mercury, R

    - 메시지 말고 RPC 인터페이스를 proto 파일에 정의하고 stub 코드가 자동생성된다.

    - AddOn에 의해 RPC 구현체의 자동생성이 가능하다.

    - 패킷캡쳐 프로그램인 wireshark의 플러그인 존재

    - NetBeans IDE의 플러그인 존재

    - 컴파일러의 소스로 제공. linux, Unix는 빌드필요. windows의 경우 실행파일 protoc.exe만 따로 제공.

    - java의 경우 필요한 jar파일을 maven repository에서 다운받을 수 있다.

    - 메시지의 필드는 태그값(optional sint64 id = 1; 정의의 1)에 의해 관리된다. 

    - 필드의 값 설정의 횟수의 타입은 required, optional, repeated 세가지가 있다. required는 반드시 한번, optional은 0 또는 1회, repeated는 횟수 제한없다.

    - 필드 타입은 double, float, bool, string, bytes와 정수형 관련 int32, int64, uint32, uint64, sint32, sint64, fixed32, fixed64, sfixed32, sfixed64들이 있다.

    - java의 경우 생성될 코드의 패키지와 클래스 이름의 개별 지정이 가능하다.

    - c++의 경우 네임스페이스의 정의가 가능하다.

    - nested 메시지 가능.

    - 타 파일에 정의한 메시지를 import 가능.

    - 컴파일 시 성능 최적화, 크기 최적화가 가능하고(메시지별) 일부 기능을 제외한 lite 버전이 가능하다.

    - 필드의 메타데이타를 사용하기 위한 reflection, descriptor 지원.

    - 정의한 메시지가 수정된되더라도 하위 호환이 보장된다. 즉 모든 시스템을 업그레이드하지 않아도 된다. 이쪽 시스템에서 새로운 메시지로 보냈을 때 저쪽에서는 기존의 필드는 처리하고, 모르는 필드는 무시한다. 

    - java에서는 각 메시지 마다 read only의 Message와 read & write의 Builder 두개가 있다. immutable인 String과 같이 성능상의 이유인 듯.


성능

테스트 환경은 Linux 2.6.18 Ubuntu, Intel Xeon X5460 @ 3.16GHz, memory 8G, java 1.5였다.
싱글 thread로 테스트 하였으며, java 컴파일 시 비디버깅모드로 하였다.
Protocol Buffers의 버번은 2.3.0.

측정된 값은 인코딩된 데이타 사이즈, 인코딩 시간, 디코딩 시간이었으며, 인코딩 시간은 자동생성된 코드에 정의된 객체를 생성하고 직렬활를 하는 시간이며, 디코딩 시간은 역직렬활를 하여 객체를 받기 까지의 시간이다.

두가지 데이타가 사용되었으며, 이 중 짧은 데이타는 다음과 같다.(Json으로 표현)

{
    "result":       100,
    "vcID": 9,
    "srcCSAID":     1,
    "targetCSAID":  2,
    "css":  [{
        "srcCSID":      1,
        "newCSID":      2
     }],
    "legs": [{
        "srcCSID":      1,
        "newCSID":      2,
        "srcLegID":     1,
        "newLegID":     3
     }, {
        "srcCSID":      1,
        "newCSID":      2,
        "srcLegID":     2,
        "newLegID":     4
    }]
}


인코딩된 메시지 사이즈는 짧은 메시지가 34byte, 긴 메시지가 1234 byte였다.
c++은 초당 130만회, 11만회 직렬화와 역직렬화를 하였다.
java는 초당  87만회, 6만회 직렬화와 역직렬화를 하였다.

같은 조건에서 cJSON, Pxml2를 사용한 결과를 비교하면 Protocol Buffers가 100배, 10배 빠르다. 메지시 사이즈는 2배, 1.6배 짧다.






Reference

Protocol Buffers home
    http://code.google.com/intl/ko-KR/apis/protocolbuffers/
Third-Party Add-ons for Protocol Buffers
    http://code.google.com/p/protobuf/wiki/ThirdPartyAddOns
Protocol Buffers c++ API
    http://code.google.com/intl/ko-KR/apis/protocolbuffers/docs/reference/cpp/index.html
Protocol Buffers Java API
    http://code.google.com/intl/ko-KR/apis/protocolbuffers/docs/reference/java/index.html