原文串連: 一文瞭解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流程總結為以下幾步:
四:代碼的簡單實現
概念永遠都是枯燥的,只有實戰才能真正理解問題。下面我們使用代碼基於以上的步驟來實現一個簡單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 mainimport ( "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 */@RestControllerpublic 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 */@Servicepublic 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