1. 程式人生 > >grpc實戰——構建一個簡單的名稱解析服務

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

借用一下官方文件中的圖示,大家大概就能懂整個流程了,這個圖示也很好地展現了grpc語言無關的特性,服務端和客戶端可以是完全不一樣的兩個語言(但是必須是grpc支援的語言,目前主流的語言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實踐體驗。
另外,歡迎大家轉載,轉載時請註明出處,謝謝!

童鞋們如果有疑問或者想和我交流的話有兩種方式:

第一種

評論留言

第二種