RPC 基础系列

1. RPC 基础系列

1.1. 聊聊 RPC

什么是 RPC? RPC 好处? 常用的 RPC 框架? RPC 原理?

1.1.1. 前言

学习 Dubbo 时, 里面讲到了 GRPC, 然后最新做的项目中, 也用到了 GRPC, 再结合之前用到的 Thrift, 感觉这块知识掌握程度还是比较弱, 只停留在会用的阶段, 然后也没有形成体系, 就想把这块知识整体梳理一下。

1.1.2. 什么是 RPC

RPC(Remote Procedure Call Protocol) 远程过程调用协议。一个通俗的描述是: 客户端在不知道调用细节的情况下, 调用存在于远程计算机上的某个对象, 就像调用本地应用程序中的对象一样。

比较正式的描述是: 一种通过网络从远程计算机程序上请求服务, 而不需要了解底层网络技术的协议。

那么我们至少从这样的描述中挖掘出几个要点:

  • RPC 是协议: 既然是协议就只是一套规范, 那么就需要有人遵循这套规范来进行实现。目前典型的 RPC 实现包括: Dubbo、Thrift、GRPC、Hetty 等。
  • 网络协议和网络 IO 模型对其透明: 既然 RPC 的客户端认为自己是在调用本地对象。那么传输层使用的是 TCP/UDP 还是 HTTP 协议, 又或者是一些其他的网络协议它就不需要关心了。
  • 信息格式对其透明: 我们知道在本地应用程序中, 对于某个对象的调用需要传递一些参数, 并且会返回一个调用结果。至于被调用的对象内部是如何使用这些参数, 并计算出处理结果的, 调用方是不需要关心的。那么对于远程调用来说, 这些参数会以某种信息格式传递给网络上的另外一台计算机, 这个信息格式是怎样构成的, 调用方是不需要关心的。
  • 应该有跨语言能力: 为什么这样说呢? 因为调用方实际上也不清楚远程服务器的应用程序是使用什么语言运行的。那么对于调用方来说, 无论服务器方使用的是什么语言, 本次调用都应该成功, 并且返回值也应该按照调用方程序语言所能理解的形式进行描述。

1.1.3. 为什么要用 RPC

其实这是应用开发到一定的阶段的强烈需求驱动的。如果我们开发简单的单一应用, 逻辑简单、用户不多、流量不大, 那我们用不着。当我们的系统访问量增大、业务增多时, 我们会发现一台单机运行此系统已经无法承受。此时, 我们可以将业务拆分成几个互不关联的应用, 分别部署在各自机器上, 以划清逻辑并减小压力。此时, 我们也可以不需要 RPC, 因为应用之间是互不关联的。

当我们的业务越来越多、应用也越来越多时, 自然的, 我们会发现有些功能已经不能简单划分开来或者划分不出来。此时, 可以将公共业务逻辑抽离出来, 将之组成独立的服务 Service 应用 。而原有的、新增的应用都可以与那些独立的 Service 应用 交互, 以此来完成完整的业务功能。

所以此时, 我们急需一种高效的应用程序之间的通讯手段来完成这种需求, 所以你看, RPC 大显身手的时候来了!

其实描述的场景也是服务化 、微服务和分布式系统架构的基础场景。即 RPC 框架就是实现以上结构的有力方式。

1.1.4. 常用的 RPC 框架

  • Thrift: thrift 是一个软件框架, 用来进行可扩展且跨语言的服务的开发。它结合了功能强大的软件堆栈和代码生成引擎, 以构建在 C++, Java, Python, PHP, Ruby, Erlang, Perl, Haskell, C#, Cocoa, JavaScript, Node.js, Smalltalk, and OCaml 这些编程语言间无缝结合的、高效的服务。
  • gRPC: 一开始由 google 开发, 是一款语言中立、平台中立、开源的远程过程调用 (RPC) 系统。
  • Dubbo: Dubbo 是一个分布式服务框架, 以及 SOA 治理方案。其功能主要包括: 高性能 NIO 通讯及多协议集成, 服务动态寻址与路由, 软负载均衡与容错, 依赖分析与降级等。Dubbo 是阿里巴巴内部的 SOA 服务化治理方案的核心框架, Dubbo 自 2011 年开源后, 已被许多非阿里系公司使用。
  • Spring Cloud: Spring Cloud 由众多子项目组成, 如 Spring Cloud Config、Spring Cloud Netflix、Spring Cloud Consul 等, 提供了搭建分布式系统及微服务常用的工具, 如配置管理、服务发现、断路器、智能路由、微代理、控制总线、一次性 token、全局锁、选主、分布式会话和集群状态等, 满足了构建微服务所需的所有解决方案。Spring Cloud 基于 Spring Boot, 使得开发部署极其简单。

