RPC与GRPC快速上手(附demo源码)

RPC 快速上手远程过程调用

1. RPC的基本概念

远程过程调用

wikipedia 中关于 RPC 的定义如下:

In distributed computing, a remote procedure call (RPC) is when a computer 
program causes a procedure (subroutine) to execute in a different address 
space(commonly on another computer on a shared network), which is coded as if 
it were a normal (local) procedure call, without the programmer explicitly 
coding the details for the remote interaction.

用中文翻译过来大概就是:

RPC的核心在于使进程间通信像进程内通信一样简单,更直白一点调用其他应用的方法好似调用本地方法一样简单方便。进程内之所以方便是因为写操作系统的人把复杂的事情给处理了,让业务研发专注于业务逻辑。进程间的调用简单是因为写RPC框架的人把复杂的工作给做了,使业务研发继续专注于业务逻辑。

举个例子:比如计算 a + b

如果仅仅是封装为一个函数,其实是很简单的。俩个入参,一个a, 一个b。

public int myAdd(int a, int b){
        return a + b;
    }

远程过程调用是指将这个函数封装到远程的另外一台机器上,也就是本机将 a 和 b 俩个参数打包起来放到一个数据包中,然后通过网络传输的方式,传递到远程的计算机,远程计算机通过函数计算完以后,将结果通过网络传输的方式 返回给调用方。最终就将结果计算出来了。

为什么需要远程过程调用

在分布式系统中,要想让服务A调用服务B中的方法,最先想到的就是通过HTTP请求实现。是的,这是很常见的,例如服务B暴露Restful接口,然后让服务A调用它的接口。基于Restful的调用方式因为可读性好(服务B暴露出的是Restful接口,可读性当然好)而且HTTP请求可以通过各种防火墙,因此非常不错。

然而,在高并发的时候,基于Restful的远程过程调用有着明显的缺点,主要是效率低、封装调用复杂。当存在大量的服务间调用时,这些缺点变得更为突出。因为 HTTP 协议属于无状态协议,客户端无法对请求和响应进行关联,每次请求都需要重新建立连接,响应完成后再关闭连接。因此,对于要求高性能的 RPC 来说,HTTP 协议基本很难满足需求,所以 RPC 会选择设计更紧凑的私有协议。

服务A调用服务B的过程是应用间的内部过程,牺牲可读性提升效率、易用性是可取的。基于这种思路,RPC产生了。

通常,RPC要求在调用方中放置被调用的方法的接口。调用方只要调用了这些接口,就相当于调用了被调用方的实际方法,十分易用。于是,调用方可以像调用内部接口一样调用远程的方法,而不用封装参数名和参数值等操作。

和 http 的区别和联系

在广义上来说, http 是实现 rpc的一种方式。rpc 是一种思想,是指利用网络中的另外一台计算机,进行一个函数的计算,然后调用方进行一次调用,拿到这个结果。

从狭义上来说,一些比较常见的 rpc框架,比如 thrift, dubbo, grpc等,一次http 调用和 一次 rpc调用在真正执行的时候,还是有比较大的区别的。

2. RPC中使用到的技术

序列化

网络传输的数据必须是二进制数据,但调用方请求的出入参数都是对象。对象是肯定没法直接在网络中传输的,需要提前把它转成可传输的二进制,并且要求转换算法是可逆的,这个过程我们一般叫做“序列化”。
也就是将 程序内存中的数据结构 转化为 传输过程中的数据结构,一般来说,就是 纯文本结构 或者是 byte数组,序列化 : JSON 序列化, xml , 属于文本类型的序列化方式。JDK 序列化, Hessian, Protobuf。

首选的还是 Hessian 与 Protobuf,因为他们在性能、时间开销、空间开销、通用性、兼容性和安全性上,都满足了我们的要求。其中 Hessian 在使用上更加方便,在对象的兼容性上更好;Protobuf 则更加高效,通用性上更有优势。

网络IO

RPC 是解决进程间通信的一种方式。一次 RPC 调用,本质就是服务消费者与服务提供者间的一次网络信息交换的过程。服务调用者通过网络 IO 发送一条请求消息,服务提供者接收并解析,处理完相关的业务逻辑之后,再发送一条响应消息给服务调用者,服务调用者接收并解析响应消息,处理完相关的响应逻辑,一次 RPC 调用便结束了。可以说,网络通信是整个 RPC 调用流程的基础。

