gRPC作为当前最热门的RPC框架之一,以其独特的跨语言、跨平台特性,赢得许多公司的青睐。
老实说,之前我只是道听途说并没有认真去研究,今天我会根据官网的demo展开介绍整个gRPC的功能,
后面一篇会介绍gRPC如何整合到SpringCloud。
我这里只提供了搭建demo工程的资料,建议自己动手来操作。没有截图项目也是因为官方的资料相当齐全,没必要重复造轮子。
在直接使用gRPC之前,我们先了解下它的所有特性。官方描述 我就不展开讲了,gRPC有以下几点主要功能:
自己创建一个maven项目就好,没有别的要求(All in one,暂时不需要分模块)。想要偷懒的同学也可以借鉴我的项目,这里已经给你准备好了。但是我建议自己动手尝试搭建比较好,不然印象不太深刻,等于没学(有个词儿叫行动废人,虽然不好听但目的是希望能让你动手实践)。
说明: 这里之所以没用官方的demo是因为我本地实在编译不出来,而且官方给的项目太大,对于学习来说没必要全部下载。(我只想要examples模块,但是必须强制全部下载,就很烦)
当然,要下载的朋友看这里 。
官方的中文文档地址是这个 ,可以按照官方提供的文档做。但如果只是玩具,那我这种All on one的方式我觉得更简单。
我们直接拿官方的例子来讲解,官方代码地址 。里面有很多例子,我这里只讲部分。
我们到官方地址 抄作业时要注意,整个目录的结构。
proto文件只能放在模块的src/main下面,注意位置还有名字是否弄错,不然生成不了代码。
服务端:代码地址
客户端:代码地址
我们知道gRPC不仅仅是一个helloworld就能描述清楚的,我下面将官方的代码例子做个分类,顺便总结下。
上述代码想表达的意思是,服务端在收到全部的客户端数据之后再响应回客户端处理结果。实际情况可以是服务端先处理一部分,然后返回部分。
也可以是任意顺序, 重要的是了解如何使用Stream的相关API来做交互。
(还是helloworld的例子)
这个就没啥好说的,用了Http2.0的TLS特性,默认就是开启的。如果客户端要传明文则必须在channel中配置,如下所示。
官方并没提供,下面都是自己的尝试
Client端在创建ManagedChannel时指定SLB策略。默认实现只有 pick_first
和 round_robin
,这里以round_robin
为例子讲解。
target字符串做调整
新增两个类,代码如下。
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 + "]"; } }
新增文件io.grpc.NameResolverProvider
Server端启动多个实现,记得要改端口(可以在server端加些日志来验证)
说明:上面的代码是从grpc-spring-boot-start中抄过来的,后面会细讲。原生的grpc只能用dns,本地没法做,所以采用这种方式验证SLB。
其实在这个过程中我们也能看到,如果要基于grpc功能组件做扩展也是极方便的。
负载均衡的实现是基于静态地址的,也就是说没有动态的服务发现。当然也可以自己扩展,这个会在后面集成grpc-spring-boot-start时会讲。
扩展时只需要实现io.grpc.LoadBalancerProvider然后再配置注册文件就好
这里主要是一些重试(容错)策略的配置,例子中通过人为的注入延迟来测试。了解下参数含义就好
其实我们会发现,原生的grpc整个治理能力偏弱。熔断、降级等功能基本没有,也无法做动态的服务发现,只支持dns来做负载均衡(别看文档里写的有zookeeper协议,但实际不支持)。流量的调度能力也比较弱,并没有类似分组、版本的概念,无法做到灰度发布。
有很多人在grpc的基础上已经添加了服务治理的解决方案,在后面的grpc-spring-boot-start会讲到,大家不用担心。
为什么市面上其他的RPC框架功能都比这齐全,但是越来越多的公司开始转向gRPC了呢?
我个人认为主要是gRPC的传输协议以及服务定义方式都是天然的支持跨语言的,而服务端编程语言越来越多样化,因此越来越多的公司转向gRPC,即使他们要为此写很多的gRPC插件。
不过工作量并没有想想的那么多,比如Dubbo3.0目前就将他的传输层使用gRPC,而服务治理能力则是使用它自身的,只需要将服务治理能力适配到gRPC插件中就好。
grpc官方文档