從Protocol Buffers 到 gRPC

從Protocol Buffers 到 gRPC https://www.jianshu.com/p/6c9f90538efe

從 Protocol Buffers 到 gRPC

標簽: ProtoBuf gRPC HTTP/2

我們項目中準備使用 Protocol Buffers 來進行服務器和客戶端的消息交互,采用 gRPC 開源框架,服務器使用 Java,客戶端有 Android 和 iOS。


[TOC]


一、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]

2.2 字段限制

字段限制共有 3 類:

required: 必須賦值的字段(proto3 中不聲明的字段默認為 required) 。 optional: 可有可無的字段。 repeated: 可重復字段 (變長字段), 類似于數組。

由于一些歷史原因, repeated 字段并沒有想象中那么高效, 新版本中允許使用特殊的選項來獲得更高效的編碼:

repeated int32 samples = 4 [packed=true];

2.3 Tags

我們從 message 的定義看到,消息中的每一個字段后面都跟著一個數值。而這個數值作為這個字段在 message 中的唯一標示(Tag),序列化時相當于 key-value 鍵值對中的 key。 在使用時應該將 1-15 留給頻繁使用的字段,因為 1-15 使用一個字節編碼,16-2047 使用 2 個字節編碼。可以指定的最小的 Tag 為 $1$,最大為 $2^{29}-1$(即 $536,870,911$),但是不能使用 19000-19999 之間的值,因為這些值是預留值,強行使用會導致編譯失敗。

2.4 具體使用

編寫好 proto 文件后將其編譯成對應語言的類文件(下面會講解使用 protoc 編譯),具體調用我們以 Java 為例,大致如下:

// 新建一個Person對象
Person kido =
  Person.newBuilder()
    .setId(1234)
    .setName("Kido")
    .setEmail("[email protected]")
    .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();

……

更多的使用和說明,請查看相關官方說明文檔,此處不再贅述… 下面來講一下 protoc 源碼的編譯以及使用。

3. Protoc 源碼的編譯以及使用

3.1 安裝 ProtocolBuffer 工具

3.1.1 下載源碼

git clone [email protected]:google/protobuf.git

注: 也可以從 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)"

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

brew install autoconf automake libtool curl

安裝完成后執行以下腳本:

./autogen.sh

在運行這個腳本的時候,如果遇到 gmock 下載被墻的問題,點擊這個地址手動下載即可: gmock-1.7.0.zip 下載之后,將它丟到源代碼目錄即可。同時將腳本中的

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

注釋掉再執行即可。

3.1.3 編譯

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

查看是否安裝完畢

protoc --version

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

運行的時候,如若遇到 xxx 找不到的問題,則安裝即可。例如在 mac 上出現 “pkg-config 找不到” 的問題,則執行:

brew install pkg-config


二、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;
}

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;
}

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();
    }
  }

……

更多的使用和說明,請查看相關官方說明文檔,此處不再贅述… 下面來講一下 gRPC 源碼的編譯以及使用。

3. gRPC 源碼的編譯以及使用

(事實上只有當我們需要修改 gRPC 相關源碼的時候,才需要在修改后對其進行編譯操作。)

3.1 以 gRPC-Java 為例

3.1.1 下載源碼

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

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 

編譯順利的話,可以在對應的文件夾(all, netty, okhttp, protobuf 等)的 build 文件夾里找到生成的 lib。

當然,如果你想把編譯好的 jar 添加到你的本地庫,以方便后續工程的依賴引用,可以執行以下命令:

$ ./gradlew install

3.1.3 protoc-gen-grpc-java 的編譯和使用

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

# 編譯插件
$ ../gradlew java_pluginExecutable

# 測試插件是否ok
$ ../gradlew test

若上述運行成功,代表插件成功生成,可以在 $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"

同樣的,如果你想把編譯好的 codegen 插件添加到你的本地庫,以方便后續工程的依賴引用,可以執行以下命令:

