从Protocol Buffers 到 gRPC

从Protocol Buffers 到 gRPC

标签: ProtoBuf gRPC HTTP/2

我们项目中准备使用Protocol Buffers来进行服务器和客户端的消息交互,采用gRPC开源框架,服务器使用Java,客户端有Android和iOS。



一、Protocol Buffers

Protocol Buffers是google的一个开源项目,它是用于结构化数据串行化的灵活、高效、自动的方法,例如XML/JSON,不过它比XML/JSON更小、更快、也更简单。你可以定义自己的数据结构,然后使用代码生成器生成的代码来读写这个数据结构。你甚至可以在无需重新部署程序的情况下更新数据结构。

1. 文档

关于Protocol Buffers的语法、使用、源码、编译方法等,可参考以下官方链接:

Protocol Buffers 使用指南 
Protocol Buffers 源码 on Github

2. 使用

2.1 定义一个消息类型 (官方例子)
// [START declaration]
syntax = "proto3";
package tutorial;
// [END declaration]

// [START java_declaration] protoc编译后生成的java包结构名以及外部调用类名
option java_package = "com.example.tutorial";
option java_outer_classname = "AddressBookProtos";
// [END java_declaration]

// [START csharp_declaration]
option csharp_namespace = "Google.Protobuf.Examples.AddressBook";
// [END csharp_declaration]

// [START messages]
message Person {
  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;
}
// [END messages]
 
 
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 37
  • 38
  • 39
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 37
  • 38
  • 39
2.2 字段限制

字段限制共有3类:

required:必须赋值的字段(proto3中不声明的字段默认为required) 。 
optional:可有可无的字段。 
repeated:可重复字段(变长字段),类似于数组。

由于一些历史原因,repeated字段并没有想象中那么高效,新版本中允许使用特殊的选项来获得更高效的编码:

repeated int32 samples = 4 [packed=true];
 
 
  • 1
  • 1
2.3 Tags

我们从message的定义看到,消息中的每一个字段后面都跟着一个数值。而这个数值作为这个字段在message中的唯一标示(Tag),序列化时相当于key-value键值对中的key。 
在使用时应该将1-15留给频繁使用的字段,因为1-15使用一个字节编码,16-2047使用2个字节编码。可以指定的最小的Tag为 1 ,最大为 2291 (即 536,870,911 ),但是不能使用19000-19999之间的值,因为这些值是预留值,强行使用会导致编译失败。

2.4 具体使用

编写好proto文件后将其编译成对应语言的类文件(下面会讲解使用protoc编译),具体调用我们以Java为例,大致如下:

// 新建一个Person对象
Person kido =
  Person.newBuilder()
    .setId(1234)
    .setName("Kido")
    .setEmail("everlastxgb@gmail.com")
    .addPhone(
      Person.PhoneNumber.newBuilder()
        .setNumber("10086")
        .setType(Person.PhoneType.HOME))
    .build();

// 写对象
OutputStream outputStream;
//...
kido.writeTo(outputStream);

// 读对象
byte[] data;
InputStream inputStream;
//...
Person.parseFrom(data).toBuilder();
// or
Person.parseFrom(inputStream).toBuilder();
 
 
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24

……

更多的使用和说明,请查看相关官方说明文档,此处不再赘述… 下面来讲一下protoc源码的编译以及使用。

3. Protoc源码的编译以及使用

3.1 安装ProtocolBuffer工具

3.1.1 下载源码

git clone git@github.com:google/protobuf.git
 
 
  • 1
  • 1

注: 也可以从protobuf/releases下载最新的release代码。

3.1.2 执行自动化脚本 
下载完成后cd到工程目录下,运行autogen脚本。 
PB依赖autoconf、automake、libtool、curl,在各个平台上安装这些依赖即可。比如在mac上,用brew安装: 
(如果你的mac没安装brew,那么请先安装brew,其实也很简单,如下,命令行执行以下脚本)。

/usr/bin/ruby -e "$(curl -fsSL https://raw.githubusercontent.com/Homebrew/install/master/install)"
 
 
  • 1
  • 1

安装brew成功后使用”brew install”就可以很方便安装我们想要的其它套件了。

brew install autoconf automake libtool curl
 
 
  • 1
  • 1

