一:什么是RPC
简介:
RPC:Remote Procedure Call,远程过程调用。简单来说就是两个进程之间的数据交互。正常服务端的接口服务是提供给用户端(在Web开发中就是浏览器)或者自身调用的,也就是本地过程调用。和本地过程调用相对的就是:假如两个服务端不在一个进程内怎么进行数据交互?使用RPC。尤其是现在微服务的大量实践,服务与服务之间的调用不可避免,RPC更显得尤为重要。
原理:
计算机的世界中不管使用哪种技术,核心都是对数据的操作。RPC不过是将数据的操作垮了一个维度而已。解决的问题本质上只是数据在不同进程间的传输。说的再多一些,就要了解网络模型的知识,七层也好,四层五层也罢。这个不是本文的重点。
我们所说的RPC一般是指在传输层使用TCP协议进行的数据交互,也有很多基于HTTP的成熟框架。
盗用网络上一张图片说明:

上图描述了一个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框架选型
Dubbo:
阿里开源的基于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倍,但没办法,它有个好爹,现在我们的开发环境考虑最多的还是生态,不得不向Google爸爸臣服。
三: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的基础
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16
| syntax = "proto3";
service Simple { rpc SayHello(HelloRequest) returns (HelloReplay) {} }
//请求的结构体 message HelloRequest { string name = 1; }
message HelloReplay { string message = 1; }
|
通过上面的注释可以看出此文件是一个简单的RPC远程方法描述。
2:使用Golang实现sever端
根据官方文档使用如下命令安装针对Go的gRPC:
1
| $ go get -u google.golang.org/grpc
|
但是由于我们有伟大的长城,一般这条命令都不会下载成功。但Google的文件一般都会在github存有一份镜像。我们可以使用如下命令:
1
| $ go get -u github.com/grpc/grpc-go
|
随后将下载的文件夹重命名为grpc,并放入一个新建的google.golang.org的文件夹中。🤷♀️
当我们安装完gRPC并定义好了远程接口调用的具体信息后,我们要使用protocol编译器生成我们的stub程序。
我们安装的Protocol Buffers是用来编译我们的.proto文件的,但是编译后的文件是不能被Java、C、Go等这些语言使用。Google针对不同的语言有不同的编译器。本次我们使用Golang语言,所以要安装针对Golang的编译器,根据官方提供的命令执行:
1
| $ go get -u github.com/golang/protobuf/protoc-gen-go
|
但有可能我们会下载不成功,因为这个会依赖很多Golang的类库,这些类库和上面安装gRPC一样,鉴于墙的原因,还要执行一系列繁琐的改文件夹的步骤。但这个不是我们的重点,就不细说了。
安装成功之后我们就可以建立Go的project了。
本次我们建立一个grpc-server的项目,然后将前面写的simple.proto放入项目proto的package中。
随后在项目的目录下使用命令行执行如下命令:
1
| protoc -I grpc-server/ proto/simple.proto --go_out=plugins=grpc:simple
|
这样就将simple.proto编译成了Go语言对应的stub程序了。
随后我们就可以写我们server端的代码了:main.go。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45
| 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的插件编译器:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73
| <?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>lib</artifactId> <version>0.0.1-SNAPSHOT</version>
<name>lib</name> <description>gRPC lib</description>
<properties> <os-maven-plugin.version>1.5.0.Final</os-maven-plugin.version> <protobuf-maven-plugin.version>0.5.1</protobuf-maven-plugin.version> <protoc.version>3.5.1-1</protoc.version> <protobuf.version>3.6.0</protobuf.version> <grpc.version>1.13.1</grpc.version> </properties>
<dependencies> <dependency> <groupId>com.google.protobuf</groupId> <artifactId>protobuf-java</artifactId> <version>${protobuf.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> </dependencies>
<build> <extensions> <extension> <groupId>kr.motd.maven</groupId> <artifactId>os-maven-plugin</artifactId> <version>${os-maven-plugin.version}</version> </extension> </extensions> <plugins> <plugin> <groupId>org.xolstice.maven.plugins</groupId> <artifactId>protobuf-maven-plugin</artifactId> <version>${protobuf-maven-plugin.version}</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> </project>
|
将定义好的simple.proto文件拷贝项目proto的package下。随后右键:Run Maven——compile。

生成完成后将target下图中的两个文件拷贝到client项目目录中。

之后就是编写我们的业务代码进行gRPC的远程调用了。本次我们写一个简单的web程序模拟远程的调用。
定义一个class:SimpleClient:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35
| package org.js.client.grpc;
import io.grpc.ManagedChannel; import io.grpc.ManagedChannelBuilder;
import java.util.concurrent.TimeUnit;
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代码:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22
| 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;
@RestController public class HelloController { @Autowired private IHelloService helloService;
@GetMapping("/{name}") public String sayHello(@PathVariable String name) { return helloService.sayHello(name); } }
|
service实现类:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33
| 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;
@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端的返回。
完整代码:
server:https://github.com/jia-shun/grpc-server
client:https://github.com/jia-shun/grpc-client

快掏出你的大手机扫我