Protobuf 协议简介

2018-05-24 Thursday     misc

Protobuf (Google Protocol Buffers) 是 google 开发的的一套用于数据存储,网络通信时用于协议编解码的工具库,与 XML 和 JSON 数据格式类似,但采用的是二进制的数据格式,具有更高的传输,打包和解包效率。

相比 JSON 来说,Protobuf 的效率、编解码速度更快、数据体积更小,带来的问题是数据可读性变差,协议升级比较麻烦。

protobuf introduce

简介

可以通过 protoc 生成编译后的文件,例如。

$ protoc --go_out=plugins=grpc:. users.proto

生成一个 users.pb.go 文件,其中定义了客户端和服务端的方法。

常用参数如下。

-IPATH, --proto_path=PATH
    用来指定 import 的查找目录,可以多次指定,此时将会按照顺序查找,不指定则默认使用当前目录。
	当多个 proto 文件之间有互相依赖时,需要 import 其他几个 proto 文件,这时候就要用 -I 来指定搜索目录。

--go_out
   通过 golang 的编译插件,支持 golang 的编译,支持以下参数。
       plugins=  指定插件,目前只支持grpc,也就是 plugins=grpc
       最后是指定生成的目录路径,注意是与proto文件的相对路径。

GoLang 语言支持

在 GoLang 中使用 Protobuf ,有两个可选的包 goprotobuf(官方) 和 gogoprotobuf ,或者完全兼容前者,而且据说它生成的代码质量和编解码性能均比官方的要高一些。

安装

首先是安装官方的版本。

----- 安装protobuf库文件
$ go get github.com/golang/protobuf/proto
----- 安装插件,用来生成proto.pb.go文件
$ go get github.com/golang/protobuf/protoc-gen-go

接着可以安装 gogo 版本,其中有 protoc-gen-gogoprotoc-gen-gofast 两个插件可用,后者生成的代码更加复杂,但是性能也更高 (快5~7倍)。

----- 安装两个插件
$ go get github.com/gogo/protobuf/protoc-gen-gogo
$ go get github.com/gogo/protobuf/protoc-gen-gofast
----- 安装库文件(可选)
$ go get github.com/gogo/protobuf/gogoproto

最后是使用上述的插件生成编译后的 go 文件。

$ protoc --go_out=. *.proto
$ protoc --gogo_out=. *.proto
$ protoc --gofast_out=. *.proto

示例1

这里采用的是 proto2 的示例,目录结构如下。

.
 |-foobar.go
 |-example/
   |-example.proto
syntax = "proto2";

package example;

enum FOO { X = 17; };

message Test {
        required string label = 1;
        optional int32 type = 2 [default=77];
        repeated int64 reps = 3;
        optional group OptionalGroup = 4 {
                required string RequiredField = 5;
        }
}
package main

