架构02-访问远程服务
1、远程服务调用
(1)RPC 的起源和概念
- 历史背景:RPC(Remote Procedure Call,远程服务调用) 在计算机科学中有超过 40 年的历史。
- 关注度:尽管历史悠久,但仍然受到广泛关注。
- 原因分析:
- 微服务风潮的推动。
- 开发者对 RPC 认知的模糊。
- RPC 的定义:
- 定义:RPC 是一种语言级别的通讯协议,允许程序在一台计算机上调用另一台计算机上的程序。
- 施乐 Palo Alto 研究中心:首次提出远程服务调用的定义。
(2)本地方法调用的过程
public static void main(String[] args) {
System.out.println("hello world");
}
- 步骤:
- 传递方法参数:将字符串
hello world
的引用压栈。
- 确定方法版本:根据方法签名确定执行版本。
- 执行被调方法:从栈中获取参数并执行逻辑。
- 返回执行结果:将结果压栈并恢复指令流。
(3)进程间通讯(IPC)
- 障碍:
- 参数传递:不同进程没有共享的栈内存。
- 方法版本选择:不同语言实现的程序难以确定方法版本。
- 解决办法:
- 管道(Pipe)/具名管道(Named Pipe):用于进程间传递字符流或字节流。
- 信号(Signal):通知目标进程有事件发生。
- 信号量(Semaphore):用于进程间的同步协作。
- 消息队列(Message Queue):适合传递大量信息。
- 共享内存(Shared Memory):效率最高的进程间通讯形式。
- 本地套接字接口(IPC Socket):适用于不同机器间的进程通信。
(4)通信的成本
- 早期目标:将 RPC 作为 IPC 的特例,实现远程调用与本地调用的一致性。
- 问题:
- 通信成本被忽视,导致性能下降。
- 服务端和客户端的角色划分。
- 异常处理和多线程竞争。
- 网络利用效率和连接复用。
- 参数和返回值的表示。
- 网络可靠性和故障处理。
- Andrew Tanenbaum 的观点:透明的调用形式增加了程序员的工作复杂度。
(5)RPC 框架要解决的三个基本问题
- 如何表示数据?
- 包括传递给方法的参数和方法的返回值。
- 解决方法:将数据转换为某种中立的数据流格式(序列化),再转换回不同语言中的数据类型(反序列化)。
- 常见的序列化协议:JSON、XML、Protocol Buffers、Thrift 等。
- 如何传递数据?
- 通过网络在两个服务 Endpoint 之间相互操作、交换数据。
- 解决方法:使用应用层协议(如 HTTP、gRPC)和传输层协议(如 TCP、UDP)。
- 常见的 Wire Protocol:HTTP、gRPC、Thrift、JSON-RPC 等。
- 如何表示方法?
- 在不同语言中表示和找到方法。
- 解决方法:使用接口描述语言(IDL)为每个方法规定一个通用且不重复的编号(如 UUID)。
- 常见的 IDL:CORBA IDL、Thrift IDL、gRPC IDL 等。
(6)统一的RPC
- DCE/RPC
- 由惠普和 Apollo 提出,面向 Unix 系统。
- 限制:仅支持 Unix 系统,不支持对象传递。
- ONC RPC
- 由 Sun Microsystems 提出,基于 TCP/IP 网络,支持 C 语言。
- 限制:不支持对象传递。
- CORBA
- 由 OMG 发布,支持多语言,面向对象。
- 失败原因:设计过于复杂,实现互不兼容。
- DCOM
- 由微软提出,支持多语言,但受限于 Windows 系统。
- 失败原因:操作系统限制。
- Web Service
- 以 XML 为基础,支持多语言。
- 失败原因:性能差,协议过于复杂。
(7)分裂的RPC
- 简单、普适、高性能这三点,一直没有一个同时满足以上三点的“完美RPC协议”出现,所以远程服务器调用这个小小的领域,逐渐进入百家争鸣的战国时代。
- RMI(Sun/Oracle):面向 Java 语言,支持远程对象调用。
- Thrift(Facebook/Apache):支持多语言,高性能,基于 TCP 协议。
- Dubbo(阿里巴巴/Apache):支持多语言,高性能,插件化设计。
- gRPC(Google):支持多语言,高性能,基于 HTTP/2 协议。
- Motan2(新浪):支持多语言,高性能。
- Finagle(Twitter):支持多语言,高性能。
- brpc(百度):支持多语言,高性能。
- .NET Remoting(微软):面向 .NET 语言,支持远程对象调用。
- Arvo(Hadoop):面向 Hadoop 生态,支持多语言。
- JSON-RPC 2.0(公开规范):简单易用,适合 Web 浏览器。现代 RPC 框架的发展趋势
- 现代 RPC 框架的发展趋势:高层次与插件化
- 不再仅限于调用远程服务,还管理远程服务。
- 设计为扩展点,实现核心能力的可配置。
- 代表框架:Thrift、Dubbo。
2、REST设计风格
(1)REST 与 RPC 的对比
- 思想上的差异:面向资源 vs 面向过程
- REST:面向资源编程,抽象目标是资源。
- RPC:面向过程编程,抽象目标是方法。
- 概念上的不同
- REST:不是一种协议,而是一种风格,没有强制性的规范。
- RPC:是一种远程服务调用协议,有明确的规范和规约文档。
- 使用范围上的差异
- REST:
- 适合浏览器端消费的远程服务。
- 适合移动端、桌面端或分布式服务端的节点间通信,前提是网络不是性能瓶颈。
- RPC:
- 适合分布式对象应用。
- 适合追求远程服务调用效率的场景。
(2)REST 起源
- **提出者:**Roy Thomas Fielding
- **时间:**2000年
- 全称:Representational State Transfer(表现层状态转化)
- 背景:
- Fielding是HTTP协议(1.0版和1.1版)的主要设计者、Apache服务器软件的作者之一、Apache基金会的第一任主席。
- 他的博士论文《Architectural Styles and the Design of Network-based Software Architectures》探讨了软件和网络的交叉点。
- 目的:理解并评估以网络为基础的应用软件的架构设计,获得功能强、性能好、适宜通信的架构。
(3)REST 核心概念
- 资源(Resources)
- 定义:网络上的一个实体或具体信息,可以用URI(统一资源定位符)指向。
- 示例:文本、图片、服务等。
- 特点:每个资源对应一个特定的URI,URI是资源的地址或唯一识别符。
- 表现层(Representation)
- 定义:资源的具体呈现形式。
- 示例:
- 文本:txt、HTML、XML、JSON等格式。
- 图片:JPG、PNG等格式。
- 重要性:URI只代表资源的位置,具体表现形式应在HTTP请求的头信息中指定(如Accept和Content-Type字段)。
- 状态转化(State Transfer)
- 定义:客户端通过HTTP协议操作服务器端资源,实现状态转化。
- HTTP动词:
- GET:获取资源
- POST:新建资源(也可用于更新资源)
- PUT:更新资源
- DELETE:删除资源
(4)REST 的优缺点
- 优点:
- **降低学习成本:**标准的 HTTP 方法,不需要额外学习。
- **资源的集合与层次结构:**资源是名词,天然具有集合与层次结构。
- **绑定于 HTTP 协议:**复用 HTTP 协议的语义和基础支持。
- 缺点:
- 面向资源的编程思想只适合做 CRUD
- 观点:HTTP 的基本方法容易让人联想到 CRUD 操作,但 REST 的范围远不止于此。
- 解决方案:可以使用自定义方法,按 Google 推荐的 REST API 风格来拓展 HTTP 标准方法。
- REST 与 HTTP 完全绑定,不适用于高性能传输
- 观点:REST 依赖 HTTP 协议,不适用于需要直接控制传输细节的场景。
- 解决方案:使用其他协议(如 gRPC)来处理高性能传输。
- REST 不利于事务支持
- 观点:REST 不支持刚性 ACID 事务,但可以支持最终一致性。
- 解决方案:使用分布式事务协议(如 2PC/3PC)来处理强一致性需求。
- REST 没有传输可靠性支持
- 观点:HTTP 协议缺乏对传输可靠性的支持。
- 解决方案:通过幂等性设计和重试机制来提高可靠性。
- REST 缺乏对资源进行“部分”和“批量”的处理能力
- 观点:HTTP 协议缺乏对部分和批量操作的支持。
- 解决方案:使用 GraphQL 等新技术来解决这些问题。
(5)Richardson 成熟度模型
- Richardson 将 REST 服务接口按照“REST 的程度”从低到高分为 0 至 3 共 4 级:

- 第 0 级:The Swamp of Plain Old XML
- 特点:基于 RPC 风格,使用单一的 Endpoint,通过参数和动作来区分不同的操作。
- 示例:
POST /appointmentService?action=query HTTP/1.1
{
date: "2020-03-04",
doctor: "mjones"
}
POST /appointmentService?action=confirm HTTP/1.1
{
appointment: {
date: "2020-03-04",
start: "14:00",
doctor: "mjones"
},
patient: {
name: "xx",
age: 30
}
}
- 第 1 级:Resources
- 特点:引入资源的概念,Endpoint 是名词而非动词,每次请求包含资源 ID。
- 示例:
POST /doctors/mjones HTTP/1.1
{
date: "2020-03-04"
}
POST /schedules/1234 HTTP/1.1
{
name: "xx",
age: 30
}
- 第 2 级:HTTP Verbs
- 特点:使用 HTTP 标准方法(GET、POST、PUT、DELETE)来操作资源,使用 HTTP 状态码来表示操作结果,处理认证授权。
- 示例:
GET /doctors/mjones/schedule?date=2020-03-04&status=open HTTP/1.1
POST /schedules/1234 HTTP/1.1
{
name: "xx",
age: 30
}
- 第 3 级:Hypermedia Controls (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: "confirm schedule", href: "/schedules/1234" }
]
},
{
id: 5678,
start: "16:00",
end: "16:50",
doctor: "mjones",
links: [
{ rel: "confirm schedule", href: "/schedules/5678" }
]
}
],
links: [
{ rel: "doctor info", href: "/doctors/mjones/info" }
]
}
(6)常见设计错误
- **URI包含动词:**URI应为名词,动词应放在HTTP协议中。
- 错误示例:
/posts/show/1
- 正确示例:
/posts/1
+ GET
方法
- **动词无法用HTTP动词表示:**将动词转换为名词,作为资源。
- 错误示例:
POST /accounts/1/transfer/500/to/2
- 正确示例:
POST /transaction
+ from=1&to=2&amount=500.00
- **URI中加入版本号:**不同版本应视为同一种资源的不同表现形式,版本号应在HTTP请求头信息的Accept字段中指定。
http://www.example.com/app/1.0/foo
http://www.example.com/app/1.1/foo
http://www.example.com/app/2.0/foo
- 正确示例:
Accept: vnd.example-com.foo+json; version=1.0
Accept: vnd.example-com.foo+json; version=1.1
Accept: vnd.example-com.foo+json; version=2.0
3、REST 设计指南
(1)协议
(2)域名
- **专用域名:**建议将API部署在专用域名下,例如
https://api.example.com
。
- **主域名下:**如果API简单且不会扩展,可以考虑放在主域名下,例如
https://example.org/api/
。
(3)版本
- **URL中包含版本号:**推荐将API的版本号放入URL,例如
https://api.example.com/v1/
。
- **HTTP头信息:**另一种做法是将版本号放在HTTP头信息中,但不如放入URL方便和直观。GitHub采用这种做法。
(4)路径(Endpoint)
- **资源表示:**每个网址代表一种资源,网址中不能有动词,只能有名词,且名词通常与数据库的表格名对应。
- **复数形式:**API中的名词应使用复数形式,例如:
https://api.example.com/v1/zoos
https://api.example.com/v1/animals
https://api.example.com/v1/employees
(5)HTTP动词
- 常用动词:
GET
(SELECT):从服务器取出资源。
POST
(CREATE):在服务器新建一个资源。
PUT
(UPDATE):在服务器更新资源(客户端提供改变后的完整资源)。
PATCH
(UPDATE):在服务器更新资源(客户端提供改变的属性)。
DELETE
(DELETE):从服务器删除资源。
- 不常用动词:
HEAD
:获取资源的元数据。
OPTIONS
:获取信息,关于资源的哪些属性是客户端可以改变的。
(6)过滤信息(Filtering)
?limit=10
:指定返回记录的数量。
?offset=10
:指定返回记录的开始位置。
?page=2&per_page=100
:指定第几页及每页的记录数。
?sortby=name&order=asc
:指定返回结果按照哪个属性排序及排序顺序。
?animal_type_id=1
:指定筛选条件。
(7)状态码(Status Codes)
200 OK
:服务器成功返回用户请求的数据。
201 CREATED
:用户新建或修改数据成功。
202 Accepted
:请求已进入后台排队(异步任务)。
204 NO CONTENT
:用户删除数据成功。
400 INVALID REQUEST
:用户请求有错误。
401 Unauthorized
:用户没有权限。
403 Forbidden
:用户已授权但访问被禁止。
404 NOT FOUND
:请求的记录不存在。
406 Not Acceptable
:请求的格式不可得。
410 Gone
:请求的资源被永久删除。
422 Unprocesable entity
:创建对象时发生验证错误。
500 INTERNAL SERVER ERROR
:服务器发生错误。
(8)错误处理(Error handling)
- 返回出错信息:状态码为4xx时,返回的JSON中应包含
error
键及其对应的出错信息,例如:
{
"error": "Invalid API key"
}
(9)返回结果
GET /collection
:返回资源对象的列表(数组)。
GET /collection/resource
:返回单个资源对象。
POST /collection
:返回新生成的资源对象。
PUT /collection/resource
:返回完整的资源对象。
PATCH /collection/resource
:返回完整的资源对象。
DELETE /collection/resource
:返回一个空文档。
(10)Hypermedia API
- 提供链接:返回结果中提供链接,指向其他API方法,使用户不查文档也能知道下一步操作。例如:
{
"link": {
"rel": "collection https://www.example.com/zoos",
"href": "https://api.example.com/zoos",
"title": "List of zoos",
"type": "application/vnd.yourformat+json"
}
}
- HATEOAS:Hypermedia API的设计原则,GitHub的API采用了这种设计。
(11)其他
- **身份认证:**API的身份认证应使用OAuth 2.0框架。
- **数据格式:**服务器返回的数据格式应尽量使用JSON,避免使用XML。