1.1.5. RPC 原理

1.1.5.1. RPC 调用流程

要让网络通信细节对使用者透明, 我们需要对通信细节进行封装, 我们先看下一个 RPC 调用的流程涉及到哪些通信细节:

  1. 服务消费方 (client) 调用以本地调用方式调用服务;
  2. client stub 接收到调用后负责将方法、参数等组装成能够进行网络传输的消息体;
  3. client stub 找到服务地址, 并将消息发送到服务端;
  4. server stub 收到消息后进行解码;
  5. server stub 根据解码结果调用本地的服务;
  6. 本地服务执行并将结果返回给 server stub;
  7. server stub 将返回结果打包成消息并发送至消费方;
  8. client stub 接收到消息, 并进行解码;
  9. 服务消费方得到最终结果。
    RPC 的目标就是要 2~8 这些步骤都封装起来, 让用户对这些细节透明。
1.1.5.2. 如何做到透明化远程服务调用

怎么封装通信细节才能让用户像以本地调用方式调用远程服务呢? 对 java 来说就是使用代理! java 代理有两种方式: 1) jdk 动态代理; 2) 字节码生成。尽管字节码生成方式实现的代理更为强大和高效, 但代码维护不易, 大部分公司实现 RPC 框架时还是选择动态代理方式。

下面简单介绍下动态代理怎么实现我们的需求。我们需要实现 RPCProxyClient 代理类, 代理类的 invoke 方法中封装了与远端服务通信的细节, 消费方首先从 RPCProxyClient 获得服务提供方的接口, 当执行 helloWorldService.sayHello(“test”) 方法时就会调用 invoke 方法。

public class RPCProxyClient implements java.lang.reflect.InvocationHandler{
    private Object obj;
    public RPCProxyClient(Object obj){
        this.obj=obj;
    }
    /**
     * 得到被代理对象; 
     */
    public static Object getProxy(Object obj){
        return java.lang.reflect.Proxy.newProxyInstance(obj.getClass().getClassLoader(),
                obj.getClass().getInterfaces(), new RPCProxyClient(obj));
    }
    /**
     * 调用此方法执行
     */
    public Object invoke(Object proxy, Method method, Object[] args)
            throws Throwable {
        //结果参数; 
        Object result = new Object();
        // ... 执行通信相关逻辑
        // ...
        return result;
    }
}
public class Test {
     public static void main(String[] args) {         
     HelloWorldService helloWorldService = (HelloWorldService)RPCProxyClient.getProxy(HelloWorldService.class);
     helloWorldService.sayHello("test");
    }
}

其实就是通过动态代理模式, 在执行该方法的前后对数据进行封装和解码等, 让用感觉就像是直接调用该方法一样, 殊不知, 我们对方法前后都经过了复杂的处理。

1.1.5.3. 如何对消息进行编码和解码
1.1.5.3.1. 确定消息数据结构

客户端的请求消息结构一般需要包括以下内容:

  • 接口名称: 在我们的例子里接口名是"HelloWorldService", 如果不传, 服务端就不知道调用哪个接口了;
  • 方法名: 一个接口内可能有很多方法, 如果不传方法名服务端也就不知道调用哪个方法;
  • 参数类型&参数值: 参数类型有很多, 比如有 bool、int、long、double、string、map、list, 甚至如 struct 等, 以及相应的参数值;
  • 超时时间 + requestID(标识唯一请求 id)
    服务端返回的消息结构一般包括以下内容:
  • 状态 code + 返回值
  • requestID

1.1.6. 序列化

一旦确定了消息的数据结构后, 下一步就是要考虑序列化与反序列化了。

  • 什么是序列化? 序列化就是将数据结构或对象转换成二进制串的过程, 也就是编码的过程。
  • 什么是反序列化? 将在序列化过程中所生成的二进制串转换成数据结构或者对象的过程。
  • 为什么需要序列化? 转换为二进制串后才好进行网络传输嘛!
  • 为什么需要反序列化? 将二进制转换为对象才好进行后续处理!

现如今序列化的方案越来越多, 每种序列化方案都有优点和缺点, 它们在设计之初有自己独特的应用场景, 那到底选择哪种呢? 从 RPC 的角度上看, 主要看三点:

  • 通用性: 比如是否能支持 Map 等复杂的数据结构;
  • 性能: 包括时间复杂度和空间复杂度, 由于 RPC 框架将会被公司几乎所有服务使用, 如果序列化上能节约一点时间, 对整个公司的收益都将非常可观, 同理如果序列化上能节约一点内存, 网络带宽也能省下不少;
  • 可扩展性: 对互联网公司而言, 业务变化飞快, 如果序列化协议具有良好的可扩展性, 支持自动增加新的业务字段, 而不影响老的服务, 这将大大提供系统的灵活度。

目前互联网公司广泛使用 Protobuf、Thrift、Avro 等成熟的序列化解决方案来搭建 RPC 框架, 这些都是久经考验的解决方案。

消息里为什么要有 requestID? 这个问题很简单, 就不说明了, 你能回答出来么?

1.1.7. 如何发布自己的服务

这个我前面的很多文章都提到过, Java 常用 zookeeper, Go 常用 ETCD, 服务端进行注册和心跳, 客户端获取机器列表, 没啥高深的, 比如 zookeeper:

1.2. 一文搞懂 gRPC 和 Thrift 的基本原理和区别

1.2.1. 前言

之前对于 gRPC 和 Thrift 只停留在会用的阶段, 虽然也初步了解过两者的执行流程, 但时间一长又忘了, 如果让我评估两者如何选型, 我更是蒙圈。所以就想把之前学习的知识整理一下, 来填补自己的知识盲区。

1.2.2. Rpc 回顾

RPC 框架的目标就是让远程服务调用更加简单、透明, RPC 框架负责屏蔽底层的传输方式 (TCP 或者 UDP)、序列化方式 (XML/Json/ 二进制)和通信细节。服务调用者可以像调用本地接口一样调用远程的服务提供者, 而不需要关心底层通信细节和调用过程。

1.2.3. gRPC

1.2.3.1. gRPC 简介

gRPC 是一个高性能、通用的开源 RPC 框架, 其由 Google 2015 年主要面向移动应用开发并基于 HTTP/2 协议标准而设计, 基于 ProtoBuf 序列化协议开发, 且支持众多开发语言。
由于是开源框架, 通信的双方可以进行二次开发, 所以客户端和服务器端之间的通信会更加专注于业务层面的内容, 减少了对由 gRPC 框架实现的底层通信的关注。
如下图, DATA 部分即业务层面内容, 下面所有的信息都由 gRPC 进行封装。

1.2.3.2. gRPC 特点
  • 语言中立, 支持多种语言;
  • 基于 IDL 文件定义服务, 通过 proto3 工具生成指定语言的数据结构、服务端接口以及客户端 Stub;
  • 通信协议基于标准的 HTTP/2 设计, 支持双向流、消息头压缩、单 TCP 的多路复用、服务端推送等特性, 这些特性使得 gRPC 在移动端设备上更加省电和节省网络流量;
  • 序列化支持 PB(Protocol Buffer) 和 JSON, PB 是一种语言无关的高性能序列化框架, 基于 HTTP/2 + PB, 保障了 RPC 调用的高性能。