安装完成后执行以下脚本:

./autogen.sh
 
 
  • 1
  • 1

在运行这个脚本的时候,如果遇到gmock下载被墙的问题,点击这个地址手动下载即可: gmock-1.7.0.zip 下载之后,将它丢到源代码目录即可。同时将脚本中的

# curl $curlopts -O https://googlemock.googlecode.com/files/gmock-1.7.0.zip 
 
 
  • 1
  • 1

注释掉再执行即可。

3.1.3 编译

$ ./configure
$ make
$ make check
$ sudo make install
 
 
  • 1
  • 2
  • 3
  • 4
  • 1
  • 2
  • 3
  • 4

查看是否安装完毕

protoc --version
 
 
  • 1
  • 1
3.2 使用protoc编译.proto文件

针对编译生成不同语言,有cpp_out, java_out, objc_out等,目前PB支持的语言见下表:

Language Flag
C++ (include C++ runtime and protoc) cpp_out
Java java_out
Python python_out
Objective-C objc_out
C# csharp_out
JavaNano javanano_out
JavaScript js_out
Ruby ruby_out
Go go_out
PHP php_out

这里我们假设编译“addressbook.proto”生成对应的java文件。则命令行cd到该proto文件所在目录,执行:

protoc --java_out=. addressbook.proto
 
 
  • 1
  • 1

运行的时候,如若遇到xxx找不到的问题,则安装即可。例如在mac上出现“pkg-config找不到”的问题,则执行:

brew install pkg-config
 
 
  • 1
  • 1

二、gRPC

gRPC是一个高性能、通用的开源RPC框架,其由Google主要面向移动应用开发并基于HTTP/2协议标准而设计,基于ProtoBuf(Protocol Buffers)序列化协议开发,且支持众多开发语言。gRPC提供了一种简单的方法来精确地定义服务和为iOS、Android和后台支持服务自动生成可靠性很强的客户端功能库。客户端充分利用高级流和链接功能,从而有助于节省带宽、降低的TCP链接次数、节省CPU使用、和电池寿命。

1. 文档

关于gRPC的语法、使用、源码、编译方法等,可参考以下官方链接:

gRPC的介绍和使用指南 
gRPC 源码 on Github

2. 使用

gRPC支持多种语言,并能够基于语言自动生成客户端和服务端功能库。目前,在GitHub上已提供了C版本grpc、Java版本grpc-java 和 Go版本grpc-go,其它语言的版本正在积极开发中,其中 grpc支持C、C++、Node.js、Python、Ruby、Objective-C、PHP和C#等语言,grpc-java也已经支持Android开发。

通信方式有几种,如下:

  • Simple RPC
  • Server-side streaming RPC
  • Client-side streaming RPC
  • Bidirectional streaming RPC

下面以Simple RPC为例简单演示一下

2.1 定义一个消息以及RPC服务 (官方例子)
syntax = "proto3";

option java_package = "io.grpc.examples";

package helloworld;

// The greeter service definition.
service Greeter {
  // Sends a greeting
  rpc SayHello (HelloRequest) returns (HelloReply) {}
}

// The request message containing the user's name.
message HelloRequest {
  string name = 1;
}

// The response message containing the greetings
message HelloReply {
  string message = 1;
}
 
 
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
2.2 具体使用

编写好proto文件后将其编译成对应语言的类文件(下面会讲解使用protoc+grpc plugin编译),具体调用我们以Java为例,大致如下:

Client 端:

// ...
import io.grpc.ManagedChannel;
import io.grpc.ManagedChannelBuilder;
import io.grpc.examples.helloworld.nano.GreeterGrpc;
import io.grpc.examples.helloworld.nano.HeadRequest;
import io.grpc.examples.helloworld.nano.HelloReply;
import io.grpc.examples.helloworld.nano.HelloRequest;

// ...
String mHost = "192.168.1.11";
String mPort = "50051";
ManagedChannel mChannel = ManagedChannelBuilder.forAddress(mHost, mPort)
                        .usePlaintext(true)
                        .build();
sayHello(mChannel);
// ...
private String sayHello(ManagedChannel channel) {
    GreeterGrpc.GreeterBlockingStub stub = GreeterGrpc.newBlockingStub(channel);
    HelloRequest message = new HelloRequest();
    message.name = mMessage;
    HelloReply reply = stub.sayHello(message);
    return reply.message;
}
 
 
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23

