频道栏目
首页 > 程序开发 > 综合编程 > 其他综合 > 正文
REST-RPC服务接口实例分析
2017-02-18 10:10:00           
收藏   我要投稿

REST-RPC服务接口实例分析:无论设计原生手机App,或是前面文章曾提及过的“变脸式应用”(一种无网页刷新的多页面Web应用),都需要后端应用服务器提供业务支持。于是,如何设计后端服务接口是开发前必须考虑清楚的一件事。

谈及接口设计,我们需要从两个维度来考虑:协议及原型,简称2P维度。

原型定义了一个调用的抽象形式。假定要做上门送餐业务,每个“商户”是个对象,取名为”Store”,那么一个“查询商户列表”接口,可以设计其原型为:

Store.query() -> tbl(id, name, dscr)

原型中,描述了调用名为Store.query,参数为空,返回Table类型的数据表示一张表(下文有对此类型介绍),表中的每行有id及name等几列,代表一个商户对象的属性。

接着,需要设计协议来实现它。可以这样来实现上述调用:客户端对服务端的请求基于HTTP协议,使用HTTP GET或POST方法,将调用名放在URL末尾,将参数放在URL参数中(此处没有),服务端返回数据使用JSON格式来描述。于是,客户端需要像这样发出HTTP请求:

GET /api/Store.query

这便是筋斗云框架的服务接口设计。事实上,筋斗云框架是对DACA架构的实现,DACA全称“分布式访问和控制架构”,其中就有定义客户端-服务器如何通讯,即对上例中的设计方案进一步规范化,称为BQP协议(业务查询协议);DACA还规定了客户端如何调用服务接口,称为客户端公共调用接口,如下文将介绍的callSvr调用。

BQP协议的设计风格介于RESTful和RPC之间,故称为REST-RPC风格。Leonard Richardson 和 Sam Ruby 在他们的著作 RESTful Web Services 中引入了术语 REST-RPC 混合架构(中文版译文),它不像SOAP或XML-RPC那样使用额外的信封格式来包装调用名和参数,而是直接通过HTTP传输数据,这与 REST 样式的 Web 服务是类似的;但是它不使用标准的HTTP PUT/DELETE等方法操作资源,而且在URI中存储调用名(例子中是Store.query)。

我们考察协议设计的主要原则有:

清晰易懂

易实现

传输及处理效率高

对照这些原则,RESTful风格清晰易懂,但像HTTP PUT/DELETE等方法的兼容性并不好,不论服务端或客户端在实现上都会遇到障碍;而使用RPC风格的设计,不仅可读性差很多,而且封包解包效率较低。所以,从实用的角度,筋斗云的设计思想认为,REST-RPC是目前更好的选择。

在BQP协议中,业务接口分为函数调用型接口(如login调用)和对象调用型接口(如Store.query调用)。函数调用型接口可以自由设计原型,而对象调用型接口其实是一种特殊的函数调用,用于操作业务对象,有相对固定的原型,设计者可以对它加以裁减或扩展。两类接口在通讯协议及客户端使用上没有太大区别,其主要区别在于后端服务的实现模型不同。

本文只讨论对象调用型接口。BQP协议定义了一个对象的五种标准操作:查询列表(query),获取明细(get),添加(add),更新(set)和删除(del)。下文将详细举例说明,我们先假定有“商户”(Store)这个对象,其数据模型描述如下:

@Store: id, name, addr, tel, dscr

这表示商户表Store,有id, name等字段。注意:DACA规范建议,在设计数据模型时,应以id作为主键。

DACA规范要求客户端应提供callSvr方法来调用服务接口,在筋斗云前端中,该接口为JS函数,其原型为

callSvr(ac, param?, fn?, postParam?, userOptions?) -> XMLHttpRequest

callSvr(ac, fn?, postParam?, userOptions?) -> XMLHttpRequest

其中ac表示调用名(action),param和postParam分别为通过URL和POST内容传递的参数,如果没有param,可以忽略该参数(即第二种原型)。fn为回调函数,调用格式为fn(data),其中参数data为返回的JSON对象,类型参考接口原型中的返回值描述。

带问号的参数表示可缺省。

函数返回XMLHttpRequest对象,与jQuery中的$.ajax返回值相同。

