访问远程服务

访问远程服务

  • 进程间通信
  • 关于 RPC 的三个基本问题
  • REST 设计风格
    • 基本概念
    • REST 的六大原则
    • RMM
      • The Swamp of Plain Old XML:完全不 REST
      • Resources:开始引入资源的概念
      • HTTP Verbs:引入统一接口
      • Hypermedia Controls:超媒体控制(超文本驱动)

为了将我们的系统设计的更加复杂一点,让它的功能更加多一些,让用户的体验更好一些,我们应该将单机的服务慢慢转换成分布式的系统,而分布式的系统有一个非常重要的前提条件:各个主机之间是如何交互的

进程间通信

远程服务调用的出现是为了让计算机能够跟调用本地方法一样去调用远程方法,而刚好 Socket 是网络栈的统一接口,各个系统中也都有提供标准接口可以直接使用

那我们先从先从进程间通信(Inter-Process Communication,本地 IPC 通讯)讲起,在计算机操作系统中,进程间的通信方式应该大致分为四类,加上进程间同步,一个可以数出以下几种方式:

  • 共享内存:两个进程共享同一块物理内存空间,这种方式需要依靠某种同步操作,如互斥锁和信号量等,因为可能会出现修改丢失问题,所以两个进程对内存空间的访问必须是互斥的

  • 管道通信:管道是一种在内存中具有一定空间的缓冲区,在 Linux 系统中叫 pipe 文件,与在磁盘中的 .txt 等文件相似,只能一个进程向文件中写入数据,另一个进程从文件中读取数据,一个进程写数据时另外一个进程不能读数据,两个进程需要互斥访问管道;管道又分匿名管道和有名管道。我们经常使用的命令行中的 “ | ” 操作符,就是典型的管道

  • 消息传递:直接将消息从一个进程传到另一个进程中,消息(message)是一种封装了数据的东西,比如报文;这种通信方式又分直接通信(通过系统源语实现,一个进程通过发送源语将消息挂到另一个进程的消息访问队列中,进程通过接受源语接受)和间接通信(消息队列),信号和消息队列应该是属于消息传递的

  • 套接字接口:主要实现为套接字(一个存放了目的地址、目的端口、传输层协议、自己地址的数据结构)、RPC,主要用与网络间进程的通信,不过也可以在同一台主机上通信(原始套接字)

  • 信号:信号用于通知目标进程有某种事件发生,比如 kill 命令,由 shell 进程向指定的 pid 发送消息

  • 信号量:信号量用于在两个进程之间同步协作,相当与操作系统提供的特殊变量,程序可以通过这个进行 wait 与 notify

关于 RPC 的三个基本问题

远程服务调用是指位于互不重合的内存地址空间中的两个程序,在语言层面上,以同步的方式使用带宽有限的信道来传输程序控制信息

在实现这个的过程中我们慢慢发现 RPC 一定绕不开三个问题,几乎所有的 RPC 协议都是围绕着解决以下三个基本问题:

  • 如何表示数据:这里的数据包括传递给方法的参数以及方法执行后的返回值在,这些在网络中如何表示呢。在不同的硬件指令集、操作系统下,同样的数据有2不同的表现实现,可行的做法是将两边的数据定义事先约定好的中立数据结构来传递。对,就是序列化
  • 如何传递数据:我们有了数据在网络中的表示,那这些数据该如何传输呢,我们是用 http 还是 tcp?发生异常、超时、事务等情况的时候,网络如何处理呢
  • 如何确定方法:在本地调用方法的时候,编译器会根据指针确定方法的位置,但是在 rpc 里,每种语言的方法签名都有可能有差别,如何表示同一个方法需要有一个统一的跨语言的标准才行,比如用一个 UUID 来确定方法

REST 设计风格

很多人会拿 rest 与 rpc 来比较,REST 是一种面向资源编程的风格(REST 是 Representational State Transfer 表征状态转移的缩写 ),是风格而不是协议,具体如何实现他们没有限定死

REST 无论是在思想上、概念上,还是使用范围上,与 RPC 都不尽相同,即面向资源的编程思想与面向过程的编程思想两者之间的区别

由于绑定 HTTP 协议,性能瓶颈也明显,但在移动端、桌面端或者分布式服务端的节点之间通讯这一块,REST 照样有宽阔的用武之地

基本概念

