gRPC基于Golang和Java的簡單實現

原文連接: 一文了解RPC以及gRPC基于Golang和Java的簡單實現

一:什么是RPC

  • 簡介:RPC:Remote Procedure Call,遠程過程調用。簡單來說就是兩個進程之間的數據交互。正常服務端的接口服務是提供給用戶端(在Web開發中就是瀏覽器)或者自身調用的,也就是本地過程調用。和本地過程調用相對的就是:假如兩個服務端不在一個進程內怎么進行數據交互?使用RPC。尤其是現在微服務的大量實踐,服務與服務之間的調用不可避免,RPC更顯得尤為重要。

  • 原理:計算機的世界中不管使用哪種技術,核心都是對數據的操作。RPC不過是將數據的操作垮了一個維度而已。解決的問題本質上只是數據在不同進程間的傳輸。說的再多一些,就要了解網絡模型的知識,七層也好,四層五層也罷。這個不是本文的重點。我們所說的RPC一般是指在傳輸層使用TCP協議進行的數據交互,也有很多基于HTTP的成熟框架。

    盜用網絡上一張圖片說明:

    gRPC流程

    上圖描述了一個RPC的完整調用流程:

    1:client向client stub發起方法調用請求。

    2:client stub接收到請求后,將方法名,請求參數等信息進行編碼序列化。

    3:client stub通過配置的ip和端口使用socket通過網絡向遠程服務器server發起請求。

    4:遠程服務器server接收到請求,解碼反序列化請求信息。

    5:server將請求信息交給server stub,server stub找到對應的本地真實方法實現。

    6:本地方法處理調用請求并將返回的數據交給server stub。

    7:server stub 將數據編碼序列化交給操作系統內核,使用socket將數據返回。

    8:client端socket接收到遠程服務器的返回信息。

    9:client stub將信息進行解碼反序列化。

    10:client收到遠程服務器返回的信息。

    上圖中有一個stub(存根)的概念。stub負責接收本地方法調用,并將它們委托給各自的具體實現對象。server端stub又被稱為skeleton(骨架)。可以理解為代理類。而實際上基于Java的RPC框架stub基本上也都是使用動態代理。我們所說的client端和server端在RPC中一般也都是相對的概念。

    而所謂的RPC框架也就是封裝了上述流程中2-9的過程,讓開發者調用遠程方法就像調用本地方法一樣。


二:常用RPC框架選型

  • Duboo:

    阿里開源的基于TCP的RPC框架,基本上是國內生產環境應用最廣的開發框架了。使用zookeeper做服務的注冊與發現,使用Netty做網絡通信。遺憾的是不能跨語言,目前只支持Java。

  • Thrift:

    Facebook開源的跨語言的RPC框架,通過IDL來定義RPC的接口和數據類型,使用thrift編譯器生成不同語言的實現。據說是目前性能最好的RPC框架,只是暫沒使用過。

  • gRPC:

    這個是我們今天要聊的重點。gRPC是Google的開源產品,是跨語言的通用型RPC框架,使用Go語言編寫。 Java語言的應用同樣使用了Netty做網絡通信,Go采用了Goroutine做網絡通信。序列化方式采用了Google自己開源的Protobuf。請求的調用和返回使用HTTP2的Stream。

  • SpringCloud:

    SpringCloud并不能算一個RPC框架,它是Spring家族中一個微服務治理的解決方案,是一系列框架的集合。但在這個方案中,微服務之間的通信使用基于HTTP的Restful API,使用Eureka或Consul做服務注冊與發現,使用聲明式客戶端Feign做服務的遠程調用。這一系列的功能整合起來構成了一套完整的遠程服務調用。

如何選擇:

如果公司項目使用Java并不牽扯到跨語言,且規模并沒有大到難以治理,我推薦Dubbo。如果項目規模大,服務調用錯綜復雜,我推薦SpringCloud。

如果牽扯到跨語言,我推薦gRPC,這也是目前我司的選擇。即使Thrift性能是gRPC的2倍,但沒辦法,它有個好爹,現在我們的開發環境考慮最多的還是生態。


三:gRPC的原理

一個RPC框架必須有兩個基礎的組成部分:數據的序列化和進程數據通信的交互方式。

對于序列化gRPC采用了自家公司開源的Protobuf。什么是Protobuf?先看一句網絡上 大部分的解釋:

Google Protocol Buffer(簡稱 Protobuf)是一種輕便高效的結構化數據存儲格式,平臺無關、語言無關、可擴展,可用于通訊協議和數據存儲等領域。

上句有幾個關鍵點:它是一種數據存儲格式,跨語言,跨平臺,用于通訊協議和數據存儲。

這么看和我們熟悉的JSON類似,但其實著重點有些本質的區別。JSON主要是用于數據的傳輸,因為它輕量級,可讀性好,解析簡單。Protobuf主要是用于跨語言的IDL,它除了和JSON、XML一樣能定義結構體之外,還可以使用自描述格式定于出接口的特性,并可以使用針對不同語言的protocol編譯器產生不同語言的stub類。所以天然的適用于跨語言的RPC框架中。

而關于進程間的通訊,無疑是Socket。Java方面gRPC同樣使用了成熟的開源框架Netty。使用Netty Channel作為數據通道。傳輸協議使用了HTTP2。

通過以上的分析,我們可以將一個完整的gRPC流程總結為以下幾步:

  • 通過.proto文件定義傳輸的接口和消息體。

  • 通過protocol編譯器生成server端和client端的stub程序。

  • 將請求封裝成HTTP2的Stream。

  • 通過Channel作為數據通信通道使用Socket進行數據傳輸。


四:代碼的簡單實現

概念永遠都是枯燥的,只有實戰才能真正理解問題。下面我們使用代碼基于以上的步驟來實現一個簡單gRPC。為了體現gRPC跨語言的特性,這次我們使用兩種語言:Go實現server端,Java作為client端來實現。

1:安裝Protocol Buffers,定義.proto文件

登錄Google的 github下載對應Protocol Buffers版本。

安裝完成后當我們執行protoc命令如果返回如下信息說明安裝成功。

protoc

下面我們定義一個simple.proto文件,這也是后續我們實現gRPC的基礎

syntax = "proto3"; //定義了我們使用的Protocol Buffers版本。

 //表明我們定義了一個命名為Simple的服務(接口),內部有一個遠程rpc方法,名字為SayHello。
 //我們只要在server端實現這個接口,在實現類中書寫我們的業務代碼。在client端調用這個接口。
 service Simple{
    rpc SayHello(HelloRequest) returns (HelloReplay){}
 }

 //請求的結構體
 message HelloRequest{
     string name = 1;
 }
 //返回的結構體
 message HelloReplay{
     string message = 1;
 }

通過上面的注釋可以看出此文件是一個簡單的RPC遠程方法描述。

2:使用Golang實現sever端

根據官方文檔使用如下命令安裝針對Go的gRPC:

$ go get -u google.golang.org/grpc

但是由于我們有偉大的長城,一般這條命令都不會下載成功。但Google的文件一般都會在github存有一份鏡像。我們可以使用如下命令:

$ go get -u github.com/grpc/grpc-go

隨后將下載的文件夾重命名為go,并放入一個新建的google.golang.org的文件夾中。???♀?

當我們安裝完gRPC并定義好了遠程接口調用的具體信息后,我們要使用protocol編譯器生成我們的stub程序。

我們安裝的Protocol Buffers是用來編譯我們的.proto文件的,但是編譯后的文件是不能被Java、C、Go等這些語言使用。Google針對不同的語言有不同的編譯器。本次我們使用Golang語言,所以要安裝針對Golang的編譯器,根據官方提供的命令執行:

$ go get -u github.com/golang/protobuf/protoc-gen-go

但有可能我們會下載不成功,因為這個會依賴很多Golang的類庫,這些類庫和上面安裝gRPC一樣,鑒于墻的原因,還要執行一系列繁瑣的改文件夾的步驟。但這個不是我們的重點,就不細說了。

安裝成功之后我們就可以建立Go的project了。

本次我們建立一個grpc-server的項目,然后將前面寫的simple.proto放入項目proto的package中。

隨后在項目的目錄下使用命令行執行如下命令:

protoc -I grpc-server/ proto/simple.proto --go_out=plugins=grpc:simple

這樣就將simple.proto編譯成了Go語言對應的stub程序了。

隨后我們就可以寫我們server端的代碼了:main.go。

package main

import (
    "context"
    "grpc-server/proto"
    "fmt"
    "net"
    "log"
    "google.golang.org/grpc"
    "google.golang.org/grpc/reflection"
)

const(
    port = ":50051"
)

type server struct{}

func (s *server) SayHello(ctx context.Context,req *simple.HelloRequest) (*simple.HelloReplay, error){

    fmt.Println(req.Name)

    return &simple.HelloReplay{Message:"hello =======> " + req.Name},nil
}

func main(){
    lis,err := net.Listen("tcp",port)

    if err != nil {
        log.Fatal("fail to listen")
    }

    s := grpc.NewServer()

    simple.RegisterSimpleServer(s,&server{})

    reflection.Register(s)

    if err:= s.Serve(lis);err != nil{
        log.Fatal("fail to server")
    }
}

以上的代碼都是模板代碼,main函數是socket使用Go的標準實現。作為開發者我們只關注遠程服務提供的具體接口實現即可。

最終我們的項目目錄是這樣的:

go-server

就這樣一個使用Go語言實現的最簡單server端就完成了。

3:使用Java實現client端

相對來說Java實現就簡單一些,首先我們可以使用熟悉的Maven插件進行stub代碼的生成。

新建一個grpc-client的父項目,兩個子項目:client和lib。lib用于stub程序的代碼生成。

lib項目編輯pom.xml,添加gRPC針對Java的插件編譯器:

<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
    xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>

    <groupId>org.js</groupId>
    <artifactId>grpc-client</artifactId>
    <version>0.0.1-SNAPSHOT</version>
    <packaging>pom</packaging>

    <modules>
        <module>client</module>
    </modules>

    <name>grpc-client</name>
    <description>Demo project for Spring Boot</description>

    <properties>
        <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
        <project.reporting.outputEncoding>UTF-8</project.reporting.outputEncoding>
        <java.version>1.8</java.version>
        <grpc.version>1.13.1</grpc.version>
        <springboot.version>2.0.4.RELEASE</springboot.version>
    </properties>
    <dependencyManagement>
        <dependencies>
            <dependency>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-starter</artifactId>
                <version>${springboot.version}</version>
            </dependency>
            <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>

            <dependency>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-starter-web</artifactId>
                <version>${springboot.version}</version>
            </dependency>
        </dependencies>
    </dependencyManagement>


    <build>
        <plugins>
            <plugin>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-maven-plugin</artifactId>
            </plugin>
        </plugins>
    </build>

</project>

將定義好的simple.proto文件拷貝項目proto的package下。隨后右鍵:Run Maven——compile。

maven

生成完成后將target下圖中的兩個文件拷貝到client項目目錄中。

target

之后就是編寫我們的業務代碼進行gRPC的遠程調用了。本次我們寫一個簡單的web程序模擬遠程的調用。

定義一個class:SimpleClient:

package org.js.client.grpc;

import io.grpc.ManagedChannel;
import io.grpc.ManagedChannelBuilder;

import java.util.concurrent.TimeUnit;

/**
 * @author JiaShun
 * @date 2018/8/11 12:11
 */
public class SimpleClient {
    private final ManagedChannel channel;
    private final SimpleGrpc.SimpleBlockingStub blockingStub;
    public SimpleClient(String host, int port){
        this(ManagedChannelBuilder.forAddress(host, port).usePlaintext());
    }

    private SimpleClient(ManagedChannelBuilder<?> channelBuilder){
        channel = channelBuilder.build();
        blockingStub = SimpleGrpc.newBlockingStub(channel);
    }

    public void shutdown()throws InterruptedException{
        channel.shutdown().awaitTermination(5, TimeUnit.SECONDS);
    }

    public String sayHello(String name){
        SimpleOuterClass.HelloRequest req = SimpleOuterClass.HelloRequest.newBuilder().setName(name).build();
        SimpleOuterClass.HelloReplay replay = blockingStub.sayHello(req);
        return replay.getMessage();
    }
}

基本都是模板代碼。下面再編寫一個簡單的web請求:

controller代碼:

package org.js.client.controller;

import org.js.client.service.IHelloService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RestController;

/**
 * @author JiaShun
 * @date 2018/8/10 22:20
 */
@RestController
public class HelloController {
    @Autowired
    private IHelloService helloService;

    @GetMapping("/{name}")
    public String sayHello(@PathVariable String name){
        return helloService.sayHello(name);
    }
}

service實現類:

package org.js.client.service;

import org.js.client.grpc.SimpleClient;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Service;

/**
 * @author JiaShun
 * @date 2018/8/10 22:22
 */
@Service
public class HelloServiceImpl implements IHelloService{
    private Logger logger = LoggerFactory.getLogger(HelloServiceImpl.class);
    @Value("${gRPC.host}")
    private String host;
    @Value("${gRPC.port}")
    private int port;

    @Override
    public String sayHello(String name) {
        SimpleClient client = new SimpleClient(host,port);
        String replay = client.sayHello(name);
        try {
            client.shutdown();
        } catch (InterruptedException e) {
            logger.error("channel關閉異常:err={}",e.getMessage());
        }
        return replay;
    }

}

就這么簡單。

隨后我們測試一下:

分別啟動Go server端,Java client端。

gRPC-start

訪問:http://localhost:8080/jiashun

gRPC-test

可以發現server端打印出了client端的請求,client端也收到了server端的返回。

完整代碼:

server:https://github.com/jia-shun/grpc-server

client:https://github.com/jia-shun/grpc-client

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。

推薦閱讀更多精彩內容

  • Spring Cloud為開發人員提供了快速構建分布式系統中一些常見模式的工具(例如配置管理,服務發現,斷路器,智...
    卡卡羅2017閱讀 134,869評論 18 139
  • GRPC是基于protocol buffers3.0協議的. 本文將向您介紹gRPC和protocol buffe...
    二月_春風閱讀 18,021評論 2 28
  • 福貴是從敗家后才開始真正意義上的活著的。 我常常喜歡用辯證的方法去看問題。比如幸與不幸。你看,福貴...
    楊沐云舒閱讀 842評論 3 10
  • 標簽放在<>中 是閉合的 有一個或多個屬性 標簽和屬性名常用小寫 常用屬性 id 規定了屬性在界面中唯一的標識 ...
    小九喵喵閱讀 272評論 0 0
  • 開始于晚上公司的聚餐. 大家隨意的聊著聊著,突然陷入了一種尷尬的情形. 我無法融入其中,無論是什么話題,我都覺得自...
    李方鳴閱讀 244評論 0 0