远程服务调用的出现是为了让计算机能够跟调用本地方法一样去调用远程方法,而刚好 Socket 是网络栈的统一接口,各个系统中也都有提供标准接口可以直接使用
那我们先从先从进程间通信(Inter-Process Communication,本地 IPC 通讯)讲起,在计算机操作系统中,进程间的通信方式应该大致分为四类,加上进程间同步,一个可以数出以下几种方式:
共享内存:两个进程共享同一块物理内存空间,这种方式需要依靠某种同步操作,如互斥锁和信号量等,因为可能会出现修改丢失问题,所以两个进程对内存空间的访问必须是互斥的
管道通信:管道是一种在内存中具有一定空间的缓冲区,在 Linux 系统中叫 pipe 文件,与在磁盘中的 .txt 等文件相似,只能一个进程向文件中写入数据,另一个进程从文件中读取数据,一个进程写数据时另外一个进程不能读数据,两个进程需要互斥访问管道;管道又分匿名管道和有名管道。我们经常使用的命令行中的 “ | ” 操作符,就是典型的管道
消息传递:直接将消息从一个进程传到另一个进程中,消息(message)是一种封装了数据的东西,比如报文;这种通信方式又分直接通信(通过系统源语实现,一个进程通过发送源语将消息挂到另一个进程的消息访问队列中,进程通过接受源语接受)和间接通信(消息队列),信号和消息队列应该是属于消息传递的
套接字接口:主要实现为套接字(一个存放了目的地址、目的端口、传输层协议、自己地址的数据结构)、RPC,主要用与网络间进程的通信,不过也可以在同一台主机上通信(原始套接字)
信号:信号用于通知目标进程有某种事件发生,比如 kill 命令,由 shell 进程向指定的 pid 发送消息
信号量:信号量用于在两个进程之间同步协作,相当与操作系统提供的特殊变量,程序可以通过这个进行 wait 与 notify
远程服务调用是指位于互不重合的内存地址空间中的两个程序,在语言层面上,以同步的方式使用带宽有限的信道来传输程序控制信息
在实现这个的过程中我们慢慢发现 RPC 一定绕不开三个问题,几乎所有的 RPC 协议都是围绕着解决以下三个基本问题:
很多人会拿 rest 与 rpc 来比较,REST 是一种面向资源编程的风格(REST 是 Representational State Transfer 表征状态转移的缩写 ),是风格而不是协议,具体如何实现他们没有限定死
REST 无论是在思想上、概念上,还是使用范围上,与 RPC 都不尽相同,即面向资源的编程思想与面向过程的编程思想两者之间的区别
由于绑定 HTTP 协议,性能瓶颈也明显,但在移动端、桌面端或者分布式服务端的节点之间通讯这一块,REST 照样有宽阔的用武之地
先来了解一些 REST 的概念:
资源:也就是内容本身,比如你正在读一篇<
表征:资源的表达形式,如 HTML,PDF 等
状态:上下文状态。当你读完这篇文章,想看后面是什么内容时,你向服务端发出给我下一篇文章的请求。但是下一篇是个相对的概念,必须依赖你当前正在阅读的文章是哪一篇才能正确回应,这类在特定语境中产生的上下文信息被称为状态
转移:无论状态是由服务端还是客户端来提供,取下一篇文章这个行为逻辑只能由服务端来提供,因为只有服务端拥有该资源及其表征形式。服务端通过某种方式,把用户当前阅读的文章转变成下一篇文章,这就被称为表征状态转移
统一接口:HTTP 请求方法就是统一接口
超文本驱动:PC 软件客户端一般都是有专门的控制器来驱动状态转移,而 web 端是由服务器发出响应来驱动的,也叫超文本驱动。任何网站的导航(状态转移)行为都不是预置于浏览器代码中的,而是通过服务器发出的请求响应信息(超文本)来驱动的
自描述消息:资源可能有多种表征,所以消息中一般有自描述信息,即告知服务器该消息的类型的消息,如 “Content-Type”
一套理想的、完全满足 REST 的系统应该满足以下六大原则:
REST 的以资源为主体的服务设计风格,可以带来不少好处:
《RESTful Web APIs》和《RESTful Web Services》的作者 Leonard Richardson 曾提出过一个衡量“服务有多么 REST”的 Richardson 成熟度模型(Richardson Maturity Model),便于那些原本不使用 REST 的系统,能够逐步地导入 REST
即 RPC 的调用方式,例子如下
医院开放了一个/appointmentService的 Web API,传入日期、医生姓名作为参数,可以得到该时间段该名医生的空闲时间,该 API 的一次 HTTP 调用如下所示:
POST /appointmentService?action=query HTTP/1.1
{date: "2020-03-04", doctor: "mjones"}
然后服务器会传回一个包含了所需信息的回应:
HTTP/1.1 200 OK
[
{start:"14:00", end: "14:50", doctor: "mjones"},
{start:"16:00", end: "16:50", doctor: "mjones"}
]
得到了医生空闲的结果后,我觉得 14:00 的时间比较合适,于是进行预约确认,并提交了我的基本信息:
POST /appointmentService?action=comfirm HTTP/1.1
{
appointment: {date: "2020-03-04", start:"14:00", doctor: "mjones"},
patient: {name: icyfenix, age: 30, ……}
}
如果预约成功,那我能够收到一个预约成功的响应。如果发生了问题,譬如有人在我前面抢先预约了,那么我会在响应中收到某种错误信息:
HTTP/1.1 200 OK
{
code: 0,
message: "Successful confirmation of appointment"
}
HTTP/1.1 200 OK
{
code: 1
message: "doctor not available"
}
第 0 级是 RPC 的风格,如果需求永远不会变化,也不会增加,那它完全可以良好地工作下去。但是,如果你不想为预约医生之外的其他操作、为获取空闲时间之外的其他信息去编写额外的方法,或者改动现有方法的接口,那还是应该考虑一下如何使用 REST 来抽象资源
通往 REST 的第一步是引入资源的概念,在 API 中基本的体现是围绕着资源而不是过程来设计服务,说的直白一点,可以理解为服务的 Endpoint 应该是一个名词而不是动词。此外,每次请求中都应包含资源的 ID,所有操作均通过资源 ID 来进行(主键 ID)
POST /doctors/mjones HTTP/1.1
{date: "2020-03-04"}
然后服务器传回一组包含了 ID 信息的档期清单,注意,ID 是资源的唯一编号,有 ID 即代表“医生的档期”被视为一种资源:
HTTP/1.1 200 OK
[
{id: 1234, start:"14:00", end: "14:50", doctor: "mjones"},
{id: 5678, start:"16:00", end: "16:50", doctor: "mjones"}
]
我还是觉得 14:00 的时间比较合适,于是又进行预约确认,并提交了我的基本信息:
POST /schedules/1234 HTTP/1.1
{name: icyfenix, age: 30, ……}
第 1 级遗留三个问题都可以靠引入统一接口来解决。HTTP 协议的七个标准方法是经过精心设计的,只要架构师的抽象能力够用,它们几乎能涵盖资源可能遇到的所有操作场景。REST 的做法是把不同业务需求抽象为对资源的增加、修改、删除等操作来解决第一个问题;使用 HTTP 协议的 Status Code,可以涵盖大多数资源操作可能出现的异常,而且 Status Code 也是可以自定义扩展,以此解决第二个问题;依靠 HTTP Header 中携带的额外认证、授权信息来解决第三个问题,这个在实战中并没有体现,请参考安全架构中的“ 凭证 ”相关内容
按这个思路,获取医生档期,应采用具有查询语义的 GET 操作进行:
GET /doctors/mjones/schedule?date=2020-03-04&status=open HTTP/1.1
然后服务器会传回一个包含了所需信息的回应:
HTTP/1.1 200 OK
[
{id: 1234, start:"14:00", end: "14:50", doctor: "mjones"},
{id: 5678, start:"16:00", end: "16:50", doctor: "mjones"}
]
我仍然觉得 14:00 的时间比较合适,于是又进行预约确认,并提交了我的基本信息,用以创建预约,这是符合 POST 的语义的:
POST /schedules/1234 HTTP/1.1
{name: icyfenix, age: 30, ……}
如果预约成功,那我能够收到一个预约成功的响应:
HTTP/1.1 201 Created
Successful confirmation of appointment
如果发生了问题,譬如有人在我前面抢先预约了,那么我会在响应中收到某种错误信息:
HTTP/1.1 409 Conflict
doctor not available
第 2 级是目前绝大多数系统所到达的 REST 级别(这是有原因的)。但仍不是完美的,至少还存在一个问题:你是如何知道预约 mjones 医生的档期是需要访问/schedules/1234这个服务 Endpoint 的?也许你甚至第一时间无法理解为何我会有这样的疑问,这当然是程序代码写的呀!但 REST 并不认同这种已烙在程序员脑海中许久的想法。RMM 中的 Hypermedia Controls、Fielding 论文中的 HATEOAS 和现在提的比较多的“超文本驱动”,所希望的是除了第一个请求是有你在浏览器地址栏输入所驱动之外,其他的请求都应该能够自己描述清楚后续可能发生的状态转移,由超文本自身来驱动。所以,当你输入了查询的指令之后:
GET /doctors/mjones/schedule?date=2020-03-04&status=open HTTP/1.1
服务器传回的响应信息应该包括诸如如何预约档期、如何了解医生信息等可能的后续操作:
HTTP/1.1 200 OK
{
schedules:[
{
id: 1234, start:"14:00", end: "14:50", doctor: "mjones",
links: [
{rel: "comfirm schedule", href: "/schedules/1234"}
]
},
{
id: 5678, start:"16:00", end: "16:50", doctor: "mjones",
links: [
{rel: "comfirm schedule", href: "/schedules/5678"}
]
}
],
links: [
{rel: "doctor info", href: "/doctors/mjones/info"}
]
}
如果做到了第 3 级 REST,那服务端的 API 和客户端也是完全解耦的,你要调整服务数量,或者同一个服务做 API 升级将会变得非常简单。但是缺点是将原先在客户端就可以使用的行为放到了服务端,并且用大量的数据来处理状态转移这件事就很有争议