以上调用为异步方式,即该函数执行后立即结束,待服务端数据返回再回调函数fn。也可以做同步调用,只要将函数名callSvr改为callSvrSync,即意味着该函数将等服务端返回数据才结束,而且,其返回值不再是XMLHttpRequest对象,而是服务接口返回的JSON对象。我们在Chrome控制台窗口测试接口时,常用同步调用以方便看到结果。

只要熟悉这几个客户端接口,就可以根据设计文档中的接口原型调用任何接口了,不必再对BQP底层协议细节有深入了解。

添加对象

BQP协议中定义对象添加操作的原型如下:

{object}.add()(POST fields...) -> id

一般在原型定义中参数部分只用一个括号,表示参数通过URL或POST内容传递都可以。而这里出现了两个括号,就表示URL参数和POST参数不可混用,两个括号依次表示URL参数和POST参数。

这样,我们添加一个商户,可以用:

var postParam = {name: "华莹小吃", addr: "银科路88号", tel: "13712345678"};

callSvr("Store.add", api_StoreAdd, postParam);

function api_StoreAdd(data) {

// 根据原型定义中的返回值,data是id值。

alert("id=" + data);

}

由于没有URL参数,所以callSvr的第二个参数可以省略。如果想写完整,会像这样:

callSvr("Store.add", null, api_StoreAdd, postParam);

调用成功,则会调用指定的回调函数,如果调用失败,则前端框架会接管错误处理,调用者一般不必关心。

更新对象

原型为:

{object}.set(id)(POST fields...)

其中未指定返回值,表示调用成功时无特定返回值。筋斗云后端会返回字符串”OK”。

假如要更新id=8这家商户对应的联系电话:

var param = {id: 8};

var postParam = {tel: "13812345678"};

callSvr("Store.set", param, api_StoreSet, postParam);

function api_StoreSet(data)

{

alert("更新成功");

}

注意:要更新的字段一定要放在POST参数中。

置空一个字段

在BQP协议中,设置一个字段为空串一般是被服务端忽略的,但在set操作中,如果在postParam中设置某个字段为空串(或特定字符串"null"),则表示清空该字段。

要清空某商户的地址:

var postParam = {addr: ""};

// 或者 var postParam = {addr: "null"};

callSvr("Store.set", {id: 8}, api_StoreSet, postParam);

下次用Store.get获取该商户时,可见属性addr值为null (注意:不是字段串"null")

删除对象

原型为:

{object}.del(id)

调用很简单,假如要删除id=8对应的商户:

callSvr("Store.del", {id: 8}, function (data) {

alert("删除成功");

});

获取对象详情

原型为:

{object}.get(id, res?) -> {fields...}

其默认返回对象对应主表中的字段,设计时也可以为返回内容增加子对象或虚拟字段(实现方法参考筋斗云后端文档)。

假定在设计“获取商户”接口时,增加一个子对象“商品列表”名为items,设计接口原型为:

Store.get(id, res?) -> {id, name, addr, tel, @items=[item]}

item:: {id, name, price}

(注意:在设计接口原型时,用的是“蚕茧表示法”层层解析和描述对象类型,不在本文讨论范围内,详见相关文章。)

根据接口,要获取一个商户的详情可以这样调用:

callSvr("Store.get", {id: 8}, api_StoreGet);

function api_StoreGet(data) { ... }

返回数据data像这样:

{

id: 8,

name: "华莹小吃",

addr: "银科路88号",

tel: "13812345678",

items: [

{id: 1001, name: "鲜肉小笼", price: 10.0},

{id: 1002, name: "大肉粽", price: 8.0}

...

]

}

在URL中的可选参数res它表示”result”,即返回字段的列表,多个字段中间用逗号分隔。如果你不想返回默认的字段,可以通过该参数指定想要哪些字段。

例:获取商户详情,只返回店名和电话:

callSvr("Store.get", {id: 8, res: "name,tel"}, api_StoreGet);

function api_StoreGet(data)

{

// data示例:{name: "华莹小吃", tel: "13812345678"}

}

查询对象列表

查询操作是标准操作中最灵活和最复杂的,它的可选参数很多,原型有两种(返回内容的格式不同):

{object}.query(res?, cond?, orderby?, distinct?=0, _pagesz?=20, _pagekey?, _fmt?) -> tbl(field1,field2,...)

{object}.query(wantArray=1, ...) -> [{field1,field2,...}]

第一种原型返回特别的Table类型(下文介绍,可以转成对象数组),好处是数据精练,而且支持分页;第二种原型多了wantArray参数的设置(其它参数用法相同),返回类型变成对象数组,支持子对象,然而它不支持分页操作,一般使用较少。

