共计 5624 个字符,预计需要花费 15 分钟才能阅读完成。
在单体利用时,一次服务调用产生在同一台机器上的同一个过程外部,也就是说调用产生在本机外部,因而也被叫作本地办法调用。在进行服务化拆分之后,服务提供者和服务消费者运行在两台不同物理机上的不同过程内,它们之间的调用相比于本地办法调用,可称之为近程办法调用,简称 RPC(Remote Procedure Call),那么 RPC 调用是如何实现的呢?
在介绍 RPC 调用的原理之前,先来设想一下一次电话通话的过程。首先,呼叫者 A 通过查问号码簿找到被呼叫者 B 的电话号码,而后拨打 B 的电话。B 接到复电提醒时,如果不便接听的话就会接听;如果不不便接听的话,A 就得始终期待。当期待超过一段时间后,电话会因超时被挂断,这个时候 A 须要再次拨打电话,始终等到 B 闲暇的时候,能力接听。
RPC 调用的原理与此相似,我习惯把服务消费者叫作 客户端 ,服务提供者叫作 服务端,两者通常位于网络上两个不同的地址,要实现一次 RPC 调用,就必须先建设网络连接。建设连贯后,单方还必须依照某种约定的协定进行网络通信,这个协定就是通信协议。单方可能失常通信后,服务端接管到申请时,须要以某种形式进行解决,解决胜利后,把申请后果返回给客户端。为了缩小传输的数据大小,还要对数据进行压缩,也就是对数据进行序列化。
下面就是 RPC 调用的过程,由此可见,想要实现调用,你须要解决四个问题:
- 客户端和服务端如何建设网络连接?
- 服务端如何解决申请?
- 数据传输采纳什么协定?
- 数据该如何序列化和反序列化?
客户端和服务端如何建设网络连接?
依据我的实践经验,客户端和服务端之间基于 TCP 协定建设网络连接最罕用的路径有两种。
1. HTTP 通信
HTTP 通信是基于应用层 HTTP 协定的,而 HTTP 协定又是基于传输层 TCP 协定的。一次 HTTP 通信过程就是发动一次 HTTP 调用,而一次 HTTP 调用就会建设一个 TCP 连贯,经验一次下图所示的“三次握手”的过程来建设连贯。
三次握手过程详解
第一次握手:
- 1、【客户端】向【服务端】发送连贯申请报文,标记 ACK=1,SYN=1,客户端序列号 seq=x,客户端进入期待状态。
第二次握手:
- 1、【服务端】收到申请报文,将收到的报文缓存起来,缓存客户端 seq=x
- 3、【服务端】向【客户端】发送确认报文,生成一个【服务端】seq=y,标记 ACK=1,SYN=1,【服务端】本人的序列号 seq=y,确认序列号 ACK_Number=x+1,发送给【客户端】
第三次握手:
- 1、【客户端】收到服务端发送的确认报文,将收到的报文存起来,缓存【服务端】seq=y
- 2、【客户端】发送确认报文给【服务端】,标记 ACK=1,SYN=0,【客户端】本人的序列号 seq=x+1,确认序列号 ACK_Number=y+1
- 3、【客户端】、【服务端】都会进入 ESTABLISHED (连贯已建设状态)
实现申请后,再经验一次“四次挥手”的过程来断开连接。
四次挥手过程详解
第一次挥手:
- 1、【客户端】向【服务端】发送开释连贯报文,并进行发送数据,被动敞开 TCP 连贯
- 2、标记 FIN=1,【客户端序列号】seq=x,该序号等于后面曾经传送过来的数据的最初一个字节的序号加 1
- 3、这时,客户端 FIN—WAIT-1 (终止期待 1)状态,期待服务端确认
第二次挥手:
- 1、【服务端】收到开释连贯报文,将收到的报文缓存起来,缓存【客户端】seq=x
- 2、【服务端】向【客户端】收回确认开释报文,标记 ACK=1,【服务端序列号】seq=y,确认序列号 ACK_Number=a+1
(a 为【服务端】后面曾经传送过的数据的最初一个字节的序号) - 3、此时【服务端】进入 CLOSE—WAIT(敞开期待)状态
- 4、此时 TCP 服务器过程应该告诉下层的利用过程,因为【客户端】到【服务端】这个方向的连贯就开释了,这时 TCP 处于半敞开状态,即【客户端】曾经没有数据要发了,但【服务端】若发送数据,【客户端】仍要承受,也就是说从【服务端】到【客户端】这个方向的连贯并没有敞开,这个状态可能会继续一些工夫。
第三次挥手:
- 1、【客户端】收到【服务端】确认报文,并缓存起来
- 2、此时【客户端】进入 FIN—WAIT(终止期待 2)状态,期待【服务端】发动开释连贯报文
- 3、如果【服务端】没有数据要发送给【客户端】了,【服务端】的利用过程就会告诉 TCP 开释连贯
- 4、此时【服务端】向【客户端】发送开释连贯报文,标记 FIN=1,确认序列号 ACK_Number=a+1(与第二次挥手的确认号统一),【服务端序号】seq=z+1(z 为半敞开状态发送的数据的最初一个字节的序号)
- 5、此时【服务端】进入最初确认状态,期待【客户端】确认
第四次挥手:
- 1、【客户端】收到【服务端】开释连贯申请,必须收回确认
- 2、【客户端】向【服务端】发送确认报文,标记 ACK=1,确认号序列号 ACK_Numbe=z+1+1,【客户端序号】seq=x+1(x 为第一次挥手的 seq)
- 3、此时【客户端】进入期待状态,必须通过工夫期待计时器设置的工夫 2 倍 MSL(报文最大生存工夫)后,【客户端】才进入 CLOSED 状态,MSL 叫做最长报文寿命,RFC 倡议设为 2 分钟,理论利用中是 30 秒。在这 2 倍 MSL 期间【客户端】进入 TIME—WAIT 状态后,要通过 4 分钟能力进入到 CLOSED 状态。
- 4、【服务端】只有收到了【客户端】的确认后,就进入了 CLOSED 状态
- 5、当【客户端】和【服务端】都进入 CLOSED 状态后,连贯就齐全开释了
2. Socket 通信
Socket 通信是基于 TCP/IP 协定的封装,建设一次 Socket 连贯至多须要一对套接字,其中一个运行于客户端,称为 ClientSocket;另一个运行于服务器端,称为 ServerSocket。就像下图所形容的,Socket 通信的过程分为四个步骤:服务器监听、客户端申请、连贯确认、数据传输。
- 服务器监听:ServerSocket 通过调用 bind()函数绑定某个具体端口,而后调用 listen()函数实时监控网络状态,期待客户端的连贯申请。
- 客户端申请:ClientSocket 调用 connect()函数向 ServerSocket 绑定的地址和端口发动连贯申请。
- 服务端连贯确认:当 ServerSocket 监听到或者接管到 ClientSocket 的连贯申请时,调用 accept()函数响应 ClientSocket 的申请,同客户端建设连贯。
- 数据传输:当 ClientSocket 和 ServerSocket 建设连贯后,ClientSocket 调用 send()函数,ServerSocket 调用 receive()函数,ServerSocket 解决完申请后,调用 send()函数,ClientSocket 调用 receive()函数,就能够失去失去返回后果。
间接了解可能有点形象,你能够把这个过程套入后面我举的“打电话”的例子,能够不便你了解 Socket 通信过程。
当客户端和服务端建设网络连接后,就能够发动申请了。但网络不肯定总是牢靠的,常常会遇到网络闪断、连贯超时、服务端宕机等各种异样,通常的解决伎俩有两种。
- 链路存活检测:客户端须要定时地发送心跳检测音讯(个别是通过 ping 申请)给服务端,如果服务端间断 n 次心跳检测或者超过规定的工夫都没有回复音讯,则认为此时链路曾经生效,这个时候客户端就须要从新与服务端建设连贯。
- 断连重试:通常有多种状况会导致连贯断开,比方客户端被动敞开、服务端宕机或者网络故障等。这个时候客户端就须要与服务端从新建设连贯,但个别不能立即实现重连,而是要期待固定的距离后再发动重连,防止服务端的连贯回收不及时,而客户端霎时重连的申请太多而把服务端的连接数占满。
服务端如何解决申请?
假如这时候客户端和服务端曾经建设了网络连接,服务端又该如何解决客户端的申请呢?通常来讲,有三种解决形式。
- 同步阻塞形式(BIO),客户端每发一次申请,服务端就生成一个线程去解决。当客户端同时发动的申请很多时,服务端须要创立很多的线程去解决每一个申请,如果达到了零碎最大的线程数瓶颈,新来的申请就没法解决了。
- 同步非阻塞形式 (NIO),客户端每发一次申请,服务端并不是每次都创立一个新线程来解决,而是通过 I / O 多路复用技术进行解决。就是把多个 I / O 的阻塞复用到同一个 select 的阻塞上,从而使零碎在单线程的状况下能够同时解决多个客户端申请。这种形式的劣势是开销小,不必为每个申请创立一个线程,能够节俭零碎开销。
- 异步非阻塞形式(AIO),客户端只须要发动一个 I / O 操作而后立刻返回,等 I / O 操作真正实现当前,客户端会失去 I / O 操作实现的告诉,此时客户端只须要对数据进行解决就好了,不须要进行理论的 I / O 读写操作,因为真正的 I / O 读取或者写入操作曾经由内核实现了。这种形式的劣势是客户端无需期待,不存在阻塞期待问题。
从后面的形容,能够看进去不同的解决形式实用于不同的业务场景:
- BIO 实用于连接数比拟小的业务场景,这样的话不至于零碎中没有可用线程去解决申请。这种形式写的程序也比较简单直观,易于了解。
- NIO 实用于连接数比拟多并且申请耗费比拟轻的业务场景,比方聊天服务器。这种形式相比 BIO,相对来说编程比较复杂。
- AIO 实用于连接数比拟多而且申请耗费比拟重的业务场景,比方波及 I / O 操作的图片服务器。这种形式相比另外两种,编程难度最大,程序也不易于了解。
下面两个问题就是“通信框架”要解决的问题,你能够基于现有的 Socket 通信,在服务消费者和服务提供者之间建设网络连接,而后在服务提供者一侧基于 BIO、NIO 和 AIO 三种形式中的任意一种实现服务端申请解决,最初再破费一些精力去解决服务消费者和服务提供者之间的网络可靠性问题。这种形式对于 Socket 网络编程、多线程编程常识都要求比拟高,感兴趣的话能够尝试本人实现一个通信框架。但我倡议最为稳当的形式是应用成熟的开源计划,比方 Netty、MINA 等,它们都是通过业界大规模利用后,被充沛论证是很牢靠的计划。
假如客户端和服务端的连贯曾经建设了,服务端也能正确地解决申请了,接下来实现一次失常地 RPC 调用还须要解决两个问题,即数据传输采纳什么协定以及数据该如何序列化和反序列化。
数据传输采纳什么协定?
首先来看第一个问题,数据传输采纳什么协定?
最罕用的有 HTTP 协定,它是一种凋谢的协定,各大网站的服务器和浏览器之间的数据传输大都采纳了这种协定。还有一些定制的公有协定,比方阿里巴巴开源的 Dubbo 协定,也能够用于服务端和客户端之间的数据传输。无论是凋谢的还是公有的协定,都必须定义一个“契约”,以便服务消费者和服务提供者之间可能达成共识。服务消费者依照契约,对传输的数据进行编码,而后通过网络传输过来;服务提供者从网络上接管到数据后,依照契约,对传输的数据进行解码,而后解决申请,再把解决后的后果进行编码,通过网络传输返回给服务消费者;服务消费者再对返回的后果进行解码,最终失去服务提供者解决后的返回值。
通常协定契约包含两个局部:音讯头和音讯体。其中音讯头寄存的是协定的公共字段以及用户扩大字段,音讯体寄存的是传输数据的具体内容。
以 HTTP 协定为例,次要分为音讯头和音讯体两局部,其中音讯头中寄存的是协定的公共字段,比方 Server 代表是服务端服务器类型、Content-Length 代表返回数据的长度、Content-Type 代表返回数据的类型;音讯体中寄存的是具体的返回后果,这里就是一段 HTML 网页代码。
数据该如何序列化和反序列化?
再看第二个问题,数据该如何序列化和反序列化。
个别数据在网络中进行传输前,都要先在发送方一端对数据进行编码,通过网络传输达到另一端后,再对数据进行解码,这个过程就是序列化和反序列化。
为什么要对数据进行序列化和反序列化呢?要晓得网络传输的耗时一方面取决于网络带宽的大小,另一方面取决于数据传输量。要想放慢网络传输,要么进步带宽,要么减小数据传输量,而对数据进行编码的次要目标就是减小数据传输量。比方一部高清电影原始大小为 30GB,如果通过非凡编码格局解决,能够减小到 3GB,同样是 100MB/ s 的网速,下载工夫能够从 300s 减小到 30s。
罕用的序列化形式分为两类:文本类如 XML/JSON 等,二进制类如 PB/Thrift 等,而具体采纳哪种序列化形式,次要取决于三个方面的因素。
- 反对数据结构类型的丰盛度。数据结构品种反对的越多越好,这样的话对于使用者来说在编程时更加敌对,有些序列化框架如 Hessian 2.0 还反对简单的数据结构比方 Map、List 等。
- 跨语言反对。序列化形式是否反对跨语言也是一个很重要的因素,否则应用的场景就比拟局限,比方 Java 序列化只反对 Java 语言,就不能用于跨语言的服务调用了。
- 性能。次要看两点,一个是序列化后的压缩比,一个是序列化的速度。以罕用的 PB 序列化和 JSON 序列化协定为例来比照剖析,PB 序列化的压缩比和速度都要比 JSON 序列化高很多,所以对性能和存储空间要求比拟高的零碎选用 PB 序列化更适合;而 JSON 序列化尽管性能要差一些,但可读性更好,更适宜对外部提供服务。
总结
明天咱们一起理解服务调用须要解决的几个问题:
- 通信框架。它次要解决客户端和服务端如何建设连贯、治理连贯以及服务端如何解决申请的问题。
- 通信协议。它次要解决客户端和服务端采纳哪种数据传输协定的问题。
- 序列化和反序列化。它次要解决客户端和服务端采纳哪种数据编解码的问题。
这三个局部就组成了一个残缺的 RPC 调用框架,通信框架提供了根底的通信能力,通信协议形容了通信契约,而序列化和反序列化则用于数据的编 / 解码。一个通信框架能够适配多种通信协议,也能够采纳多种序列化和反序列化的格局,比方服务化框架 Dubbo 不仅反对 Dubbo 协定,还反对 RMI 协定、HTTP 协定等,而且还反对多种序列化和反序列化格局,比方 JSON、Hession 2.0 以及 Java 序列化等。
本文由 mdnice 多平台公布