import (
        "fmt"
        "log"
        "reflect"

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

func main() {
        test := &example.Test{
                Label: proto.String("hello"),
                Type:  proto.Int32(17),
                Optionalgroup: &example.Test_OptionalGroup{
                        RequiredField: proto.String("good bye"),
                },
        }

        data, err := proto.Marshal(test)
        if err != nil {
                log.Fatal("marshaling error: ", err)
        }
        fmt.Println("type:", reflect.TypeOf(data))

        newTest := &example.Test{}
        err = proto.Unmarshal(data, newTest)
        if err != nil {
                log.Fatal("unmarshaling error: ", err)
        }

        if test.GetLabel() != newTest.GetLabel() {
                log.Fatalf("data mismatch %q != %q", test.GetLabel(), newTest.GetLabel())
        }
}

然后通过 protoc --go_out=. *.proto 生成,其中 Marshal() 接口返回的数据是 []uint8 或者 []byte 类型。

示例2

目录结构如下。

.
 |-foobar.go
 |-example.proto
 |-example/
syntax = "proto3";

package example;

message Person {
        string email = 1;

        enum PhoneType {
                MOBILE = 0;
                HOME = 1;
                WORK = 2; }

        message PhoneNumber {
                string number = 1;
                PhoneType type = 2;
        }

        repeated PhoneNumber phones = 2;

        oneof UserID {
                string name = 3;
                uint32 id =4;
        }

}
package main

import (
        "log"

        pb "./example"

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

func main() {

        p := pb.Person{
                Email: "foobar@example.com",
                Phones: []*pb.Person_PhoneNumber{
                        {Number: "12345678", Type: pb.Person_HOME},
                },
                UserID: &pb.Person_Name{
                        Name: "foobar",
                },
        }

        data, err := proto.Marshal(&p)
        if err != nil {
                log.Fatal("marshaling error: ", err)
        }

        ps := &pb.Person{}
        err = proto.Unmarshal(data, ps)
        if err != nil {
                log.Fatal("unmarshaling error: ", err)
        }

        switch x := ps.UserID.(type) {
        case *pb.Person_Name:
                log.Printf("Got name %s\n", ps.GetName())
        case *pb.Person_Id:
                log.Printf("Got ID %d\n", ps.GetId())
        case nil:
        default:
                log.Fatal("Person.UserID has unexpected type %T", x)
        }

        log.Printf("ID: %d, Name: %s, Email: %s\n",
                ps.GetId(), ps.GetName(), ps.GetEmail())
        for idx, val := range ps.GetPhones() {
                log.Printf("Phone%d = %+v\n", idx, val)
        }
}
$ protoc example.proto --go_out=plugins=grpc:example

示例3

目录结构如下。

.
 |-server.go
 |-client.go
 |-proto/
   |-userinfo/         用来保存生成的go文件
   |-userinfo.proto

其中 userinfo.proto 的内容如下。

syntax = "proto3";

package userinfo;

enum Gender {
        MALE = 0;
        FEMALE = 1;
};

message Addr {
        string city = 1;
}

message Contact {
        string email = 1;
}

message UserInfo {
        string name = 1;
        int32 age = 2;
        Gender gender = 3;

        oneof method {
                Addr addr = 5;
                Contact cont = 6;
        }
}

客户端和服务端的代码如下。

package main

import (
        pb "./proto/userinfo"
        "fmt"
        "net"

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

func main() {
        Addr := "localhost:6600"
        var conn net.Conn
        var err error

        conn, err = net.Dial("tcp", Addr)
        if err != nil {
                fmt.Println("connecting to", Addr, "fail", err)
                return
        }
        fmt.Println("connect to", Addr, "success")
        defer conn.Close()

	Request := &pb.UserInfo{
                Name:   "foobar",
                Age:    15,
                Gender: pb.Gender_MALE,
                Method: &pb.UserInfo_Addr{
                        Addr: &pb.Addr{
                                City: "foobar",
                        },
                },
        }

        data, err := proto.Marshal(Request)
        if err != nil {
                panic(err)
        }
        fmt.Printf("%d bytes per package\n", len(data))

        for i := 0; i < 100; i++ {
                conn.Write(data)
        }
}
package main

import (
        pb "./proto/userinfo"
        "fmt"
        "io"
        "net"

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

func main() {
        listener, err := net.Listen("tcp", "localhost:6600")
        if err != nil {
                panic(err)
        }

        for {
                conn, err := listener.Accept()
                if err != nil {
                        panic(err)
                }
                fmt.Println("New connect from", conn.RemoteAddr())
                go readMessage(conn)
        }
}

func readMessage(conn net.Conn) {
        defer conn.Close()

        count := 0
        buf := make([]byte, 4096, 4096)
        for {
                cnt, err := conn.Read(buf)
                if err == io.EOF {
                        fmt.Println("Client", conn.RemoteAddr(), "quit.")
                        return
                } else if err != nil {
                        panic(err)
                }

                offset := 0
                for {
                        Response := &pb.UserInfo{}

                        err = proto.Unmarshal(buf[offset:cnt], Response)
                        if err != nil {
                                panic(err)
                        }
                        bytes := proto.Size(Response)
                        if bytes == 0 {
                                break
                        }
                        count++

                        fmt.Printf("[%03d] Got data(%d) from %s, data(%d) %v\n",
					count, cnt, conn.RemoteAddr(), bytes, Response)

                        switch x := Response.Method.(type) {
                        case *pb.UserInfo_Addr:
                                fmt.Printf("Addr %+v\n", x.Addr)
                        case *pb.UserInfo_Cont:
                                fmt.Println("Cont")
                        case nil:
                        default:
                                panic(fmt.Sprintf("proto: unexpected type %T in oneof", x))
                        }
                        offset += bytes
                }
        }
}

然后,在根目录下执行如下命令。

$ protoc -I proto proto/userinfo.proto --go_out=plugins=grpc:proto/userinfo

$ GOPATH=$PWD:$GOPATH go run server.go
$ GOPATH=$PWD:$GOPATH go run client.go

C 语言支持

源码安装最新版本的 protoc 。

----- 如果非Release版本需要执行如下脚本生成configure文件
$ ./autogen.sh
----- 指定安装路径
$ ./configure --prefix=/usr/local/protobuf
$ make
----- 如下测试流程会很耗时,可以忽略
$ make check
# make install

protobuf-c 最新版本是支持 proto3 的,如果平台没有对应的包,那么可以直接从源码开始编译。

PKG_CONFIG_PATH=/usr/local/protobuf/lib/pkgconfig \
./configure --includedir=/usr/local/protobuf/include/google/protobuf --prefix=/usr/local/protobuf-c

注意,通过上述命令安装完 protobuf-c 之后,头文件实际会安装在 /usr/local/protobuf/include/google/protobuf/protobuf-c 目录下。

因为按转到了非标准目录下,在编译时就需要通过 -I-L 参数指定目录路径;包括在执行时添加 LD_LIBRARY_PATH=/usr/local/protobuf-c/lib/ 变量。

示例

如下是一个示例,相关的代码也可以参考 github protobuf-c examples

/* request.proto */
syntax = "proto2";

message Request {
        required string hostname = 1;

        enum ProtocolType {
                ICMP = 0;
                TCP = 1;
        };
        required ProtocolType protocol = 2;
        optional uint32 interval = 3;
        optional uint32 timeout = 4;
};

测试代码如下。

#include <stdio.h>
#include <errno.h>
#include <stdlib.h>
#include <string.h>
#include "request.pb-c.h"

/* NOTE: Keep this same with request.proto */
enum {
        PROTOCOL_TYPE_ICMP = 0,
        PROTOCOL_TYPE_TCP  = 0
};

int main()
{
        Request req = REQUEST__INIT, *request;
        unsigned char *buffer = NULL;
        size_t len ;

        req.hostname = "192.168.9.10";
        req.protocol = PROTOCOL_TYPE_ICMP;
        req.has_interval = 1;
        req.interval = 10;

        len = request__get_packed_size(&req);
        fprintf(stdout, "Packed size is %lu\n", len);
        assert(len);
        buffer = (unsigned char *)malloc(len);
        if (buffer == NULL) {
                fprintf(stderr, "Out of memory\n");
                return -1;
        }
        request__pack(&req, buffer);

        request = request__unpack(NULL, len, buffer);
        if (request == NULL) {
                fprintf(stderr, "error unpacking incoming message\n");
                return -1;
        }

        printf("   hostname = %s\n", request->hostname);
        printf("   protocol = %d\n", request->protocol);
        if (request->has_interval)
                printf("   interval = %d\n", request->interval);

        request__free_unpacked(request, NULL);
        free(buffer);

        return 0;
}

Makefile 编译。

OUTDIR=proto-out
PROTOFILE=request

all: pre example

pre:
        mkdir -p ${OUTDIR}

example: example.c
        protoc-c --c_out=${OUTDIR} ${PROTOFILE}.proto
        gcc -Wall -I${OUTDIR} ${OUTDIR}/${PROTOFILE}.pb-c.c $^ -o $@ -lprotobuf-c

clean:
        rm -rf ${OUTDIR} example

版本 V3

V2 和 V3 的改动包括了:

  1. 只保留 repeated 来标记数组类型,而 optional 和 required 都被去掉了;
  2. 删除 default,当序列化和反序列化端有不同的默认值时,会导致最终的结果不一致;

另外,在 V3 的版本其中有个 oneof 特性,而 protobuf-c 的实现比较坑,没有提供对应的 API 接口,需要进行手动设置。例如:

message Request {
	oneof addr {
		string state = 1;
		string contry = 2;
	};
};

那么在进行序列化时,需要通过如下方式设置。

Request req = REQUEST__INIT;
req.addr_case = REQUEST__ADDR_STATE;
req.state = "china";

而在进行反序列化时,可以通过 switch 进行判断,例如:

switch (request->addr_case) {
case REQUEST__ADDR_STATE:
	printf("Got state: %s\n", request->state);
	break;
case REQUEST__ADDR__NOT_SET:
default:
	break;
}

插件实现

Google 的 Protocol Buffer 是一种以二进制格式对消息进行编码和解码的库,目前支持大部分的语言,如果要实现其它语言,需要先实现一个 protoc 编译器的插件。

CentOS 可通过 yum install protobuf protobuf-compiler 安装解析器,然后通过如下方式获取相应分支的源码:

$ git clone https://github.com/google/protobuf
$ git checkout v2.5.0

Python 插件

这里使用如下的一个 proto 文件 hello.proto

enum Greeting {
	NONE = 0;
	MR = 1;
	MRS = 2;
	MISS = 3;
}
message Hello {
	required Greeting greeting = 1;
	required string name = 2;
}

编译器 protoc 的接口非常简单,编译器将在 stdin 上传递一个 CodeGeneratorRequest 消息,你的插件将在 stdout 的 CodeGeneratorResponse 中输出生成的代码。

第一步是编写读取请求的代码并写一个空的响应:

#!/usr/bin/env python

import sys
from google.protobuf.compiler import plugin_pb2 as plugin

def generate_code(request, response):
    pass

if __name__ == '__main__':
    # Read request message from stdin
    data = sys.stdin.read()
    # Parse request
    request = plugin.CodeGeneratorRequest()
    request.ParseFromString(data)
    # Create response
    response = plugin.CodeGeneratorResponse()
    # Generate code
    generate_code(request, response)
    # Serialise response message
    output = response.SerializeToString()
    # Write to stdout
    sys.stdout.write(output)

核心部分是从 stdin 读取 protoc 请求的接口代码,遍历 AST 并在 stdout 上写入响应。



如果喜欢这里的文章,而且又不差钱的话,欢迎打赏个早餐 ^_^


About This Blog

Recent Posts

Categories

Related Links

  • RTEMS
    RTEMS
  • GNU
  • Linux Kernel
  • Arduino

Search


This Site was built by Jin Yang, generated with Jekyll, and hosted on GitHub Pages
©2013-2018 – Jin Yang