网络IO 中涉及到很多的系统调用。java中有一个比较有名的rpc框架, dubbo. 底层网络传输结构就使用了 netty, netty 又是基于 nio的,nio 是基于 IO 多路复用的,这其中会涉及到系统调用 syscall.

考虑到系统内核的支持、编程语言的支持以及 IO 模型本身的特点,RPC 框架在网络通信的处理上,我们更倾向选择 IO 多路复用的方式。

动态代理: 面向接口编程,屏蔽RPC处理流程

在项目中,当我们要使用 RPC 的时候,我们一般的做法是先找服务提供方要接口,通过 Maven 或者其他的工具把接口依赖到我们项目中。我们在编写业务逻辑的时候,如果要调用提供方的接口,我们就只需要通过依赖注入的方式把接口注入到项目中就行了,然后在代码里面直接调用接口的方法。

我们都知道,接口里并不会包含真实的业务逻辑,业务逻辑都在服务提供方应用里,但我们通过调用接口方法,确实拿到了想要的结果,是不是感觉有点神奇呢?想一下,在 RPC 里面,我们是怎么完成这个魔术的。

这里面用到的核心技术就是前面说的动态代理。RPC 会自动给接口生成一个代理类,当我们在项目中注入接口的时候,运行过程中实际绑定的是这个接口生成的代理类。这样在接口方法被调用的时候,它实际上是被生成代理类拦截到了,这样我们就可以在生成的代理类里面,加入远程调用逻辑。

3. 远程过程调用在使用过程中的注意事项

  • 下游服务的服务能力,避免因为你的调用把别人给调挂了,要事前协商好qps等,做好限流。

  • 调用服务异常时,要考虑降级、重试等措施。

  • 核心的服务不能强依赖非核心的服务,避免核心服务因为非核心服务异常而不可用。

  • 设置合理超时时间。因为 rpc毕竟需要走网络调用,存在网络耗时。超时间太短,可能导致服务提供端实际执行成功,消费端却因为超时报错结束。这就有可能导致数据状态不一致。另外,整个链路的超时需要合理设置,如A-> B-> C,A的超时时间要大于B。

  • 设置合理的重试次数。默认情况下,如 dubbo 重试次数为2,调用失败的情况下,框架会重新调用。而有些服务不能重复调用。服务提供者应该是最熟悉自己服务的,所以服务提供者可以设置默认超时时间以及重试次数,消费者不设置,就会采用服务提供者参数设置。

4. 快速上手GRPC

GRPC 是由 Google 开发并且开源的一款高性能、跨语言的 RPC 框架,当前支持 C、Java 和 Go 等语言,当前 Java 版本最新 Release 版为 1.27.0。gRPC 有很多特点,比如跨语言,通信协议是基于标准的 HTTP/2 设计的,序列化支持 PB(Protocol Buffer)和 JSON,整个调用示例如下图所示:
在这里插入图片描述

现在以计算 a+b 为例,看一下如何使用 GRPC。

