API 接口设计: REST、GraphQL和GRPC怎么选择?
前言
为了快速、大规模地集成应用程序,API是使用协议或规范实现的,这些协议或规范定义了通过网络传递的消息的语义和语法。这些规范组成了API体系结构。
随着时间的推移,不同的API架构风格已经发布。 每一种都有自己的标准化数据交换模式,选择的丰富引发了关于哪种架构风格是最好的无休止的争论。
Web API设计实在是一个挺主要的设计话题,许多公司都市有公司层面的Web API设计规范,险些所有的项目在详细设计阶段都市举行API设计,项目开发后都市有一份API文档供测试和联调。本文实验凭据自己的明白总结一下REST、GraphQL和GRPC常见的三种API设计气概以及设计思量点。
一、REST
REST即表述性状态传递(英文:Representational State Transfer,简称REST),它符合特定的指南,是 Web API 实现的约束。是 Roy Fielding 博士在他的博士论文中提出来的一种软件架构风格。它鼓励客户端和服务器以无状态模式交换信息。 请记住,并非所有 API 都是 REST,但所有 RESTful 服务都是 API。REST使服务器端数据可用简单的格式表示,通常是JSON和XML。
1、REST如何工作
REST并不像SOAP那样严格定义,RESTful架构应该遵守六个架构约束。
- 统一接口:允许以统一的方式与给定的服务器进行交互,而不考虑设备或应用类型。
- 无状态:处理请求的必要状态,就像请求本身所包含的那样,服务器不需要存储任何与会话有关的内容。
- 缓存
- 客户端-服务器架构:允许任何一方独立进化
- 应用程序的分层系统
- 服务器向客户机提供可执行代码的能力
事实上,有些服务只是在一定程度上是RESTful的。它们以RPC风格为核心,将较大的服务分解为资源,并有效地使用HTTP基础设施。但关键部分是使用超媒体又名HATEOAS,即Hypertext As The Engine of Application State的缩写。基本上,这意味着每一个响应,REST API都会提供元数据,链接到所有关于如何使用API的相关信息。这就是实现客户端和服务器解耦的原因。因此,API提供者和API消费者都可以独立发展而不妨碍他们的交流。
REST是一种架构气概,有四个级别的成熟度:
- 级别0:界说一个 URI,所有操作是对此 URI 发出的 POST 请求。
- 级别1:为各个资源单独建立 URI。
- 级别2:使用 HTTP 方式来界说对资源执行的操作。
- 级别3:使用超媒体(HATEOAS)。
级别0实在就是类RPC的气概,级别3是真正的REST,大多数号称REST的API在级别2。
“HATEOAS是REST的一个关键特性。它是真正的REST REST的原因。因为大多数人没有使用HATEOAS,他们实际上是在使用HTTP RPC。” 这是Reddit上表达的一些激进意见。事实上,HATEOAS是REST最成熟的版本。然而,要实现这一目标,需要比目前通常使用和构建的API客户端先进得多的智能API是很困难的。所以,如今即使是非常好的REST API也不一定能做到。这也是为什么HATEOAS主要作为RESTful API设计的长期发展愿景。
2、REST架构
REST 架构的设计范式侧重于分配 HTTP 请求方法(GET、POST、PUT、PATCH、DELETE)和 URL 端点之间的关系。
- GET:获取资源详情或资源列表。对于collection类型的URI(好比/customers)就是获取资源列表,对于item类型的URI(好比/customers/1)就是获取一个资源。
- POST:建立资源,请求体是新资源的内容。往往POST是用于为聚集新增资源。
- PUT:建立或修改资源,请求体是新资源的内容。往往PUT用于单个资源的新增或修改。实现上必须幂等。
- PATCH:部门修改资源,请求体是修改的那部门内容。PUT一样平常要求提交整个资源举行修改,而PATCH用于修改部门内容(好比某个属性)。
- DELETE:移除资源。和GET一样,对于collection类型的URI(好比/customers)就是删除所有资源,对于item类型的URI(好比/customers/1)就是删除一个资源。
在 REST 架构中,方法和端点的每个组合得到不同的封装功能。如果客户端需要的数据特定端点 / 方法不提供,则可能需要额外请求。从 REST 请求返回的数据格式依赖于端点—不能保证这些数据会按照前端需要的方式进行格式化。为使用来自响应的数据(格式与缺省情况下从端点返回的格式不同),必须在客户端编写数据解析和数据操作。
3、REST优点
- 解耦客户端和服务器。尽可能地将客户端和服务器解耦,REST可以实现比RPC更好的抽象。一个具有抽象层次的系统,能够对其细节进行封装,以更好地识别和维持其属性。这使得REST API具有足够的灵活性,可以随着时间的推移而发展,同时保持一个稳定的系统。
- 可发现性。客户端和服务器之间的通信描述了一切,因此不需要外部文档就能理解如何与REST API交互。
- 缓存友好。重用了很多HTTP工具,REST是唯一允许在HTTP层面上缓存数据的风格。相比之下,在任何其他API上实现缓存都需要配置一个额外的缓存模块。
- 支持多种格式。支持多种格式存储和交换数据的能力是目前REST成为构建公共API的主流选择的原因之一。
4、REST缺点
- 多端 (多次数据交互):在 RESTful 服务中一个 URL 表示一个资源。因此,当要获取多个资源时你必须请求多个不同的 URL,进而带来多次数据交互。
当我们考虑一个博客应用。一篇博客下面有多条评论的情形。通常我们要调用的 URL 如下:
GET /posts/ - 获取特定的博客文章
GET /posts//comments - 获取上面博客文章关联的所有评论
GET /posts//comments/ - 获取特定博客下的特定评论
你会发现我们要请求的 URL 多了不少。这是因为实体 (这里可以理解为博文和评论)之间的关联关系更加复杂了。随着应用变得越来越复杂,管理这些 API 也变得更加困难。
- 过度获取 / 获取后 数据:有时候,当您请求 API 接口时,您会获得不必要的数据和相关数据,有时候您无法获得足够的数据,所以您最终会进行多次往返。 这是 RESTful 服务中的常见问题。 在某些情况下,您可能只需要 2 – 3 个值,但您可以获得大约 20 – 25 个值作为响应。 这只会通过增加响应时间,导致传输大量未使用的数据。 在后一种情况下,您获取的信息可能需要比从单个 URL 获取的信息要多,因此有必要进行多次往返。 这也导致客户端获取所有所需数据所花费的时间成本增加。
- API 版本控制:API 版本控制是一种遵循的方法,以避免使用响应格式的更改来破坏客户端应用程序。 当 API 响应格式发生更改时,将创建新版本。 这样做是为了使生产客户端应用程序可以按预期运行,并为开发人员提供一些休息的时间来迁移到新的 API 版本。但是这个版本控制是一个问题,因为当新版本发布时,它意味着新的 URL。 API 的维护和使用变得困难,并且经常导致重复的代码。
- 弱类型:并非我们从 RESTful 服务收到的所有数据都是强类型的,即它们没有正确地给出特定数据。 这在记录 API 时会成为问题,因为我们必须通过调用 URL 来指定客户端可以期望的数据类型。
- 客户端被蒙在鼓里:在收到响应结构之前,客户端不知道响应结构。所以,客户端是被蒙在鼓里的。这可能经常导致一些错误和数据无法正确处理,从而降低了消耗 API 的可靠性。
5、REST使用场景
管理API。最常见的API类型是专注于管理系统中的对象并面向许多使用者的API。REST使此类API具有强大的可发现性和良好的文档,并且非常适合这种对象模型。
简单的资源驱动型应用程序。REST是连接不需要灵活查询的资源驱动型应用的宝贵方法。
二、GraphQL
GraphQL 既是一种用于 API 的查询语言也是一个满足你数据查询的运行时。 GraphQL 对你的 API 中的数据提供了一套易于理解的完整描述,使得客户端能够准确地获得它需要的数据,而且没有任何冗余,也让 API 更容易地随着时间推移而演进,还能用于构建强大的开发者工具。
GraphQL是一种描述如何进行精确数据请求的语法。对于具有大量相互引用的复杂实体的应用程序数据模型来说,实现GraphQL是值得的。如今,GraphQL的生态系统正在通过Apollo、GraphiQL和GraphQL Explorer等库和强大的工具进行扩展。
1、GraphQL 架构
与 RESTful API 一样,GraphQL API 设计用于处理 HTTP 请求并对这些请求提供响应。无论如何,这就是相似之处。REST API 构建在请求方法和端点之间的连接上,而 GraphQL API 被设计为只通过一个端点,始终使用 POST 请求进行查询,其 URL 通常是 yourdomain.com/graphql。自从 2015 年 Facebook 开源 GraphQL 规范以来,它就在前端 Web 开发中迅速流行起来。
GraphQL从构建一个模式开始,它是对你在GraphQL API中可能进行的所有查询以及它们返回的所有类型的描述。构建模式很困难,因为它需要在模式定义语言(SDL)中使用强类型。
在查询之前有了模式,客户可以根据模式来验证他们的查询,以确保服务器能够响应它。在到达后端应用时,GraphQL操作会针对整个模式进行解释,并与前端应用的数据进行解析。API向服务器发送一个大型查询,返回一个JSON响应,其中包含我们请求的数据的确切形状。除了RESTful CRUD操作外,GraphQL还有订阅功能,可以从服务器上获得实时通知。
2、具体实例
启用 GraphQL 逻辑的服务器端逻辑由定义了服务器功能的 Documents 组成。这些 Documents 包含可执行文件和类型系统定义。顾名思义,类型系统定义为每个数据字段定义可接受的类型和格式输入及结果。
可执行文件包含要处理的可能的操作列表,其中包括操作类型(查询、修改或订阅)、操作名称、要查询或写入的字段和一个选择集,该选择集准确定义了将从操作返回的数据。选择集是 GraphQL 的较大价值所在——它们允许客户端查询特定的数据集并接收包含所请求信息的响应:不多不少。
GraphQL 查询解析:
下面是一个结构化的 GraphQL 查询,用于获取特定书籍的数据,包括作者的姓和名:
GET /graphql?query={ books(id:12) { authors { firstName, lastName } title, yearPublished, length }
{
Query { // operation type
books (id:12) { // operation endpoint
authors { // requested fields
firstName
lastName
}
title
yearPublished
}
}
}
这一切都可以通过一个查询由 GraphQL 服务器逻辑解析和处理完成。当把它与 REST 架构中相同结构的请求进行比较时,GraphQL 的优势就开始显现出来了。让我们看看下面的 REST 请求结构,然后重点讨论其中的一些差异!
REST 请求解析:
要向 REST API 发出相同的请求,客户端首先需要向能够返回图书数据的端点发送一个请求,并将图书 id 作为参数传入:GET /books/12这个请求可能会返回一个包含特定图书所有数据的对象,例如:
{
"title" : "The Hitchhiker's Guide to the Galaxy",
"authorID": 42,
"yearPublished" : 1978,
"length": 208,
"genre": "Science Fiction"
}
在我们的例子里,与相同的 GraphQL 查询相比,该响应有两个缺点:REST 响应包含类似 genre 这样的额外数据,返回的信息超出了我们的需求。REST 需要再发送一个请求来获得我们实际上正在查找的数据:这个特定作者的所有书籍。
为了获得这些数据,我们需要使用我们的 authorID 发出一个额外的请求:
GET /authors/42
这个请求的响应应该包含我们正在查找的所有数据:
{
"firstName": "Douglas",
"lastName": "Adams"
}
现在我们已经有了需要的所有书籍和作者数据,响应解析由客户端完成。现在,前端应用程序必须将来自不同端点的数据组合在一起,用于实现期望的功能。总的来说,与 REST API 相比,GraphQL 提供的性能优势可以为前端开发人员带来回报。使用 GraphQL 规范创建服务器可能需要更多的设置以及编写预测性的服务器端逻辑来解析和处理请求。
虽然 GraphQL 的设置成本可能比传统的 REST 架构要高,但是,更易于维护的代码、健壮的开发工具和精简的客户端查询所带来的好处通常会超过成本。
3、GraphQL 的优点
GraphQL 是由 Facebook 发明的,主要是为了克服 REST 的缺点。
3.1 一次请求获取到所有
一个 GraphQL 服务只暴露一个端点以便客户端能传输必要的查询去检索数据。 使用前面考虑过的相同示例,让我们看看 GraphQL 查询
{
findPost(id: <postId>) {
id
title
content
author
comments {
id
comment
commentedBy
}
}
}
正如你所看到的,我们仅仅通过单个请求获取到了所有必要的数据。 所以当你想要一个新字段你只需要将它添加进查询中,它将在响应中呈现。
3.2 强类型
GraphQL 被强类型模式所控制。这些类型既可以是原始的也可以是派生的。强类型系统允许 API 自文档化,从而使客户端知道在查询特定查询时会的到什么响应。
3.3 客户端驱动
GraphQL 提供了一种声明式语法,以便客户端精确地指定它们所需的字段。 这消除了由于客户端根据模式向 GraphQL 服务器声明其数据需求而导致数据冗余和不充分的可能性。
3.4 API 演变
因为在 GraphQL 中一切都是模式(schema)驱动,新增字段不会影响现存字段,而且 GraphQL 还为废弃字段提供 @deprecated 注释,所以对 GraphQL 的扩展并不是问题。 这就消除了 API 版本控制的需要。
3.5 传输层不可知
这是 GraphQL 的一个非常棒的优点。 API 服务器可以通过类似 HTTP, HTTPS, WebSockets, TCP, UDP 等协议进行信息交换。 这是因为 GraphQL 甚少关心信息如何在客户端与服务器之间进行交换。
4、GraphQL 的缺点
GraphQL 很棒,这是一个众所周知的事实。但是世界上的任何东西都是有缺陷的,GraphQL 也无法置身事外。
4.1 缓存功能不成熟
GraphQL 不支持浏览器和移动手机缓存,这一点区别于使用本地 HTTP 缓存机制的 RESTful 服务,因此我们要为实现 GraphQL 缓存付出额外努力。虽然有 Relay 这样的工具提供了一些缓存支持,但是它们还没有 RESTful 服务使用的缓存机制成熟。
4.2 检验与错误报告
RESTful 服务利用 HTTP 状态代码来处理可能遇到的不同错误。对开发人员来说,这使得 APIs 的检验变得非常简单和轻松。但是使用 GraphQL 服务总是返回 200 OK 响应。一个典型的 GraphQL 错误消息是这样的,状态码为 200 OK 。
4.3 设置相对复杂
设置GraphQL端点可能比RESTful Web服务相对复杂。 如果您的数据结构相对简单并且不需要不断更改,则避免GraphQL是值得的。
5、GraphQL使用场景
移动设备API。在这种情况下,网络性能和单条消息的有效载荷优化是很重要的。所以,GraphQL为移动设备提供了更高效的数据加载。
复杂的系统和微服务。GraphQL能够将多个系统集成的复杂性隐藏在其API背后。它从多个地方聚合数据,然后将它们合并到一个全局模式中。这对于传统的基础设施或随着时间的推移而扩展的第三方API来说,尤其重要。
三、GRPC
gRPC 是一个高性能、开源和通用的 RPC 框架,面向移动和 HTTP/2 设计。目前提供 C、Java 和 Go 语言版本,分别是:grpc, grpc-java, grpc-go. 其中 C 版本支持 C, C++, Node.js, Python, Ruby, Objective-C, PHP 和 C# 支持。
gRPC 基于 HTTP/2 标准设计,带来诸如双向流、流控、头部压缩、单 TCP 连接上的多复用请求等特性。这些特性使得其在移动设备上表现更好,更省电和节省空间占用。
1、gRPC结构图
- 客户端(gRPC Stub)调用 A 方法,发起 RPC 调用。
- 对请求信息使用 Protobuf 进行对象序列化压缩(IDL)。
- 服务端(gRPC Server)接收到请求后,解码请求体,进行业务逻辑处理并返回。
- 对响应结果使用 Protobuf 进行对象序列化压缩(IDL)。
- 客户端接受到服务端响应,解码请求体。回调被调用的 A 方法,唤醒正在等待响应(阻塞)的客户端调用并返回响应结果。
从Google内部的服务器到您自己的台式机,gRPC客户端和服务器可以在各种环境中运行并相互通信,并且可以使用gRPC支持的任何语言编写。
例如,您可以使用Go,Python或Ruby的客户端轻松地用Java创建gRPC服务器。此外,最新的Google API的接口将具有gRPC版本,可让您轻松地在应用程序中内置Google功能。
原理:
IDL(proto buffer) + RPC
netty:异步/事件驱动的 网络应用程序服务器框架(高性能)
Http2:流式、双向
protobuf:序列化(节省网络带宽)
2、gRPC的主要优点
- 现代高性能轻量级 RPC 框架。
- 协定优先 API 开发,默认使用协议缓冲区,允许与语言无关的实现。
- 可用于多种语言的工具,以生成强类型服务器和客户端。
- 支持客户端、服务器和双向流式处理调用。
- 使用
Protobuf
二进制序列化减少对网络的使用。
3、gRPC的缺点
- 浏览器支持受限:绝大数浏览器不支持
HTTP/2
- 非人工可读取:proto文件规定的格式在通讯中会序列化成二进制数据,人工解析较为困难。
4、gRPC的适用场景
- 微服务:gRPC 设计用于低延迟和高吞吐量通信。 gRPC 对于效率至关重要的轻量级微服务非常有用。
- 点对点实时通信:gRPC 对双向流式传输提供出色的支持。 gRPC 服务可以实时推送消息而无需轮询。
- 多语言环境:gRPC 工具支持所有常用的开发语言,因此,gRPC 是多语言环境的理想选择。
- 网络受限环境:gRPC 消息使用 Protobuf(一种轻量级消息格式)进行序列化。 gRPC 消息始终小于等效的 JSON 消息。
5、gRPC语言和平台支持情况
6、Nods.js使用实例
gRPC 是对 RPC 的一个新尝试,最大特点是使用 protobufs 语言格式化数据。
RPC 主要用来做服务器之间的方法调用,影响其性能最重要因素就是 序列化/反序列化 效率。RPC 的目的是打造一个高效率、低消耗的服务调用方式,因此比较适合 IOT 等对资源、带宽、性能敏感的场景。而 gRPC 利用 protobufs 进一步提高了序列化速度,降低了数据包大小。
gRPC 主要用于服务之间传输,这里拿 Nodejs 举例:
1、定义接口。由于 gRPC 使用 protobufs,所以接口定义文件就是 helloword.proto
:
// The greeting service definition.
service Greeter {
// Sends a greeting
rpc SayHello (HelloRequest) returns (HelloReply) {}
// Sends another greeting
rpc SayHelloAgain (HelloRequest) returns (HelloReply) {}
}
// The request message containing the user's name.
message HelloRequest {
string name = 1;
}
// The response message containing the greetings
message HelloReply {
string message = 1;
}
这里定义了服务 Greeter
,拥有两个方法:SayHello
与 SayHelloAgain
,通过 message
关键字定义了入参与出参的结构。
事实上利用 protobufs,传输数据时仅传送很少的内容,作为代价,双方都要知道接口定义规则才能序列化/反序列化。
2、定义服务器:
function sayHello(call, callback) {
callback(null, { message: "Hello " + call.request.name });
}
function sayHelloAgain(call, callback) {
callback(null, { message: "Hello again, " + call.request.name });
}
function main() {
var server = new grpc.Server();
server.addProtoService(hello_proto.Greeter.service, {
sayHello: sayHello,
sayHelloAgain: sayHelloAgain
});
server.bind("0.0.0.0:50051", grpc.ServerCredentials.createInsecure());
server.start();
}
我们在 50051
端口支持了 gRPC 服务,并注册了服务 Greeter
,并对 sayHello
sayHelloAgain
方法做了一些业务处理,并返回给调用方一些数据。
3、定义客户端:
function main() {
var client = new hello_proto.Greeter(
"localhost:50051",
grpc.credentials.createInsecure()
);
client.sayHello({ name: "you" }, function(err, response) {
console.log("Greeting:", response.message);
});
client.sayHelloAgain({ name: "you" }, function(err, response) {
console.log("Greeting:", response.message);
});
}
可以看到,客户端和服务端同时需要拿到 proto 结构,客户端数据发送也要依赖 proto 包提供的方法,框架会内置做掉序列化/反序列化的工作。
也有一些额外手段将 gRPC 转换为 http 服务,让网页端也享受到其高效、低耗的好处。但是不要忘了,RPC 最常用的场景是 IOT 等硬件领域,网页场景也许不会在乎节省几 KB 的流量。
总结
每个API项目都有不同的要求和需求。通常情况下,架构的选择取决于:
- 使用的编程语言;
- 您正在开发的环境,以及你所拥有的资源,包括人力和财力。
了解每一种设计风格的所有权衡,API设计师就可以选择最适合项目的那一种。