一、RPC
1. 什么是RPC
RPC(Remote Procedure Call Protocol)远程过程调用协议。一个通俗的描述是:客户端在不知道调用细节的情况下,调用存在于远程计算机上的某个对象,就像调用本地应用程序中的对象一样。
比较正式的描述是:一种通过网络从远程计算机程序上请求服务,而不需要了解底层网络技术的协议。
- RPC是协议:既然是协议就只是一套规范,那么就需要有人遵循这套规范来进行实现。目前典型的RPC实现包括:Dubbo、Thrift、GRPC、Hetty等。
- 网络协议和网络IO模型对其透明:既然RPC的客户端认为自己是在调用本地对象。那么传输层使用的是TCP/UDP还是HTTP协议,又或者是一些其他的网络协议它就不需要关心了。
- 信息格式对其透明:我们知道在本地应用程序中,对于某个对象的调用需要传递一些参数,并且会返回一个调用结果。至于被调用的对象内部是如何使用这些参数,并计算出处理结果的,调用方是不需要关心的。那么对于远程调用来说,这些参数会以某种信息格式传递给网络上的另外一台计算机,这个信息格式是怎样构成的,调用方是不需要关心的。
- 应该有跨语言能力:为什么这样说呢?因为调用方实际上也不清楚远程服务器的应用程序是使用什么语言运行的。那么对于调用方来说,无论服务器方使用的是什么语言,本次调用都应该成功,并且返回值也应该按照调用方程序语言所能理解的形式进行描述。
2. 为什么要用RPC
\qquad
应用开发到一定的阶段的强烈需求驱动的。如果我们开发简单的单一应用,逻辑简单、用户不多、流量不大,那我们用不着。当我们的系统访问量增大、业务增多时,我们会发现一台单机运行此系统已经无法承受。此时,我们可以将业务拆分成几个互不关联的应用,分别部署在各自机器上,以划清逻辑并减小压力。此时,我们也可以不需要RPC,因为应用之间是互不关联的。
\qquad
当我们的业务越来越多、应用也越来越多时,自然的,我们会发现有些功能已经不能简单划分开来或者划分不出来。此时,可以将公共业务逻辑抽离出来,将之组成的服务Service应用 。而原有的、新增的应用都可以与那些的Service应用 交互,以此来完成完整的业务功能。
所以此时,我们急需一种高效的应用程序之间的通讯手段来完成这种需求,所以你看,RPC大显身手的时候来了!
其实描述的场景也是服务化 、微服务和分布式系统架构的基础场景。即RPC框架就是实现以上结构的有力方式。
2.1 常见的RPC框架
- Thrift:thrift是一个软件框架,用来进行可扩展且跨语言的服务的开发。它结合了功能强大的软件堆栈和代码生成引擎,以构建在 C++, Java, Python, PHP, Ruby, Erlang, Perl, Haskell, C#, Cocoa, JavaScript, Node.js, Smalltalk, and OCaml 这些编程语言间无缝结合的、高效的服务。——
- gRPC:一开始由 google 开发,是一款语言中立、平台中立、开源的远程过程调用(RPC)系统。——Google
- 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, 使得开发部署极其简单。
3. RPC原理
3.1 RPC调用流程
RPC的目标就是要2~8这些步骤都封装起来,让用户对这些细节透明。
3.2 如何做到透明化远程服务调用
在 Go 语言中,实现透明化的远程服务调用(RPC)可以通过类似于 Java 动态代理的方式来封装通信细节,使得用户可以像调用本地方法一样调用远程服务。以下是一个简单的介绍,说明如何在 Go 中实现这一点。
3.2.1 动态代理的概念
在 Go 中,虽然没有 Java 中的动态代理机制,但可以通过反射和接口来实现类似的功能。我们可以创建一个代理对象,该对象实现了目标接口,并在方法调用时封装通信逻辑。
3.2.2 实现 RPC 代理
以下是一个简单的示例,展示如何在 Go 中实现一个 RPC 代理:
package main
import (
"fmt"
"reflect"
)
type HelloWorldService interface {
SayHello(name string) string
}
type RPCProxyClient struct {
target interface{}
}
func NewRPCProxyClient(target interface{}) *RPCProxyClient {
return &RPCProxyClient{target: target}
}
func (p *RPCProxyClient) Invoke(methodName string, args ...interface{}) (interface{}, error) {
method := reflect.ValueOf(p.target).MethodByName(methodName)
if !method.IsValid() {
return nil, fmt.Errorf("method %s not found", methodName)
}
in := make([]reflect.Value, len(args))
for i, arg := range args {
in[i] = reflect.ValueOf(arg)
}
result := method.Call(in)
return result[0].Interface(), nil
}
type HelloWorldServiceImpl struct{}
func (h *HelloWorldServiceImpl) SayHello(name string) string {
return "Hello, " + name
}
func main() {
service := &HelloWorldServiceImpl{}
proxy := NewRPCProxyClient(service)
result, err := proxy.Invoke("SayHello", "test")
if err != nil {
fmt.Println("Error:", err)
} else {
fmt.Println("Result:", result)
}
}
其实就是通过动态代理模式,在执行该方法的前后对数据进行封装和解码等,让用于感觉就像是直接调用该方法一样,殊不知,我们对方法前后都经过了复杂的处理。
3.3 如何对消息进行编码和解码
3.3.1 确定消息数据结构
客户端的请求消息结构一般需要包括以下内容:
- 接口名称:如果不传,服务端就不知道调用哪个接口了;
- 方法名:一个接口内可能有很多方法,如果不传方法名服务端也就不知道调用哪个方法;
- 参数类型&参数值:参数类型有很多,比如有bool、int、long、double、string、map、list,甚至如struct等,以及相应的参数值;
- 超时时间 + requestID(标识唯一请求id)
服务端返回的消息结构一般包括以下内容:
3.3.2 序列化
一旦确定了消息的数据结构后,下一步就是要考虑序列化与反序列化了。
**什么是序列化?**序列化就是将数据结构或对象转换成二进制串的过程,也就是编码的过程。
**什么是反序列化?**将在序列化过程中所生成的二进制串转换成数据结构或者对象的过程。
**为什么需要序列化?**转换为二进制串后才好进行网络传输嘛!
**为什么需要反序列化?**将二进制转换为对象才好进行后续处理!
现如今序列化的方案越来越多,每种序列化方案都有优点和缺点,它们在设计之初有自己独特的应用场景,那到底选择哪种呢?从RPC的角度上看,主要看三点:
- 通用性:比如是否能支持Map等复杂的数据结构;
- 性能:包括时间复杂度和空间复杂度,由于RPC框架将会被公司几乎所有服务使用,如果序列化上能节约一点时间,对整个公司的收益都将非常可观,同理如果序列化上能节约一点内存,网络带宽也能省下不少;
- 可扩展性:对互联网公司而言,业务变化飞快,如果序列化协议具有良好的可扩展性,支持自动增加新的业务字段,而不影响老的服务,这将大大提供系统的灵活度。
3.3.3 消息中的 requestID
requestID 用于唯一标识每个请求,确保在并发环境中能够正确匹配请求和响应,避免混淆。
3.4 如何发布自己的服务
Java常用zookeeper,Go常用ETCD,服务端进行注册和心跳,客户端获取机器列表,没啥高深的,比如zookeeper:
二、gRPC
1. 简介
gRPC是一个高性能、通用的开源RPC框架,其由Google 2015年主要面向移动应用开发并基于HTTP/2协议标准而设计,基于ProtoBuf序列化协议开发,且支持众多开发语言。
由于是开源框架,通信的双方可以进行二次开发,所以客户端和服务器端之间的通信会更加专注于业务层面的内容,减少了对由gRPC框架实现的底层通信的关注。
如下图,DATA部分即业务层面内容,下面所有的信息都由gRPC进行封装。
2. gRPC特点
- 语言中立,支持多种语言;
- 基于 IDL 文件定义服务,通过 proto3 工具生成指定语言的数据结构、服务端接口以及客户端 Stub;
- 通信协议基于标准的 HTTP/2 设计,支持双向流、消息头压缩、单 TCP 的多路复用、服务端推送等特性,这些特性使得 gRPC 在移动端设备上更加省电和节省网络流量;
- 序列化支持 PB(Protocol Buffer)和 JSON,PB 是一种语言无关的高性能序列化框架,基于 HTTP/2 + PB, 保障了 RPC 调用的高性能。
3. gPRC 交互过程
- 交换机在开启gRPC功能后充当gRPC客户端的角色,采集服务器充当gRPC服务器角色;
- 交换机会根据订阅的事件构建对应数据的格式(GPB/JSON),通过Protocol Buffers进行编写proto文件,交换机与服务器建立gRPC通道,通过gRPC协议向服务器发送请求消息;
- 服务器收到请求消息后,服务器会通过Protocol Buffers解译proto文件,还原出最先定义好格式的数据结构,进行业务处理;
- 数据处理完后,服务器需要使用Protocol Buffers重编译应答数据,通过gRPC协议向交换机发送应答消息;
- 交换机收到应答消息后,结束本次的gRPC交互。
简单地说,gRPC就是在客户端和服务器端开启gRPC功能后建立连接,将设备上配置的订阅数据推送给服务器端。我们可以看到整个过程是需要用到Protocol Buffers将所需要处理数据的结构化数据在proto文件中进行定义。
4. 优势
4.1 ProtoBuf
ProtoBuf在gRPC的框架中主要有三个作用:
- 定义数据结构
- 定义服务接口
- 通过序列化和反序列化,提升传输效率
为什么ProtoBuf会提高传输效率呢?
使用XML、JSON进行数据编译时,数据文本格式更容易阅读,但进行数据交换时,设备就需要耗费大量的CPU在I/O动作上,自然会影响整个传输速率。Protocol Buffers不像前者,它会将字符串进行序列化后再进行传输,即二进制数据。
如何支撑跨平台,多语言呢?
Protocol Buffers自带一个编译器也是一个优势点。前面提到的proto文件就是通过编译器进行编译的,proto文件需要编译生成一个类似库文件,基于库文件才能真正开发数据应用。具体用什么编程语言编译生成这个库文件呢?由于现网中负责网络设备和服务器设备的运维人员往往不是同一组人,运维人员可能会习惯使用不同的编程语言进行运维开发,那么Protocol Buffers其中一个优势就能发挥出来——跨语言。
4.2 基于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的新特性:
三、Thrift
1. 简介
thrift是一种可伸缩的跨语言服务的RPC软件框架。它结合了功能强大的软件堆栈的代码生成引擎,以建设服务,高效、无缝地在多种语言间结合使用。2007年由贡献到apache基金,是apache下的顶级项目,具备如下特点:
- 支持多语言:C、C++ 、C# 、D 、Delphi 、Erlang 、Go 、Haxe 、Haskell 、Java 、JavaScript、node.js 、OCaml 、Perl 、PHP 、Python 、Ruby 、SmallTalk
- 消息定义文件支持注释,数据结构与传输表现的分离,支持多种消息格式
- 包含完整的客户端/服务端堆栈,可快速实现RPC,支持同步和异步通信
2. 框架结构
Thrift是一套包含序列化功能和支持服务通信的RPC(远程服务调用)框架,也是一种微服务框架。其主要特点是可以跨语言使用,这也是这个框架最吸引人的地方。
3. 网络栈结构
thirft使用socket进行数据传输,数据以特定的格式发送,接收方进行解析。我们定义好thrift的IDL文件后,就可以使用thrift的编译器来生成双方语言的接口、model,在生成的model以及接口代码中会有解码编码的代码。thrift网络栈结构如下:
3.1 Transport层
代表Thrift的数据传输方式,Thrift定义了如下几种常用数据传输方式:
- TSocket: 阻塞式socket;
- TFramedTransport: 以frame为单位进行传输,非阻塞式服务中使用;
- TFileTransport: 以文件形式进行传输。
3.2 TProtocol层
代表thrift客户端和服务端之间传输数据的协议,通俗来讲就是客户端和服务端之间传输数据的格式(例如json等),thrift定义了如下几种常见的格式:
- TBinaryProtocol: 二进制格式;
- TCompactProtocol: 压缩格式;
- TJSONProtocol: JSON格式;
- TSimpleJSONProtocol: 提供只写的JSON协议。
3.3 Server模型
- TSimpleServer: 简单的单线程服务模型,常用于测试;
- TThreadPoolServer: 多线程服务模型,使用标准的阻塞式IO;
- TNonBlockingServer: 多线程服务模型,使用非阻塞式IO(需要使用TFramedTransport数据传输方式);
- THsHaServer: THsHa引入了线程池去处理,其模型读写任务放到线程池去处理,Half-sync/Half-async处理模式,Half-async是在处理IO事件上(accept/read/write io),Half-sync用于handler对rpc的同步处理;