Server 端:

// ...
import io.grpc.Server;
import io.grpc.ServerBuilder;
import io.grpc.stub.StreamObserver;

// ...
Server server = ServerBuilder.forPort(port)
        .addService(new GreeterImpl())
        .build()
        .start();
// ...
private class GreeterImpl extends GreeterGrpc.AbstractGreeter {

    @Override
    public void sayHello(HelloRequest req, StreamObserver<HelloReply> responseObserver) {
      HelloReply reply = HelloReply.newBuilder().setMessage("Hello " + req.getName()).build();
      responseObserver.onNext(reply);
      responseObserver.onCompleted();
    }
  }
 
 
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20

……

更多的使用和说明,请查看相关官方说明文档,此处不再赘述… 下面来讲一下gRPC源码的编译以及使用。

3. gRPC源码的编译以及使用

(事实上只有当我们需要修改gRPC相关源码的时候,才需要在修改后对其进行编译操作。)

3.1 以gRPC-Java为例

3.1.1 下载源码

$ git clone https://github.com/grpc/grpc-java.git
 
 
  • 1
  • 1

3.1.2 编译Java工程依赖的jar包

如果要编译proto文件自动生成gRPC代码,除了必备的protoc之外,还需要一个代码生成器插件(对于java,是protoc-gen-grpc-java),如果我们不需要重新编译其中的proto文件,就不需要去编译生成这个插件。想要在编译的时候跳过这点,仅需在工程根目录下新建gradle.properties文件,然后添加skipCodegen=true即可。

skipCodegen=true,表示“Skipping the build of codegen and compilation of proto files”

下载完成后cd到工程目录下,命令行输入如下:

$ ./gradlew build 
 
 
  • 1
  • 1

编译顺利的话,可以在对应的文件夹(all, netty, okhttp, protobuf等)的build文件夹里找到生成的lib。

当然,如果你想把编译好的jar添加到你的本地库,以方便后续工程的依赖引用,可以执行以下命令:

$ ./gradlew install
 
 
  • 1
  • 1

3.1.3 protoc-gen-grpc-java 的编译和使用

# 进入grpc-java的工程根目录:
$ cd $GRPC_JAVA_ROOT/compiler

# 编译插件
$ ../gradlew java_pluginExecutable

# 测试插件是否ok
$ ../gradlew test

 
 
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9

若上述运行成功,代表插件成功生成,可以在$GRPC_JAVA_ROOT/compiler/build/exe/java_plugin中看到”protoc-gen-grpc-java” 。那么,接下来可以配合protoc将.proto文件生成对应的java代码文件了:

# To compile a proto file and generate Java interfaces out of the service definitions:
$ protoc --plugin=protoc-gen-grpc-java=build/exe/java_plugin/protoc-gen-grpc-java \
  --grpc-java_out="$OUTPUT_FILE" --proto_path="$DIR_OF_PROTO_FILE" "$PROTO_FILE"

# To generate Java interfaces with protobuf nano:
$ protoc --plugin=protoc-gen-grpc-java=build/exe/java_plugin/protoc-gen-grpc-java \
  --grpc-java_out=nano:"$OUTPUT_FILE" --proto_path="$DIR_OF_PROTO_FILE" "$PROTO_FILE"

 
 
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8

同样的,如果你想把编译好的codegen插件添加到你的本地库,以方便后续工程的依赖引用,可以执行以下命令:

$ ../gradlew install
 
 
  • 1
  • 1

关于上述的protobuf和gRPC的相关编译,我是在MAC-OS下执行,按照我编写的步骤一般都很顺利。


三、gRPC 在不同平台上的使用方法

上述内容对ProtoBuf和gRPC的语法和使用进行了简单的说明,而重点讲解了其源码编译这一块。如果你看得云里雾里,没关系,使用起来其实很简单。(详细的使用请参考文中提到的官方文档地址)

1. Android

1.1 相关配置

可以选择将proto文件放置于src/main/proto文件夹或其他你想要的位置。

然后在Project的build.gradle中添加protobuf-gradle-plugin:

 dependencies {
        classpath 'com.android.tools.build:gradle:1.5.0'
        classpath "com.google.protobuf:protobuf-gradle-plugin:0.7.4"
    }
 
 
  • 1
  • 2
  • 3
  • 4
  • 1
  • 2
  • 3
  • 4

在主Module中添加:

apply plugin: 'com.google.protobuf'

// ...

protobuf {
    protoc {
        artifact = 'com.google.protobuf:protoc:3.0.0-beta-2'
    }
    plugins {
        grpc {
            artifact = 'io.grpc:protoc-gen-grpc-java:0.14.0' 
        }
    }
    generateProtoTasks {
        all().each { task ->
            task.builtins {
                javanano {
                    // Options added to --javanano_out
                    option 'ignore_services=true'
                }
            }

            task.plugins {
                grpc {
                    // Options added to --grpc_out
                    option 'nano'
                }
            }
        }
    }
}

dependencies {
    compile 'com.google.code.findbugs:jsr305:3.0.0'
    compile 'com.google.guava:guava:18.0'
    compile 'com.squareup.okhttp:okhttp:2.2.0'
    compile 'javax.annotation:javax.annotation-api:1.2'

    compile 'io.grpc:grpc-okhttp:0.14.0'
    compile 'io.grpc:grpc-protobuf-nano:0.14.0' 
    compile 'io.grpc:grpc-stub:0.14.0' 
}
 
 
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 37
  • 38
  • 39
  • 40
  • 41
  • 42
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 37
  • 38
  • 39
  • 40
  • 41
  • 42

很智能的,添加上述依赖后,Rebuild Project后,会自动帮你生成java文件。关于protobuf-gradle-plugin的源码以及更多声明和使用方法可参见以下链接: 
https://github.com/google/protobuf-gradle-plugin

1.2 相关使用

关键代码演示如下:

// ...
import io.grpc.ManagedChannel;
import io.grpc.ManagedChannelBuilder;
import io.grpc.examples.helloworld.nano.GreeterGrpc;
import io.grpc.examples.helloworld.nano.HeadRequest;
import io.grpc.examples.helloworld.nano.HelloReply;
import io.grpc.examples.helloworld.nano.HelloRequest;

// ...

// // 实际使用请替换成grpc server监听的真实"host:port"
String mHost = "0.0.0.0";
String mPort = "50051";
ManagedChannel mChannel = ManagedChannelBuilder.forAddress(mHost, mPort)
                        .usePlaintext(true)
                        .build();
sayHello(mChannel);
// ...
private String sayHello(ManagedChannel channel) {
    GreeterGrpc.GreeterBlockingStub stub = GreeterGrpc.newBlockingStub(channel);
    HelloRequest message = new HelloRequest();
    message.name = mMessage;
    HelloReply reply = stub.sayHello(message);
    return reply.message;
}
 
 
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
1.3 相关链接

更多关于Android(Java)的使用和说明,请查看相关官方说明文档,此处不再赘述…

gRPC-Java 源码 on Github 
Android examples on Github 
gRPC- Java 使用指南

2. iOS

2.1 相关编译和安装

一切开始之前,请先下载grpc的源码:

 $ git clone https://github.com/grpc/grpc.git
 $ cd grpc
 $ git submodule update --init
 
 
  • 1
  • 2
  • 3
  • 1
  • 2
  • 3

鉴于后面我们将运行C++语言的Server例子做测试,所以此处我们顺便可以先make编译安装gRPC C Core library:

 $ make
 $ [sudo] make install
 
 
  • 1
  • 2
  • 1
  • 2

我们想要根据proto生成Objective-C的代码,需要先安装protoc + protoc-gen-objcgrpc,安装方法有几种,如下:

1.在线安装

在Mac OS X上,请先安装homebrew。然后执行以下命令安装protoc和gRPC protoc插件:(需翻墙)

$ curl -fsSL https://goo.gl/getgrpc | bash -
 
 
  • 1
  • 1

2.本地安装

既然我们有源码,就要随时做好对源码的修改以及编译的准备。 
首先,安装protoc,参考上述”Protocol Buffer 3.1节”。 
接着,cd到源码根目录下,编译protoc的gRPC相关插件:

make plugins
 
 
  • 1
  • 1

