1. 介紹
gRPC 是一個高性能的開源 RPC 框架,最初由 Google 開發。
RPC 是什么?在客戶端應用里可以像調用本地方法對象一樣直接調用另一臺不同機器上的服務端應用的方法。同時支持跨語言的異構系統。國內開源的 RPC 框架有阿里Dubbo、螞蟻金服的 SOFA-RPC、百度 bRPC、新浪 Motan等等。
廢話不多說,直接就開始使用 gRPC。文末附源碼鏈接。
2. 概述
本文將使用以下步驟使用 gRPC 創建典型的C/S服務:
- 首先在
.proto
文件中定義服務: gRPC 使用protobuf
作為 IDL,明確定義了參數及類型。 - 通過
protobuf
編譯器自動生成客戶端-服務端通信 Stub 的代碼。 - 創建服務器端的程序,并對 stub 進行實現。
- 創建客戶端應用程序,使用生成的 stub 進行 RPC 調用服務端方法。
我們先來定義一個簡單的 HelloService
服務,它返回問候語和姓名。
3. Maven 依賴
這里添加 grpc-netty
, grpc-protobuf
和 grpc-stub
三個依賴:
<dependency>
<groupId>io.grpc</groupId>
<artifactId>grpc-netty</artifactId>
<version>1.16.1</version>
</dependency>
<dependency>
<groupId>io.grpc</groupId>
<artifactId>grpc-protobuf</artifactId>
<version>1.16.1</version>
</dependency>
<dependency>
<groupId>io.grpc</groupId>
<artifactId>grpc-stub</artifactId>
<version>1.16.1</version>
</dependency>
4.定義 .proto
文件
創建 HelloService.proto
文件,并開始如下定義:
syntax = "proto3"; // 告訴編譯器此文件使用什么版本的語法,而且默認情況下,編譯器在單個 Java 文件中生成所有 Java 代碼。
option java_multiple_files = true; // 可選配置,再次告訴編譯器所有內容都將在單個文件中生成。
package org.baeldung.grpc; //最后,我們指定要用于生成的 Java 類的包。
// 定義消息結構體,相當于 Http Request
// 并對每個屬性都定義數據類型,需要為每個屬性分配一個唯一編號,稱為標記。此標記由 protobuf 用于表示屬性,而不是使用屬性名稱。
// 因此,與 JSON 不同,我們每次都會傳遞屬性名稱 name,而 protobuf 將使用數字 1 來表示 name。
message HelloRequest {
string firstName = 1;
string lastName = 2;
}
// 定義消息結構體,相當于 Http Response
// 這里的響應體定義與 `HelloRequest` 類似。需要注意的是,我們可以在多個消息類型之間使用相同的標記:
message HelloResponse {
string greeting = 1;
}
// 最后,讓我們定義服務(method)。對于我們的 HelloService,我這里定義了一個 hello() 操作:單次接受一個請求并返回一個響應
service HelloService {
rpc hello(HelloRequest) returns (HelloResponse);
}
另外要指出的是,gRPC 還支持通過將 stream
關鍵字來標示是否進行流式處理。
也就是說,客戶端和服務端交互有四種情況,客戶端流式/非流式——服務端流式/非流式。
5. 生成代碼
現在,我們需要將 HelloService.proto
文件傳遞 protobuf 編譯器 protoc
來生成 Java 文件。有多種方法可以觸發此功能。
- 如果采用 protoc 編譯器 的方式,這也是最簡單的一種方式:
首先你需要下載編譯器:https://developers.google.com/protocol-buffers/docs/downloads,并通過如下命令來將 HelloService.proto 生成 Java 文件:
protoc -I=$SRC_DIR --java_out=$DST_DIR $SRC_DIR/HelloService.proto
- 當然,你也可以用 Maven 插件的方式:
gRPC 提供了 protobuf-maven-plugin, 在Maven
中添加如下配置:
<build>
<extensions>
<extension>
<groupId>kr.motd.maven</groupId>
<artifactId>os-maven-plugin</artifactId>
<version>1.6.1</version>
</extension>
</extensions>
<plugins>
<plugin>
<groupId>org.xolstice.maven.plugins</groupId>
<artifactId>protobuf-maven-plugin</artifactId>
<version>0.6.1</version>
<configuration>
<protocArtifact>
com.google.protobuf:protoc:3.3.0:exe:${os.detected.classifier}
</protocArtifact>
<pluginId>grpc-java</pluginId>
<pluginArtifact>
io.grpc:protoc-gen-grpc-java:1.4.0:exe:${os.detected.classifier}
</pluginArtifact>
</configuration>
<executions>
<execution>
<goals>
<goal>compile</goal>
<goal>compile-custom</goal>
</goals>
</execution>
</executions>
</plugin>
</plugins>
</build>
上面的 os-maven
插件可以生成各種與平臺相關的屬性。
6. 創建服務端程序
不過無論您使用上面哪種方法生成代碼,都將生成以下關鍵文件:
-
HelloRequest.java
文件, 包含 HelloRequest 請求類型定義 -
HelloResponse.java
文件,它包 HelloResponse 響應類型定義 -
HelloServiceImplBase.java
文件,它包含抽象類HelloServiceImplBase
,它提供了我們在服務接口中定義的所有操作的實現
6.1 重寫抽象類 HelloServiceImplBase
的實現
抽象類 HelloServiceImplBase
的默認實現是拋出運行時異常 io.grpc.StatusRuntimeException
,用于指出該方法未被實現。
讓我們來擴展此類并重寫服務定義中提到的 hello()
方法:
public class HelloServiceImpl extends HelloServiceImplBase {
@Override
public void hello(
HelloRequest request, StreamObserver<HelloResponse> responseObserver) {
String greeting = new StringBuilder()
.append("Hello, ")
.append(request.getFirstName())
.append(" ")
.append(request.getLastName())
.toString();
HelloResponse response = HelloResponse.newBuilder()
.setGreeting(greeting)
.build();
responseObserver.onNext(response);
responseObserver.onCompleted();
}
}
如果我們將 hello()
的簽名與我們在 HellService.proto
文件中寫入的簽名進行比較,我們會注意到它不會返回 HelloResponse
。相反,它將第二個參數稱為 StreamObserver<HelloResponse>,它是 響應觀察者
,是服務器調用其響應的回調
。
正如最開始提到的那樣,客戶端將獲得進行阻塞調用或非阻塞調用(流式)的選項。
gRPC 使用生成器(builder)創建對象。我們使用 HelloResponse.newBuilder() 并設置"hello" 問候語以生成 HelloResponse 對象。我們將此對象設置為響應觀察者的 onNext()方法,將其發送到客戶端。
最后,我們需要調用 "onCompleted()" 來指定我們已完成對這次 RPC 的處理,否則連接將掛起,客戶端將等待更多信息進來。
6.2 運行服務端程序
接下來,我們需要啟動 gRPC 服務器來監聽傳入的請求:
public class GrpcServer {
public static void main(String[] args) {
Server server = ServerBuilder
.forPort(8080)
.addService(new HelloServiceImpl()).build();
server.start();
server.awaitTermination();
}
}
在這里,我們再次使用生成器(ServerBuilder
) 在端口 8080 上創建一個 gRPC 服務器,并添加我們定義的 HelloServiceImpl
服務。start()
將啟動服務器。在我們的示例中,我們將調用 awaittermination()
以保持服務器在后臺保持運行。
創建客戶端程序
gRPC 提供了一個通道構造,用于抽象基礎詳細信息,如連接、連接池、負載平衡等。
public class GrpcClient {
public static void main(String[] args) {
// 這里使用 `ManagedChannelBuilder` 創建通道,并指定需要連接的服務器地址和端口。
ManagedChannel channel = ManagedChannelBuilder.forAddress("localhost", 8080)
.usePlaintext() //這里將使用純文本,無需任何加密
.build();
/** 接下來,我們需要創建一個 Stub,我們將用它來進行實際的調用遠程的 hello() 方法。Stub(存根)是客戶端與服務器交互的主要方式。使用自動生成Stub時,Stub 類包含了用于包裝通道(channel)的構造函數。
**/
HelloServiceGrpc.HelloServiceBlockingStub stub
= HelloServiceGrpc.newBlockingStub(channel);
// 是時候進行 hello() RPC 調用了。在這里,我們傳遞 Hello 請求。我們可以使用newBuilder 來設置 HelloRequest 對象的姓、名屬性。并得到從服務器返回的 HelloResponse 對象。
HelloResponse helloResponse = stub.hello(HelloRequest.newBuilder()
.setFirstName("test")
.setLastName("gRPC")
.build());
channel.shutdown();
}
}
需要說明的是,上面使用阻塞/同步的存根(newBlockingStub),以便 RPC 調用等待服務器響應,并將返回響應或引發異常。有兩種類型的存根由 gRPC 提供,另外一種便于非阻塞/異步調用。
8. 總結
在本文中,介紹了如何使用 gRPC 來簡化兩個服務之間的通信開發,與此同時,我們可以更加專注地定義服務以及更加專注的實現我們的業務邏輯。
最后,Github 源碼獲取, 關注公眾號(摳腚Coding筆記)并回復:【grpc-demo】
文章首發在公眾號:摳腚Coding筆記
并由博客一文多發平臺 OpenWrite 發布!