在微服务这个时代,不论是传输还是内网调用,以及跨语言的传输,RPC都是不二的选择。说到RPC(Remote Process Communication,远程过程调用)就不得不说到进程间通信(Inter-process Communication,简称IPC),IPC是指多个进程之间传送数据或信号的一些技术或方法。而IPC又分为本地过程调用(LPC)和远程过程调用(RPC),这两者的区别就是 LPC的调用可以共享内存空间,比较方便;而RPC的调用双方则不在同一个主机中,无法像LPC那样方便。说到底,RPC就是在本地来 调用远程的方法。而RPC框架要做的就是 让远程调用 像本地调用一样方便,而这个调用过程则是RPC框架需要做的工作。这个工作涉及到大概 两个方面:序列化协议 和 *传输协议*。
1、常见的序列化协议有:
基于文本(text)的:XML、JSON
基于二进制(binary)的: Protocol Buffer、Thrift 等
2、常见的传输协议有:
传输层的: TCP(基于socket)、UDP
应用层的:HTTP1.1、HTTP2.0
RPC简介
现如今已经出现了许多优秀的RPC框架,比如:gRPC、Dubbo、Thrift等。这些框架在 序列化协议和传输协议两部分都有不同的选择,各有优劣。不过,没必要全部都去学习一遍,可以简单尝试一种,大概了解运行机制即可。业界主流的 RPC 框架整体上分为三类:
- 支持多语言的 RPC 框架,比较成熟的有 Google 的 gRPC、Apache(Facebook)的 Thrift;
- 只支持特定语言的 RPC 框架,例如新浪微博的 Motan;
- 支持服务治理等服务化特性的分布式服务框架,其底层内核仍然是 RPC 框架, 例如阿里的 Dubbo。
随着微服务的发展,基于语言中立性原则构建微服务,逐渐成为一种主流模式,例如对于后端并发处理要求高的微服务,比较适合采用 Go 语言构建,而对于前端的 Web 界面,则更适合 Java 和 JavaScript。
RPC和restful api对比
REST是一种设计风格,它的很多思维方式与RPC是完全冲突的。 RPC的思想是把本地函数映射到API,也就是说一个API对应的是一个function,我本地有一个getAllUsers,远程也能通过某种约定的协议来调用这个getAllUsers。至于这个协议是Socket、是HTTP还是别的什么并不重要; RPC中的主体都是动作,是个动词,表示我要做什么。 而REST则不然,它的URL主体是资源,是个名词。而且也仅支持HTTP协议,规定了使用HTTP Method表达本次要做的动作,类型一般也不超过那四五种。这些动作表达了对资源仅有的几种转化方式。RPC的根本问题是耦合。RPC客户端以多种方式与服务实现紧密耦合,并且很难在不中断客户端的情况下更改服务实现。RPC更偏向内部调用,REST更偏向外部调用。一般我们考虑用 RPC 而不是 HTTP 构建自己的服务,通常是考虑到下面的因素:
- 接口是否需要 Schema 约束
- 是否需要更高效的传输协议(TCP,HTTP 2.0)
- 是否对数据包的大小非常敏感
比如 HTTP 是基于文本的协议,头部有非常多冗余(对于 RPC 服务而言)。HTTP 中我们用的最多就是 RESTful ,而 RESTful 是个弱 Schema 约束,大家通过文档沟通,但是如果我就是不在实现的时候对接口文档约定的参数做检查,你也不能把我怎么样。这个时候 Thrift 这种序列化协议的优势就体现出来了,由于 Schema 的存在,可以保证服务端接受的参数和 Schema 保持一致。
GRPC框架
GRPC是Google基于protocol buffer传输协议开发的一个RPC框架。gRPC 里客户端应用可以像调用本地对象一样直接调用另一台不同的机器上服务端应用的方法,使得您能够更容易地创建分布式应用和服务。与许多 RPC 系统类似,gRPC 也是基于以下理念:定义一个服务,指定其能够被远程调用的方法(包含参数和返回类型)。在服务端实现这个接口,并运行一个 gRPC 服务器来处理客户端调用。在客户端拥有一个存根能够像服务端一样的方法。
grpc框架有以下特点
- 语言中立,支持多种语言通信;
- 基于 IDL ( 接口定义语言(Interface Define Language))文件定义服务,通过 proto3 工具生成指定语言的数据结构、服务端接口以及客户端 Stub;
- 通信协议基于标准的 HTTP/2 设计,支持·双向流、消息头压缩、单 TCP 的多路复用、服务端推送等特性,这些特性使得 gRPC 在移动端设备上更加省电和节省网络流量;
- 序列化支持 PB(Protocol Buffer)和 JSON,PB 是一种语言无关的高性能序列化框架,基于 HTTP/2 + PB, 保障了 RPC 调用的高性能。
gRPC原则和诉求
- 通用并且高性能 : 该框架应该适用于绝大多数用例场景,相比针对特定用例的框架,该框架只会牺牲一点性能。
- 分层的: 该框架的关键是必须能够独立演进。对数据传输格式(Wire Format)的修改不应该影响应用层。
- 负载无关的 : 不同的服务需要使用不同的消息类型和编码,例如protocol buffers、JSON、XML和Thrift,协议上和实现上必须满足这样的诉求。类似地,对负载压缩的诉求也因应用场景和负载类型不同而不同,协议上应该支持可插拔的压缩机制。
- 流 : 存储系统依赖于流和流控来传递大数据集。像语音转文本或股票代码等其它服务,依靠流表达时间相关的消息序列。
- 阻塞式和非阻塞式:支持异步和同步处理在客户端和服务端间交互的消息序列。这是在某些平台上缩放和处理流的关键。
- 取消和超时 : 有的操作可能会用时很长,客户端运行正常时,可以通过取消操作让服务端回收资源。当任务因果链被追踪时,取消可以级联。客户端可能会被告知调用超时,此时服务就可以根据客户端的需求来调整自己的行为。
- Lameducking : 服务端必须支持优雅关闭,优雅关闭时拒绝新请求,但继续处理正在运行中的请求。
- 流控: 在客户端和服务端之间,计算能力和网络容量往往是不平衡的。流控可以更好的缓冲管理,以及保护系统免受来自异常活跃对端的拒绝服务(DOS)攻击。
- 可插拔的 : 数据传输协议(Wire Protocol)只是功能完备API基础框架的一部分。大型分布式系统需要安全、健康检查、负载均衡和故障恢复、监控、跟踪、日志等。实现上应该提供扩展点,以允许插入这些特性和默认实现。
- API扩展 :可能的话,在服务间协作的扩展应该最好使用接口扩展,而不是协议扩展。这种类型的扩展可以包括健康检查、服务内省、负载监测和负载均衡分配。
- 元数据交换 :常见的横切关注点,如认证或跟踪,依赖数据交换,但这不是服务公共接口中的一部分。部署依赖于他们将这些特性以不同速度演进到服务暴露的个别API的能力。
gRPC的Java示例
创建一个maven项目,创建
src/main/proto
目录 (插件寻找proto文件的默认目录),在其中添加定义好的远程API接口hello.proto
文件,如下// 使用该proto文件可以定义交互的服务接口,基于该文件编译成的源文件可以分别复制到 client端和server端,便于两者使用 syntax = "proto3"; // 定义语法类型 package hello; // 定义作用域 option java_multiple_files = false; // 表示下面的message不需要编译成多个java文件 option java_outer_classname = "HelloMessage"; // 表示下面的message编译成的java类文件的名字 option java_package = "grpc"; //指定该proto文件编译成的java源文件的包名 service Hello { // 定义服务 rpc sayHello(HelloRequest) returns(HelloResponse) {} } message HelloRequest { // 定义请求的消息体 string name = 1; } message HelloResponse { // 定义回复的消息体 string message = 1; }
可能有人会对这个
.proto
文件有疑问,不知道这个文件有什么作用。我们在使用JSON时开发接口时也需要用Work或Excel写接口文档,以方便前后端联调。那这个.proto
文件其实就是定义了gRPC的请求响应的报文,只不过它是以proto3的语法来约束,而我们用Work或Excel写的接口文档是自己定义的,形式不同罢了。但是这个.proto
除了有文档的作用,还有另一个作用就是开源通过这个文件反向生成代码,这需要借助谷歌的GRPC的工具在
pom.xml
文件中添加`gRPC依赖与插件,如下<!-- grpc依赖 --> <dependencies> <dependency> <groupId>io.grpc</groupId> <artifactId>grpc-netty-shaded</artifactId> <version>1.31.1</version> </dependency> <dependency> <groupId>io.grpc</groupId> <artifactId>grpc-protobuf</artifactId> <version>1.31.1</version> </dependency> <dependency> <groupId>io.grpc</groupId> <artifactId>grpc-stub</artifactId> <version>1.31.1</version> </dependency> </dependencies> <!-- grpc插件 --> <build> <finalName>com.ytf.rpc.demo</finalName> <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.0</version> <configuration> <protocArtifact>com.google.protobuf:protoc:3.1.0:exe:${os.detected.classifier}</protocArtifact> <pluginId>grpc-java</pluginId> <pluginArtifact>io.grpc:protoc-gen-grpc-java:1.0.1:exe:${os.detected.classifier}</pluginArtifact> </configuration> <executions> <execution> <goals> <goal>compile</goal> <goal>compile-custom</goal> </goals> </execution> </executions> </plugin> <plugin> <groupId>org.apache.maven.plugins</groupId> <artifactId>maven-compiler-plugin</artifactId> <version>2.3.2</version> <configuration> <source>1.8</source> <target>1.8</target> </configuration> </plugin> </plugins> </build>
执行 mvn clean compile 命令,然后在生成的 target/generated-sources/ptotobuf 文件夹中找到 服务接口对应的类文件 和 接口参数/返回值对应的类文件。将这两个文件分别复制到client端 和 server端的java源文件中。如下图两个红框是proto插件生成的类,把它们复制到工程中的 java 目录下,复制过去后会报类名重复,删除target下的类文件即可
编写server端代码
package com.example.service; import io.grpc.Server; import io.grpc.ServerBuilder; import java.io.IOException; /** * @Auther: Lushunjian * @Date: 2021/3/6 15:29 * @Description: */ public class GrpcServer { private Server server; /** * @param port 服务端占用的端口 */ public GrpcServer(int port) { server = ServerBuilder.forPort(port) // 将具体实现的服务添加到gRPC服务中 .addService(new HelloRpcService()) .build(); } public void start() throws IOException { server.start(); } public void shutdown() { server.shutdown(); } /** * 阻塞server直到关闭程序 * @throws InterruptedException */ public void blockUntilShutdown() throws InterruptedException { if (server != null) { server.awaitTermination(); } } }
继承生成的proto中定义的服务接口,重写方法
```java
package com.example.service;
import com.example.grpc.HelloGrpc;
import com.example.grpc.HelloWorldProto;
/**
* @Auther: Lushunjian
* @Date: 2021/3/6 15:22
* @Description:
*/
public class HelloRpcService extends HelloGrpc.HelloImplBase {
/**
* proto文件被编译后,在生成的HelloGrpc的抽象内部类HelloImplBase中包含了 proto中定义的服务接口的简单实现
* 该HelloImpl类需要重写这些方法,添加需要的处理逻辑
*/
public void sayHello(HelloWorldProto.HelloRequest request,
io.grpc.stub.StreamObserver<HelloWorldProto.HelloResponse> responseObserver) {
String name = request.getName();
HelloWorldProto.HelloResponse resp = HelloWorldProto.HelloResponse.newBuilder()
.setMessage("Hello! I am the grpc server,your name is " + name + "!")
.build();
// 调用onNext()方法来通知gRPC框架把reply 从server端 发送回 client端
responseObserver.onNext(resp);
// 表示完成调用
responseObserver.onCompleted();
}
}
server端服务启动类
package com.example.start;
import com.example.service.GrpcServer;
import java.io.IOException;
/**
* @Auther: Lushunjian
* @Date: 2021/3/6 15:31
* @Description:
*/
public class HelloWorldApp {
public static void main(String[] args) throws IOException, InterruptedException {
GrpcServer serverDemo=new GrpcServer(8081);
//启动server
serverDemo.start();
System.out.println("grpc server start");
//block 一直到退出程序
serverDemo.blockUntilShutdown();
}
}
编写client端代码,刚上图中proto生成的两个类文件也要拷贝到客户端代码中,客户端也需要用到其中的类
package com.example; import com.example.grpc.HelloGrpc; import com.example.grpc.HelloWorldProto; import io.grpc.ManagedChannel; import io.grpc.ManagedChannelBuilder; /** * @Auther: Lushunjian * @Date: 2021/3/6 15:42 * @Description: */ public class GrpcClient { private final HelloGrpc.HelloBlockingStub blockingStub; /** * @param host gRPC服务的主机名 * @param port gRPC服务的端口 */ public GrpcClient(String host, int port) { ManagedChannel managedChannel = ManagedChannelBuilder.forAddress(host, port) // 使用非安全机制传输 .usePlaintext() .build(); blockingStub = HelloGrpc.newBlockingStub(managedChannel); } public String sayHello(String name) { HelloWorldProto.HelloRequest request = HelloWorldProto.HelloRequest.newBuilder().setName(name).build(); HelloWorldProto.HelloResponse resp = blockingStub.sayHello(request); return resp.getMessage(); } public static void main(String[] args) throws Exception { GrpcClient client = new GrpcClient("127.0.0.1", 8081); String reply = client.sayHello("Frank"); System.out.println("服务端返回:"+reply); } }
分别启动服务端和客户端
启动客户端调用服务端的方法,客户端输出了服务端返回的
Hello! I am the grpc server,your name is + name
,说明grpc服务运行正常