1.2.3.3. gRPC 交互过程
  • 交换机在开启 gRPC 功能后充当 gRPC 客户端的角色, 采集服务器充当 gRPC 服务器角色;
  • 交换机会根据订阅的事件构建对应数据的格式 (GPB/JSON), 通过 Protocol Buffers 进行编写 proto 文件, 交换机与服务器建立 gRPC 通道, 通过 gRPC 协议向服务器发送请求消息;
  • 服务器收到请求消息后, 服务器会通过 Protocol Buffers 解译 proto 文件, 还原出最先定义好格式的数据结构, 进行业务处理;
  • 数据处理完后, 服务器需要使用 Protocol Buffers 重编译应答数据, 通过 gRPC 协议向交换机发送应答消息;
  • 交换机收到应答消息后, 结束本次的 gRPC 交互。

简单地说, gRPC 就是在客户端和服务器端开启 gRPC 功能后建立连接, 将设备上配置的订阅数据推送给服务器端。我们可以看到整个过程是需要用到 Protocol Buffers 将所需要处理数据的结构化数据在 proto 文件中进行定义。

1.2.3.4. 什么是 Protocol Buffers?

你可以理解 ProtoBuf 是一种更加灵活、高效的数据格式, 与 XML、JSON 类似, 在一些高性能且对响应速度有要求的数据传输场景非常适用。ProtoBuf 在 gRPC 的框架中主要有三个作用:

  • 定义数据结构
  • 定义服务接口
  • 通过序列化和反序列化, 提升传输效率
1.2.3.5. 为什么 ProtoBuf 会提高传输效率呢?

我们知道使用 XML、JSON 进行数据编译时, 数据文本格式更容易阅读, 但进行数据交换时, 设备就需要耗费大量的 CPU 在 I/O 动作上, 自然会影响整个传输速率。Protocol Buffers 不像前者, 它会将字符串进行序列化后再进行传输, 即二进制数据。

可以看到其实两者内容相差不大, 并且内容非常直观, 但是 Protocol Buffers 编码的内容只是提供给操作者阅读的, 实际上传输的并不会以这种文本形式, 而是序列化后的二进制数据。字节数会比 JSON、XML 的字节数少很多, 速率更快。

1.2.3.6. 如何支撑跨平台, 多语言呢?

Protocol Buffers 自带一个编译器也是一个优势点。前面提到的 proto 文件就是通过编译器进行编译的, proto 文件需要编译生成一个类似库文件, 基于库文件才能真正开发数据应用。具体用什么编程语言编译生成这个库文件呢? 由于现网中负责网络设备和服务器设备的运维人员往往不是同一组人, 运维人员可能会习惯使用不同的编程语言进行运维开发, 那么 Protocol Buffers 其中一个优势就能发挥出来——跨语言。

从上面的介绍, 我们得出在编码方面 Protocol Buffers 对比 JSON、XML 的优点:

  • 简单, 体积小, 数据描述文件大小只有 1/10 至 1/3;
  • 传输和解析的速率快, 相比 XML 等, 解析速度提升 20 倍甚至更高;
  • 可编译性强。
1.2.3.7. 基于 HTTP 2.0 标准设计

除了 Protocol Buffers 之外, 从交互图中和分层框架可以看到, gRPC 还有另外一个优势——它是基于 HTTP 2.0 协议的。

由于 gRPC 基于 HTTP 2.0 标准设计, 带来了更多强大功能, 如多路复用、二进制帧、头部压缩、推送机制。这些功能给设备带来重大益处, 如节省带宽、降低 TCP 连接次数、节省 CPU 使用等。gRPC 既能够在客户端应用, 也能够在服务器端应用, 从而以透明的方式实现两端的通信和简化通信系统的构建。

HTTP 版本分为 HTTP 1.X、 HTTP 2.0, 其中 HTTP 1.X 是当前使用最广泛的 HTTP 协议, HTTP 2.0 称为超文本传输协议第二代。HTTP 1.X 定义了四种与服务器交互的方式, 分别为: GET、POST、PUT、DELETE, 这些在 HTTP 2.0 中均保留。HTTP 2.0 的新特性:

  • 双向流、多路复用
  • 二进制帧
  • 头部压缩

1.2.4. Thrift

1.2.4.1. Thrift 简介

thrift 是一种可伸缩的跨语言服务的 RPC 软件框架。它结合了功能强大的软件堆栈的代码生成引擎, 以建设服务, 高效、无缝地在多种语言间结合使用。2007 年由 facebook 贡献到 apache 基金, 是 apache 下的顶级项目, 具备如下特点:

  • 支持多语言: C、C++ 、C# 、D 、Delphi 、Erlang 、Go 、Haxe 、Haskell 、Java 、JavaScript、node.js 、OCaml 、Perl 、PHP 、Python 、Ruby 、SmallTalk
  • 消息定义文件支持注释, 数据结构与传输表现的分离, 支持多种消息格式
  • 包含完整的客户端/服务端堆栈, 可快速实现 RPC, 支持同步和异步通信
1.2.4.2. Thrift 框架结构

Thrift 是一套包含序列化功能和支持服务通信的 RPC(远程服务调用)框架, 也是一种微服务框架。其主要特点是可以跨语言使用, 这也是这个框架最吸引人的地方。
图中 code 是用户实现的业务逻辑, 接下来的 Service.Client 和 write()/read() 是 thrift 根据 IDL 生成的客户端和服务端的代码, 对应于 RPC 中 Client stub 和 Server stub。TProtocol 用来对数据进行序列化与反序列化, 具体方法包括二进制, JSON 或者 Apache Thrift 定义的格式。TTransport 提供数据传输功能, 使用 Apache Thrift 可以方便地定义一个服务并选择不同的传输协议。

1.2.4.3. Thrift 网络栈结构

thirft 使用 socket 进行数据传输, 数据以特定的格式发送, 接收方进行解析。我们定义好 thrift 的 IDL 文件后, 就可以使用 thrift 的编译器来生成双方语言的接口、model, 在生成的 model 以及接口代码中会有解码编码的代码。thrift 网络栈结构如下:

1.2.4.4. Transport 层

代表 Thrift 的数据传输方式, Thrift 定义了如下几种常用数据传输方式:

  • TSocket: 阻塞式 socket;
  • TFramedTransport: 以 frame 为单位进行传输, 非阻塞式服务中使用;
  • TFileTransport: 以文件形式进行传输。
1.2.4.5. TProtocol 层

代表 thrift 客户端和服务端之间传输数据的协议, 通俗来讲就是客户端和服务端之间传输数据的格式(例如 json 等), thrift 定义了如下几种常见的格式:

  • TBinaryProtocol: 二进制格式;
  • TCompactProtocol: 压缩格式;
  • TJSONProtocol: JSON 格式;
  • TSimpleJSONProtocol: 提供只写的 JSON 协议。
1.2.4.6. Server 模型
  • TSimpleServer: 简单的单线程服务模型, 常用于测试;
  • TThreadPoolServer: 多线程服务模型, 使用标准的阻塞式 IO;
  • TNonBlockingServer: 多线程服务模型, 使用非阻塞式 IO(需要使用 TFramedTransport 数据传输方式);
  • THsHaServer: THsHa 引入了线程池去处理, 其模型读写任务放到线程池去处理, Half-sync/Half-async 处理模式, Half-async 是在处理 IO 事件上 (accept/read/write io), Half-sync 用于 handler 对 rpc 的同步处理;
1.2.4.7. gRPC VS Thrift

功能比较
直接贴上网上的两幅截图:

性能比较
也是基于网上测试的结果, 仅供参考:

  • 整体上看, 长连接性能优于短连接, 性能差距在两倍以上;
  • 对比 Go 语言的两个 RPC 框架, Thrift 性能明显优于 gRPC, 性能差距也在两倍以上;
  • 对比 Thrift 框架下的的两种语言, 长连接下 Go 与 C++的 RPC 性能基本在同一个量级, 在短连接下, Go 性能大概是 C++的二倍;
  • 对比 Thrift&C++下的 TSimpleServer 与 TNonblockingServer, 在单进程客户端长连接的场景下, TNonblockingServer 因为存在线程管理开销, 性能较 TSimpleServer 差一些; 但在短连接时, 主要开销在连接建立上, 线程池管理开销可忽略;
  • 两套 RPC 框架, 以及两大语言运行都非常稳定, 5w 次请求耗时约是 1w 次的 5 倍;

1.2.5. 如何选型

什么时候应该选择 gRPC 而不是 Thrift:

  • 需要良好的文档、示例
  • 喜欢、习惯 HTTP/2、ProtoBuf
  • 对网络传输带宽敏感

什么时候应该选择 Thrift 而不是 gRPC:

  • 需要在非常多的语言间进行数据交换
  • 对 CPU 敏感
  • 协议层、传输层有多种控制要求
  • 需要稳定的版本
  • 不需要良好的文档和示例

1.2.6. 总结

这篇文章应该非常详细介绍 gRPC 和 Thrift 两者的特点和区别, 目前我还没有发现有哪篇文章总结的比我这还要好, 当然除了源码解读部分(个人不建议上来就解读源码, 知道执行流程和区别, 便于我们使用和选型就可以)。

通篇总结下来, 总结如下:

  • GRPC 主要就是搞了个 ProtoBuf, 然后采用 HTTP 协议, 所以协议部分没有重复造轮子, 重点就在 ProtoBuf 上。
  • Thrift 的数据格式是用的现成的, 没有单独搞一套, 但是它在传输层和服务端全部是自己造轮子, 所以可以对协议层、传输层有多种控制要求。

1.3. gRPC 简单示例

1.3.1. 前言

我们不能只看原理, 而忽略实践, 也不能只关注实践, 而忽略原理, 应该两者兼顾!

1.3.2. gRPC 示例

代码: git@github.com:lml200701158/rpc-study.git

1.3.2.1. 项目结构

我们先看一下项目结构:

生成 protobuf 文件
helloworld.proto

syntax = "proto3";
option java_multiple_files = true;
option java_package = "io.grpc.examples.helloworld";
option java_outer_classname = "HelloWorldProto";
option objc_class_prefix = "HLW";
package helloworld;
// The greeting service definition.
service Greeter {
  // Sends a greeting
  rpc SayHello (HelloRequest) returns (HelloReply) {}
}
// The request message containing the user's name.
message HelloRequest {
  string name = 1;
}
// The response message containing the greetings
message HelloReply {
  string message = 1;
}

这里提供了一个 SayHello() 方法, 然后入参为 HelloRequest, 返回值为 HelloReply, 可以看到 proto 文件只定义了入参和返回值的格式, 以及调用的接口, 至于接口内部的实现, 该文件完全不用关心。

pom.xml

<?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">
    <parent>
        <artifactId>rpc-study</artifactId>
        <groupId>org.example</groupId>
        <version>1.0-SNAPSHOT</version>
    </parent>
    <modelVersion>4.0.0</modelVersion>
    <artifactId>grpc-demo</artifactId>
    <dependencies>
        <dependency>
            <groupId>io.grpc</groupId>
            <artifactId>grpc-netty-shaded</artifactId>
            <version>1.14.0</version>
        </dependency>
        <dependency>
            <groupId>io.grpc</groupId>
            <artifactId>grpc-protobuf</artifactId>
            <version>1.14.0</version>
        </dependency>
        <dependency>
            <groupId>io.grpc</groupId>
            <artifactId>grpc-stub</artifactId>
            <version>1.14.0</version>
        </dependency>
    </dependencies>
    <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:1.14.0:exe:${os.detected.classifier}</pluginArtifact>
                </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>6</source>
                    <target>6</target>
                </configuration>
            </plugin>
        </plugins>
    </build>
</project>

这里面的 build 其实是为了安装 protobuf 插件, 里面其实有 2 个插件我们需要用到, 分别为 protobuf:compile 和 protobuf:compile-javanano, 当我们直接执行时, 会生成左侧文件, 其中 GreeterGrpc 提供调用接口, Hello 开头的文件功能主要是对数据进行序列化, 然后处理入参和返回值。

可能有同学会问, 你把文件生成到 target 中, 我想放到 main.src 中, 你可以把这些文件 copy 出来, 或者也可以通过工具生成:

  • 下载 protoc.exe 工具 , 下载地址: https://github.com/protocolbuffers/protobuf/releases
  • 下载 protoc-gen-grpc 插件 , 下载地址: http://jcenter.bintray.com/io/grpc/protoc-gen-grpc-java/
