grpc實戰——構建一個簡單的名稱解析服務

grpc實戰——構建一個簡單的名稱解析服務

境說明:
語言主要用java實現,ide使用的是idea,使用maven作為軟件項目管理工具。

本篇主要是對grpc的一個實戰過程。主要完成服務簡單調用,後面的文章中我會陸續給大家分享服務器流式調用、客戶端流式調用和雙向流式調用。關於grpc,大家大概知道這是一個Google開源的一套rpc(遠程服務調用框架)就可以了。在使用grpc的時候需要定義一個的服務,以及服務中提供的方法,當然也需要定義服務的方法中所涉及到的入參和出參。簡而言之,就是在服務端會實現服務的接口,並開啟grpc服務器來接收客戶端的請求。客戶端則在連接上服務端後,會得到一個stub。利用這個stub可以調用相應的遠程服務,操作上就像調用本地服務一樣,框架屏蔽了大量的底層工作,包括網絡傳輸協議,序列化對象等,使得開發非常簡單。

grpc實戰——構建一個簡單的名稱解析服務

借用一下官方文檔中的圖示,大家大概就能懂整個流程了,這個圖示也很好地展現了grpc語言無關的特性,服務端和客戶端可以是完全不一樣的兩個語言(但是必須是grpc支持的語言,目前主流的語言grpc都已經提供了支持)。


第一步:創建項目

這裡我們主要是創建一個多模塊項目名稱為grpc,然後在其中創建兩個模塊grpc-server和grpc-client。不會創建的童鞋,可以查看我的另外一篇文章idea創建多模塊項目。創建完成後整體項目結構如圖所示:

grpc實戰——構建一個簡單的名稱解析服務

第二步:安裝protobuf support插件

安裝該插件後,idea則對.proto文件有了編輯支持,相對來說更友好一些。

在設置界面中,選擇Plugins,然後點擊Browse repositories,在彈出頁面搜索框中搜索protobuf support,安裝即可。

grpc實戰——構建一個簡單的名稱解析服務

grpc實戰——構建一個簡單的名稱解析服務

第三步: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目錄中,項目結構如下圖所示:

grpc實戰——構建一個簡單的名稱解析服務

因為我們已經安裝了編譯插件,在idea中可以利用maven編譯proto文件了。

grpc實戰——構建一個簡單的名稱解析服務

可以看到編譯完成後,項目結構中多了一個target目錄,裡面就存放著我們需要用到的java類。感興趣的童鞋可以在實踐過程中,直接去研究一下。generated-sources目錄下主要存放了生成的java代碼。

grpc實戰——構建一個簡單的名稱解析服務

其中NameServiceGrpc是一個很重要的類,直接關係到我們的服務,我們自己提供的服務需要繼承它的一個內部類,在客戶端中則是可以從中得到一個stub用於調用。Ip類和Name類則主要是消息類型,這裡主要是作為一個參數類型。

第六步:寫服務端代碼

服務端其實需要做兩件事。

第一件,實現提供的服務,這是本職工作。

第二件,啟動一個grpc服務器,用於接收客戶端的請求。

那麼在這裡,我也把兩件事分開做。

實現提供的服務

要實現提供的服務,只需要寫一個類繼承NameServiceGrpc.NameServiceImplBase這個類即可,然後因為我們定義的方法是getIpByName,那麼我們也實現這個方法,值得一提的是,需要注意一下這個方法的參數是固定的,不能隨心所欲地寫。主要代碼如下:

public class NameServiceImplBaseImpl extends NameServiceGrpc.NameServiceImplBase {
private Map<string> map = new HashMap<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 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;
}
}
/<string>/<string>

在這裡,名稱服務主要是存儲在一個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目錄如下:

grpc實戰——構建一個簡單的名稱解析服務

現在,我們可以開始寫客戶端代碼了,主要代碼如下:

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服務器

grpc實戰——構建一個簡單的名稱解析服務

第九步:啟動客戶端

args參數設置為Sunny David Tom

grpc實戰——構建一個簡單的名稱解析服務

至此,一個簡單的名稱解析服務就做完了。


分享到:


相關文章: