从零开始学grpc(一)

前言

gRPC作为当前最热门的RPC框架之一,以其独特的跨语言、跨平台特性,赢得许多公司的青睐。 老实说,之前我只是道听途说并没有认真去研究,今天我会根据官网的demo展开介绍整个gRPC的功能, 后面一篇会介绍gRPC如何整合到SpringCloud。

我这里只提供了搭建demo工程的资料,建议自己动手来操作。没有截图项目也是因为官方的资料相当齐全,没必要重复造轮子。

gRPC总览

在直接使用gRPC之前,我们先了解下它的所有特性。官方描述 我就不展开讲了,gRPC有以下几点主要功能:

  1. 使用Protocol Buffer 定义服务。
  2. 语言和平台的中立性
  3. 双向流式通讯
  4. 基于HTTP2.0身份认证、SLB、tracing、health check
  5. 组件可扩展。

快速搭建

环境准备

  • 操作系统:Windowns
  • JDK版本:1.8
  • 编辑工具:IntelliJ IDEA Community Edition 2021.1.2 x64

创建项目

自己创建一个maven项目就好,没有别的要求(All in one,暂时不需要分模块)。想要偷懒的同学也可以借鉴我的项目,这里 已经给你准备好了。 但是,建议自己动手尝试搭建比较好,不然印象不太深刻,等于没学(有个词儿叫行动废人,虽然不好听但目的是希望能让你动手实践)。

说明: 这里之所以没用官方的demo是因为我本地实在编译不出来,而且官方给的项目太大,对于学习来说没必要全部下载。(我只想要examples模块,但是必须强制全部下载,就很烦) 当然,要下载的朋友看这里 。 官方的中文文档地址是这个 ,可以按照官方提供的文档做。但如果只是玩具,那我这种All on one的方式我觉得更简单。

Helloworld

我们直接拿官方的例子来讲解,官方代码地址 。里面有很多例子,我这里只讲部分。

proto文件

我们到官方地址 抄作业时要注意,整个目录的结构。 proto文件只能放在模块的src/main下面,注意位置还有名字是否弄错,不然生成不了代码。 img.png

代码

服务端:代码地址

客户端:代码地址

特性讲解

我们知道gRPC不仅仅是一个helloworld就能描述清楚的,我下面将官方的代码例子做个分类,顺便总结下。

Stream

例子

总结

上述代码想表达的意思是,服务端在收到全部的客户端数据之后再响应回客户端处理结果。实际情况可以是服务端先处理一部分,然后返回部分。 也可以是任意顺序, 重要的是了解如何使用Stream的相关API来做交互。

TLS

例子

(还是helloworld的例子)

总结

这个就没啥好说的,用了Http2.0的TLS特性,默认就是开启的。如果客户端要传明文则必须在channel中配置,如下所示。 img.png

SLB 和 Health Check

例子

官方并没提供,下面都是自己的尝试

  1. Client端在创建ManagedChannel时指定SLB策略。默认实现只有 pick_firstround_robin ,这里以round_robin为例子讲解。 img.png

  2. target字符串做调整 img.png

img.png

  1. 新增两个类,代码如下。
public class MyNameResolverProvider extends NameResolverProvider {

  /**
   * The constant containing the scheme that will be used by this factory.
   */
  public static final String STATIC_SCHEME = "static";

  private static final Pattern PATTERN_COMMA = Pattern.compile(",");

  @Nullable
  @Override
  public NameResolver newNameResolver(final URI targetUri, final NameResolver.Args args) {
    if (STATIC_SCHEME.equals(targetUri.getScheme())) {
      return of(targetUri.getAuthority(), args.getDefaultPort());
    }
    return null;
  }

  /**
   * Creates a new {@link NameResolver} for the given authority and attributes.
   *
   * @param targetAuthority The authority to connect to.
   * @param defaultPort The default port to use, if none is specified.
   * @return The newly created name resolver for the given target.
   */
  private NameResolver of(final String targetAuthority, int defaultPort) {
    requireNonNull(targetAuthority, "targetAuthority");
    // Determine target ips
    final String[] hosts = PATTERN_COMMA.split(targetAuthority);
    final List<EquivalentAddressGroup> targets = new ArrayList<>(hosts.length);
    for (final String host : hosts) {
      final URI uri = URI.create("//" + host);
      int port = uri.getPort();
      if (port == -1) {
        port = defaultPort;
      }
      targets.add(new EquivalentAddressGroup(new InetSocketAddress(uri.getHost(), port)));
    }
    if (targets.isEmpty()) {
      throw new IllegalArgumentException("Must have at least one target, but was: " + targetAuthority);
    }
    return new MyStaticNameResolver(targetAuthority, targets);
  }