执行成功后,会看到根目录的 bins/opt 下面生成对应不同语言的”grpc_xxx_plugin”,此处我们只需要用到”grpc_objective_c_plugin”,将其链接到环境变量:

ln -s `pwd`/bins/opt/grpc_objective_c_plugin /usr/local/bin/protoc-gen-objcgrpc
 
 
  • 1
  • 1
2.2 相关使用

上述工具依赖安装好之后,我们来讲一下使用。且看Github上面的HelloWorld例子:

cd examples/objective-c/helloworld
 
 
  • 1
  • 1

可以看到,官方示例中采用CocoaPods依赖的方式。 
我们先安装:

brew install cocoapods
 
 
  • 1
  • 1

接着重点关注一下两个文件 “Podfile” 和 “HelloWorld.podspec”

Podfile

可以看到示例的Podfile是本地依赖。

source 'https://github.com/CocoaPods/Specs.git'
platform :ios, '8.0'

pod 'Protobuf', :path => "../../../third_party/protobuf"
pod 'BoringSSL', :podspec => "../../../src/objective-c"
pod 'gRPC', :path => "../../.."

target 'HelloWorld' do
  # Depend on the generated HelloWorld library.
  pod 'HelloWorld', :path => '.'
end
 
 
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11

当然我们也可以按照需要改为在线依赖:

可先使用pod search搜索对应的依赖的线上版本:

pod search Protobuf
# - Versions: 3.0.0-beta-2, 3.0.0-alpha-4.1, 3.0.0-alpha-3 [master repo]

pod search BoringSSL
# - Versions: 3.0, 2.0, 1.0 [master repo]

pod search gRPC
# - Versions: 0.13.0, 0.12.0, 0.11.1, 0.6.0, 0.5.1, 0.0.2 [master repo]
 
 
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8

知道版本号后,可以对应修改Podfile,大概示例如下:

...
pod 'Protobuf', '~>3.0.0-beta-2'
pod 'BoringSSL', '~>3.0'
pod 'gRPC', '~>0.13.0'
...
# 事实上,由于我们已有HelloWorld.podspec写好了引用,所以实际上述依赖根本不用写...(可以直接注释)
 
 
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6

HelloWorld.podspec

通过”pod search”我们可以知道Protobuf线上最新release版本是”3.0.0-beta-2”,grpc线上最新release版本是”0.13.0”,所以这里进行了相应的版本号修改:

Pod::Spec.new do |s|
  s.name     = "HelloWorld"
  s.version  = "0.0.1"
  s.license  = "New BSD"

  s.ios.deployment_target = "7.1"
  s.osx.deployment_target = "10.9"

  # Base directory where the .proto files are.
  src = "../../protos"

  # Directory where the generated files will be placed.
  dir = "Pods/" + s.name

  # Run protoc with the Objective-C and gRPC plugins to generate protocol messages and gRPC clients.
  s.prepare_command = <<-CMD
    mkdir -p #{dir}
    protoc -I #{src} --objc_out=#{dir} --objcgrpc_out=#{dir} #{src}/helloworld.proto
  CMD

  s.subspec "Messages" do |ms|
    ms.source_files = "#{dir}/*.pbobjc.{h,m}", "#{dir}/**/*.pbobjc.{h,m}"
    ms.header_mappings_dir = dir
    ms.requires_arc = false
    ms.dependency "Protobuf", "~> 3.0.0-beta-2"
  end

  s.subspec "Services" do |ss|
    ss.source_files = "#{dir}/*.pbrpc.{h,m}", "#{dir}/**/*.pbrpc.{h,m}"
    ss.header_mappings_dir = dir
    ss.requires_arc = true
    ss.dependency "gRPC", "~> 0.13.0"
    ss.dependency "#{s.name}/Messages"
  end
end
 
 
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35

上述podspec例子中的.proto文件是放置在工程外部目录,实际使用我们可以放在工程目录,方便管理和编译。同时,实际上我们真实项目中的.proto文件可能有多个,而且会分目录存放。针对这种情况,大概示例如下:

  s.prepare_command = "protoc --objc_out=. --objcgrpc_out=. *.proto **/*.proto"
  ...
    ms.source_files = "*.pbobjc.{h,m}", "**/*.pbobjc.{h,m}"
  ...
    ss.source_files = "*.pbrpc.{h,m}", "**/*.pbrpc.{h,m}"
 
 
  • 1
  • 2
  • 3
  • 4
  • 5
  • 1
  • 2
  • 3
  • 4
  • 5