先来了解一些 REST 的概念:

  • 资源:也就是内容本身,比如你正在读一篇<>的文章,这篇文章的内容称之为资源。可以理解为信息、数据

  • 表征:资源的表达形式,如 HTML,PDF 等

  • 状态:上下文状态。当你读完这篇文章,想看后面是什么内容时,你向服务端发出给我下一篇文章的请求。但是下一篇是个相对的概念,必须依赖你当前正在阅读的文章是哪一篇才能正确回应,这类在特定语境中产生的上下文信息被称为状态

  • 转移:无论状态是由服务端还是客户端来提供,取下一篇文章这个行为逻辑只能由服务端来提供,因为只有服务端拥有该资源及其表征形式。服务端通过某种方式,把用户当前阅读的文章转变成下一篇文章,这就被称为表征状态转移

  • 统一接口:HTTP 请求方法就是统一接口

  • 超文本驱动:PC 软件客户端一般都是有专门的控制器来驱动状态转移,而 web 端是由服务器发出响应来驱动的,也叫超文本驱动。任何网站的导航(状态转移)行为都不是预置于浏览器代码中的,而是通过服务器发出的请求响应信息(超文本)来驱动的

  • 自描述消息:资源可能有多种表征,所以消息中一般有自描述信息,即告知服务器该消息的类型的消息,如 “Content-Type”

REST 的六大原则

一套理想的、完全满足 REST 的系统应该满足以下六大原则:

  • 客户端与服务端分离:就是我们通常说的网页,前后端分离,将用户界面所关注的逻辑和数据存储所关注的逻辑分离开来。与之对应的是以前完全基于服务端控制和渲染(如 JSF 这类)框架
  • 无状态:无状态是 REST 的核心原则,REST 风格的服务设计的别扭很可能的一个原因是服务端持有着比较重的状态。REST 希望服务器不要去负责维护状态,每一次从客户端发送的请求中,应包括所有的必要的上下文信息,会话信息也由客户端负责保存维护,服务端依据客户端传递的状态来执行业务处理逻辑,驱动整个应用的状态变迁。可以将 Cookie 的使用当成无状态的实践之一,但是极大部分系统都不可能达到真正的无状态,因为大量的上下文状态会膨胀到客户端无法承受的程度
  • 可缓存:REST 希望软件系统能够如同万维网一样,允许客户端和中间的通讯传递者(譬如代理)将部分服务端的应答缓存起来。缓存整个应答的资源数据
  • 分层系统:这里所指的并不是表示层、服务层、持久层这种意义上的分层。而是指客户端一般不需要知道是否直接连接到了最终的服务器,抑或连接到路径上的中间服务器。中间服务器可以通过负载均衡和共享缓存的机制提高系统的可扩展性,这样也便于缓存、伸缩和安全策略的部署。该原则的典型的应用是内容分发网络 CDN,比如 GitHub Pages 的源服务器与国内的 CDN 服务器
  • 统一接口:REST 希望开发者面向资源编程,希望软件系统设计的重点放在抽象系统该有哪些资源上,而不是抽象系统该有哪些行为(服务)上。面向资源编程的抽象程度通常更高。抽象程度高意味着坏处是往往距离人类的思维方式更远,而好处是往往通用程度会更好。想要在架构设计中合理恰当地利用统一接口,Fielding 建议系统应能做到每次请求中都包含资源的 ID,所有操作均通过资源 ID 来进行;建议每个资源都应该是自描述的消息;建议通过超文本来驱动应用状态的转移
  • 按需代码: 按需代码被 Fielding 列为一条可选原则。它是指任何按照客户端(譬如浏览器)的请求,将可执行的软件程序从服务器发送到客户端的技术,按需代码赋予了客户端无需事先知道所有来自服务端的信息应该如何处理、如何运行的宽容度

REST 的以资源为主体的服务设计风格,可以带来不少好处:

  • 降低的服务接口的学习成本:统一接口(Uniform Interface)是 REST 的重要标志,将对资源的标准操作都映射到了标准的 HTTP 方法上去
  • 资源天然具有集合与层次结构
  • REST 绑定于 HTTP 协议。面向资源编程不是必须构筑在 HTTP 之上,但 REST 是,这是缺点,也是优点。因为 HTTP 本来就是面向资源而设计的网络协议

RMM

《RESTful Web APIs》和《RESTful Web Services》的作者 Leonard Richardson 曾提出过一个衡量“服务有多么 REST”的 Richardson 成熟度模型(Richardson Maturity Model),便于那些原本不使用 REST 的系统,能够逐步地导入 REST

The Swamp of Plain Old XML:完全不 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"
}

Resources:开始引入资源的概念

第 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, ……}

HTTP Verbs:引入统一接口

第 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

Hypermedia Controls:超媒体控制(超文本驱动)

第 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 升级将会变得非常简单。但是缺点是将原先在客户端就可以使用的行为放到了服务端,并且用大量的数据来处理状态转移这件事就很有争议

你可能感兴趣的:(分布式,架构,网络,服务器,架构)