Table类型

如果未指定参数wantArray(第一种原型),则返回的内容为Table类型,这种格式不可以返回子对象(如上节get操作中的子对象商品列表items),比如取商户列表:

callSvr("Store.query", api_StoreQuery);

function api_StoreQuery(data) { ... }

回调函数api_StoreQuery中的data参数格式为:

{

h: [ "id", "name", "addr", "tel"]

d: [

[ 8, "华莹小吃", "银科路88号", "13812345678"],

[ 9, ... ]

...

]

nextkey: 998

}

其中属性h为列名数组,d表示数据行数组,每行的值数组与列名数组中元素一一对应。

如果存在属性nextkey,则表示这只是一部分数据,要取下一页数据,可以用同样的查询,带上参数_pagekey设置为该值,如

callSvr("Store.query", {_pagekey: 998});

这种Table结构设计有利于传输效率的提高,同时便于分页机制的设计。

筋斗云前端提供函数rs2Array,可将这个数据转换成通常用的对象数组:

var arr = rs2Array(data);

得到的arr像这样:

[

{id: 8, name: "华莹小吃", addr: "银科路88号", tel: "13812345678"},

{id: 9, ...}

...

]

查询参数

对象查询支持灵活的查询条件(通过参数cond - condition),排序方法(参数orderby),返回字段(参数res,与get操作一样)。

如果你了解SQL语句,则会发现这些参数用起来很简单。

参数res指定返回字段, 多个字段以逗号分隔,例如, res=”field1,field2”.

参数cond指定查询条件,其语法类似SQL语句的”WHERE”子句,例如”field1>100 AND field2=’hello’”,注意字符串值要加上单引号。

参数orderby指定排序条件,语法可参照SQL语句的”ORDER BY”子句,例如:orderby=”id desc”,也可以多字段依次排序:”tm desc,status” (按时间倒排,再按状态正排)

例如,要查询所有id小于10且名字中以”华莹”开头的商户,返回结果按名字(name)排序:

var cond = "id<10 and name like '华莹%'";

var param = {res: "id,name,addr", cond: cond, orderby: "name"};

callSvr("Store.query", param, api_StoreQuery);

function api_StoreQuery(data)

{

// 先用rs2Array将table类型的数据转成对象数组

var arr = rs2Array(data);

// 遍历每个商户

arr.forEach(function(store) {

// 由于指定了res参数,store对象类型为:{id, name, addr}

});

}

尽管这些参数值类似SQL语句,但它们有一些安全限制:

res, orderby只能是字段(或虚拟字段)列表,不能出现函数、子查询等。

cond可以由多个条件通过and或or组合而成,而每个条件的左边是字段名,右边是常量。不允许对字段运算,不允许子查询(不可以有select等关键字)。

像参数cond中出现以下情况都不允许:

left(type, 1)='A' -- 条件左边只能是字段,不允许计算或函数

type=type2 -- 字段与字段比较不允许

type in (select type from table2) -- 子表不允许

分页支持

参数_pagesz和_pagekey用于支持分页。_pagesz指定每次返回多少条数据(默认一次返回20条)。

下面是一个获取所有商户的例子。第一次查询:

callSvr("Store.query")

返回数据像这样:

{nextkey: 10800910, h: [id, ...], d: [...]}

其中的nextkey表示数据未返回完,要查询下一页时需填写_pagekey字段。

第二次查询(下一页):

callSvr("Store.query", {_pagekey=10800910});

返回:

{nextkey: 10800931, h: [...], d: [...]}

仍返回nextkey字段说明还可以继续查询,再查询下一页:

callSvr("Store.query", {_pagekey=10800931});

返回:

{h: [...], d: [...]}

返回数据中不带nextkey属性,表示所有数据获取完毕。

如果想在首次查询时返回总记录数,可以设置_pagekey=0:

callSvr("Store.query", {_pagekey: 0})

点击复制链接 与好友分享!回本站首页
上一篇:SDOI2016 Round 1解题报告
下一篇:zookeeper和kafka实践
相关文章
图文推荐
文章
推荐
点击排行

关于我们 | 联系我们 | 广告服务 | 投资合作 | 版权申明 | 在线帮助 | 网站地图 | 作品发布 | Vip技术培训 | 举报中心

版权所有: 红黑联盟--致力于做实用的IT技术学习网站