理解完这两个文件之后,就到了安装生成对应OC代码的步骤了,在HelloWorld目录下,执行: 
(理解完之后,你会发现其实Podfile中只需要引用HelloWorld.podspec即可)

pod install

# 若想避免不必要的更新cocoapods的spec仓库,可适当追加一些参数,如下:
pod install --verbose --no-repo-update
# 但若Podfile依赖改动了,则记得要更新
pod update
 
 
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6

安装成功后可以看到目录下生成了”Podfile.lock”文件和”Pods”文件夹(里面就是对应的OC代码)

安装过程部分内容涉及翻墙的,所以最好开启VPN

2.3 相关Objective-C代码示例
#import <UIKit/UIKit.h>
#import "AppDelegate.h"

#import <GRPCClient/GRPCCall+ChannelArg.h>
#import <GRPCClient/GRPCCall+Tests.h>
#import <HelloWorld/Helloworld.pbrpc.h>

// 实际使用请替换成grpc server监听的真实"host:port"
static NSString * const kHostAddress = @"0.0.0.0:50051";

int main(int argc, char * argv[]) {
  @autoreleasepool {
    [GRPCCall useInsecureConnectionsForHost:kHostAddress];
    [GRPCCall setUserAgentPrefix:@"HelloWorld/1.0" forHost:kHostAddress];

    HLWGreeter *client = [[HLWGreeter alloc] initWithHost:kHostAddress];

    HLWHelloRequest *request = [HLWHelloRequest message];
    request.name = @"Objective-C";

    [client sayHelloWithRequest:request handler:^(HLWHelloReply *response, NSError *error) {
      NSLog(@"%@", response.message);
    }];

    return UIApplicationMain(argc, argv, nil, NSStringFromClass([AppDelegate class]));
  }
}

 
 
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
2.4 测试

Objective-C成功运行后,我们想要测试其与Server的通信是否成功,则需要一个Server端的服务。我们且用官方的C++代码实现的helloworld server:

# 假设当前处于grpc工程根目录

cd examples/cpp/helloworld/
make
./greeter_server
# 运行成功会出现"Server listening on 0.0.0.0:50051"

# 可以顺带另开窗口执行C++ Client试试

./greeter_client
# 运行成功会输出"Greeter received: Hello world"
 
 
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11

server端开启监听后,运行上述我们编写的ObjC代码,如果看到在控制台有log输出“Hello Objective-C”,证明成功通信。

如果你make失败,请确认是否已经’sudo make install’ 安装了gRPC C Core library。

2.5 问题汇总
  1. pod install 成功后打开工程编译出现error:“…different version of protoc which is incompatible with your Protocol Buffer library…”的问题。

    答: 很明显是protoc编译器和依赖的库版本不一致的问题。由于我安装protoc是使用master最新代码,而依赖的protobuf库是release的3.0.0-beta-2,也就是说我的protoc版本太新了(30001)。

    解决方法: 下载release的protobuf 3.0.0-beta-2版本重新编译安装protoc。步骤如下:

    
    # 进入protobuf工程根目录
    
    make        
    make install
       
       
    • 1
    • 2
    • 3
    • 4
    • 5
    • 1
    • 2
    • 3
    • 4
    • 5
  2. pod install 时出现“… –objcgrpc_out: protoc-gen-objcgrpc: Plugin killed by signal 11.”

    答: protoc-gen-objcgrpc跟当前的protoc版本冲突问题。由于我重新安装了protoc,而没重新对应编译grpc生成protoc-gen-objcgrpc。

    解决方法: 重新编译grpc,重新生成protoc-gen-objcgrpc插件并链接到本地。步骤如下:

    
    # 进入grpc工程根目录
    
    make clean
    make        
    make install
    make plugins
    rm /usr/local/bin/protoc-gen-objcgrpc
    ln -s `pwd`/bins/opt/grpc_objective_c_plugin /usr/local/bin/protoc-gen-objcgrpc
       
       
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
2.6 相关链接

更多关于Objective-C的使用和说明,请查看相关官方说明文档,此处不再赘述…