引入以下的 maven 依赖
<?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>a.b.c</groupId>
  <artifactId>grpc-demo</artifactId>
  <version>1.0-SNAPSHOT</version>

  <properties>
    <java.version>1.8</java.version>
    <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
    <grpc.version>1.34.1</grpc.version><!-- CURRENT_GRPC_VERSION -->
    <protobuf.version>3.12.0</protobuf.version>
    <protoc.version>3.12.0</protoc.version>
    <maven.compiler.source>1.8</maven.compiler.source>
    <maven.compiler.target>1.8</maven.compiler.target>
  </properties>
  <dependencyManagement>
    <dependencies>
      <dependency>
        <groupId>io.grpc</groupId>
        <artifactId>grpc-bom</artifactId>
        <version>${grpc.version}</version>
        <type>pom</type>
        <scope>import</scope>
      </dependency>
    </dependencies>
  </dependencyManagement>
  <dependencies>
    <dependency>
      <groupId>io.grpc</groupId>
      <artifactId>grpc-netty-shaded</artifactId>
      <scope>runtime</scope>
    </dependency>
    <dependency>
      <groupId>io.grpc</groupId>
      <artifactId>grpc-protobuf</artifactId>
    </dependency>
    <dependency>
      <groupId>io.grpc</groupId>
      <artifactId>grpc-stub</artifactId>
    </dependency>
    <dependency>
      <groupId>com.google.protobuf</groupId>
      <artifactId>protobuf-java-util</artifactId>
      <version>${protobuf.version}</version>
    </dependency>
    <dependency>
      <groupId>junit</groupId>
      <artifactId>junit</artifactId>
      <version>4.12</version>
      <scope>test</scope>
    </dependency>
  </dependencies>
 <!-- protobuf-maven-plugin  插件 -->
 <!-- protoSourceRoot标签定义了protobuf文件的位置-->
  <build>
    <extensions>
      <extension>
        <groupId>kr.motd.maven</groupId>
        <artifactId>os-maven-plugin</artifactId>
        <version>1.6.2</version>
      </extension>
    </extensions>
    <plugins>
      <plugin>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-maven-plugin</artifactId>
        <configuration>
          <excludes>
            <exclude>
              <groupId>org.projectlombok</groupId>
              <artifactId>lombok</artifactId>
            </exclude>
          </excludes>
        </configuration>
      </plugin>
      <plugin>
        <groupId>org.xolstice.maven.plugins</groupId>
        <artifactId>protobuf-maven-plugin</artifactId>
        <version>0.6.1</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>
          <protoSourceRoot>src/main/resources/proto</protoSourceRoot>
        </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>
        <configuration>
          <source>1.8</source>
          <target>1.8</target>
        </configuration>
      </plugin>
    </plugins>
  </build>

</project>
定义对应的 protobuf 文件。

在 maven 文件中,已经约定好在 src/main/resources/proto 这个目录下定义
protobuf文件。新建 add.proto 文件,定义如下:

 syntax = "proto3";
 
 package grpc;
 option java_package = "a.b.c";
 option java_outer_classname = "AddServiceProto";
 option java_multiple_files = true;
 
 service AddService{
     rpc add(AddRequest) returns (AddReply){}
 }
 
 message AddRequest{
     int32 a = 1;
     int32 b = 2;
 }
 
 message AddReply{
     int32 res = 1;
 }
使用 mvn clean install 命令,生成相应的 java文件。

使用 mvn clean install 命令,生成的文件,是在 target 目录下的。如下:
在这里插入图片描述

定义 server 端
 package a.b.c;
 
 import io.grpc.ServerBuilder;
 import io.grpc.stub.StreamObserver;
 
 import java.io.IOException;
 
 /**
  * Author : LG
  */
 public class AddServer extends AddServiceGrpc.AddServiceImplBase {
     public static void main(String[] args) throws IOException {
         ServerBuilder.forPort(9999)
                 .addService(new AddServer())
                 .build()
                 .start();
         System.out.println("server start at 9999");
         while (true){
 
         }
     }
 
     public void add(AddRequest request, StreamObserver<AddReply> responseObserver) {
         int res = myAdd(request.getA(),request.getB());
         responseObserver.onNext(AddReply.newBuilder().setRes(res).build());
         responseObserver.onCompleted();
     }
 
     private int myAdd(int a, int b){
         return a + b;
     }
 }
定义 client 端
package a.b.c;
import io.grpc.ManagedChannel;
import io.grpc.ManagedChannelBuilder;
public class AddClient {
   AddServiceGrpc.AddServiceBlockingStub stub;
   ManagedChannel channel;

   public static void main(String[] args) {
       int a = 101;
       int b = 102;
       AddClient client = new AddClient();

       AddReply reply =
               client.stub.add(AddRequest.newBuilder().setA(a).setB(b).build());
       System.out.println(reply.getRes());
   }

   public AddClient(){
       channel = ManagedChannelBuilder
               .forAddress("127.0.0.1",9999)
               .usePlaintext()
               .build();
       stub =
               AddServiceGrpc.newBlockingStub(channel);
   }
}
启动

以上步骤定义完以后,先启动 server, 再启动 client。

5. 总结

无论你是一个初级开发者还是高级开发者,RPC 都应该是你日常开发过程中绕不开的一个话题,所以作为软件开发者的我们,真的很有必要详细地了解 RPC 实现细节。只有这样,才能帮助我们更好地在日常工作中使用 RPC。

  • 2
    点赞
  • 4
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 1
    评论
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

爱coding的同学

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值