查看原文
其他

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

21CTO 2020-09-09

The following article is from 技术琐话 Author 王启军

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

优秀API的设计原则

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


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

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

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

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

· 安全:设计时要考虑超出预期的情况如何处理,给出被限流的情况。下面是GitHub API使用的两个相关的头部参数

  ¡  X-RateLimit-Limit:每小时允许发送请求的最大值。

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

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

服务间通信——RPC

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

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

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

(2)调用本地stub,本地stub对消息进行序列化、封装等处理。

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

(4)服务端网络通信模块把接收到的消息转发给stub进行反序列化。

(5)stub转发给业务服务进行处理并返回结果。

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

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

(8)客户端网络通信模块将消息转发给stub。

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

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

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

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

影响RPC性能因素如下。

· 序列化。常用的RPC序列化协议包括:Thrift、Protobuf、Avro、Kryo、MsgPack 、Hessian、Jackson。

 

2-11  RPC调用流程图

· 传输协议。常用传输协包括:HTTP、Socket、TCP、UDP等

· 连接连接包括:长连接短连接。 

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

序列化——Protobuf

Protobuf是由Google开源的消息传输协议,用于将结构化的数据序列化、反序列化通过网络进行传输,目前被广泛应用的主要版本有Protobuf 2和Protobuf 3。Protobuf首先解决的是如何在不同语言之间传递数据的问题,目前支持的编程语言有Java、C++、Python、Ruby、PHP、Go等。通过Protobuf可以很容易地实现一个Client和一个Server之间的数据传递,Client和Server可以使用不同语言。

Protobuf是一个高性能、易扩展的序列化框架,通常是RPC调用追求高性能的首选。它本身非常简单,易于开发,结合Netty可以非常便捷地实现RPC调用。同时,可以利用Netty为Protobuf解决有关Socket通信中“半包、粘包”等问题(反序列化时,字节成帧)。

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

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

1.安装客户端

通过brew可以实现在Mac下安装Protobuf,执行命令如下。

brew install protobuf

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

/usr/local/Cellar/protobuf/3.5.1: 267 files, 18.0MB

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

protoc –version

2.安装工具

为了在编辑proto文件的时候获得更好的体验,可以先安装插件。idea里可以安装Protobuf Support,在插件库中搜索后直接安装即可,如图2-12所示。

 

2-12  idea安装Protobuf Support插件

Protobuf Support可以对语法高亮显示、错误提示,如图2-13所示。

 

2-13  Protobuf Support插件效果

3.编辑proto文件

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

2-3  Protobuf的数据类型和Java的数据类型的对照表

.proto

C++

Java

Python

Go

Ruby

C#

double

double

double

float

float64

Float

double

float

float

float

float

float32

Float

float

int32

int32

int

int

int32

Fixnum or Bignum

int

int64

int64

long

ing/long[3]

int64

Bignum

long

uint32

uint32

int[1]

int/long[3]

uint32

Fixnum or Bignum

uint

uint64

uint64

long[1]

int/long[3]

uint64

Bignum

ulong

sint32

int32

int

intj

int32

Fixnum or Bignum

int

sint64

int64

long

int/long[3]

int64

Bignum

long

fixed32

uint32

int[1]

int

uint32

Fixnum or Bignum

uint

fixed64

uint64

long[1]

int/long[3]

uint64

Bignum

ulong

sfixed32

int32

int

int

int32

Fixnum or Bignum

int

sfixed64

int64

long

int/long[3]

int64

Bignum

long

bool

bool

boolean

boolean

bool

TrueClass/FalseClass

bool

string

string

String

str/unicode[4]

string

String(UTF-8)

string

bytes

string

ByteString

str

[]byte

String(ASCII-8BIT)

ByteString

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

syntax = "proto3";//声明支持的版本是proto 3 option java_package = "com.cloudnative.protobuf";//声明包名,可选option java_outer_classname="ProductProtos";//声明类名,可选 message Product { int32 id = 1; string name = 2; string price = 3; enum ColorType {//定义枚举类型 WHITE = 0; RED = 1; BLACK = 2; }}

在命令行执行如下代码。

protoc --java_out=../java product.proto

--java_out表示在目标目录中生成Java代码。由于已将product.proto放到src/mian/java/proto目录下,所以--java_out=../java参数会将生成的Java类创建到java目录下,如图2-14所示。

 

