愈来愈多的人开始意识到,网站即软件,并且是一种新型的软件。html
网站开发,彻底能够采用软件开发的模式。可是传统上,软件和网络是两个不一样的领域,不多有交集;软件开发主要针对单机环境,网络则主要研究系统之间的通讯。前端
互联网的兴起,使得这两个领域开始融合,如今咱们必须考虑,如何开发在互联网环境中使用的软件。git
RESTful架构,就是目前最流行的一种互联网软件架构。它结构清晰、符合标准、易于理解、扩展方便,因此正获得愈来愈多网站的采用。github
EST这个词,是Roy Thomas Fielding在他2000年的博士论文中提出的。算法
"本文研究计算机科学两大前沿----软件和网络----的交叉点。长期以来,软件研究主要关注软件设计的分类、设计方法的演化,不多客观地评估不一样的设计选择对系统行为的影响。而相反地,网络研究主要关注系统之间通讯行为的细节、如何改进特定通讯机制的表现,经常忽视了一个事实,那就是改变应用程序的互动风格比改变互动协议,对总体表现有更大的影响。我这篇文章的写做目的,就是想在符合架构原理的前提下,理解和评估以网络为基础的应用软件的架构设计,获得一个功能强、性能好、适宜通讯的架构。"apache
Fielding将他对互联网软件的架构原则,定名为REST,即Representational State Transfer的缩写。我对这个词组的翻译是"表现层状态转化"。json
若是一个架构符合REST原则,就称它为RESTful架构。后端
要理解RESTful架构,最好的方法就是去理解Representational State Transfer这个词组究竟是什么意思,它的每个词表明了什么涵义。若是你把这个名称搞懂了,也就不难体会REST是一种什么样的设计。设计模式
资源(Resources)
所谓"资源",就是网络上的一个实体,或者说是网络上的一个具体信息。它能够是一段文本、一张图片、一首歌曲、一种服务,总之就是一个具体的实在。你能够用一个URI(统一资源定位符)指向它,每种资源对应一个特定的URI。要获取这个资源,访问它的URI就能够,所以URI就成了每个资源的地址或独一无二的识别符。
REST的名称"表现层状态转化"中,省略了主语。"表现层"其实指的是"资源"(Resources)的"表现层"。api
表现层(Representation)
"资源"是一种信息实体,它能够有多种外在表现形式。咱们把"资源"具体呈现出来的形式,叫作它的"表现层"(Representation)。
好比,文本能够用txt格式表现,也能够用HTML格式、XML格式、JSON格式表现,甚至能够采用二进制格式;图片能够用JPG格式表现,也能够用PNG格式表现。
URI只表明资源的实体,不表明它的形式。严格地说,有些网址最后的".html"后缀名是没必要要的,由于这个后缀名表示格式,属于"表现层"范畴,而URI应该只表明"资源"的位置。它的具体表现形式,应该在HTTP请求的头信息中用Accept和Content-Type字段指定,这两个字段才是对"表现层"的描述。
状态转化(State Transfer)
访问一个网站,就表明了客户端和服务器的一个互动过程。在这个过程当中,势必涉及到数据和状态的变化。
互联网通讯协议HTTP协议,是一个无状态协议。这意味着,全部的状态都保存在服务器端。所以,若是客户端想要操做服务器,必须经过某种手段,让服务器端发生"状态转化"(State Transfer)。
客户端用到的手段,只能是HTTP协议。具体来讲,就是HTTP协议里面,四个表示操做方式的动词:GET、POST、PUT、DELETE。它们分别对应四种基本操做:GET用来获取资源,POST用来新建资源(也能够用于更新资源),PUT用来更新资源,DELETE用来删除资源。
综合上面的解释,咱们总结一下什么是RESTful架构:
RESTful API more
必须有一种统一的机制,方便不一样的前端设备与后端进行通讯。这致使API构架的流行,甚至出现"API First"的设计思想。RESTful API是目前比较成熟的一套互联网应用程序的API设计理论。
REST自己并无创造新的技术、组件或服务,而隐藏在RESTful背后的理念就是使用Web的现有特征和能力, 更好地使用现有Web标准中的一些准则和约束。虽然REST自己受Web技术的影响很深, 可是理论上REST架构风格并非绑定在HTTP上,只不过目前HTTP是惟一与REST相关的实例。 因此上面描述的REST也是经过HTTP实现的REST。
RPC(Remote Procedure Call),即远程过程调用,是一个分布式系统间通讯的必备技术。RPC 最核心要解决的问题就是在分布式系统间,如何执行另一个地址空间上的函数、方法,就仿佛在本地调用同样。
下面依次展开每一个部分。
TCP 协议是 RPC 的 基石,通常来讲通讯是创建在 TCP 协议之上的,并且 RPC 每每须要可靠的通讯,所以不采用 UDP。
RPC 传输的 message 也就是 TCP body 中的数据,这个 message 也一样能够包含 header+body。body 也常常叫作 payload。
TCP 协议栈存在端口的概念,端口是进程获取数据的渠道。
作一个高性能 /scalable 的 RPC,须要可以知足:
Socket I/O 能够看作是两者之间的桥梁,如何更好地协调两者,去知足前面说的两点要求,有一些模式(pattern)是能够应用的。RPC 框架可选择的 I/O 模型严格意义上有 5 种,这里不讨论基于 信号驱动 的 I/O(Signal Driven I/O)。它们分别是:
这里不细说每种I/O模型。这里举一个形象的例子,读者就能够领会这四种I/O的区别,就用银行办业务 这个生活的场景描述。
传统的阻塞 I/O模型
一个柜员服务全部客户,可见当客户填写单据的时候也就是发生网络I/O的时候,柜员(也就是宝贵的线程或者进程)就会被阻塞,白白浪费了 CPU 资源,没法服务后面的请求。
若是一个柜员不够,那么就并发处理,对应采用线程池或者多进程方案,一个客户对应一个柜员,这明显加大了并发度,在并发不高的状况下性可以用,可是仍然存在柜员被 I/O 阻塞的可能。
I/O 多路复用
存在一个大堂经理,至关于代理,它来负责全部的客户,只有当客户写好单据后,才把客户分配一个柜员处理,能够想象柜员不用阻塞在 I/O 读写上,这样柜员效率会很是高,这也就是 I/O 多路复用的精髓。
异步 I/O
彻底不存在大堂经理,银行有一个自然的“高级的分配机器”,柜员注册本身负责的业务类型,例如 I/O 可读,那么由这个“高级的机器”负责I/O读,当可读时候,经过回调机制,把客户已经填写完毕的单据主动交给柜员,回调其函数完成操做。
重点说下高性能,而且工业界广泛使用的方案,也就是后两种。
I/O 多路复用
基于内核,创建在epoll或者kqueue上实现,I/O多路复用最大的优点是用户能够在一个线程内同时处理多个Socket的I/O请求。经过一个线程监听所有的TCP链接,有任何事件发生就通知用户态处理便可。
异步 I/O
这里重点说下同步 I/O 和异步I/O,理论上前三种模型都叫作同步I/O,同步是指用户线程发起I/O请求后须要等待或者轮询内核I/O完成后再继续,而异步是指用户线程发起I/O请求直接退出,当内核I/O操做完成后会通知用户线程来调用其回调函数。
I/O 多路复用每每对应 Reactor 模式,异步 I/O 每每对应 Proactor。
Reactor 通常使用epoll+事件驱动的经典模式,经过分治的手段,把耗时的网络链接、安全认证、编码等工做交给专门的线程池或者进程去完成,而后再去调用真正的核心业务逻辑层,这在 *nix 系统中被普遍使用。
著名的 Redis、Nginx、Node.js 的 Socket I/O 都用的这个,而 Java 的 NIO 框架 Netty 也是,Spark 2.0 RPC 所依赖的一样采用了 Reactor 模式。
Proactor在*nix中没有很好的实现,可是在Windows上大放异彩(例如 IOCP 模型)。
说个具体的例子,Thrift 做为一个融合了 序列化+RPC 的框架,提供了不少种 Server 的构建选项。
在Reactor中实现读:
- 注册读就绪事件和相应的事件处理器
- 事件分离器等待事件
- 事件到来,激活分离器,分离器调用事件对应的处理器。
- 事件处理器完成实际的读操做,处理读到的数据,注册新的事件,而后返还控制权。
在Proactor中实现读:
- 处理器发起异步读操做(注意:操做系统必须支持异步IO)。在这种状况下,处理器无视IO就绪事件,它关注的是完成事件。
- 事件分离器等待操做完成事件
- 在分离器等待过程当中,操做系统利用并行的内核线程执行实际的读操做,并将结果数据存入用户自定义缓冲区,最后通知事件分离器读操做完成。
- 事件分离器呼唤处理器。
- 事件处理器处理用户自定义缓冲区中的数据,而后启动一个新的异步操做,并将控制权返回事件分离器。
能够看出,两个模式的相同点,都是对某个IO事件的事件通知(即告诉某个模块,这个IO操做能够进行或已经完成)。在结构上,二者也有相同点:demultiplexor负责提交IO操做(异步)、查询设备是否可操做(同步),而后当条件知足时,就回调handler;不一样点在于,异步状况下(Proactor),当回调handler时,表示IO操做已经完成;同步状况下(Reactor),回调handler时,表示IO设备能够进行某个操做(can read or can write)。
序列化和反序列化,是作对象到二进制数据的转换。
程序是能够理解对象的,对象通常含有schema或者结构,基于这些语义来作特定的业务逻辑处理。
考察一个序列化框架通常会关注如下几点:
序列化方式很是多,常见的有 Protocol Buffers, Avro,Thrift,XML,JSON,MessagePack,Kyro,Hessian,Protostuff,Java Native Serialize,FST。
TCP 只是 binary stream 通道,是binary数据的可靠搬用工,它不懂 RPC 里面包装的是什么。而在一个通道上传输 message,势必涉及 message 的识别。
举个例子,正以下图中的例子,ABC+DEF+GHI 分 3 个 message,也就是分 3 个 Frame 发送出去,而接收端分四次收到 4 个 Frame。
Socket I/O 的工做完成得很好,可靠地传输过去,这是 TCP 协议保证的,可是接收到的是 4 个 Frame,不是本来发送的 3 个 message 对应的 3 个 Frame。
这种状况叫作发生了 TCP 粘包和半包 现象,AB、H、I 的状况叫作半包,CDEFG的状况叫作粘包。虽然顺序是对的,可是分组彻底和以前对应不上。
这时候应用层如何作语义级别的 message 识别是个问题,只有作好了协议的结构,才能把一整个数据片断作序列化或者反序列化处理。
好比:memcache的换行符、http中的固定长度头。
RPC 框架不光要处理 Network I/O、序列化、协议栈。还有不少不肯定性问题要处理,这里的不肯定性就是由 网络的不可靠 带来的麻烦。
例如如何保持长链接心跳?网络闪断怎么办?重连、重传?链接超时?这些都很是的细碎和麻烦,因此说开发好一个稳定的 RPC 类库是一个很是系统和细心的工程。
可是好在工业界有一群人就致力于提供平台似的解决方案,例如 Java 中的 Netty,它是一个强大的异步、事件驱动的网络 I/O 库,使用 I/O 多路复用的模型,作好了上述的麻烦处理。
它是面向对象设计模式的集大成者,使用方只须要会使用 Netty 的各类类,进行扩展、组合、插拔,就能够完成一个高性能、可靠的 RPC 框架。
著名的 gRPC Java 版本、Twitter 的 Finagle 框架、阿里巴巴的 Dubbo、新浪微博的 Motan、Spark 2.0 RPC 的网络层(能够参考 kraps-rpc:https://github.com/neoremind/kraps-rpc)都采用了这个类库。
RPC 是须要让上层写业务逻辑来实现功能的,如何优雅地启停一个 server,注入 endpoint,客户端怎么连,重试调用,超时控制,同步异步调用,SDK 是否须要交换等等,都决定了基于 RPC 构建服务,甚至 SOA 的工程效率与生产力高低。这里不作展开,看各类 RPC 的文档就知道他们的易用性如何了。
国内
国外
上述列出来的都是如今互联网企业经常使用的解决方案,暂时不考虑传统的 SOAP,XML-RPC 等。这些是有网络资料的,实际上不少公司内部都会针对本身的业务场景,以及和公司内的平台相融合(好比监控平台等),自研一套框架,可是异曲同工,都逃不掉刚刚上面所列举的 RPC 的要考虑的各个部分。
http比如普通话,rpc比如团伙内部黑话。讲普通话,好处就是谁都听得懂,谁都会讲。
讲黑话,好处是能够更精简、更加保密、更加可定制,坏处就是要求“说”黑话的那一方(client端)也要懂,并且一旦你们都说一种黑话了,换黑话就困难了。
这个问题实际上是有理解误区的,首先 http 和 rpc 并非一个并行概念。rpc是远端过程调用,其调用协议一般包含传输协议和编码协议。
传输协议包含: 如著名的 gRPC 使用的 http2 协议,也有如dubbo一类的自定义报文的tcp协议。编码协议包含: 如基于文本编码的 xml json,也有二进制编码的 protobuf binpack 等。
所以问题应该是:为何要使用自定义 tcp 协议的 rpc 作后端进程通讯?
要解决这个问题就应该搞清楚 http 使用的 tcp 协议,和咱们自定义的 tcp 协议在报文上的区别。首先要否定一点 http 协议相较于自定义tcp报文协议,增长的开销在于链接的创建与断开。
http协议是支持链接池复用的,也就是创建必定数量的链接不断开,并不会频繁的建立和销毁链接。另外要说的是http也可使用protobuf这种二进制编码协议对内容进行编码,所以两者最大的区别仍是在传输协议上。
通用定义的http1.1协议的tcp报文包含太多废信息,一个POST协议的格式大体以下:
HTTP/1.0 200 OK
Content-Type: text/plain
Content-Length: 137582
Expires: Thu, 05 Dec 1997 16:00:00 GMT
Last-Modified: Wed, 5 August 1996 15:55:28 GMT
Server: Apache 0.84<html>
<body>Hello World</body>
</html>
即便编码协议也就是body是使用二进制编码协议,报文元数据也就是header头的键值对却用了文本编码,很是占字节数。如上图所使用的报文中有效字节数仅仅占约 30%,也就是70%的时间用于传输元数据废编码。固然实际状况下报文内容可能会比这个长,可是报头所占的比例也是很是可观的。
自定义tcp协议能够极大地简化传输头内容。
所谓的效率优点是针对http1.1协议来说的,http2.0协议已经优化编码效率问题,像grpc这种rpc库使用的就是http2.0协议。这么来讲吧http容器的性能测试单位一般是kqps,自定义tpc协议则一般是以10kqps到100kqps为基准简单来讲成熟的rpc库相对http容器,
更多的是封装了“服务发现”,"错误重试"一类面向服务的高级特性。能够这么理解,rpc框架是面向服务的更高级的封装。若是把一个http server容器上封装一层服务发现和函数代理调用,那它就已经能够作一个rpc框架了。
附:thrift和http共存的一个示例