  @Override
  public String getDefaultScheme() {
    return STATIC_SCHEME;
  }

  @Override
  protected boolean isAvailable() {
    return true;
  }

  @Override
  protected int priority() {
    return 4; // Less important than DNS
  }

  @Override
  public String toString() {
    return "StaticNameResolverProvider [scheme=" + getDefaultScheme() + "]";
  }
}
public class MyStaticNameResolver extends NameResolver {

  private final String authority;
  private final ResolutionResult result;

  /**
   * Creates a static name resolver with only a single target server.
   *
   * @param authority The authority this name resolver was created for.
   * @param target The target address of the server to use.
   */
  public MyStaticNameResolver(final String authority, final EquivalentAddressGroup target) {
    this(authority, ImmutableList.of(requireNonNull(target, "target")));
  }

  /**
   * Creates a static name resolver with multiple target servers.
   *
   * @param authority The authority this name resolver was created for.
   * @param targets The target addresses of the servers to use.
   */
  public MyStaticNameResolver(final String authority, final Collection<EquivalentAddressGroup> targets) {
    this.authority = requireNonNull(authority, "authority");
    if (requireNonNull(targets, "targets").isEmpty()) {
      throw new IllegalArgumentException("Must have at least one target");
    }
    this.result = ResolutionResult.newBuilder()
      .setAddresses(ImmutableList.copyOf(targets))
      .build();
  }

  /**
   * Creates a static name resolver with multiple target servers.
   *
   * @param authority The authority this name resolver was created for.
   * @param result The resolution result to use..
   */
  public MyStaticNameResolver(final String authority, final ResolutionResult result) {
    this.authority = requireNonNull(authority, "authority");
    this.result = requireNonNull(result, "result");
  }

  @Override
  public String getServiceAuthority() {
    return this.authority;
  }

  @Override
  public void start(final Listener2 listener) {
    listener.onResult(this.result);
  }

  @Override
  public void refresh() {
    // Does nothing
  }

  @Override
  public void shutdown() {
    // Does nothing
  }

  @Override
  public String toString() {
    return "StaticNameResolver [authority=" + this.authority + ", result=" + this.result + "]";
  }
}
  1. 新增文件io.grpc.NameResolverProvider img.png

img_1.png

  1. Server端启动多个实现,记得要改端口(可以在server端加些日志来验证)

说明:上面的代码是从grpc-spring-boot-start中抄过来的,后面会细讲。原生的grpc只能用dns,本地没法做,所以采用这种方式验证SLB。 其实在这个过程中我们也能看到,如果要基于grpc功能组件做扩展也是极方便的。

总结

负载均衡的实现是基于静态地址的,也就是说没有动态的服务发现。当然也可以自己扩展,这个会在后面集成grpc-spring-boot-start时会讲。 扩展时只需要实现io.grpc.LoadBalancerProvider然后再配置注册文件就好

Hedging(Http相关的容错)

例子

总结

这里主要是一些重试(容错)策略的配置,例子中通过人为的注入延迟来测试。了解下参数含义就好

写在最后

其实我们会发现,原生的grpc整个治理能力偏弱。熔断、降级等功能基本没有,也无法做动态的服务发现,只支持dns来做负载均衡(别看文档里写的有zookeeper协议,但实际不支持)。流量的调度能力也比较弱,并没有类似分组、版本的概念,无法做到灰度发布。 有很多人在grpc的基础上已经添加了服务治理的解决方案,在后面的grpc-spring-boot-start会讲到,大家不用担心。

为什么市面上其他的RPC框架功能都比这齐全,但是越来越多的公司开始转向gRPC了呢?

我个人认为主要是gRPC的传输协议以及服务定义方式都是天然的支持跨语言的,而服务端编程语言越来越多样化,因此越来越多的公司转向gRPC,即使他们要为此写很多的gRPC插件。 不过工作量并没有想想的那么多,比如Dubbo3.0目前就将他的传输层使用gRPC,而服务治理能力则是使用它自身的,只需要将服务治理能力适配到gRPC插件中就好。

踩坑笔记

  • Idea抽风,扫描不到生成的文件。解决办法: 删除idea缓存,然后重启idea。
  • proto文件生成的代码不全。解决办法: protobuf插件除了运行comple,还要运行custome img.png

传送门

grpc官方文档