1.3.2.2. 服务端和客户端

HelloWorldClient.java

public class HelloWorldClient {
    private final ManagedChannel channel;
    private final GreeterGrpc.GreeterBlockingStub blockingStub;
    private static final Logger logger = Logger.getLogger(HelloWorldClient.class.getName());
    public HelloWorldClient(String host,int port){
        channel = ManagedChannelBuilder.forAddress(host,port)
                .usePlaintext(true)
                .build();
        blockingStub = GreeterGrpc.newBlockingStub(channel);
    }
    public void shutdown() throws InterruptedException {
        channel.shutdown().awaitTermination(5, TimeUnit.SECONDS);
    }
    public  void greet(String name){
        HelloRequest request = HelloRequest.newBuilder().setName(name).build();
        HelloReply response;
        try{
            response = blockingStub.sayHello(request);
        } catch (StatusRuntimeException e)
        {
            logger.log(Level.WARNING, "RPC failed: {0}", e.getStatus());
            return;
        }
        logger.info("Message from gRPC-Server: "+response.getMessage());
    }
    public static void main(String[] args) throws InterruptedException {
        HelloWorldClient client = new HelloWorldClient("127.0.0.1",50051);
        try{
            String user = "world";
            if (args.length > 0){
                user = args[0];
            }
            client.greet(user);
        }finally {
            client.shutdown();
        }
    }
}

这个太简单了, 就是连接服务端口, 调用 sayHello() 方法。

HelloWorldServer.java
public class HelloWorldServer {
    private static final Logger logger = Logger.getLogger(HelloWorldServer.class.getName());
    private int port = 50051;
    private Server server;
    private void start() throws IOException {
        server = ServerBuilder.forPort(port)
                .addService(new GreeterImpl())
                .build()
                .start();
        logger.info("Server started, listening on " + port);
        Runtime.getRuntime().addShutdownHook(new Thread() {
            @Override
            public void run() {
                System.err.println("*** shutting down gRPC server since JVM is shutting down");
                HelloWorldServer.this.stop();
                System.err.println("*** server shut down");
            }
        });
    }
    private void stop() {
        if (server != null) {
            server.shutdown();
        }
    }
    // block 一直到退出程序
    private void blockUntilShutdown() throws InterruptedException {
        if (server != null) {
            server.awaitTermination();
        }
    }
    public static void main(String[] args) throws IOException, InterruptedException {
        final HelloWorldServer server = new HelloWorldServer();
        server.start();
        server.blockUntilShutdown();
    }
    // 实现 定义一个实现服务接口的类
    private class GreeterImpl extends GreeterGrpc.GreeterImplBase {
        @Override
        public void sayHello(HelloRequest req, StreamObserver<HelloReply> responseObserver) {
            HelloReply reply = HelloReply.newBuilder().setMessage(("Hello " + req.getName())).build();
            responseObserver.onNext(reply);
            responseObserver.onCompleted();
            System.out.println("Message from gRPC-Client:" + req.getName());
            System.out.println("Message Response:" + reply.getMessage());
        }
    }
}

主要是实现 sayHello() 方法, 里面对数据进行了简单处理, 入参为"W orld", 返回的是"Hello World"

1.3.2.3. 启动服务

先启动 Server, 返回如下:

再启动 Client, 返回如下:

同时 Server 返回如下:

1.3.3. 后记

这个 Demo 看起来很简单, 我 TM 居然搞了大半天, 一开始是因为不知道需要执行 2 个不同的插件来生成 protobuf, 以为只需要点击 protobuf:compile 就可以, 结果发现 protobuf:compile-javanano 也需要点一下。

还有就是我自己喜欢作, 感觉通过插件生成 protobuf 不完美, 我想通过自己下载的插件, 手动生成 protobuf 文件, 结果手动生成的没有搞定, 自动生成的方式也不可用, 搞了半天才发现是缓存的问题, 最后直接执行"Invalidate Caches / Restart"才搞定。

应征了一句话"no zuo no die", 不过这个过程还是需要经历的。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

打赏作者

云满笔记

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

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

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

打赏作者

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

抵扣说明:

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

余额充值