原文連接: 一文了解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命令如果返回如下信息說明安裝成功。
下面我們定義一個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端就完成了。
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。
生成完成后將target下圖中的兩個文件拷貝到client項目目錄中。
之后就是編寫我們的業務代碼進行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端。
訪問:http://localhost:8080/jiashun
可以發現server端打印出了client端的請求,client端也收到了server端的返回。
完整代碼: