grpc實戰——構建一個簡單的名稱解析服務
第一步:建立專案
這裡我們主要是建立一個多模組專案名稱為grpc,然後在其中建立兩個模組grpc-server和grpc-client。不會建立的童鞋,可以檢視我的另外一篇文章idea建立多模組專案。建立完成後整體專案結構如圖所示:
第二步:安裝protobuf support外掛
安裝該外掛後,idea則對.proto檔案有了編輯支援,相對來說更友好一些。
在設定介面中,選擇Plugins,然後點選Browse repositories,在彈出頁面搜尋框中搜索protobuf support,安裝即可。
第三步:pom.xml配置
<dependency>
<groupId >io.grpc</groupId>
<artifactId>grpc-netty</artifactId>
<version>${grpc.version}</version>
</dependency>
<dependency>
<groupId>io.grpc</groupId>
<artifactId>grpc-protobuf</artifactId >
<version>${grpc.version}</version>
</dependency>
<dependency>
<groupId>io.grpc</groupId>
<artifactId>grpc-stub</artifactId>
<version>${grpc.version}</version>
</dependency >
其中版本主要是這樣的(版本很重要!如果出現不相容的版本,很可能程式就跑不起來,而且比較難找錯)。這裡官方文件已經用上了grpc
1.13.0,然而阿里雲似乎找不到,所以這裡只能用1.12.0版本了。
<properties>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
<java.version>1.8</java.version>
<grpc.version>1.12.0</grpc.version>
<protoc.version>3.5.1-1</protoc.version>
</properties>
另外,還需要配置一下外掛protobuf maven外掛,有了這個外掛之後,我們才可以用idea來編譯.proto檔案。否則,手工編譯的方式較為麻煩。這裡需要注意一點的就是protoc-gen-grpc-java的版本需要和之前grpc依賴的版本一致。
<build>
<extensions>
<extension>
<groupId>kr.motd.maven</groupId>
<artifactId>os-maven-plugin</artifactId>
<version>1.5.0.Final</version>
</extension>
</extensions>
<plugins>
<plugin>
<groupId>org.xolstice.maven.plugins</groupId>
<artifactId>protobuf-maven-plugin</artifactId>
<version>0.5.1</version>
<configuration>
<protocArtifact>com.google.protobuf:protoc:${protoc.version}:exe:${os.detected.classifier}</protocArtifact>
<pluginId>grpc-java</pluginId>
<pluginArtifact>io.grpc:protoc-gen-grpc-java:${grpc.version}:exe:${os.detected.classifier}</pluginArtifact>
</configuration>
<executions>
<execution>
<goals>
<goal>compile</goal>
<goal>compile-custom</goal>
</goals>
</execution>
</executions>
</plugin>
</plugins>
</build>
第四步:定義服務和引數
grpc定義服務的方式主要還是通過protocal buffer的形式(這也是服務呼叫過程中序列化的方式),protocal buffer是另外一個Google的開源專案(這裡不得不感慨一下,谷歌是真的強)。
這裡我們需要在一個.proto檔案中定義我們的服務以及引數,不熟悉的童鞋可以去自行了解一下具體的語法,不過直接看我的程式碼應該也沒什麼問題,畢竟語法相對還是比較好理解的。
我們首先在main目錄下建立一個proto資料夾,在這個資料夾中建立我們的.proto檔案,比如我們這裡取名為nameService.proto,定義如下:
syntax = "proto3";
option java_multiple_files = true;
option java_package = "io.grpc.examples.nameserver";
option java_outer_classname = "NameProto";
option objc_class_prefix = "NS";
package nameserver;
// 定義服務
service NameService {
// 服務中的方法,用於根據Name型別的引數獲得一個Ip型別的返回值
rpc getIpByName (Name) returns (Ip) {}
}
//定義Name訊息型別,其中name為其序列為1的欄位
message Name {
string name = 1;
}
//定義Ip訊息型別,其中ip為其序列為1的欄位
message Ip {
string ip = 1;
}
根據註釋,大家應該還是比較容易理解這個服務定義和訊息定義的,值得一提的是其中的java_package的選項,這個主要是為java服務的,可以用來指定一個符合java規範的報名(因為預設報名對於java規範來說不是那麼友好),這個選項只對編譯成java程式碼有效。
第五步:編譯nameService.proto
目前,我們主要將proto檔案放在了grpc-server的proto目錄中,專案結構如下圖所示:
因為我們已經安裝了編譯外掛,在idea中可以利用maven編譯proto檔案了。
可以看到編譯完成後,專案結構中多了一個target目錄,裡面就存放著我們需要用到的java類。感興趣的童鞋可以在實踐過程中,直接去研究一下。generated-sources目錄下主要存放了生成的java程式碼。
其中NameServiceGrpc是一個很重要的類,直接關係到我們的服務,我們自己提供的服務需要繼承它的一個內部類,在客戶端中則是可以從中得到一個stub用於呼叫。Ip類和Name類則主要是訊息型別,這裡主要是作為一個引數型別。
第六步:寫服務端程式碼
服務端其實需要做兩件事。
第一件,實現提供的服務,這是本職工作。
第二件,啟動一個grpc伺服器,用於接收客戶端的請求。
那麼在這裡,我也把兩件事分開做。
實現提供的服務
要實現提供的服務,只需要寫一個類繼承NameServiceGrpc.NameServiceImplBase這個類即可,然後因為我們定義的方法是getIpByName,那麼我們也實現這個方法,值得一提的是,需要注意一下這個方法的引數是固定的,不能隨心所欲地寫。主要程式碼如下:
public class NameServiceImplBaseImpl extends NameServiceGrpc.NameServiceImplBase {
private Map<String,String> map = new HashMap<String,String>();
private Logger logger = Logger.getLogger(NameServiceImplBaseImpl.class.getName());
public NameServiceImplBaseImpl() {
map.put("Sunny","125.216.242.51");
map.put("David","117.226.178.139");
}
@Override
public void getIpByName(Name request, StreamObserver<Ip> responseObserver) {
logger.log(Level.INFO,"requst is coming. args=" + request.getName());
Ip ip = Ip.newBuilder().setIp(getName(request.getName())).build();
responseObserver.onNext(ip);
responseObserver.onCompleted();
}
public String getName(String name){
String ip = map.get(name);
if(ip == null){
return "0.0.0.0";
}
return ip;
}
}
在這裡,名稱服務主要是儲存在一個map中,想要做的更好的童鞋可以嘗試放到資料庫中。在構造方法中,向map中加入一些條目。在getIpByName中,onNext方法用於向客戶端返回結果,而onComplete方法則用於告訴客戶端,這次呼叫已經完成。這些都是相對固定的套路。細心的童鞋可能注意到了,我們map實際存的是String型別的值,而非Name和Ip型別的值,那麼我們需要有一定的轉換,實際上getName方法會返回一個String型別的ip,在getIpByName中轉換為Ip型別後才進行返回。另外,需要特別說一下,大家可以去看一下proto檔案編譯後的原始碼,訊息型別生成的java類中,構造方法都是私有方法,因此我們只能通過類似Ip.newBuilder().setIp(getName(request.getName())).build()的方法來構造相應的引數物件,這些型別中都有一個Builder的內部類,可以用來輔助生成這些型別的物件。
構建grpcserver類用於接收客戶端的請求。程式碼如下:
public class NameServer {
private Logger logger = Logger.getLogger(NameServer.class.getName());
private static final int DEFAULT_PORT = 8088;
private int port;//服務埠號
private Server server;
public NameServer(int port) {
this(port,ServerBuilder.forPort(port));
}
public NameServer(int port, ServerBuilder<?> serverBuilder){
this.port = port;
server = serverBuilder.addService(new NameServiceImplBaseImpl()).build();
}
private void start() throws IOException {
server.start();
logger.info("Server has started, listening on " + port);
Runtime.getRuntime().addShutdownHook(new Thread() {
@Override
public void run() {
NameServer.this.stop();
}
});
}
private void stop() {
if(server != null)
server.shutdown();
}
private void blockUntilShutdown() throws InterruptedException {
if (server != null) {
server.awaitTermination();
}
}
public static void main(String[] args) throws IOException, InterruptedException {
NameServer nameServer;
if(args.length > 0){
nameServer = new NameServer(Integer.parseInt(args[0]));
}else{
nameServer = new NameServer(DEFAULT_PORT);
}
nameServer.start();
nameServer.blockUntilShutdown();
}
}
其中主要是start方法用於啟動伺服器並接收客戶端的請求。在server中新增名稱解析服務服務實在構造方法中進行的。另外,blockUntilShutdown方法則會讓server阻塞到程式退出為止。
第七步:寫客戶端程式碼
同樣的,也和服務端一樣,我們把nameService.proto檔案放到proto目錄下,再執行編譯過程,也可以直接將grpc-server中的檔案拷貝到grpc-client模組中。這裡我們還是同樣進行一次編譯。得到grpc-client目錄如下:
現在,我們可以開始寫客戶端程式碼了,主要程式碼如下:
public class NameClient {
private static final String DEFAULT_HOST = "localhost";
private static final int DEFAULT_PORT = 8088;
private ManagedChannel managedChannel;
private NameServiceGrpc.NameServiceBlockingStub nameServiceBlockingStub;
public NameClient(String host, int port) {
this(ManagedChannelBuilder.forAddress(host,port).usePlaintext(true).build());
}
public NameClient(ManagedChannel managedChannel) {
this.managedChannel = managedChannel;
this.nameServiceBlockingStub = NameServiceGrpc.newBlockingStub(managedChannel);
}
public void shutdown() throws InterruptedException {
managedChannel.shutdown().awaitTermination(5, TimeUnit.SECONDS);
}
public String getIpByName(String n){
Name name = Name.newBuilder().setName(n).build();
Ip ip = nameServiceBlockingStub.getIpByName(name);
return ip.getIp();
}
public static void main(String[] args) {
NameClient nameClient = new NameClient(DEFAULT_HOST,DEFAULT_PORT);
for(String arg : args){
String res = nameClient.getIpByName(arg);
System.out.println("get result from server: " + res + " as param is " + arg);
}
}
}
客戶端類中主要有兩個成員變數,一個是channel通道,主要用於通訊,另外一個則是stub存根,我們客戶端需要遠端呼叫服務,在得到stub後,只需要呼叫stub的相應服務即可,操作相對來說非常簡單,程式碼中用getIpByName方法對遠端服務呼叫進行了包裝。另外,我們這裡在main函式裡多次呼叫方法,引數args中的變數。需要注意的是,這裡channel要設定成明文傳輸,即usePlainText設定為true,否則還需要配置ssl(官方文件中沒用明文傳輸)。
第八步:啟動grpc伺服器
第九步:啟動客戶端
args引數設定為Sunny David Tom
至此,一個簡單的名稱解析服務就做完了。
再次說明一下,點選grpc名稱服務可以看到本專案的原始碼。歡迎大家fork實踐體驗。
另外,歡迎大家轉載,轉載時請註明出處,謝謝!
童鞋們如果有疑問或者想和我交流的話有兩種方式:
第一種
評論留言