RPC 协议
RPC(Remote Procedure Call Protocol)即远程过程调用协议,它是一种通过网络从远程计算机程序上请求服务的协议,允许一个计算机程序可以像调用本地服务一样调用远程服务
。
RPC的主要作用是不同的服务间方法调用就像本地调用一样便捷,它隐藏了网络通信的细节,使得开发者可以像调用本地函数一样调用远程函数,而无需关注底层网络通信的复杂性。
由于客户端和服务端部署在不同的机器上,服务间的调用就会涉及到网络通信,就需要写一堆网络通信相关的代码。
例如在调用 RESTful 服务时,调用端需要使用 HttpClient 设置很多参数,再去解析状态和返回值,非常复杂且易出错。
而 RPC 的方式可以让我们像调用本地服务一样调用远程服务,而且不必关心网络通信的细节。
RPC原理
Socket套接字
网络上的两个程序通过一个双向的通信连接实现数据的交换,这个连接的一端被称为 Socket。
Socket 用于描述 IP 地址和端口,是一个通信连接的句柄,可以用来实现不同的计算机之间的通信,是网络编程接口的具体实现。
Socket 套接字是客户端/服务端网络机构程序的基本组成部分,为程序提供了一种相对简单的机制,可以与另一个本地或远程机器上的程序建立连接,并可以来回发送消息。
我们可以使用它的收发消息功能来设计我们的分布式应用程序,也可以使用这些收发功能把 RPC 调用包装成透明的远程服务调用。
调用过程
实现透明的远程调用的重点是创建客户存根(类似于代理模式中的代理),在生成代理代码后,代理的代码就能与远程服务端通信了,通信的过程是由RPC 框架实现,而调用者就像调用本地代码一样。
对于客户端而言,存根函数就像普通的本地函数,但实际上包含了通过网络发送和接收消息的代码。
- 第1 步,客户端调用本地的客户端存根方法,客户端存根方法会将参数打包并封装成一个或多个网络消息体并发送到服务端。
- 第 2 步,客户端存根通过系统调用,使用操作系统内核提供的 Socket 套接字接口来向远程服务发送我们编码的网络消息。
- 第 3 步,网络消息由内核通过协议(TCP/UDP)传输到远程服务端。
- 第 4 步,服务端存根接收客户端发送的消息,并对参数消息解码,将参数从标准的网络格式转换为特定的语言格式。
- 第 5 步,服务端存根调用服务端的方法,并且将从客户端接收的参数传递给该方法,它来运行具体的功能并返回,这部分代码的执行对客户端来说就是远程过程调用。
- 第 6 步,服务端的方法在执行完成后,会把结果返回到服务端存根代码中。
- 第 7 步,服务端存根在将该返回值编码且序列化后,通过一个或多个网络消息发送给客户端。
- 第 8 步,消息通过网络发送到客户端存根中。
- 第 9 步,客户端存根从本地 Socket 接口中读取结果消息。
- 第 10 步,客户端存根再将结果返回给客户端函数,并且将消息从网络二进制形式转换为本地语言格式,这样就完成了远程服务调用,客户端代码继续执行后续的操作。
IDL接口
为了生成客户端和服务器的存根函数,我们需要定义一个远程调用接口的定义文件,该文件是使用一种叫做 IDL 的接口定义语言编写的。
接口定义类似于函数原型声明:函数名称、输入参数和返回参数。
在 RPC 编译器运行后,客户端和服务端的程序可以编译并链接到各自的存根函数。客户端存根必须实现初始化 RPC 通信的机制,找到服务器并进行连接,并能与远程服务器通信,并对远程过程调用失败的情况进行处理。
RPC框架-gRPC
gRPC 是 Google 主导开发的一个高性能开源的 RPC 框架,基于 HTTP/2 协议标准设计而成,并用 ProtoBuf 作为序列化工具和接口定义语言(IDL),支持多语言开发。
定义序列化数据结构
在一个.proto
文件中定义数据的格式,这个格式由一系列messages
组成,每个message
包含多个字段,字段就是数据的名称和值的配对。
cpp
// The request message containing the user's name.
message HelloRequest {
string name = 1;
}
// The response message containing the greetings
message HelloReply {
string message = 1;
}
}
接着通过.proto
文件来定义gRPC服务,其中RPC方法的参数和返回类型都是上述定义的message
。
cpp
// The greeter service definition.
service Greeter {
// Sends a greeting
rpc SayHello (HelloRequest) returns (HelloReply) {}
}
最后通过protoc
编译器以及gRPC插件来从.proto
文件中生成代码。
protoc是Protocol Buffers的编译器,它能够根据.proto文件中定义的数据结构生成各种语言的数据访问类。在gRPC中,protoc与gRPC插件一起使用,可以生成gRPC客户端和服务器的代码,以及用于填充、序列化和检索消息类型的常规协议缓冲区代码。
实战
项目结构
- api:存放公共代码;
- server:服务端;
- client:客户端。
api
1)首先引入需要的依赖
xml
<properties>
<grpc.version>1.9.0</grpc.version>
<protobuf-java.version>3.5.1</protobuf-java.version>
</properties>
<dependencies>
<dependency>
<groupId>com.google.protobuf</groupId>
<artifactId>protobuf-java</artifactId>
<version>${protobuf-java.version}</version>
</dependency>
<dependency>
<groupId>io.grpc</groupId>
<artifactId>grpc-all</artifactId>
<version>${grpc.version}</version>
<exclusions>
<exclusion>
<artifactId>guava</artifactId>
<groupId>com.google.guava</groupId>
</exclusion>
</exclusions>
</dependency>
<dependency>
<groupId>com.google.guava</groupId>
<artifactId>guava</artifactId>
<version>24.1.1-jre</version>
</dependency>
<dependency> <!-- necessary for Java 9+ -->
<groupId>org.apache.tomcat</groupId>
<artifactId>annotations-api</artifactId>
<version>6.0.53</version>
<scope>provided</scope>
</dependency>
</dependencies>
2)引入插件
xml
<build>
<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.1</version>
<configuration>
<protocArtifact>com.google.protobuf:protoc:3.5.1-1: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>
这个插件可以将我们编写的 .proto 文件自动转为对应的 Java 类。
3)创建 proto 目录,并定义.proto
文件
cpp
syntax = "proto3";
option java_package = "org.young.grpc.demo";
package product;
service ProductInfo {
rpc addProduct (Product) returns (ProductId);
rpc getProduct(ProductId) returns(Product);
}
message Product {
string id = 1;
string name=2;
string description=3;
float price=4;
}
message ProductId {
string value = 1;
}
4)使用插件生成 java 代码
compile
和 compile-custom
都要执行, compile
用来编译消息对象,compile-custom
则依赖消息对象,生成接口服务。
server
server 项目依赖 api,在 server 中,提供 ProductInfo 的具体实现。
cpp
@Slf4j
public class ProductInfoImpl extends ProductInfoGrpc.ProductInfoImplBase {
/**
* @param request
* @param responseObserver
*/
@Override
public void addProduct(ProductOuterClass.Product request, StreamObserver<ProductOuterClass.ProductId> responseObserver) {
log.info("request:{}", request.toString());
responseObserver.onNext(ProductOuterClass.ProductId.newBuilder().setValue(request.getId()).build()); // 方法响应
responseObserver.onCompleted(); // 标记 RPC 调用完成
}
/**
* @param request
* @param responseObserver
*/
@Override
public void getProduct(ProductOuterClass.ProductId request, StreamObserver<ProductOuterClass.Product> responseObserver) {
responseObserver.onNext(ProductOuterClass.Product.newBuilder().setId(request.getValue()).setName("test_grpc").build());
responseObserver.onCompleted();
}
}
然后再启动该项目
cpp
public class ServerBootStrap {
Server server;
public static void main(String[] args) throws Exception {
ServerBootStrap serverBootStrap = new ServerBootStrap();
serverBootStrap.start();
serverBootStrap.blockUntilShutdown();
}
void start() throws Exception{
server = NettyServerBuilder.forPort(50091).addService(new ProductInfoImpl())
.build()
.start();
Runtime.getRuntime().addShutdownHook(new Thread() {
@Override
public void run() {
ServerBootStrap.this.stop();
}
});
}
private void stop() {
if (server != null) {
server.shutdown();
}
}
private void blockUntilShutdown() throws InterruptedException {
if (server != null) {
server.awaitTermination();
}
}
}
client
client 项目也需要依赖 api ,然后直接进行方法调用,如下:
cpp
@Slf4j
public class GrpcClient {
public static void main(String[] args) {
ManagedChannel managedChannel = ManagedChannelBuilder.forAddress("localhost", 50091).usePlaintext(true).build();
ProductInfoGrpc.ProductInfoBlockingStub stub = ProductInfoGrpc.newBlockingStub(managedChannel);
ProductOuterClass.Product req = ProductOuterClass.Product.newBuilder()
.setId("1")
.setPrice(100.0f)
.setName("test-grpc")
.setDescription("test-grpc")
.build();
ProductOuterClass.ProductId productId = stub.addProduct(req);
log.info("productId.getValue:{}", productId.getValue());
ProductOuterClass.Product product = stub.getProduct(ProductOuterClass.ProductId.newBuilder().setValue("99999").build());
log.info("product:{}", product.toString());
}
}