本文将从大的框架层面来聊聊RPC原理和实现,既然叫跨语言RPC,也将以thrift为例讲讲跨语言RPC如何实现。
在 SOA(面向服务架构,Service-Oriented Architecture)和微服务大行其道的今天,服务之间的远程调用已经遍布各个互联网公司。做为服务器端程序,需要考虑性能同时也要考虑与各种语言之间方便的通讯。采用http协议简单,但性能不高。采用TCP通讯,则需要考虑封包、解包、粘包等等很多因素,而且想写个高效的TCP服务,也很难。其实,对于此类需求,采用RPC(Remote Procedure Call Protocol)编程最靠谱。使用 RPC 编程被认为是在分布式环境中运行的客户机和服务器应用程序之间进行可靠通信的最强大、最高效的方法之一。在分布式系统服务群中开发应用,了解RPC一些原理和实现架构,还是很有必要的。
远程过程调用RPC,就是客户端基于某种传输协议通过网络向服务提供端请求服务处理,然后获取返回数据(对于oneway模式则不返还响应结果);而这种调用对于客户端而言,和调用本地服务一样方便,开发人员不需要了解具体底层网络传输协议。简单讲,就是本地调用的逻辑处理的过程放在的远程的机器上,而不是本地服务代理来处理。
LPC(Local Procedure Call) VS RPC
既然存在RPC这种远程过程调用,必然会有与之对应的本地过程调用了。本地过程调用在Windows编程中称为LPC,在linux编程中更习惯称之为IPC(Inter-Process Call,内部进程调用),即进程间通信。本质上就是本地机器上的不同进程之间通信协作的调用方式。进程间通信的方式有:管道、共享内存、信号量、Socket套接字、消息队列和信号。这些方式的通信原理和实践我就不细说了了,下面只介绍一下socket通信,因为socket通信同时也是RPC通信的方式。
Socket
Socket一般情况下是用在不同的两台机器的不同进程之间通信的,当Socket创建时的类型为 AF_LOCAL或AF_UNIX时,则是本地进程通信了(当然你也可以直接使用网络套接字,如果你觉得走下网络更酷,或者以后便于服务分离)。
关于Socket的API介绍,这里就省略了。服务端/客户端模式的介绍和示例相对很常见,也很容易开发和理解。
从使用网络套接字Socket来实现进程间通信这个角度来说,其和RPC并没有什么不同了,所以有些文献分类时,说广义来讲RPC也应该包括LPC(IPC),因为从大的来讲,单机进程通信其实算是远程过程调用的一种特殊简化的方式而已。
说完单机的服务调用,在互联网时代,自然要讲web服务(Web Service)了。
Web Service技术
Web Service一般有两种定义,狭义VS广义:
狭义定义:特指 W3C组织制定的web service规范技术。其包括SOAP(一个基于XML的可扩展消息信封格式,需同时绑定一个网络传输协议。这个协议通常是HTTP或HTTPS,但也可能是SMTP或XMPP)、WSDL(一个XML格式文档,用以描述服务端口访问方式和使用协议的细节。通常用来辅助生成服务器和客户端代码及配置信息)和UDDI(一个用来发布和搜索WEB服务的协议,应用程序可借由此协议在设计或运行时找到目标WEB服务)。从上面三个定义就可以看出,这种规范技术是一个重量级的协议。
广义定义:泛指网络系统对外提供web服务所使用的技术??梢圆慰枷旅鎤eb服务的技术体系结构图来理解。
web service被W3C设立规范之初,SOAP( Simple Object Access Protocol,简单对象访问协议)方案就被提出来。但是,随着服务化技术和架构的发展,SOAP多少有点过于复杂,因此就出现了简化版的REST(REpresentational State Transfort,表示性状态转移)方案。此后,由于分布式服务应用越来越大,对性能和易用性上面要求越来越大,因此就出现了RPC框架(很多时候,RPC并不被当做一种web service方案。在绝大部分博客中,介绍web service 只会讨论 SOAP和REST)。
SOAP
SOAP是基于XML数据格式来交换数据的;其内部定义了一套复杂完善的XML标签,标签中包含了调用的远程过程、参数、返回值和出错信息等等,通信双方根据这套标签来解析数据或者请求服务。与SOAP相关的配套协议是WSDL (Web Service Description Language),用来描述哪个服务器提供什么服务,怎样找到它,以及该服务使用怎样的接口规范,类似我们现在聊服务治理中的服务发现功能。SOAP服务整体流程是:1)获得该服务的WSDL描述,根据WSDL构造一条格式化的SOAP请求发送给服务器,2)接收一条同样SOAP格式的应答,3)根据先前的WSDL解码数据。绝大多数情况下,请求和应答使用HTTP协议传输,那么发送请求就使用HTTP的POST方法。
REST
由于SOAP方案过于庞大复杂,在很多简单的web服务应用场景中,轻量级的REST就出现替代SOAP方案了。和SOAP相比,REST只是对URI做了一些规范,数据才有JSON格式,底层传输使用HTTP/HTTPS来通信,因此,所有web服务器都可以快速支持该方案;开发人员也可以快速学习和使用。
由于数据返回格式是自定义的,绝大部分使用JSON,这种数据结构节省带宽,并且前端JavaScript能天生支持。但是REST是基于HTTP协议的无状态规范,所以只能适应无状态场景。
RPC
RPC家族中,RMI是Java制定的远程通信协议。RMI既然是Java的标准RPC组件,那必然其他编程语言就无法使用了;因此,Thrift这种基于IDL来跨语言的RPC组件就出现了。Thrift的使用者,只需要按照Thrift官方规定的方式来写API结构,然后生成对应语言的API接口,继而就可以跨语言完成远程过程调用了。但是,作为服务化的组件,如果没有服务治理来完成大规模应用集群中服务调用管理工作,则运维工作则是非常繁重的,因此类似dubbo这种包含服务治理的RPC组件出现了。
前面扯了很多跟RPC相关的定义以及RPC的演变。下面介绍RPC的原理。
RPC
RMI介绍
RMI(Remote Method Invocation,远程方法调用)也就是RPC本身的实现方式。在JDK 1.2的时候,引入到Java体系的。当应用比较小,性能要求不高的情况下,使用RMI还是挺方便快捷的。下面先看看RMI的调用流程。
概念说明:
stub(桩):stub实际上就是远程过程在客户端上面的一个代理proxy。当我们的客户端代码调用API接口提供的方法的时候,RMI生成的stub代码块会将请求数据序列化,交给远程服务端处理,然后将结果反序列化之后返回给客户端的代码。这些处理过程,对于客户端来说,基本是透明无感知的。
remote:这层就是底层网络处理了,RMI对用户来说,屏蔽了这层细节。stub通过remote来和远程服务端进行通信。
skeleton(骨架):和stub相似,skeleton则是服务端生成的一个代理proxy。当客户端通过stub发送请求到服务端,则交给skeleton来处理,其会根据指定的服务方法来反序列化请求,然后调用具体方法执行,最后将结果返回给客户端。
registry(服务发现):借助JNDI发布并调用了rmi服务。实际上,JNDI就是一个注册表,服务端将服务对象放入到注册表中,客户端从注册表中获取服务对象。rmi服务,在服务端实现之后需要注册到rmi server上,然后客户端从指定的rmi地址上lookup服务,调用该服务对应的方法即可完成远程方法调用。registry是个很重要的功能,当服务端开发完服务之后,要对外暴露,如果没有服务注册,则客户端是无从调用的,即使服务端的服务就在那里。
RPC架构剖析
远程过程调用RPC就是本地动态代理隐藏通信细节,通过组件序列化请求,走网络到服务端,执行真正的服务代码,然后将结果返回给客户端,反序列化数据给调用方法的过程。
常用RPC框架如下
- Dubbo是Alibaba开发的一个RPC框架,远程接口基于Java Interface, 依托于Spring框架。
- gRPC的Java实现的底层网络库是基于Netty开发而来,其Go实现是基于net库。
- Thrift是Apache的一个项目(http://thrift.apache.org),前身是Facebook开发的一个RPC框架,采用thrift作为IDL (Interface description language)。是支持跨语言的RPC框架。
常见的RPC序列化协议
- XML(Extensible Markup Language)是一种常用的序列化和反序列化协议,具有跨机器,跨语言等优点。狭义web service就是基于SOAP消息传递协议(一个基于XML的可扩展消息信封格式)来进行数据交换的。
- Hessian是一个动态类型,简洁的,可以移植到各个语言的二进制序列化对象协议。采用简单的结构化标记、采用定长的字节记录值、采用引用取代重复遇到的对象。
- JSON(Javascript Object Notation)起源于弱类型语言Javascript, 是采用"Attribute-value"的方式来描述对象协议。与XML相比,其协议比较简单,解析速度比较快。
- Protocol Buffers 是google提供的一个开源序列化框架,是一种轻便高效的结构化数据存储格式,可以用于结构化数据串行化,或者说序列化。它很适合做数据存储或 RPC 数据交换格式。可用于通讯协议、数据存储等领域的语言无关、平台无关、可扩展的序列化结构数据格式。同 XML 相比, Protobuf 的主要优点在于性能高。它以高效的二进制方式存储,比 XML 小 3 到 10 倍,快 20 到 100 倍。
- Thrift 既是rpc框架,同时也具有自己内部定义的传输协议规范(TProtocol)和传输数据标准(TTransports),通过IDL脚本对传输数据的数据结构(struct) 和传输数据的业务逻辑(service)根据不同的运行环境快速的构建相应的代码,并且通过自己内部的序列化机制对传输的数据进行简化和压缩提高高并发、 大型系统中数据交互的成本。
本文将从两个部分介绍RPC。以dubbo为例介绍一个RPC的完整架构以及一个java语言的RPC框架简易实现demo;另一部分是剖析跨语言RPC框架thrift以及demo。
通用RPC架构
最近刚升为apache顶级项目的dubbo可以说是java语言中RPC架构最流行的框架。同时Dubbo的文档也是开源软件中写的最详细的文档之一,细看dubbo官方文档。下图是dubbo的整体设计:
图例说明:
图中左边淡蓝背景的为服务消费方使用的接口,右边淡绿色背景的为服务提供方使用的接口,位于中轴线上的为双方都用到的接口。
图中从下至上分为十层,各层均为单向依赖,右边的黑色箭头代表层之间的依赖关系,每一层都可以剥离上层被复用,其中,Service 和 Config 层为 API,其它各层均为 SPI。
图中绿色小块的为扩展接口,蓝色小块为实现类,图中只显示用于关联各层的实现类。
图中蓝色虚线为初始化过程,即启动时组装链,红色实线为方法调用过程,即运行时调时链,紫色三角箭头为继承,可以把子类看作父类的同一个节点,线上的文字为调用的方法。
各层说明:
config 配置层:对外配置接口,以 ServiceConfig, ReferenceConfig 为中心,可以直接初始化配置类,也可以通过 spring 解析配置生成配置类
proxy 服务代理层:服务接口透明代理,生成服务的客户端 Stub 和服务器端 Skeleton, 以 ServiceProxy 为中心,扩展接口为 ProxyFactory
registry 注册中心层:封装服务地址的注册与发现,以服务 URL 为中心,扩展接口为 RegistryFactory, Registry, RegistryService
cluster 路由层(RMI没有这一层,因为直接指定具体服务端或客户端):封装多个提供者的路由及负载均衡,并桥接注册中心,以 Invoker 为中心,扩展接口为 Cluster, Directory, Router, LoadBalance
monitor 监控层(非核心,非必须):RPC 调用次数和调用时间监控,以 Statistics 为中心,扩展接口为 MonitorFactory, Monitor, MonitorService
protocol 远程调用层(从protocol 远程调用层往下4层可以看成RMI图中的Remote层):封装 RPC 调用,以 Invocation, Result 为中心,扩展接口为 Protocol, Invoker, Exporter
exchange 信息交换层:封装请求响应模式,同步转异步,以 Request, Response 为中心,扩展接口为 Exchanger, ExchangeChannel, ExchangeClient, ExchangeServer
transport 网络传输层:抽象 mina 和 netty 为统一接口,以 Message 为中心,扩展接口为 Channel, Transporter, Client, Server, Codec
serialize 数据序列化层:可复用的一些工具,扩展接口为 Serialization, ObjectInput, ObjectOutput, ThreadPool
上面的dubbo展示了一个完整的带服务治理功能的RPC框架的分层架构。下面我们写一个简易的RPC框架,我把dubbo中的一些非核心层省略掉。具体细节见demo。
- 省略掉config层,直接通过单例模式获取服务。
- 保留proxy 服务代理层,客户端采用简单的反射机制实现服务接口的动态代理,demo中对应ServiceProxyClient类;服务端初始化的时候,按一定规则写进Map映射中,这样直接获取服务实例对象即可,类似RMI的skeleton???,demo中对应ServiceProcessor类。
- 省略registry 注册中心层,demo服务只有一个实例机器提供,故直接写死ip和端口,在ClientRemoter类中getDataRemote方法中直接写死。
- 省略cluster 路由层,只有当服务实例有多个时才需要通过算法决定哪个服务实例“接待”请求。
- 省略monitor 监控层,这个是服务治理需要的,不影响核心流程调用。
- 保留protocol 远程调用层以下四层 ,统称为remote层。负责请求双方调用协议的约定,序列化、传输。client端的remote层对应demo中ClientRemoter类,将请求服务接口转化成二进制通过socket发送给服务端;服务端的remote层对应demo中的ServerRemoter类,负责客户端的二进制按照协议转化成本地的方法调用,然后又将返回结果通过按照协议翻译成二进制通过socket送给客户端。
跨语言RPC框架Thrif
Thrift 是一个轻量级、跨语言的开源RPC框架,并且能够根据 *.thrift 文件自动生成联合代码。Thrift 提供了整洁的数据传输、序列化和应用层处理。通过简单的接口定义语言,生成跨语言的的程序代码,该代码通过抽象栈构建了可供 RPC 使用的 Client 和 Server??⒄呖梢栽谏傻?Client 和 Sever 代码的基础上去实现自己的业务逻辑。
IDL(interface description language,接口定义语言)
java语言之间RPC调用中的share包类似这里讲的IDL。因为跨语言之间的调用,需要一个跨语言的服务接口定义,IDL就是为了满足这个场景而出现的。IDL是很多RPC框架用来支持跨语言环境调用的一个服务描述组件,一般都是采用文本格式来定义。接口定义文件是 Thrift 开发的核心,定义了 RPC 过程中通信的数据结构和通信的接口方法定义等。Thrift的不同版本定义IDL的语法也不太相同,这里使用Thrift-0.9.2这个版本来介绍Java下的IDL定义:
- namespace 定义包名
- struct 定义服务接口的参数,返回值使用到的类结构。如果接口的参数都是基本类型,则不需要定义struct
- service 定义接口
- 基本类型
详细的接口定义参考官方文档。
生成代码
生成代码首先需要安装thrift,网上关于thrift的安装非常多,也可以参考官网说明。
通过以下命令生成调用端的服务接口文件。
thrift -r --gen java xx.thrift //xx.thrift是使用idl定义的服务接口文件
方法调用模型分析
Thrift的方法调用模型很简单,就是通过方法名和实际方法实现类的注册完成,没有使用反射机制,类加载机制,整个调用模型和上面讲的RPC调用模型基本一致。RPC调用本质上就是一种网络编程,客户端向服务器发送消息,服务器拿到消息之后做后续动作。只是RPC这种消息比较特殊,它封装了方法调用,包括方法名,方法参数。服务端拿到这个消息之后,解码消息,然后要通过方法调用模型来完成实际服务器端业务方法的调用。
和方法调用相关的几个核心类,都可以与上面的RPC核心类对应:
- 自动生成的Iface接口,是远程方法的顶层接口
- 自动生成的Processor类及相关父类,包括TProcessor接口,TBaseProcess抽象类
- ProcessFunction抽象类,抽象了一个具体的方法调用,包含了方法名信息,调用方法的抽象过程等
- TNonblcokingServer,是NIO服务器的默认实现,通过Args参数来配置Processor等信息
- FrameBuffer类,服务器NIO的缓冲区对象,这个对象在服务器端收到全包并解码后,会调用Processor去完成实际的方法调用
- 服务器端的方法的具体实现类,实现Iface接口
更详细的看demo吧