2-14  项目目录结构

打开ProtobufProtos会发现,这是一个非常复杂的类,其代码大概1500行。

4.使用Protobuf序列化

引入Protobuf-java包,可以先通过http://mvnrepository.com查询,然后点击复制,非常方便,如图2-15所示。

 

2-15  查询结果

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

package com.cloudnative.protobuf; import com.google.protobuf.InvalidProtocolBufferException; public class TestProtobuf { public static void main(String[] args){ TestProtobuf testProtobuf =new TestProtobuf(); byte[] buf=testProtobuf.toByte(); try { ProductProtos.Product product = testProtobuf.toProduct(buf); System.out.println(product); } catch (InvalidProtocolBufferException e) { e.printStackTrace(); } } //序列化 private byte[] toByte(){ ProductProtos.Product.Builder productBuilder=ProductProtos.Product.newBuilder(); productBuilder.setId(11); productBuilder.setName("milk"); productBuilder.setPrice("4.12"); ProductProtos.Product product =productBuilder.build(); byte[] buf = product.toByteArray(); System.out.println(buf.length); return buf; } //反序列化 private ProductProtos.Product toProduct(byte[] buf) throws InvalidProtocolBufferException { return ProductProtos.Product.parseFrom(buf);}}

服务间通信RESTful

REST是Roy Thomas Fielding在2000年的博士论文中提出的。REST是Representational State Transfer的缩写,通常翻译为“表现层状态转化”。如果一个架构符合REST原则,就称它为RESTful架构。

API如何设计才能满足RESTful的要求呢?

1.协议

API是基于HTTP协议

2.域名

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

3.版本

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

4.路径

理解REST首先要理解什么是资源(Resource)。REST开发又被称作面向资源的开发。API要资源来表示,URL中不能出现动词资源是服务端可命名的一个抽象的概念,只要客户端容易理解,可以随意抽象。通常可以资源看成是一个实体例如用户、邮件、图片,用URI(统一资源定位符)指向它。经验告诉我们往往这里的资源和数据库表名是对应关系。一种观点认为DDD可以和REST API很好契合,因为REST的资源可以很好地与DDD的实体映射起来定义资源的时候,推荐用复数,假设我们要获取用户的信息,大概是这样http://api.xxx.com/v1/users/

5.方法

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

· GET:读取资源,一个或多个(常用)。

· POST:创建资源(常用)。

· PUT:修改资源,客户端提供修改后的完整资源(常用)。

· PATCH:对已知资源进行局部更新,客户端只需要提供改变的属性

· DELETE:删除、回收资源(常用)。

· HEAD:读取资源的元数据(不常用)。

· OPTIONS:读取对资源的访问权限(不常用)。

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

· GET/users/1获取用户ID为1的用户信息。

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

6.参数

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

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

7.编码

虽然RESTful并没有限制资源的表达格式,HTML/XML/JSON/纯文本/图片/视频/音频等都可以,但是通常服务端和客户端通过JSON传递信息

8.状态码

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

· 100Continue

· 200OKGET

· 201CreatedPOST/PUT/PATCH

· 202Accepted

· 204NO ContentDELETE

· 400Bad RequestPOST/PUT/PATCH

· 401Unauthorized

· 403Forbidden

· 404Not Found

· 405Method Not Allowed

· 406Not Acceptable

· 409Conflict

· 410Gone

· 412Precondition Failed

· 429Too many requests

· 500Internal Server Error

· 501Not Implemented

· 503Service Unavailable

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

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

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

· 幂等性一次和多次请求某一个资源应该具有同样的作用,客户端能够重复发起请求不必担心造成作用。

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

· 客户端发起。一般通信方式都是由客户端发起的,服务端是被调用的。随着HTTP/2的到来,这一条可能会发生变化

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

· 易用需要提供详尽的文档参数说明、示例等,API定义的URL、变量名要通俗易懂最好是英文,尽量减少自定义的缩写,让开发者容易调试和管理

· SLA。需要提供响应时间、吞吐量可用性关键指标。

RESTful已经成为业界的主流,主要是因为RESTful通常采用HTTP+JSON的方式实现,继承了HTTP和JSON的优点。相对于SOAP、RPC等方式,RESTful更加轻量、简单,支持跨语言,并且容易调试。

通过Swagger实现RESTful

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

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

2-16  基于Swagger生成API的示例

Swagger是基于OpenAPI的,OpenAPI支持YAML和JSON格式描述APIYAML相对于JSON来说更加简洁,比较适合做简洁序列化和配置文件。编写YAML文档推荐使用Swagger Editor,它提供了语法高亮、自动完成、即时预览等功能。编辑器可以在本地使用,也可以在线使用。YAML的数据结构可以用类似大纲的缩排方式呈现,结构通过缩进来表示,连续的项目通过“-”来表示,map结构里面的key/value对用“:”来分隔。

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

(1)访问官方Editor编辑器

http://editor2.swagger.io

(2)编辑YAML文件,如下所示。

swagger: "2.0" info: version: 1.0.0 title: Product API description: Product API for test schemes: - httpshost: localhostbasePath: /product paths: {}

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

swagger: "2.0"

API的描述信息,包括如API文档版本(version)、API文档名称(title)和描述信息(description)。

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

下面代码表示API的URL,采用HTTPS协议,介绍主机名(host)、根路径(basePath)。

schemes: - httpshost: localhostbasePath: /product

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

swagger: '2.0'info: version: '1.0' title: Swagger构建RESTful APIhost: 'localhost:8080'basePath: /tags: - name: product-controller description: Product Controllerpaths: /products: get: tags: - product-controller summary: 获取产品列表 description: 获取产品列表 operationId: getProductListUsingGET consumes: - application/json produces: - '*/*' responses: '200': description: OK schema: type: array items: $ref: '#/definitions/Product' '401': description: Unauthorized '403': description: Forbidden '404': description: Not Found /products/{id}: get: tags: - product-controller summary: 获取产品详细信息 description: 根据URL的id来获取产品详细信息 operationId: getProductUsingGET consumes: - application/json produces: - '*/*' parameters: - name: id in: path description: 产品ID required: true type: integer responses: '200': description: OK schema: $ref: '#/definitions/Product' '401': description: Unauthorized '403': description: Forbidden '404': description: Not Founddefinitions: Product: type: object properties: count: type: integer format: int32 desc: type: string id: type: integer format: int32 name: type: string

上面的示例定义了两个API,一个是获取Product的列表,一个是根据id获取Product的详情。

编辑完成后,可以得到如图2-17所示的文档界面。

2-17  Swagger生成文档的示例

(3)点击file-download yaml下载YAML文件。

(4)点击generate server下载服务端,点击generate client下载客户端,可以分别生成相应语言的SDK工程。

通过Spring Boot、Springfox、Swagger实现RESTful

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

Spring Boot已经家喻户晓,而Springfox是什么呢?Marty Pitt曾经编写了一个基于Spring的组件swagger-springmvc,用于将Swagger集成到Spring MVC中,Springfox是从这个组件发展而来的。

下面我们通过一个简单的示例来说明一下。首先新建一个项目,基于Maven构建,在pom中引入相应的JAR包,这里需要引入Spring Boot和Springfox的相关包。

<dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter</artifactId></dependency><dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-test</artifactId> <scope>test</scope></dependency><dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-web</artifactId></dependency><dependency> <groupId>io.springfox</groupId> <artifactId>springfox-swagger2</artifactId> <version>2.7.0</version></dependency><dependency> <groupId>io.springfox</groupId> <artifactId>springfox-swagger-ui</artifactId> <version>2.7.0</version></dependency>

编写Swagger的配置类。

@Configuration@EnableSwagger2public class Swagger2 { @Bean public Docket createRestApi() { return new Docket(DocumentationType.SWAGGER_2) .apiInfo(apiInfo()) .select() .apis(RequestHandlerSelectors.basePackage("com.cloudnative.rest")) .paths(PathSelectors.any()) .build(); } private ApiInfo apiInfo() { return new ApiInfoBuilder() .title("Swagger构建RESTful API") .description("") .termsOfServiceUrl("") .version("1.0") .build(); }

实现dto类。

public class Product { private int id; private String name; private int count; private String desc; public String getName() { return name; } public void setName(String name) { 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() { return id; } public void setId(int id) { this.id = id; } @Override public String toString() { return "Product{" + "id=" + id + ", name='" + name + '\'' + ", count=" + count + ", desc='" + desc + '\'' + '}'; }}

基于注解编写接口实现。

@RestController@RequestMapping(value="/products") //通过这里配置使下面的映射都在/products下public class ProductController { private List<Product> productList; //初始化 public ProductController(){ productList = new ArrayList<Product>(); for (int i = 0; i < 10; i++) { Product product =new Product(); product.setId(i); product.setCount(i+10); product.setName("watch"+i); product.setDesc("watch desc"+i); productList.add(product); } } @ApiOperation(value="获取产品列表", notes="获取产品列表") @RequestMapping(value={""}, method= RequestMethod.GET) public List<Product> getProductList() { return productList; } @ApiOperation(value="获取产品详细信息", notes="根据url的id来获取产品详细信息") @ApiImplicitParam(name = "id", value = "产品ID", required = true, dataType = "Integer",paramType="path") @RequestMapping(value="/{id}", method=RequestMethod.GET) public Product getProduct(@PathVariable Integer id) { return productList.get(id); }}

基于Main方法启动。

@SpringBootApplicationpublic class Application { public static void main(String[] args) { SpringApplication.run(Application.class, args); }}

访问http://localhost:8080/swagger-ui.html,自动生成的文档界面如图2-18所示。

 

2-18  生成的文档界面

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

 

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

访问http://localhost:8080/v2/api-docs可以获取接口的JSON描述文件,如图2-20所示。可以直接到Swagger官网把JSON描述文件转换为YAML文件。

2-20  转换为JSON的结构

HTTP协议的进化HTTP/2

据W3Techs统计,截至2017年11月,排在前1千万名的网站中有20.5%的支持HTTP/2。Chrome、Opera、Firefox、Internet Explorer 11、Safari、Amazon Silk和Edge浏览器都支持HTTP/2的标准化。大多数主流浏览器都在2015年底之前添加了HTTP/2支持。

HTTP/2HTTP/1.x进行了大量简化,使得性能得到了大幅提升。Akamai公司官网通过一个示例对比了HTTP/1.1和HTTP/2在性能上的差距—并发请求379张图片,HTTP/1.1需要14.70s,响应时间为3ms,而HTTP/2仅仅需要1.61s响应时间为6ms,如图2-21所示

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

我们都知道,在HTTP/1.x的协议中,浏览器在同一时间对同一域名下的请求数量是有限制的这会导致大量并发请求阻塞,这个问题也被称为线端阻塞(head-of-line blocking)。同一域名下浏览器支持的连接数,如表2-4所示。HTTP/1.1不同浏览器连接数的限制不同,很多互联网公司了解决这个问题做了大量优化,包括建立多域名,通过CDN缓存大量静态资源

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

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

浏  览  器

每个域名支持的最大并发连接

IE 9

6

IE 10

8

Firefox 4+

6

Opera 11+

6

Chrome 4+

6

Safari

4

 

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

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

HTTP/1.1如果要实现长连接,需要设置Connection:keep-alive来控制长连接时间,超时就断开TCP连接。只有在客户端发起请求的时候,服务器端才会响应。所以就算一直给服务器发送心跳包以维持长连接,也不能用来推送,只有客户端不断发起请求给服务器端,服务器才会响应,这就是pull轮询的方式。

HTTP/2引入服务端推送模式,即服务端向客户端发送数据,如2-23所示服务器可以对一个客户端请求发送多个响应,HTTP/2打破了严格的请求-响应语义,支持一次请求-多次响应的形式。由于现如今的Web界面丰富多彩,加载的资源往往非常多,服务端实际上已经知道要推送什么内容,但HTTP/1.x语义只支持客户端发起请求服务端响应数据。HTTP/2改变了这种模式只需要客户端发送一次请求,服务端便把所有的资源都推送到客户端。服务器推送的缺点是,在客户端已经缓存了资源的情况下可能会有冗余。这个问题可以通过服务器提示(Server Hint)解决

2-23  HTTP/2推送模式


HTTP/2和Protobuf的组合gRPC

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

gRPC是基于HTTP/2设计的,因此也继承了HTTP/2相应的诸多特性,这些特性使得其在移动设备上表现得更好,更节省空间、更省电。gRPC目前提供的C、Java和Go语言版本分别是grpc、grpc-java、grpc-go,其中C版本支持C、C++、Node.js、Python、Ruby、Objective-C、PHP和C#。

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

· gRPC默认使用Protobuf进行序列化和反序列化,而Protobuf是已经被证明的高效的序列化方式,因此,gRPC的序列化性能是可以得到保障的。

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

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

同时,gRPC也不是完美的。相比于非IDL描述的RPC(例如Hession、Kyro)方式,定义proto文件是一个比较麻烦的事情,而且需要额外安装客户端、插件等。另外HTTP/2相比于基于TCP的通信协议,性能上也有显著的差距。

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

(1)定义proto文件。

syntax = "proto3";//声明支持的版本是proto3 option java_multiple_files = true;//以外部类模式生成option java_package = "com.cloudnative.grpc";//声明包名,可选option java_outer_classname="ProductProtos";//声明类名,可选 message ProductRequest{ int32 id = 1;}message ProductResponse { int32 id = 1; string name = 2; string price = 3;} service ProductService{ rpc GetProduct(ProductRequest) returns(ProductResponse);}

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

<build> <extensions> <extension> <groupId>kr.motd.maven</groupId> <artifactId>os-maven-plugin</artifactId> <version>1.5.0.Final</version> </extension> </extensions> <plugins> <plugin> <groupId>org.xolstice.maven.plugins</groupId> <artifactId>protobuf-maven-plugin</artifactId> <version>0.5.0</version> <configuration> <protocArtifact>com.google.protobuf:3.5.1:exe:${os.detected.classifier} </protocArtifact> <pluginId>grpc-java</pluginId> <pluginArtifact>io.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> </plugin> </plugins></build>

在pom.xml中配置,并且执行mvn compile命令会在target/generated-sources中生成相关类,可以将相关类移到src /main/java目录下备用。

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

public class ProductService extends ProductServiceGrpc.ProductServiceImplBase{ private static final Logger logger = Logger.getLogger(GRPCServer.class.getName()); @Override public void getProduct(ProductRequest request, StreamObserver<ProductResponse> responseObserver) { logger.info("接收到客户端的信息:"+request.getId()); ProductResponse responsed; if (111==request.getId()){ responsed=ProductResponse.newBuilder().setId(111).setName ("dddd").build(); }else { responsed=ProductResponse.newBuilder().setId(0).setName("---").build(); } responseObserver.onNext(responsed); responseObserver.onCompleted(); }}

二是实现server代码。

public class GRPCServer{ private static final Logger logger = Logger.getLogger(GRPCServer.class.getName()); private final int port; private final Server server; public GRPCServer(int port){ this.port=port; this.server = ServerBuilder.forPort(port) .addService(new ProductService()) .build(); } /** Start serving requests. */ public void start() throws IOException { this.server.start(); logger.info("Server started, listening on " + port); Runtime.getRuntime().addShutdownHook(new Thread() { @Override public void run() { // Use stderr here since the logger may has been reset by its JVM shutdown hook. logger.info("*** shutting down gRPC server since JVM is shutting down"); GRPCServer.this.stop(); logger.info("*** server shut down"); } }); } /** Stop serving requests and shutdown resources. */ public void stop() { if (server != null) { server.shutdown(); } } /** * Await termination on the main thread since the grpc library uses daemon threads. */ private void blockUntilShutdown() throws InterruptedException { if (server != null) { server.awaitTermination(); } } /** * Main method. This comment makes the linter happy. */ public static void main(String[] args) throws Exception { GRPCServer server = new GRPCServer(8888); server.start(); server.blockUntilShutdown(); } }

(4)客户端实现代码。

public class GRPCClient { private static final Logger logger = Logger.getLogger(GRPCServer.class.getName()); public static void main(String[] args) { ManagedChannel channel = ManagedChannelBuilder.forAddress("localhost", 8888) .usePlaintext(true) .build(); ProductServiceGrpc.ProductServiceBlockingStub blockStub=ProductServiceGrpc.newBlockingStub(channel); ProductResponse response=blockStub.getProduct(ProductRequest.newBuilder().setId(111).build()); logger.info(response.getName()); response=blockStub.getProduct(ProductRequest.newBuilder().setId(2).build()); logger.info(response.getName()); } }

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

 

本文节选自《持续演进的Cloud Native:云原生架构下微服务最佳实践》一书,王启军 著。



鸣谢:电子工业出版社


相关推荐:


阿里VP李飞飞:下一代云原生数据库技术趋势,40页ppt

什么是云原生?是炒作还是软件开发的未来?

同志,云原生了解一下?


    您可能也对以下帖子感兴趣

    文章有问题?点此查看未经处理的缓存