初探 protobuf

protocol buffer 本质上是一个用来做 serialization/deserialization 的东西,这点上跟 avro、thrift 并无区别(见更早的 blog),它们最终的应用方向是 RPC,wiki 上有一坨做这类事情的工具,但是应用的目的并不一样。现在 protobuf 版本是 2.5。使用 protobuf 的基本过程如下:

  • 设计交互的消息,这往往使用一个 .proto 文件来描述
  • 使用 protoc 将这个消息描述文件编译成对应语言使用的库
  • 在应用程序里面使用生成的库创建对应的对象,并可以使用其中的函数将对象转换到需要的形式(如写入 stream)

protobuf 只是对数据本身的转换,并不带有格式信息,这意味着通信的双方必须都拥有同一个 .proto 产生的库,这样才能从数据映射到对应语言的对象。某些动态语言可能更倾向于将 schema 与数据一起传输,这样可以动态的构造对象,感觉各有利弊。

下面拿个简单的例子来看 protobuf 怎么用到一个通讯的问题里面。如果我们有一个系统将一则消息发送给另外一个系统,消息包括一个时间戳,一组 URL 和一个可选的 cookie,我们可以如下定义

message Work {
  required int64 timestamp = 1 ;
  repeated string urls = 2 ;
  optional string cookie = 3 ;
}

这里我们可以看到一些基本的要素,

  • 类似 C 的 struct 的定义,每个 field 需要使用一个编号,
  • 类型修饰可以为 required、repeated 和 optional,一般为了兼容性,required 一定要慎用(第一版可以用,后面的只能加 optional 了)。
  • 除了类似 C 的这些类型以外,我们可以定义 enum 或者嵌套的别的 message 形成更为复杂的结构。
  • 这里并没有 set/map 这类类型。
  • 可以通过 import 倒入其他的 proto 文件,有两类 import,通过 import public 导入的在本文件被 import 的时候也会被 import,否则就不会。
  • 有时候需要用 nested message,这时生成的类名字往往是通过内外拼接起来的。
  • message 不支持继承,但支持 extension,这时语法为 extension Work 然后加入一些自己定义的域,也就是说另外的人可以有自己不同的 Work,但是都叫 Work…
  • 可以定义 package,对应各个语言不同的

下面看看每个语言下面对应生成的是什么样的东西。

C++

通过 protoc 编译获得了两个文件,work.pb.h 和 work.pb.cc,容易发现一些有意思的东西:

  • 一般将 .cc 编译后获得一个 .o/.so/.a 供其他程序链接
  • package 对应 namespace,message 对应类,extension 是通过“黑科技”实现的
  • message 都是 public 继承 google::protobuf::Message,有用的方法主要是 serialization/deserialization 部分,包括 IsInitialized、Clear、ParseFrom…/SerializeTo…,似乎 IO 部分都是 google 自己的 stream 都没看见 std 里面的… 不过好歹有 string、array 类型的
  • 一般的域都有 has_xxx 这个 bool 操作,xxx 这个 getter 和 set_xxx 这个 setter,repeated 类型的一般有 add_xxx 返回一个指针供修改内容(message 自己有 ownership),其实难道不应该返回引用?

个么我们来写个最简单的程序

#include "work.pb.h"
#include <iostream>
#include <fstream>

int
main (int argc, char* argv[]) {
  demo::Work work ;
  work.set_timestamp (12344L) ;
  work.add_urls ("www.yahoo.com") ;
  work.add_urls ("www.google.com") ;

  std::ofstream os (argv[1]) ;
  if (work.SerializeToOstream (&os))
    std::cout << "succeeded in serialization\n" ;
  else
    std::cout << "failed in serialization\n" ;

  return 0 ;
}

如此写入后可以如此读出

#include "work.pb.h"
#include <iostream>
#include <fstream>

int
main (int argc, char* argv[]) {
  demo::Work work ;
  std::ifstream is (argv[1]) ;
  if (work.ParseFromIstream (&is))
    std::cout << "successful in deserialization\n" ;
  else
    std::cout << "failed in serialization\n" ;

  if (work.IsInitialized ()) {
    std::cout << "timestamp: " << work.timestamp ()
              << "\n" ;
    for (const std::string& url : work.urls ()) {
      std::cout << "url: " << url << "\n" ;
    }
  }

  return 0 ;
}

如果我们希望通过网络传输的话,不妨考虑一下 boost.asio,这里从 boost.asio 的 tutorial 里面借两个例子改一下,我们的 server 随机产生一个 demo::Worker,发送到 client,client 打印对应的信息。

// server side
try {
  boost::asio::io_service io_service;
  tcp::acceptor acceptor(io_service, tcp::endpoint(tcp::v4(), 52027));

  while (true) {
    tcp::socket socket(io_service);
    acceptor.accept(socket);

    std::string s ;
    demo::Work work = gen () ;
    work.SerializeToString (&s) ;
    demo::show (work) ;

    boost::system::error_code ignored_error;
    boost::asio::write(socket, boost::asio::buffer (s), ignored_error);
  }
} catch (std::exception& e) {
  std::cerr << e.what() << std::endl;
}

// client side
try {
  boost::asio::io_service io_service ;
  ip::tcp::endpoint ep (ip::address::from_string (argv[1]), 52027) ;
  ip::tcp::socket socket (io_service) ;
  socket.connect (ep) ;

  while (true) {
    boost::system::error_code error ;
    size_t len = socket.read_some(boost::asio::buffer (arr), error) ;

    if (error == boost::asio::error::eof)
      break; // Connection closed cleanly by peer.
    else if (error)
      throw boost::system::system_error(error); // Some other error.

    demo::Work work ;
    if (work.ParseFromArray (&arr[0], len))
      demo::show (work) ;
    else
      std::cout << "parsing failed\n" ;
  }
} catch (std::exception& e) {
  std::cerr << e.what() << std::endl;
}

看来通过 boost.asio 实现一个 RPC 看起来不会那么困难了,剩下的就是做成一个 framework 方便使用不同的协议调用。

python

python 的实现接近于一种“声明式”的方式,对应于一个 work_pb2 模块,打开可以看见是通过 python 的 meta programming 生成的,其中有一段大概定义的是 serialization 之后的格式。对应的 Work 类直接用反射将描述映射成为这个类的行为。如果希望前面的 server 能跟 python 交互,我们只需要

from work_pb2 import Work
import socket

if __name__ == '__main__':
    s = socket.socket (socket.AF_INET, socket.SOCK_STREAM)
    s.connect (('127.0.0.1', 52027))
    data = s.recv (128)
    w = Work.FromString (data)
    print str (w)

——————
And thou saidst, I will surely do thee good, and make thy seed as the sand of the sea, which cannot be numbered for multitude.

Advertisements
初探 protobuf

一个有关“初探 protobuf”的想法

  1. 可以通过 import 倒入其他的 proto 文件,有两类 import,通过 import public 导入的在本文件被 import 的时候也会被 import,否则就不会. 楼主试过了?为什么我加public修饰会报错。pb 2.6.1

发表评论

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out / 更改 )

Twitter picture

You are commenting using your Twitter account. Log Out / 更改 )

Facebook photo

You are commenting using your Facebook account. Log Out / 更改 )

Google+ photo

You are commenting using your Google+ account. Log Out / 更改 )

Connecting to %s