gRPC 源码 on Github 
Objective-C examples on Github 
gRPC- Objective-C 使用指南

3. More usages to be continued.


四、与ProtoBuf相关的其它

1. ProtoBuf/JSON/XML 格式互转 (Java)

1.1 简述:

可以实现ProtoBuf(byte array)与其他文本格式的XML、JSON、HTML之间的转换。 
下载protobuf-java-format-1.2.jar导入工程即可。

1.2 示例代码:

[XmlFormat],proto对象转xml

Message someProto = SomeProto.getDefaultInstance();
XmlFormat xmlFormat = new XmlFormat();
String asXml = xmlFormat.printToString(someProto);
 
 
  • 1
  • 2
  • 3
  • 1
  • 2
  • 3

[XmlFormat],xml转proto对象

Message.Builder builder = SomeProto.newBuilder();
String asXml = _load xml document from a source_;
XmlFormat xmlFormat = new XmlFormat();
xmlFormat.merge(asXml, builder);
 
 
  • 1
  • 2
  • 3
  • 4
  • 1
  • 2
  • 3
  • 4

[JsonFormat],proto对象转json

Message someProto = SomeProto.getDefaultInstance();
JsonFormat jsonFormat = new JsonFormat();
String asJson = jsonFormat.printToString(someProto);
 
 
  • 1
  • 2
  • 3
  • 1
  • 2
  • 3

[JsonFormat],json转proto对象

Message.Builder builder = SomeProto.newBuilder();
String asJson = _load json document from a source_;
JsonFormat jsonFormat = new JsonFormat();
jsonFormat.merge(asJson, builder);
 
 
  • 1
  • 2
  • 3
  • 4
  • 1
  • 2
  • 3
  • 4

[HtmlFormat],proto对象转html

Message someProto = SomeProto.getDefaultInstance();
HtmlFormat htmlFormat = new HtmlFormat();
String asHtml = htmlFormat.printToString(someProto);
 
 
  • 1
  • 2
  • 3
  • 1
  • 2
  • 3
1.3 相关链接:

protobuf-java-format-1.2.jar 
protobuf-java-format source on google code


五、与gRPC相关的其它

1. 文件上传分块

1.1 简述:

Google官方有这么一句话,如下:

Protocol Buffers are not designed to handle large messages. As a general rule of thumb, if you are dealing in messages larger than a megabyte each, it may be time to consider an alternate strategy.

大概意思就是:“Protocol Buffers不是为了处理数据量大的信息而设计的。按照经验来说,如果你准备用它处理大于1M的数据信息,你需要考虑使用一种替代策略。”

看到这个,不要慌,还是有解决方案的,官方也说了:

That said, Protocol Buffers are great for handling individual messages within a large data set. Usually, large data sets are really just a collection of small pieces, where each small piece may be a structured piece of data. Even though Protocol Buffers cannot handle the entire set at once, using Protocol Buffers to encode each piece greatly simplifies your problem: now all you need is to handle a set of byte strings rather than a set of structures. 
……

大概意思就是:“…你可以把一个很大的数据块分割为若干个小数据块…”

事实上,我觉得解决方案其实有两个: 
①. 按上述所说,把一个很大的数据块分割为若干个小数据块。 
②. 先用httpmime去上传文件得到文件的url(或者其它标志id之类的),再将该url(or id)放到message传输。

不过,既然是统一整改,就不要这么混杂地使用第二种了,直接使用第一种分割方案其实也很简单。

1.2 实现步骤:

文件分bytes + Client-side streaming

1.3 示例代码(Java):

(此处只是简单演示实现逻辑,实际具体应用需考虑周全。)

proto定义:

syntax = "proto3";

option java_multiple_files = true;
option java_package = "io.grpc.kido.fileuploader";
option java_outer_classname = "FileUploader";
option objc_class_prefix = "KFL";

package fileuploader;

service Uploader {
  rpc uploadFile (stream FileRequest) returns (FileReply) {}
}

// The request message containing part of the file.
message FileRequest {
   int64 offset = 1;// 当前分块的起始点相对于整个文件的位置
   bytes data = 2; // 当前分块的文件字节数组
}

// The response message containing the greetings
message FileReply {
  int32 status = 1;
  string message = 2;
}
 
 
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24

