智能音箱对比

迎关注我的头条号:Wooola,10 年 Java 软件开发及架构设计经验,专注于 Java、Go 语言、微服务架构,致力于每天分享原创文章、快乐编码和开源s X )技术。

API是服务之间通信的契约,通过API可以忽略服务内部实现的细节。如果接口设计良好,则可以降低团队间的耦合度,加快开发速度。

优秀API的设计原则

设计出优秀的API通常需要遵循如下原{ W H H :则。

简单:API应该符合大多数人正常的思维逻辑,避R H M @ w $ j免异想天开的交互,满足需求的同时,越简单越好。

易懂:优秀5 _ ` U j的API可读性好,尽量做到不需要文档就能读懂接口名称、参数的大+ # w概含义,提供给第三方开发者的接口要进行详细地描述,包括参数的取值范围、错误码、异常返回规则、i E u E U RSLA相关指标等。

一致:对于同A E ; - M一个公司、站点提供的API,最好有统一的规则,让开发者只要看过几个API之后,基本都能猜到剩余API的含M g P R g P k义。

稳定:最好在开始的时候就考虑好,不要轻易修改API,否则会给使用者造成非常大的麻烦。如果修改API无法避免,那么l h Y 1 F 1 B m最好通过增加接口而不是修改已有接口做到兼容。如果一定要改变已有接口,也要通过明确的版本号加以区分,并且预留足够的时间。

k - O l F全:设计时要考虑超出预期的情况如何处理,给出被限流的C + ( _ {情况。下面是GitHub API使用的两个相关的头部参数。

X-RateLimit-Limit:每小时允1 2 [ ] J H许发送请求的最大值。

X-RateLimit-Remaining:当前时间窗口剩下的可用请求数目。

如果提供c j @给第三方,则要考虑如何认证,租户之y m y # 4 J ; . m间如何隔离,并且尽量使用HTTPS协议。如果验证失败,则要返回401 Unauthorized状态码;如果没有被授权访问,则要返回403 Forbidden状态码,并且提供详细的错误信息。

服务间通信——RPC

RPC(l T +Remote Procedure Call)是指远程过W | 5 l M程调用。简单来说,RPC希望达到的效果是,调用远程方法就像调用本地方法一样。由于调用方和被调用方实际上分布在不同的机器上,一般情况下,~ = @ k会有一个框架来支撑,以便屏蔽底层通信细节,让双方开发起来更简单。

一个普通的RPC调用流程如图2-11所示,具体调用过程如下。

(1)客户端在业务服务中发起请求。

B u ! C Q 0 [ J *2)调用本地stub,本地stub对消息进行序列化、封装等处理。

Q X . n3)客户端stub调用网络通信模块将消息送达服务端,中间还可能包括寻址、建连接等一系列操作。

(4)服务端网络通信模块把接收] h y Q 1 $ t %到的消息转发给stub进行反序列化。

(5)stub转发给, , @ = d [ =E j A y R务服务进行处理并返回结果。

(6)stub将结果进行序列化,调用网络通信模块。

(7)服务端通过网络通信模块将结果返回到客户端网络通信模块中。

(8)客户端网络通信模块将消息转发给H a + n { ] j +stub。

(9)stub进行反序列化并转发给客户端业务服务。

(10)客户端得到最终结果。

我们的目标是让(2)到(9)对开发者不可见。

既然要求像调用本地方法一样调用远程方法,那就不只是要屏蔽复杂度,还要在性能上进行考虑。

影响RPC性能的因素如下。

序列化。常用的RPC序列化协议包括:Thrift、Protobuf、Avro、Kryo、MsgPac6 S r S : ` Wk 、Hessi` L Lan、Jackson。

优秀API的设计原则与实例实现RESTful

图2-11 RPC调用流程图

传输协议。常用的传输协包括:$ d i O g { FHTTP、Socket、TCP、UDP等。

连接。连接t I h T ? )包括:长连接、短连接。

IO模型。常用的网络IO模型g t p:同步阻塞IO(Blocking IO)、同步非阻塞IO(Non-blocking IO)、IO多路复用t m B o g(IO Multiplexing)、异步IO(Asynchronous IO)。

序列化——Proto! N 2 Kbuf

Protobuf是由Google开源的消息传输协议,用于将结构化的数据序列化、反序列化通过网络进行传输,目前被广泛应用的主要版本有P% g H x o C % Jrotobuf 2和Protobuf 3。Protobuf首先解决的是如何在不同语言之间传递数据的问题,目前支持的编程语言有Java、Ci o p R+w 7 M V E o R+、Python、RJ l | {uby、PHP、Go等。通过Protobuf可以很容易地实现一个Client和一个Server之间的数据传递,Client和Server可以使用不同J y S语言。

Protobuf是一个高性能、易扩展的序列化框架,通常是RPC调用追求高性能的首U R ` v z O选。它本身非常简单,易于开发,结合Netk , c Y B & S Cty可以非常便捷地实现RPC调用。同时,可以利用Netty为Protobuf解决有关SockC j p ; M Bet通信中“半包、粘包”等问题(反序列化时,字节成帧)。

Protobuf比JSON、XML等更快、更轻、更小,并且可以跨平台。Protobuf最新的版本为3.x,如果使用它,首先要编写proto文件,即IDL文件(后缀为“.proto”的文本文件),然后通过客户端生成Java相关类进行序列化、反序列化。

下面我们就简单描述一下基于Protobuf实现序列化、反序列化的步骤。

1.安装客户端

5 r @ J ] c Y L过brew可以实现在Mac下安O R 7 c [ &装Protobuf,执行命令如下z j y 2 @ R % Q

brew install protobuf

如果出现如下执行结果,意味着已经安装` [ b 0成功了。

/usr/local/CelN @ 1 U 4 X Z F [lar/protobuf/3.5.1:T m b Z Y R } r r 267 files, 18.0MB

也可以通过如下命令检验是否安装成功。

protoc –version

2.安装工具

为了在编辑pB a C i P 6 ; 3 3roto文件的时候获得更好的体$ ^ J [ ^ ? f验,可以先安装插件。idea里可以安装Prot6 q 9 H [obuf Support,在插件库中搜索后直接安装即可,如图2-1- ~ , # [ 82所示。

优秀API的设计原则与实例实现RESTful

图2-12 idea安装Protobuf Support插件

Protobu| : 3 X & V y Nf Support可以对语法高亮显示、错误提示,如图2-13所示。

优秀API的设计原则与实例实现RESTful

图2-13 Protobuf Support插件效果

3.编辑proto文件

本节采用的Protobuf 3.x,需要遵循proto 3的语法。关于Protobuf的数据类型和Java的数据类型如何对应,可以参照表2-3提供的对应关系。

优秀API的设计原则与实例实现RESTful

定义product.proto文件的过程如下。

syntax = \"proto3\";//声? * X ; d E % vC ~ , b f ) W b支持的版本是proto 3

option java_package = \"com.cloudnative.protobuf\";//声明包名,可选
option java_ou~ Q M 9 Dter_classnamh ? Z g Ce=b n S !\"ProducQ - i } N f l D htProtos\";//声明类名,可选

message Pr: t R e P ^ J #oduco k p X : H z gt {

int32 id = 1;
string name = 2;
string price =q V D s 3;

enum ColorType {//定义枚举类+ N G I 9 U
WHITE = 0;
RED = 1;
BLq / T B n a cACK = 2;
}
}

在命令行执行如下代码。

protoc --java_out=0 c / ; G../java product.! * o / ~ D $ 0 Nproto

--java_? @ Pout表示在目标目T v X录中生成Java代码。由于已将product.proto放到src/mian/java/proto目录下,所以--java_out=../java参数会将生成的Java类创建到java目录下p Q Z 6g ! E ? ` S } -如图2-14所示。