$ ../gradlew install

關于上述的 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"
    }

在主 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' 
}

很智能的,添加上述依賴后,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.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

鑒于后面我們將運行 C++ 語言的 Server 例子做測試,所以此處我們順便可以先 make 編譯安裝 gRPC C Core library:

 $ make
 $ [sudo] make install

我們想要根據 proto 生成 Objective-C 的代碼,需要先安裝 protoc + protoc-gen-objcgrpc,安裝方法有幾種,如下:

1. 在線安裝

在 Mac OS X 上,請先安裝 homebrew。然后執行以下命令安裝 protoc 和 gRPC protoc 插件:(需翻墻)

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

2. 本地安裝

既然我們有源碼,就要隨時做好對源碼的修改以及編譯的準備。 首先,安裝 protoc,參考上述 “Protocol Buffer 3.1 節”。 接著,cd 到源碼根目錄下,編譯 protoc 的 gRPC 相關插件:

make plugins

執行成功后,會看到根目錄的 bins/opt 下面生成對應不同語言的 “grpc_xxx_plugin”,此處我們只需要用到 “grpc_objective_c_plugin”, 將其鏈接到環境變量:

ln -s `pwd`/bins/opt/grpc_objective_c_plugin /usr/local/bin/protoc-gen-objcgrpc

2.2 相關使用

上述工具依賴安裝好之后,我們來講一下使用。且看 Github 上面的 HelloWorld 例子:

cd examples/objective-c/helloworld

可以看到,官方示例中采用 CocoaPods 依賴的方式。 我們先安裝:

brew install cocoapods

接著重點關注一下兩個文件 “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

當然我們也可以按照需要改為在線依賴:

可先使用 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]

知道版本號后,可以對應修改 Podfile,大概示例如下:

...
pod 'Protobuf', '~>3.0.0-beta-2'
pod 'BoringSSL', '~>3.0'
pod 'gRPC', '~>0.13.0'
...

事實上,由于我們已有 HelloWorld.podspec 里面寫好了引用,所以實際上述依賴根本不用寫…(可以直接注釋或刪除),如下:

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

target 'HelloWorld' do
  # Depend on the generated HelloWorld library.
  pod 'HelloWorld', :path => '.'
end

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

上述 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}"

理解完這兩個文件之后,就到了安裝生成對應 OC 代碼的步驟了,在 HelloWorld 目錄下,執行: (理解完之后,你會發現其實 Podfile 中只需要引用 HelloWorld.podspec 即可)

pod install

# 若想避免不必要的更新cocoapods的spec倉庫,可適當追加一些參數,如下:
pod install --verbose --no-repo-update
# 但若Podfile依賴改動了,則記得要更新
pod update

安裝成功后可以看到目錄下生成了 “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]));
  }
}

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"

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
    
    
  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
    
    
  3. pod install 時出現 “Oh no, an error occurred…”

    答: 應該是 pod 版本問題。出現問題的是 pod 1.0 版本,可能是因為 1.0 對一些信息校驗比較嚴格。我使用的是 pod 0.39 版本。

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);

[XmlFormat],xml 轉 proto 對象

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

[JsonFormat],proto 對象轉 json

Message someProto = SomeProto.getDefaultInstance();
JsonFormat jsonFormat = new JsonFormat();
String asJson = jsonFormat.printToString(someProto);

[JsonFormat],json 轉 proto 對象

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

[HtmlFormat],proto 對象轉 html

Message someProto = SomeProto.getDefaultInstance();
HtmlFormat htmlFormat = new HtmlFormat();
String asHtml = htmlFormat.printToString(someProto);

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;
}

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();
}

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.4 未完待續


六、To be continued.

本文鏈接:參與評論 ?

--EOF--

提醒:本文最后更新于 403 天前,文中所描述的信息可能已發生改變,請謹慎使用。

專題「web開發」的其它文章 ?

Comments

好日子高手社区广东好日子