Client 端:

// 关键代码

private void uploadFile(File file) throws Exception {

    StreamObserver<FileReply> responseObserver = new StreamObserver<FileReply>() {
        @Override
        public void onNext(FileReply reply) {
        }

        @Override
        public void onError(Throwable t) {
        }

        @Override
        public void onCompleted() {
        }
    };

    StreamObserver<FileRequest> requestObserver = asyncStub.uploadFile(responseObserver);
    try {
        FileRequest fileRequest = new FileRequest();
        BufferedInputStream bInputStream = new BufferedInputStream(new FileInputStream(file));
        int bufferSize = 512 * 1024; // 512k
        byte[] buffer = new byte[bufferSize];

        int tmp = 0;
        int size = 0;
        if ((tmp = bInputStream.read(buffer)) > 0) {
            size += tmp;
            fileRequest.data = buffer;
            fileRequest.offset = size;
            requestObserver.onNext(fileRequest);
        }
    } catch (Exception e) {
        // Cancel RPC
        requestObserver.onError(e);
        throw e;
    }
    // Mark the end of requests
    requestObserver.onCompleted();
}
 
 
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 37
  • 38
  • 39
  • 40
  • 41
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 37
  • 38
  • 39
  • 40
  • 41

Server 端:

// 关键代码

    public static void main(String[] args) throws Exception {
        UploaderServer server = new UploaderServer(8980);
        server.start();
        server.blockUntilShutdown();
    }

    private static class UploaderService extends UploaderGrpc.AbstractUploader {

        UploaderService() {
        }

        @Override
        public StreamObserver<FileRequest> uploadFile(StreamObserver<FileReply> responseObserver) {
            return new FileRequestObserver(responseObserver);
        }
    }

    static class FileRequestObserver implements StreamObserver<FileRequest> {
        private int status = 200;
        private String message = "";
        private BufferedOutputStream bufferedOutputStream = null;
        private StreamObserver<FileReply> responseObserver = null;

        public FileRequestObserver(final StreamObserver<FileReply> responseObserver) {
            try {
                this.responseObserver = responseObserver;
                bufferedOutputStream = new BufferedOutputStream(new FileOutputStream("md5filename"));
            } catch (Exception e) {
            }
        }

        @Override
        public void onNext(FileRequest fileRequest) {
            byte[] data = fileRequest.getData().toByteArray();
            long offset = fileRequest.getOffset();
            try {
                if (bufferedOutputStream != null) {
                    bufferedOutputStream.write(data); // 或者这里先接收保存,到时再一次写。(但会占内存)
                }
            } catch (Exception e) {
            }
            // write the file to the server by bufferStream
        }

        @Override
        public void onError(Throwable throwable) {
            String error = throwable.getMessage();
            logger.log(Level.WARNING, "onError->" + error);
            status = 500;
            message = error;
        }

        @Override
        public void onCompleted() {
            responseObserver.onNext(
            FileReply.newBuilder()
                .setMessage(message)
                .setStatus(status)
                .build()
            );
            responseObserver.onCompleted();
            try {
                if (bufferedOutputStream != null) {
                    bufferedOutputStream.flush();
                    bufferedOutputStream.close();
                }
            } catch (Exception e) {

            }
        }
    }
 
 
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 37
  • 38
  • 39
  • 40
  • 41
  • 42
  • 43
  • 44
  • 45
  • 46
  • 47
  • 48
  • 49
  • 50
  • 51
  • 52
  • 53
  • 54
  • 55
  • 56
  • 57
  • 58
  • 59
  • 60
  • 61
  • 62
  • 63
  • 64
  • 65
  • 66
  • 67
  • 68
  • 69
  • 70
  • 71
  • 72
  • 73
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 37
  • 38
  • 39
  • 40
  • 41
  • 42
  • 43
  • 44
  • 45
  • 46
  • 47
  • 48
  • 49
  • 50
  • 51
  • 52
  • 53
  • 54
  • 55
  • 56
  • 57
  • 58
  • 59
  • 60
  • 61
  • 62
  • 63
  • 64
  • 65
  • 66
  • 67
  • 68
  • 69
  • 70
  • 71
  • 72
  • 73
1.4 未完待续

六、To be continued.

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值