优秀API的设计原则与实例实现RESTful

图2-14. x r 0 v n 项目目录结构

打开P% W irotobufPrn p [ O / P 6 @otos会发现,这是一个非常复杂的类,其代码大概1500行。

4.使用Protobuf序; ~ g ,列化

引入Prm w 7otobuf-java包,可以先通过http://mvnrepository.comF X 7 *查询# H Z V m k u,然后点击复制,非常方便,如图~ j P , ? ; _2-15所示。

优秀API的设计原则与实例实现RESTful

图2-15 查询结果

实际上,Protobuf的序列化及反序列化非常简单。Protobuf生成的类中已经实现了相应{ ( W )的方法,调用即可。示例代码如下。

packa4 ? ^ge com.cloudnative.protobu= & 7f;

import com.google.protobuf.InvalidProtocolBufferException;

public class TestProtobuf {
public st{ * ! N K 2 J = Satic void main(String[] args){
TestProtobuf testProtobuf =new TestProtobua J ; 1 1 zf();
byte[] buf=testProtobuf.toByte()( 9 A t B 0 F K .;
try {( @ 2 8 S ~ B
ProductProtos.Product product = ter 4 hstProtobuf.toProduct(buf);
System.out.println(product);
} catch (InvalidProt x i iocolBufferException e) {
e.@ S ? B iprintStackTr? i iace();
}
}
//序列化
private byte[] toByY } v & ; - %te(){
ProductProtos.Product.Builder productBuilder=ProductProtos.Product.newBuilder();
proR G [ductBuilder.setId(11);
prb ) u F | ooductBuilder.setName(\"milk\");
productBuilder.setPrice(\"4J B . O.12\");
ProductProtos.Product product =productBuilder.build()J + M 2 ~;
byte[] buf = product.toByteArray();
System.outj V u U.println(buf.length);
rek W 6 k 0 F *turn buf;^ & c V e 2 d N
}
//反序列化
private ProductProtos.Product toProduct(byte[] buf) throws@ B : X r + f m 1 InvalidProtocolBufferException {
return ProductProtos.Product.parseFrom(buf);
}
}

服务间通信——RESTful

REST是RoJ % M f 6y Thomas Fielding在2! c U u W 8 8 h000年的博士论文中提出的。REST是Representational State Transfer的缩写,通常翻译为“表现层状态转化”。如果一个架构符合REST原则,就称它为RESTful架构。

API如何设计才能满足RESTfulk y a的要求呢?

1.协议

API是基于HTTP的协议。

2.域名

API要有一个域名,例如http9 & _://api.xxx.com

3.版本

API要有版本信息。当客户端数量较多或者提供给第三方使用时,很难控制客户端的兼容性,一个比较好的做法就是当已发布的服务变更时,通过版本来控制兼容性。当然,版本不能演进太快,最好的版A $ + N S U n u本化就是无须版本,例如http:/$ = w 8 6 K/api.xxx.com/v1/

4.路径

要理解REST,首先要理解什么是资源(Resource)。REST开发又被称作面向资源的开发。API要用资源来表示,URL中不能出现动词。资源是服务端可命名的一个抽象的概念,只要客户端容易理解,可以随意# E | t a . &抽象。通常可以把资源看成是一个实体,例如用户、邮件、图片等,用URI(统一资源定位符)指向它。经验告诉我们,往往这里的资源和数据库的表名是对应关6 L Q / $系。一种观点认为DDD可以和REST API很好地契合,因为REST的资源可i ? j @ n m ]以很好地与DDD的实体映射起来。定义F d ^ r J s u 5 g资源的时候,推荐用复数,假设我们要获取用户的信息,大概是这样:http://api.xxa = 8 9 & 0 x.com/v1/users/

5.方法

一般允许的方法主要包括如下几种。

  • GET:读取资源,一个或多个(常用)。
  • POST:创建资源(常用)。
  • PUT:修改资源,客户端提供1 0 R修改后的完整资源(常用)。
  • PATCH:对已知资源进行局部更新,客户端只需要提供改变的属性。
  • DELETE:删除、回收资源(常用)。
  • HE: ; % I G m O JAD:读取资源的元数据(不常用)。
  • OPTIONS:读取对资源的访问权限(不常用)。

一般情况下,GET、POST、PUT、DELETE已经足够,甚, H (至有一种观点认为,只需要使用GET、POST即可,例子如下所示。

GET/usersv G h e T/1,获取用户ID为1的用户信息。

GET/users/1/orders,获取用户ID为1的用户拥有的所有订单。

6.L ~ +参数

参数可以放到API路径中,也可以放到“?”的后面。

GET/users/1/orders
GET/orders?user_id=1

7.编码

虽然RESTful并没有限制资源的表达格式,HTML/ [ ( W s 3 n @/XML/JSO# l K ? . i ? * uN/纯文本/图片/视频/音频等都可以,但是通常服务端和客o O o户端通过JSON_ E . c 2 S 2 I传递信息。

8.状态码

用HTTP Status Code传递Server的状态信息,常用的状态码如下。

  • 100Continue
  • 200OK,GN z , x 1 ^ rET
  • 201Created,POST/PUT/PATCH
  • 202Accepted
  • 204NO Content,DELETE
  • 400Bad Request,POST/PUT/PATCH
  • 401Unauthorized
  • 403Forbidden
  • 404Not Found
  • 405Method Not Allowed
  • 406Not Acceptable
  • 409Conflict
  • 410Goneh l T k 3 K = ,
  • 412Precondition Failed
  • 429Too many^ Z h C p * requests
  • 500Internal ServC / `er E# ~ @rror
  • 501Not Implemented
  • 503ServiceQ L + x K [ Unavailable

完整信息可以参考w3官网。

要理解RESTful,还要考虑如下重要的约束条件。当然,这些条件也不是绝对的,{ 6 $ I & 4 Y (需要结合业务场景来确定。

单一职责。尽量保持接口职责单一,留给客户端^ k * 2 x n足够的操作空间,以满足不同的业务需求。对于接口粒度的大小,需要考虑的因素包括:性能( v ^ B合并请求性能更高)、一致性、灵活性及客户端的易用程度。

幂等性。一次和多次请求某一个资源f - T f n t D应该具有同样的作用,客J 3 ?户端能够重复发起请求而不必担心造成副作用。

无状态。多[ o q j次请求之间不应该存在状态耦合,无须关联过去、现在和将来的请求或者响应。

客户端发起| M Z ? b ) Q。一般通信方式都是由客户端发起的,服务端是被调用的。随着HTTP/2的到A ; 5来,这一条可. { n ~ R % & M能会发生变化。

原子性。保证所有操作是一个不可分割的单元,要么全部成功,要么全部失败,需要结合业务要求W @ Q L P S加以确定。

易用。需要提供详尽的文档、参数说明、示例等,API定义0 % / v =的URL、变量名要通俗易懂,最好是英文,尽量减少自定义的缩写,让开发D n P X A 4 S者容易调试和管理。m V z

SLA。需要提供响应时间、吞l 4 8 q 3 - [吐量、可用性等关键指标。

RESTful已, Q O ^ 4 ? k V经成为业界的主流,主要是因为RESTful通常采用HTTP+JSON的方式实现,继承了HTTP和JSON的优点。相对` D $ - [ E r {于SOAP、RPC等~ A Y = q ~ = 8方式,RESTful更加轻量、简单( | Q y,支持跨语言,t { H s ( m &并且容易调试。

通过Swagger实现RESTful

; D R c X F统的API设计通常先完成代码,然后另外补充一份说明文档,这种方m _ T `式效率比较低,文档和代码缺乏关联性。更高级一点的做法是使用T ] u yJAVADOC,把文档和注释关联起来,以提升效率,但是由于JAVADOC需要不断生成,文档难免和代码存在不一F C X致。

在此背景下,Swagger诞生了。Swagger是一个简单、功能强大、非常流行的API表达工具。基于Swagger生成API,可以得到交互式文档、自动生成代码的SDK,以及API的发现方式等。通过Swagger可以很容易地生成标准的API,示例如图2-16所示。

优秀API的设计原则与实例实现RESTful

图2-16 基于Swagger生, 5 S | d !成A) 5 c j w @PI的示例

SwaggeZ $ 1r是基于OpenAPI的,OpenAPI支持YAML和JSON格式描述APII , v V _ m B W Y。YAML相对于JSON来说更加简洁,比较适合做简洁的序列化和配置文件。编写YAML文档推荐使用Swagger Editor,它提供了语法高亮、自动完成、即时预览等功能。编辑器| O R Y C可以在本地使用,也可以在线使用。YAML的数据结构可以用类似7 S G H , b n A大纲的缩排方式呈现,结构通过缩进4 * G W a C ; H R来表示,连续的项目通过“-”来表示,map结构里面的key/value对用“:”来分隔。

基于Swagger Editor设计API,可以直接在线编辑API,也可以在本地安装,下面以在, I C u H - 2线编辑器为例介绍如何基于Swagger构建API。

(1)访问官方Editor编辑器

http://editor2.swagger.io。

u [ E ( f 5 ( d2)编辑YAML文件,如下所示。

swagger: \"2.0\"

info:
version: 1.0.0
title: Product API
description: Product API for test

schemes:
-& R ) { httpo ` S os
host: localhost
basePath: /product

paths: {}

下面简单说明一下这个文档。首先,显示OpenAPI使用的版本是2.0。

swagger:b J 4 G p Y \"2.0\~ = ? ?"

API的描述信息,包括如API文档版本(version)、API文档名称(title)和描述信息(descrig K 2 4 q R W ^ 2ption)。

info:
version: 1.0.0
title: ProducQ # % / 8 S Mt API
description: Product API for test

下面的代码表示AF ( w o , P UPI的URL,采用HTTPS协议,介绍了主机名(host)1 i S - N 6、根路径(basePath)。

schemes:
- https
host: localhost
basePath: /product

下面我们来看一个稍复杂一点的示例。

swagger: \'2.0\'
info:
version: \'1.0\'
title: Swagger构建RESTful API
host: \'localhost:8080\'
basePath: /
tags:
- name: product-controllM l ? 7 %er
description: Product Controller
paths:
/products:
get:
tags:
- product-controller
summary: 获取产品列E P J
description: 获取产品列表
operatid 1 O @ ? ~onId: getProductListUsingGET
consumes:
- app= ` I m J W U Ilication/json
prog ] S hduces:
- \'*/*\'
responses:
\'200\':
descriptionj A z ; ~ m 8: OK
schema:
type: array
items:
$ref: \'#/definitions/Product\'
\'401\':
description: Unauthorized
\'403\':
description: Forbidden
\'404\':
descriptiw E E & m h xon: Not Found

/products/{id}:
get:
tags:
- p7 W u A { Z G i /roduct-controller
summary: 获取产品详细信息
description: 根/ ` W M I G j F据URL的id来获取产品详细信息
operationId: getProductUsingGET
consumes:
- application/json
p{ 8 !roduces:6 9 L ? v }
- \'*/*\'
parameters:
- name: id
in: path
description: 产品ID
required: tru3 h R f Pe
type:j N Y m h ; L integer
responses:
\'200\':
description: OK
schema:
$ref: ^ 1 G\'#/definitions/Product\'
\'401\':
description: Unauthorized
\'403\'8 | B U G _ R D 5:
description: Forbidden
\'404\':
description: Not Found
definitions:
Product:
type: object
properties:
count:
type: integer
format: int32
desc:
type:B g v : F C string
id:
type: integer
format: inc M I 1t32
name:
tK q } x N 0 U Py) Q g N |pe: string

上面的示例定义了两个API,一个是获取P 9 U ` ; iProduct的列表,一个是根据id获w 2 B q c e取Product的详情。

编辑完成后,N . [ g } X s .可以得到如图2-17所示的文档界面。

优秀API的设计原则与实例实现RESTful

图2-17 Swagge9 1 Mr生成文档的示例

(3)点击file-downlT = } koad yaml下载YAML文件。

(4)点e z + G H击generate server下载服务端,点击generate client下载客户端,可以分别生f s ~ ; n成相应语言的SDK工程。

通过Spring Boot、Springfox、Swagger实现RESTful

上一节介绍了通过Swagger Editor实现API设计(先写YAML文件,然后生成服务端b A _ ) A B和客户端),本节介绍另外一种直接写代码、通过注解自动生成相关文档的方法。

Spring Boot$ B V ] g U +已经家喻户晓,而Springfox是什么呢?Marty Pitt曾经编写了一个基于Spring的组件swagger-springmvc,用于将Swagger集成到Spring MVC中,Springfox是从这个组件发展E i k b 5 7 ^ D而来的。

下面我们通过一个简单的示例来说明一下。首先,新建一个项目,基于Maven构建,在pom中引入相应的m $ n # Z v A 8JAR包,这里需要引k Q N K K [入Spring Boote Y z v 6 R 9和Springfox的相关包。

<h p E M y;dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.bg D F n W .oot</groupId>
<artifactId>D o @ x + ( W;spr# 2 G $ ] } f d bing-boot-starter-test</aro W K ( * h 8 htifactId>
<scq + |ope>test<, ( ` 9 Z L } 8/scope>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactI{ k R i ] g r j yd>
</dependency>
<dependency>
<groupId>io.springfox</groupId>
<artifactId>springfox-swagger2&8 u X { W U 4lt;/artifactId>
<version>2= e 9 W % 0 ).7.0</version>
</dependency>
<dependency>
<gi l * -roupId>io.springfox</groupId>
<artifaa r h G )cr w | P {tId>springfox-swagger-ui1 | T</artifactId>
<version>2.7.0</version>
</dependency>

编写Swagh _ u z K i [ger的配置类。

@Configuration
@EnableSwagger2
public class Swagger2 {

@Bean
public Docket createRestApi() {
return new Docket(DocumentationType.SWAGGER_2)
.apiInfo(apiInfo())
.select()
.apis(RequestHandlerSelectors.basePackage(\"com.cloudnative.rest\"))
.paths(PathSelectors.any())
.{ l 5 t x .build();
}

private ApiInfo apiInfo() {
return neo } E ~ y ) { 2 gw ApiInfoBuilder()
.title(\"Swagger构建RESTful API\")
.description(\"\")
.termsOfServiceUrl(\"\")
.version(\"1.0\"i q : & v d S `)
.build();
}

}

实现dto类。

public class ProductC ! 3 S s | C k {
private i_ n 4 v * . wnt id;
private String name;
private int count;
private String desc;K V ^ . H g j q

public String getName) W t O() {
return name;
}

public void setName(St5 # & ) 8ring na? 9 2 M ?me)& R ~ e D 2 ~ {
this.name = name;
}

public int getCount() {
return count;
}

public void setCount(int count) {
this.count = count;
}

public String getDesc() {
return desc;
}

public void setDesc(String desc) {
this.desc = desc;
}

public int getId() {b V N b

return id;
}

public void setId(int id) {
this.id = id;
}

@Override
public String toString() {
res p atut i * 9 W xrn \"Product{\" +
F u % 6 b\"id=\" + id +
\"( E v, name=\'\" + name + \'\\\'\' +
\", count=\" + count +
\", desc=\'\" + desc + \'\\\'\' +
\'}\';
}
}

基于注解编写接口实现。

@RestContB u . U 6 W :rollerh 5 ; o
@RequestMapping(value=\"/products\S 2 V (") /B s T A % C !/通过这里配置使下面的映射都在/products下
public class ProductController {
private List<Prod 7 6 G t F Dduct> product2 C G D ) ]List;
//初始化
public Produ# w v n 3 / nctController(){
productList = new ArrayList<Product>();
for (int i = 0; i < 10; i+s M m+) {
Product productP C w Q t I 7 =new Product();
produco . A l *t.setId(i);
prol Y Q 2 o x V Fduct.setCount(i+10);
product.setName(\"watch\"+i);
product.setDesc(\"watch desc\"+i);
productList.add(product);
}
}
@ApiOperato 8 9 X S { F ?ion(value=\"获取产品列表\", notes=\"获取产品列表\")
@RequestMapping(value={\"\"}, method= RequestMethod.GET)
pub_ * 3 y D nlic List<Product> getProductList() {
return productList;
}

@ApiOperation(value=\) T 2 , = ="获取产品详细信息\", notes=\"根据url的id来获取产品详细信息\")
@ApiImplici( $ + h ^ U m ztParam(name = \"id\", value = \"产品ID\", required = true, dataType = \"Integer\",paramType=\"path\")
@RequestMapping(value=\"/{id}\", method=RequestMethod.GET)
public Prg X : L o , oducL s Jt getProduct(@PathVariable Integer id) {
return productList.get(id);
}
}

基于Main方法启动。

@SpringBoot# T 0 HApplication
public class Application {
public static void main(String[] args) {
SpringApplication.run(Applp , f q Hication.class, args);k o u :
}
}

访问http://localhost:808/ J c ? 7 ) u :0/swagger-ui.html,自动生成的文档界j v Y 9 P I D r v面如图2-18所示。: 7 0 H

优秀API的设计原则与实例实现RESTful

图2+ f d 6 y V l-18 生成的文档界面

基于图2-18的界面进程测试,按“Try it out!”按钮执行,如图2* O Q g-19所示。

优秀API的设计原则与实例实现RESTful

图2-19 在生成的文档上进行测试

访问http://localhost:8080/v2/apo @ )i-; u H j z h 5 7 odocs可以获取接口的JSON描{ _ T述文件,如图2-20所示。可以直接到Swagger官网把JSON描述文件转换为YAML文件。

优秀API的设计原则与实例实现RESTful

图2-20 转换为JSON的结构

HTTP协议的进化——HTTi = r | z FP/2

据W3Techs统计,截至2017年1? A F (1月,排e l o 8 % l f k在前1千万名的网站中有20.5%的支持HTTP/2。Chrome、Opera、Firefox、Internet Explow $ 6 2 , A . ( crer 11` H |、Safari、Amazon4 q Z { } p 2 K Silk和Edge浏览器都支持HTTP/2的标准化。大多数主流浏览器都在2015年底之前添加了HTTP/2支持。

HTTP/2对HTTP/1.x进行了大量简化,使得性能得到了大幅度提$ e ( 5 r升。Akamai公司官网通过一个示例对比了HTTP/1.1和HTTP/2在性能上的差距——并e t C c $ W i发请求379张图片,HTTP/1.1需要14.70s,响应时间为3ms,n f /而HTTP/2仅仅需要1.61s,响应时间为6ms,如图C } ( 2 h d A q2-X R . ]21所示。

优秀API的设计原则与实例实现RESTful

图2-21 对比HTTP/1.1和HTTP/2的访问性能

我们都知^ i Y ] m q i G 9道,在HTTP/1.x的协议中,浏览器在同* { _ T一时间对同一域名下的请求数量是有限制的,这会导致大量并发请求阻塞,这个问题也被称w o q | t为线端阻塞(head-of-line blocking)。同一域名下浏览器支持的连接数,如表2-4所示。HTTP/1.1对不同浏览器连接数的限L W 6 U p # p F制不同,很多# ] n @互联网公司为了解决这个问v u | c ^ C L B U题,做了大量优化,包括建立多域名Q + j U A,通过CDN缓存大量静态资源等。

HTTP/2是基于二进制协U + ?议的,与HTTP/1.x这样的文本协议相比,显然二进制协议性能更高。另外HTTP/2使用报头压缩,降低了网络开销。HTTP/2将HTTPb B ; u F ] p协议通信分解为二进制编码帧的交换,这些帧对应着特定数据流中的8 J 6 2 H q 5消息。所有这些消息都在一个TCP连接内复用,这就是HTTP/2的多路复用机制(MultiplexingT C b 1 +)。Benjami{ Q . ( Bn在2015年写的一篇文章中描述了一个简单的例子,如果只请求3个资源,从Web页面开始渲染到加载结束,HTTP/2比HTTP/1.1节省不少时间,如图2-22所示。

优秀API的设计原则与实例实现RESTful

表2-4 同一域名下浏览器支持的连接数

优秀API的设计原则与实例实现RESTful

图2-22 HTTP/1.1和HTTP/2调用流程对比

HTTP/2完全兼容HTTP/1.1的语义,HTTP/2和HTTP/1.1[ F 5 6的大部分高级语法(例如方法、状态码、头字段和URI)都是相同的。

HTTP/1.1如果要实现长连接,需要设置Connection:keep-alive来控制长连接时I a m I间,超时就断开TCP连接。f m h只有在客户端发起请求的时候,服务器端才会响应。所以就算一直z 7 N ! x H ! b给服务器发送心跳包以维持长连接,也( . & O r h u b r不能用来推送,只有客户端不断发起请求给服务器端,服务T E = q & d ~器才会响应,这就是% t 0pull轮询的方式。

HTTP/2引入服务端推送模式,即服务端向客户端发送数据,如图2-23? & 0 V ^ G . d l所示。服务器可以对一个客户端请求发送多个响应,HTTP/2打破了严格的请求-响应语义,支持一次请求-多次响应的形式。由于现如今的WeL W b _ B Eb界面丰富多彩,加载的资源往往非常多,服务端c v . 3实际上已经知道要推送什么内容,但HTTP/M u i W Q | [ Q1.x的语义只支持客户端发起请求、服务端响应数据。HTTP/2改变了这种模式,只需要客户端发送一次请求,服务端便把所有的资源都推送到客户端。服务器推送的缺点是,在客户端已经缓存了资源的情况下可能会有冗余。这个问题可以通过服务器提示(Ser M 5 o ` J 4 :ver Hint)解决。

优秀API的设计原则与实例实现RESTful

图2-23 HTTP/2推送模式

HTTP/2和Protobuf的d L 5 x *组合——gRPC

gRPC源于被称为Stubby的Google内部项目,Google内部大量使用Stubby进行服务间通信。作为gRPC的前身,Stubby大量依赖Google的其他基础服务,所以不太方便开放出来给社区使用。随着HTTP/2的逐步成熟,2D ` a } Z Y + ] P015年初Google开源了gRPC框架。截至2017年12月,gRPC已经发布了1.7R E . 9.3版本,并且被CNCF^ { U _(云原生计算基金会)所收录。gRPC在ETCD/Kubernetes上得到了大量使用。

gRPC是基于HTTP/2设计的,因此也继承了HTTP/2相应的诸多特性,这些特o 9 S d性使得其在移动设备上表现L - p得更好,更节v D + G I n ;省空间、更省电。gRPC目前] 2 = k提供的C、Java和Go语言版本分别是grpc、grpc-java、grpc-go,! Z A U其中C版本支持C、C++、Node.js、Python、Ruby、Objective-C、PHP和C#。

说了这么多,gRPC到底能够给我们提供哪些优势呢?

gRPC默A N D c @ Q &认使用Protobuf进行序列F A Y x / A u化和反序列化,而Protobuf是已经被证明u g ) ) ^的高l , ,效的序列化方式,因此,gRPC的序列化性能是可以得到保障的。

gRPC默认采用HTTP/2进行传输。HTTP/2支持流(streaming),在批量发送数据的场景下使用流可以显著提升性能——服务端和客户端在# M ^接收数据的时候,可以不必等所有的消息全收到后才开始响应,而是在接收到第一条消息的时候就可以及时响应。例如,客户端向服务端h Y M ` T n O发送了一千条update消息,服务端不必等到所有消息接收完毕才开始处理,而是一边接收一边处理。这显然比以前的类HTTP 1.c z W * t 01的方式提供的响应更快、性能更优。e v R ; ` MgRPC的流可以分为三类:客户端流式发送、服务器流式返回,以及客户端/服务器同时流式处理,也就是单向流和双向流。在我写这本书的S W c ; / g时候,Dubbo 3.0正在酝酿中,其中一个显著的变化是新版本将以streams I f ` / (ing为内核,而不再是2.0时代的RPC,目的是去掉一切阻塞。

基于HTTP/2协议很容易实现负载均衡及流控的方案,可以利用Header做很多事情。

同时,gRPC也不是完美的。相比于非IDL7 [ C j ~描述的RPC(例如Hession、Kyro)方式,定义p# L 8 [ /roto文件是一个比较麻烦的事情% S [ i ! [,而` ; s且需要额外安装客户端、插件等。另外HTTP/2相比于基于TCP的通信协议,性C P g ; y [ & p能上也有显著的差距。

下面通过一个简单的例子来理解一下gRPC的使用方式。假设我们要开发电商中的产品服务,通过i- x G | ) id获取产品的信息,主要步骤及实现代码如下。

(1)定义proto文件。

syntax = \"pf P . ( qroto3\";//声明支持的版本是proto3

option java_multiple_files = true;//以外部类模式生成
option java_package = \"com.cloudnative.grpc\";//声明包名,可选R ? I 2 .
option java_outer_classname=\"ProductProtos\";//声明类名,可选

message ProductRequest{
int32 id = 1;
}
message ProductResponse {
int32 id = 1;
string name = 2;
string price = 3;C d [ % I { m 2
}

service ProductService{
rpc GetProduct(ProductRequest) rh N E eturns(Pr) J g GoductResponse);
}

(2)生成相关类。可以采用Protobuf中介绍的方法,在命令行执行protoc生成相关代码。如果使用Maven,则可以H y C r Z 2通过Maven插件实现。


<build>
<extensions>
<extension>
<groupId>? & & H : a $ ! hkr.motd.ma/ f y A 1 Cven<3 X s 7 r * A H;/groupId>
<artifactId>os-maven-plugin</artifactId>
<L , {version>1.5.0.Final</version>
</extension>
</extensions>
<plugins>
<plugin>
<groupId>org.xolstice] j m N Y p * O.maven.plugins</groupId>
<artifactId>protobuf-maven-plugin</A % uartifactId>
<version>0.5.0</version>
<configuration>
<protocArtifact>com.google.protobuf:3.5.1:exe:${os` p g |.detected.classifier} </protocArtifact>
<plugind M @Id>grpc-java</pluginId>
<pluginArtifact>ih } )o.grpc:protoc-gen-grpc-java:1.8.0:exe:${os.detected. classifier}</pluginArtifact>
<protocExecutable>/usr[ ) % {/local/bin/protoc</protocExecutable>
</configuration>
<executions>
<execution>
<goals>
<goal>compile</goal>
<goal>compile-custom</goal>
</goals>
</execution>
</executions>
&v m i A Klt;/plugin>
</plugins>
</build>

在pom.xmlo ( 8 P J H J中配置,并且执行mvn compile命令会在targe7 i 9 [ X Qt/generated-sources中生成相关~ I C E i类,可以I / L将相关类移到src /main/j. H [ d s O c b ^ava目录下备用。

(3)服务端实现代码。一是,实现ProductService。

public class Produc* ] K ytServiM [ ^ sce extenU R ? Q Lds ProductServiceGrpc.ProductServiceImplBase{
private static final Logger logger = Logg+ H y 6 {er.getLogger(GRPCServer.class.getName());
@Override
public void getProduct(ProductRequest request, StreamObservq | 6er<G r , % * U;ProductResponse>{ f ] : ( + responseObserver) {
log{ t P B y B I [ger.info(\"接收到客户端的信息:\"+request.getId());
ProductResponse responsed;
if (111==request.getId()){
responsed=ProductRespob ^ G I : c | onse.newBuir 0 z 9 ] ) o blder().setId(111).setNams Y @e (\"dddd\").build();
}else {
response! @ 8 7 d4 F ( F=Pr! E T [ f e & p GoductResponse.newBuilder().setId(0).setName(\"---\").build();
}
respona h n 2 lseObserver.onNext(responsed);
responseObserver.onCompleted; d S 6 b f c L G();
}
}

二是,实现server代码。


public class GRPCServer{
private static final Logger logger = Logger.getLogger(GRPCServer.class.getNa2 t ! A u L -me());

private final int port;
private final Server server;

public GRPCServer(int port){
this.port=port;
this.server = ServerBuQ ~ : filder.forPort(port)
.addService(new ProductService())
.build();
}
/** Start serving requests. */
public void start() throws IO: w . X OException {
this.server.start();
loggerd P ? % a -.info(\"Servef g G k 5 t ar started, listening on \" + port);
Runtime.getRuntime().addShutdownHook(new Thread() {
@Override
public void run() {
/% B * 6 _ | ; F d/ Use stderr here since the logger may has been r7 K w . beset by it3 N F ? ; V 7s JVM shutdown hook.) 6 ? b ` 1
logger.info(\"*** shutting down gRPC serve^ T X Q t )r since JVM is shuQ Z % dtting down\");
GRPCServer._ p | S 0 =this.stop();
logger.info(\"*** server shut down\");
}
});
}

/** StoM g Lp serving requests and shutdown resources. */
public void stop() {
if (server != null) {
server.shutdow% p g t - ) 6 Sn();
}
}

/**
* Await termij p C j A 6 pnation on the main threaZ V i R i Sd since the grpc library uses daemon thread/ ) M w Ds.
*/
pri) = lvate void bl= Z #ob R d R # -ckUntilShutdown() throws Interru0 c RptedException {
if (server != null) {
server.awaitTermination();
}
}

/**
* Main method. ThisP k } C I Q x comment makes the linter he u 4 q r w O /appy.
*/
public static void main(String[] args) throws Exception {
GRPCServer server = new GRPCServer(8888);
server.stag z 2 3 y U |rt();
server.blockUntilShutdown();
}

}

(4)客户端实现代码。

public class GRPCClient {
private static final Logger lg l y : & l logger = Logger.getLogger(GRPCSer& H o k { Z ; Kver.class.getName())? K f T | x;
public static void mE / b D ) W f oain(String[] args) {

ManagedChannel channel = ManagedChannelP S q O , ?Builder.forAddress(\"localhost\", 888a 5 : : V I % ` S8)
.usePlaintext(true)
.build();P | - X z I | 1
PrP R xoductServiceGrpc.ProductServiceBlockingStub b~ P OlockStub=ProductServiceGrpc.newBlockingStub(channel);
ProductResponse response=blockStub.getProduct(ProductRequest.newBuilder().setId(111).build());
loI 9 j { n N q [gger.info(response.getName());
resc V #ponse=blockStub.getProduct| % y ! v &(ProductR% F 8 = keques! H A f 4t.newBuilder().setId(2).build());
logger.info(response.getName());

}

}

上面是一个简单的实现,关于流式RPC可以参考官方的例子。

来源 公众号 技术琐话 | 王启军

如有侵权请联系删除

上一篇

安徽省出台政策支持5G“新基建”

下一篇

OPPO 官方剧透:全球最快 65W 超级闪充始终领先的秘密

评论已经被关闭。

插入图片
返回顶部