目录
1, 什么是 RPC?
2, 典型 RPC 调用框架
3,Thrift 框架介绍
1, 什么是 RPC?
(1)RPC(remote procedure call): 远程调用过程.
服务器 A 部署应用 a, 服务器 B 部署应用 b, 当 A 服务器调用 B 服务器上的 b 应用的函数或者方法时, 因为不在同一内存空间, 不能直接调用, 必须通过网络来表达调用的语义传达调用的数据.
既然是调用 B 机器上的服务, 那 A 机器自己也创建一个这个服务不就也可以调用了么. 原理上是可以这么做, 但是随着计算机的横向发展, 集群的出现, 使得多台机器部署成一个集群来对外提供服务. 无法在一个进程内甚至同一台计算机上完成的需求就得需要 RPC 来实现了.
RPC 的框架很多, 比如最早的额 CORBA,Java RMI,web Service 的 RPC 风格, Hessian,Thrift, 甚至是 REST API.
(2) 本地过程调用过程
RPC 就是要像调用本地的函数一样去调远程函数. 在研究 RPC 前, 我们先看看本地调用是怎么调的. 假设我们要调用函数 Multiply 来计算 lvalue * rvalue 的结果:
- int Multiply(int l, int r) {
- int y = l * r;
- return y;
- }
- int lvalue = 10;
- int rvalue = 20;
- int l_times_r = Multiply(lvalue, rvalue);
那么在第 8 行时, 我们实际上执行了以下操作:
将 lvalue 和 rvalue 的值压栈
进入 Multiply 函数, 取出栈中的值 10 和 20, 将其赋予 l 和 r
执行第 2 行代码, 计算 l * r , 并将结果存在 y
将 y 的值压栈, 然后从 Multiply 返回
第 8 行, 从栈中取出返回值 200 , 并赋值给 l_times_r
以上 5 步就是执行本地调用的过程.
(3)远程过程调用带来的新问题
在远程调用时, 我们需要执行的函数体是在远程的机器上的, 也就是说, Multiply 是在另一个进程中执行的. 这就带来了几个新问题:
Call ID 映射. 我们怎么告诉远程机器我们要调用 Multiply, 而不是 Add 或者 FooBar 呢? 在本地调用中, 函数体是直接通过函数指针来指定的, 我们调用 Multiply, 编译器就自动帮我们调用它相应的函数指针. 但是在远程调用中, 函数指针是不行的, 因为两个进程的地址空间是完全不一样的. 所以, 在 RPC 中, 所有的函数都必须有自己的一个 ID. 这个 ID 在所有进程中都是唯一确定的. 客户端在做远程过程调用时, 必须附上这个 ID. 然后我们还需要在客户端和服务端分别维护一个 {函数 <--> Call ID} 的对应表. 两者的表不一定需要完全相同, 但相同的函数对应的 Call ID 必须相同. 当客户端需要进行远程调用时, 它就查一下这个表, 找出相应的 Call ID, 然后把它传给服务端, 服务端也通过查表, 来确定客户端需要调用的函数, 然后执行相应函数的代码.
序列化和反序列化. 客户端怎么把参数值传给远程的函数呢? 在本地调用中, 我们只需要把参数压到栈里, 然后让函数自己去栈里读就行. 但是在远程过程调用时, 客户端跟服务端是不同的进程, 不能通过内存来传递参数. 甚至有时候客户端和服务端使用的都不是同一种语言(比如服务端用 C++, 客户端用 Java 或者 Python). 这时候就需要客户端把参数先转成一个字节流, 传给服务端后, 再把字节流转成自己能读取的格式. 这个过程叫序列化和反序列化. 同理, 从服务端返回的值也需要序列化反序列化的过程.
网络传输. 远程调用往往用在网络上, 客户端和服务端是通过网络连接的. 所有的数据都需要通过网络传输, 因此就需要有一个网络传输层. 网络传输层需要把 Call ID 和序列化后的参数字节流传给服务端, 然后再把序列化后的调用结果传回客户端. 只要能完成这两者的, 都可以作为传输层使用. 因此, 它所使用的协议其实是不限的, 能完成传输就行. 尽管大部分 RPC 框架都使用 TCP 协议, 但其实 UDP 也可以, 而 gRPC 干脆就用了 HTTP2.Java 的 Netty 也属于这层的东西.
所以, 要实现一个 RPC 框架, 其实只需要把以上三点实现了就基本完成了.
Call ID 映射可以直接使用函数字符串, 也可以使用整数 ID. 映射表一般就是一个哈希表.
序列化反序列化可以自己写, 也可以使用 Protobuf 或者 FlatBuffers 之类的.
网络传输库可以自己写 socket, 或者用 asio,ZeroMQ,Netty 之类.
2, 典型 RPC 调用框架
RPC 的实现和调用框架很多, 简单介绍其中几种比较典型的:
RMI(RemoteManagementInterface), 利用 java.rmi 包实现, 基于 Java 远程方法协议(Java Remote Method Protocol) 和 java 的原生序列化.
Hessian, 是一个轻量级的 remoting onhttp 工具, 使用简单的方法提供了 RMI 的功能. 基于 HTTP 协议, 采用二进制编解码.
protobuf-rpc-pro, 是一个 Java 类库, 提供了基于 Google 的 Protocol Buffers 协议的远程方法调用的框架. 基于 Netty 底层的 NIO 技术. 支持 TCP 重用 / keep-alive,SSL 加密, RPC 调用取消操作, 嵌入式日志等能.
Thrift, 是一种可伸缩的跨语言服务的软件框架. 它拥有功能强大的代码生成引擎, 无缝地支持 C + +,C#,Java,Python 和 PHP 和 Ruby.thrift 允许你定义一个描述文件, 描述数据类型和服务接口. 依据该文件, 编译器方便地生成 RPC 客户端和服务器通信代码. 最初由 Facebook 开发用做系统内个语言之间的 RPC 通信, 2007 年由 Facebook 贡献到 apache 基金 , 现在是 apache 下的 opensource 之一 . 支持多种语言之间的 RPC 方式的通信: PHP 语言 client 可以构造一个对象, 调用相应的服务方法来调用 java 语言的服务, 跨越语言的 C/S RPC 调用. 底层通讯基于 SOCKET.
Avro, 出自 Hadoop 之父 Doug Cutting, 在 Thrift 已经相当流行的情况下推出 Avro 的目标不仅是提供一套类似 Thrift 的通讯中间件, 更是要建立一个新的, 标准性的云计算的数据交换和存储的 Protocol. 支持 HTTP,TCP 两种协议.
3,Thrift 框架介绍
最常见的 RPC 工具是 Facebook 开源的 Thrift RPC 框架.
Thrift 是一个跨语言的服务部署框架, 最初由 Facebook 于 2007 年开发, 2008 年进入 Apache 开源项目. Thrift 通过一个中间语言 (IDL, 接口定义语言) 来定义 RPC 的接口和数据类型, 然后通过一个编译器生成不同语言的代码(目前支持 C++,Java, Python, PHP, Ruby, Erlang, Perl, Haskell, C#, Cocoa, Smalltalk 和 OCaml), 并由生成的代码负责 RPC 协议层和传输层的实现.
Thrift 实际上是实现了 C/S 模式, 通过代码生成工具将接口定义文件生成服务器端和客户端代码 (可以为不同语言), 从而实现服务端和客户端跨语言的支持. 用户在 Thirft 描述文件中声明自己的服务, 这些服务经过编译后会生成相应语言的代码文件, 然后用户实现服务(客户端调用服务, 服务器端提服务) 便可以了. 其中 protocol(协议层, 定义数据传输格式, 可以为二进制或者 xml 等)和 transport(传输层, 定义数据传输方式, 可以为 TCP/IP 传输, 内存共享或者文件共享等)被用作运行时库.
Thrift 的协议栈如下图所示:
重试
在 Client 和 Server 的最顶层都是用户自定义的处理逻辑, 也就是说用户只需要编写用户逻辑, 就可以完成整套的 RPC 调用流程. 用户逻辑的下一层是 Thrift 自动生成的代码, 这些代码主要用于结构化数据的解析, 发送和接收, 同时服务器端的自动生成代码中还包含了 RPC 请求的转发(Client 的 A 调用转发到 Server A 函数进行处理).
协议栈的其他模块都是 Thrift 的运行时模块:
底层 IO 模块, 负责实际的数据传输, 包括 Socket, 文件, 或者压缩数据流等.
TTransport 负责以字节流方式发送和接收 Message, 是底层 IO 模块在 Thrift 框架中的实现, 每一个底层 IO 模块都会有一个对应 TTransport 来负责 Thrift 的字节流 (Byte Stream) 数据在该 IO 模块上的传输. 例如 TSocket 对应 Socket 传输, TFileTransport 对应文件传输.
TProtocol 主要负责结构化数据组装成 Message, 或者从 Message 结构中读出结构化数据. TProtocol 将一个有类型的数据转化为字节流以交给 TTransport 进行传输, 或者从 TTransport 中读取一定长度的字节数据转化为特定类型的数据. 如 int32 会被 TBinaryProtocol Encode 为一个四字节的字节数据, 或者 TBinaryProtocol 从 TTransport 中取出四个字节的数据 Decode 为 int32.
TServer 负责接收 Client 的请求, 并将请求转发到 Processor 进行处理. TServer 主要任务就是高效的接受 Client 的请求, 特别是在高并发请求的情况下快速完成请求.
Processor(或者 TProcessor)负责对 Client 的请求做出相应, 包括 RPC 请求转发, 调用参数解析和用户逻辑调用, 返回值写回等处理步骤. Processor 是服务器端从 Thrift 框架转入用户逻辑的关键流程. Processor 同时也负责向 Message 结构中写入数据或者读出数据.
Thrift 的模块设计非常好, 在每一个层次都可以根据自己的需要选择合适的实现方式. 同时也应该注意到 Thrift 目前的特性并不是在所有的程序语言中都支持. 例如 C++ 实现中有 TDenseProtocol 没有 TTupleProtocol, 而 Java 实现中有 TTupleProtocol 没有 TDenseProtocol.
来源: http://www.bubuko.com/infodetail-3285455.html