C/C++教程

从零开始学gRPC(一)

本文主要是介绍从零开始学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下面,注意位置还有名字是否弄错,不然生成不了代码。
图片描述

代码

服务端:代码地址

客户端:代码地址

特性讲解

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

Stream

例子

  • proto: 代码地址
  • 代码目录: 代码地址

总结

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

TLS

例子

  • proto: 代码地址
  • 代码目录: 代码地址

(还是helloworld的例子)

总结

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

SLB 和 Health Check

例子

  • proto: 代码地址
  • 代码目录: 代码地址

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

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

  2. target字符串做调整
    图片描述
    图片描述

  3. 新增两个类,代码如下。

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
    图片描述
    图片描述

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

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

总结

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

Hedging

例子

  • proto: 代码地址
  • 代码目录: 代码地址
  • 配置文件: 代码地址

总结

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

写在最后

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

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

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

踩坑笔记

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

参考文献

grpc官方文档

这篇关于从零开始学gRPC(一)的文章就介绍到这儿,希望我们推荐的文章对大家有所帮助,也希望大家多多支持为之网!