常用排序算法的动画效果图
http://www.atool.org/sort.php
1 快速排序
介绍:
快速排序是由东尼·霍尔所发展的一种排序算法。在平均状况下,排序 n 个项目要Ο(n log n)次比较。在最坏状况下则需要Ο(n2)次比较,但这种状况并不常见。事实上,快速排序通常明显比其他Ο(n log n) 算法更快,因为它的内部循环(inner loop)可以在大部分的架构上很有效率地被实现出来,且在大部分真实世界的数据,可以决定设计的选择,减少所需时间的二次方项之可能性。
步骤:
- 从数列中挑出一个元素,称为 "基准"(pivot),
- 重新排序数列,所有元素比基准值小的摆放在基准前面,所有元素比基准值大的摆在基准的后面(相同的数可以到任一边)。在这个分区退出之后,该基准就处于数列的中间位置。这个称为分区(partition)操作。
- 递归地(recursive)把小于基准值元素的子数列和大于基准值元素的子数列排序。
排序效果:
详细过程:
2 归并排序
介绍:
归并排序(Merge sort,台湾译作:合并排序)是建立在归并操作上的一种有效的排序算法。该算法是采用分治法(Divide and Conquer)的一个非常典型的应用
步骤:
- 申请空间,使其大小为两个已经排序序列之和,该空间用来存放合并后的序列
- 设定两个指针,最初位置分别为两个已经排序序列的起始位置
- 比较两个指针所指向的元素,选择相对小的元素放入到合并空间,并移动指针到下一位置
- 重复步骤3直到某一指针达到序列尾
- 将另一序列剩下的所有元素直接复制到合并序列尾
排序效果:
详细过程:
3 堆排序
介绍:
堆积排序(Heapsort)是指利用堆这种数据结构所设计的一种排序算法。堆是一个近似完全二叉树的结构,并同时满足堆性质:即子结点的键值或索引总是小于(或者大于)它的父节点。
步骤:
(比较复杂,自己上网查吧)
排序效果:
详细过程:
(暂无)
4 选择排序
介绍:
选择排序(Selection sort)是一种简单直观的排序算法。它的工作原理如下。首先在未排序序列中找到最小元素,存放到排序序列的起始位置,然后,再从剩余未排序元素中继续寻找最小元素,然后放到排序序列末尾。以此类推,直到所有元素均排序完毕。
排序效果:
详细过程:
5 冒泡排序
介绍:
冒泡排序(Bubble Sort,台湾译为:泡沫排序或气泡排序)是一种简单的排序算法。它重复地走访过要排序的数列,一次比较两个元素,如果他们的顺序错误就把他们交换过来。走访数列的工作是重复地进行直到没有再需要交换,也就是说该数列已经排序完成。这个算法的名字由来是因为越小的元素会经由交换慢慢“浮”到数列的顶端。
步骤:
- 比较相邻的元素。如果第一个比第二个大,就交换他们两个。
- 对每一对相邻元素作同样的工作,从开始第一对到结尾的最后一对。在这一点,最后的元素应该会是最大的数。
- 针对所有的元素重复以上的步骤,除了最后一个。
- 持续每次对越来越少的元素重复上面的步骤,直到没有任何一对数字需要比较。
排序效果:
详细过程:
6 插入排序
介绍:
插入排序(Insertion Sort)的算法描述是一种简单直观的排序算法。它的工作原理是通过构建有序序列,对于未排序数据,在已排序序列中从后向前扫描,找到相应位置并插入。插入排序在实现上,通常采用in-place排序(即只需用到O(1)的额外空间的排序),因而在从后向前扫描过程中,需要反复把已排序元素逐步向后挪位,为最新元素提供插入空间。
步骤:
- 从第一个元素开始,该元素可以认为已经被排序
- 取出下一个元素,在已经排序的元素序列中从后向前扫描
- 如果该元素(已排序)大于新元素,将该元素移到下一位置
- 重复步骤3,直到找到已排序的元素小于或者等于新元素的位置
- 将新元素插入到该位置中
- 重复步骤2
排序效果:
(暂无)
详细过程:
7 希尔排序
介绍:
希尔排序,也称递减增量排序算法,是插入排序的一种高速而稳定的改进版本。
希尔排序是基于插入排序的以下两点性质而提出改进方法的:
- 插入排序在对几乎已经排好序的数据操作时, 效率高, 即可以达到线性排序的效率
- 但插入排序一般来说是低效的, 因为插入排序每次只能将数据移动一位
排序效果:
深入理解ob_flush和flush的区别和用法)
有关PHP的ob_flush()与flush()使用方法
注意:ob_flush()和flush()这两个函数一般要一起使用,顺序是先ob_flush(),然后flush(),它们的作用是刷新缓冲区。
这里具体的说下什么时候要用到刷新缓冲区和为什么要刷新缓冲区。
一、什么时候要刷新缓冲区
当程序中用到file_get_contents()和file_put_contens()这两个函数时,或程序中执行类似的“读写”功能或向浏览器执行输出操作时,会用到ob_flush()和flush()来刷新缓冲区。
二、为什么要刷新缓冲区
用file_get_contents()和file_put_content()为例进行讲解。
file_get_contents()和file_put_conents()这两个函数分别执行读取数据和写入数据操作,数据是先被读到内存中然后在写入文件中的,因为读取的速度比写入的速度要快,所以当你的数据被读完的时候不代表数据也写入完毕,这个时候多读的内容就会被暂时放到缓冲区中(内存),在这里需要强调一下,其实数据读取和写入是两个非常快的动作哦。
还用一种解释(当程序向浏览器执行输出操作时),个别web服务器程序,特别是Win32下的web服务器程序,在发送结果到浏览器之前,仍然会缓存脚本的输出,直到程序结束为止。如果你不想让程序执行完毕才向浏器输出,那么你也可以用到ob_flush()和flush()来刷新缓存。
其实,flush()还有一种用途,就是在没结束程序之前就进行输出,即一个循环还没结束就可以把部分结果输出到浏览器上,这个效果很类似 ajax的异步传输效果。
深入理解ob_flush和flush的区别
ob_flush/flush在手册中的描述, 都是刷新输出缓冲区, 并且还需要配套使用, 所以会导致很多人迷惑…
其实, 他们俩的操作对象不同, 有些情况下, flush根本不做什么事情..
ob_*系列函数, 是操作PHP本身的输出缓冲区.
所以, ob_flush是刷新PHP自身的缓冲区.
而flush, 严格来讲, 这个只有在PHP做为apache的Module(handler或者filter)安装的时候, 才有实际作用. 它是刷新WebServer(可以认为特指apache)的缓冲区.
在apache module的sapi下, flush会通过调用sapi_module的flush成员函数指针, 间接的调用apache的api: ap_rflush刷新apache的输出缓冲区, 当然手册中也说了, 有一些apache的其他模块, 可能会改变这个动作的结果..
有些Apache的模块,比如mod_gzip,可能自己进行输出缓存,这将导致flush()函数产生的结果不会立即被发送到客户端浏览器。
甚至浏览器也会在显示之前,缓存接收到的内容。例如 Netscape浏览器会在接受到换行或 html 标记的开头之前缓存内容,并且在接受到 </table> 标记之前,不会显示出整个表格。
一些版本的 Microsoft Internet Explorer 只有当接受到的256个字节以后才开始显示该页面,所以必须发送一些额外的空格来让这些浏览器显示页面内容所以, 正确使用俩者的顺序是. 先ob_flush, 然后flush,
当然, 在其他sapi下, 不调用flush也可以, 只不过为了保证你代码的可移植性, 建议配套使用.
buffer ---- flush()
buffer是一个内存地址空间,Linux系统默认大小一般为4096(1kb),即一个内存页。主要用于存储速度不同步的设备或者优先级不同的 设备之间传办理数据的区域。通过buffer,可以使进程这间的相互等待变少。这里说一个通俗一点的例子,你打开文本编辑器编辑一个文件的时候,你每输入 一个字符,操作系统并不会立即把这个字符直接写入到磁盘,而是先写入到buffer,当写满了一个buffer的时候,才会把buffer中的数据写入磁 盘,当然当调用内核函数flush()的时候,强制要求把buffer中的脏数据写回磁盘。
同样的道理,当执行echo,print的时候,输出并没有立即通过tcp传给客户端浏览器显示, 而是将数据写入php buffer。php output_buffering机制,意味在tcp buffer之前,建立了一新的队列,数据必须经过该队列。当一个php buffer写满的时候,脚本进程会将php buffer中的输出数据交给系统内核交由tcp传给浏览器显示。所以,数据会依次写到这几个地方echo/pring -> php buffer -> tcp buffer -> browser
php output_buffering --- ob_flush()
默认情况下,php buffer是开启的,而且该buffer默认值是4096,即1kb。你可以通过在php.ini配置文件中找到output_buffering配置.当echo,print等输出用户数据的时候,输出数据都会写入到php output_buffering中,直到output_buffering写满,会将这些数据通过tcp传送给浏览器显示。你也可以通过 ob_start()手动激活php output_buffering机制,使得即便输出超过了1kb数据,也不真的把数据交给tcp传给浏览器,因为ob_start()将php buffer空间设置到了足够大 。只有直到脚本结束,或者调用ob_end_flush函数,才会把数据发送给客户端浏览器。
这两个函数的使用怕是很多人最迷惑的一个问题,手册上对两个函数的解释也语焉不详,没有明确的指出它们的区别,似乎二者的功能都是刷新输出缓存。但在我们文章一开始的代码中如果讲fush()替换成ob_flush(),程序就再不能正确执行了。显然,它们是有区别的,否则也手册中直接说明其中一个是另外一个函数的别名即可了,没必要分别说明。那么它们的区别到底是什么呢?
在没有开启缓存时,脚本输出的内容都在服务器端处于等待输出的状态 ,flush()可以将等待输出的内容立即发送到客户端。
开启缓存后,脚本输出的内容存入了输出缓存中 ,这时没有处于等待输出状态的内容,你直接使用flush()不会向客户端发出任何内容。而 ob_flush()的作用就是将本来存在输出缓存中的内容取出来,设置为等待输出状态,但不会直接发送到客户端 ,这时你就需要先使用 ob_flush()再使用flush(),客户端才能立即获得脚本的输出。
一. flush和ob_flush的正确顺序,正确应是,先ob_flush再flush,如下:
ob_flush();
flush();
如果Web服务器的操作系统是windows系统,那顺序颠倒或者不使用ob_flush()也不会出现问题。[有待求证 ] 但是在Linux系统上就无法刷新输出缓冲。
output buffering函数
1.bool ob_start ([ callback $output_callback [, int $chunk_size [, bool $erase ]]] )
激活output_buffering机制。一旦激活,脚本输出不再直接出给浏览器,而是先暂时写入php buffer内存区域。
php默认开启output_buffering机制,只不过,通过调用ob_start()函数据output_buffering值扩展到足够 大 。也可以指定$chunk_size来指定output_buffering的值。$chunk_size默认值是0,表示直到脚本运行结束,php buffer中的数据才会发送到浏览器。如果你设置了$chunk_size的大小 ,则表示只要buffer中数据长度达到了该值,就会将buffer中 的数据发送给浏览器。
当然,你可以通过指定$ouput_callback,来处理buffer中的数据。比如函数ob_gzhandler,将buffer中的数据压缩后再传送给浏览器。
第三个参数:是否擦除缓存,可选,默认是true,如果设置为false,则在脚本执行结束前,缓存都不会被清除。
2.ob_get_contents
获取一份php buffer中的数据拷贝。值得注意的是,你应该在ob_end_clean()函数调用前调用该函数,否则ob_get_contents()返回一个空字符中。
可以使用ob_get_contents()以字符串形式获取服务端缓存的数据,
使用ob_end_flush()则会输出被缓存起来的数据,并关闭缓存。
而使用ob_end_clean()则会静默的清除服务端缓存的数据,而不会有任何数据或其他行为。
服务端的缓存是堆叠起来的,也就是说你在开启了ob_start()后,关闭之前,在其内部还 可以开启另外一个缓存ob_start()。
不过你也要务必保证关闭缓存的操作和开启缓存的操作数量一样多。
ob_start() 可以指定一个回调函数来处理缓存数据,如果一个ob_start()内部嵌套了另一个ob_start(),我们假定,外层的ob_start(),编号是A,内层的ob_start()编号是B,它们各自制定了一个回调函数分别是functionA和functionB,那么在缓存B中的数据输出时,它会先辈funcitonB回调函数处理,再交给外层的functionA回调函数处理,之后才能输出到客户端。
另外,手册说,对于某些web服务器,比如apache,在使用回调函数有可能会改变程序当前的工作目录,解决方法是在回调函数中自行手动把工作目录修改回来,用chdir函数,这点似乎不常遇到,遇到的时候记得去查手册吧。
3.ob_end_flush与ob_end_clean
这二个函数有点相似,都会关闭ouptu_buffering机制。但不同的是,ob_end_flush只是把php buffer中的数据冲(flush/send)到客户端浏览器,而ob_clean_clean将php bufeer中的数据清空(erase),但不发送给客户端浏览器。
ob_end_flush调用之前 ,php buffer中的数据依然存在,ob_get_contents()依然可以获取php buffer中的数据拷贝。
而ob_end_flush()调用之后 ob_get_contents()取到的是空字符串,同时浏览器也接收不到输出,即没有任何输出。
可以使用ob_get_contents()以字符串形式获取服务端缓存的数据,使用ob_end_flush()则会输出被缓存起来的数据,并关闭缓存。
而使用ob_end_clean()则会静默的清除服务端缓存的数据,而不会有任何数据或其他行为。
ob_start() 和 ob_end_flush() 是一对很好的搭档,可以实现对输出的控制。当成一对出现理解起来就没什么问题,但是当他们两个各自出现次数增加时,就比较难理解了.
<?php ob_start(); echo 'level 1<br/> '; ob_start(); echo 'level 2<br/> '; ob_start(); echo 'level 3<br/> '; ob_end_flush(); ob_end_flush(); ob_end_flush();
很明显,结果为:
level 1
level 2
level 3
当程序修改一下,修改一个ob_end_flush() 变成 ob_end_clean() 成为以下这个,你觉得结果会是怎样呢?附上这几个函数的讲解:
- ob_clean — 清空(擦掉)输出缓冲区
- ob_end_clean — 清空(擦除)缓冲区并关闭输出缓冲
- ob_end_flush — 冲刷出(送出)输出缓冲区内容并关闭缓冲
- ob_flush — 冲刷出(送出)输出缓冲区中的内容
- ob_start — 打开输出控制缓冲
<?php ob_start(); echo 'level 1<br/> '; ob_start(); echo 'level 2<br/> '; ob_start(); echo 'level 3<br/> '; ob_end_clean();//修改处 ob_end_flush(); ob_end_flush();
结果:
level 1
level 2
可能你会认为ob_end_clean()会清除与他最近的ob_start()的输出;其实这个说法不是很全面,看下面的例子
<?php ob_start(); echo 'level 1<br/> '; ob_start(); echo 'level 2<br/> '; ob_start(); echo 'level 3<br/> '; ob_end_clean(); //第一次修改 ob_end_flush(); ob_end_clean(); //第二次修改
这次,什么都没有输出来。
中间不是有一个ob_flush()吗?按理来说应该是输出 level2 的。
其实造成这样的主要原因是输出的多级缓冲机制。这个程序例子有三个ob_start(),就意味着他有3个缓冲区A,B,C,而其实php程序本身也有一个最终输出的缓冲区,我们就把他叫做F。
在这个程序中他这几个缓冲区是有一定层次的,C->B->A->F,F层次最高,是程序最终的输出缓冲,我们按上面的程序来进行讲解。
刚开始。 F:null
ob_start();
新建缓冲区A。 A: null -> F:null
echo 'level 1<br/> ';
程序有输出,输出进入最低的缓冲区A A: 'level 1<br/>' -> F:null
ob_start();
新建缓冲区B 。 B:null -> A: 'level 1<br/>' -> F:null
echo 'level 2<br/> ';
程序有输出,输出进入最低的缓冲区B B:'level 2<br/> ' -> A: 'level 1<br/>' ->F:null
ob_start();
新建缓冲区C C:null B:'level 2<br/> ' A: 'level 1<br/>' -> F:null
echo 'level 3<br/> ';
程序有输出,输出进入最低的缓冲区C C:'level 3<br/> ' -> B:'level 2<br/> ' -> A: 'level 1<br/>' -> F:null
ob_end_clean(); //第一次修改
缓冲区C被清空并关闭。 B:'level 2<br/> ' -> A: 'level 1<br/>' -> F:null
ob_end_flush();
缓冲区B输出到上一级的缓冲区A并关闭。 A: 'level 1<br/>level 2<br/> ' -> F:null
ob_end_clean(); //第二次修改
缓冲区A被清空并关闭。 此时缓冲区A的东西还没真正输出到最终的F中,因此也就整个程序也就没有任何的输出了。
ob其他的函数还有很多,但只要能懂得这些机理应该也是不难懂的。附上其余函数
- flush — 刷新输出缓冲
- ob_clean — 清空(擦掉)输出缓冲区
- ob_end_clean — 清空(擦除)缓冲区并关闭输出缓冲
- ob_end_flush — 冲刷出(送出)输出缓冲区内容并关闭缓冲
- ob_flush — 冲刷出(送出)输出缓冲区中的内容
- ob_get_clean — 得到当前缓冲区的内容并删除当前输出缓。
- ob_get_contents — 返回输出缓冲区的内容
- ob_get_flush — 刷出(送出)缓冲区内容,以字符串形式返回内容,并关闭输出缓冲区。
- ob_get_length — 返回输出缓冲区内容的长度
- ob_get_level — 返回输出缓冲机制的嵌套级别
- ob_get_status — 得到所有输出缓冲区的状态
- ob_gzhandler — 在ob_start中使用的用来压缩输出缓冲区中内容的回调函数。ob_start callback function to gzip output buffer
- ob_implicit_flush — 打开/关闭绝对刷送
- ob_list_handlers — 列出所有使用中的输出处理程序。
- ob_start — 打开输出控制缓冲
- output_add_rewrite_var — 添加URL重写器的值(Add URL rewriter values)
- output_reset_rewrite_vars — 重设URL重写器的值(Reset URL rewriter values)
如何获取(GET)一杯咖啡——星巴克REST案例分析
我们已习惯于在大型中间件平台(比如那些实现CORBA、Web服务协议栈和J2EE的平台)之上构建分布式系统了。在这篇文章里,我们将采取另一种做法:我们把支撑Web运行的协议和文档格式视为一种应用平台,一种可通过轻量级中间件访问的平台。我们通过一个简单的客户-服务交互的例子,展示了Web在应用集成中的作用。在这篇文章里,我们以Web为主要设计理念,提炼并分享了我们下本书《GET /connected - Web-based integration》(暂定名称)里的一些想法。
引言
我们知道,集成领域是不断变化的。Web的影响以及敏捷实践的潮流正在挑战我们的关于“良好的集成由什么构成”的观念。集成(integration)并不是一种夹在系统之间的专业活动;与此相反,现在,集成是成功方案里的不可缺少的一部分。
然而,仍有许多人误解并低估Web在企业计算中的作用。即便是那些精通Web的人士,也常常要花费很大力气才能懂得,Web不是关于支持XML over HTTP的中间件方案,也不是一种简易的RPC机制。这是相当遗憾的,因为Web不是仅能提供简单的点对点连接,它还有更大的用处;它实际上是一个健壮的集成平台。
在这篇文章里,我们将展示Web的一些值得关注的用途,我们将视之为一种可塑的、健壮的平台,它能够对企业系统做很“酷”的事。另外,工作流是企业软件最具代表性的特征。
为什么要工作流?
工作流(workflows)是企业计算的主要特征,它们基本上都是用中间件实现的(至少在计算方面)。工作流把一项工作(work)划分为多个离散的步骤(steps)以及触发步骤转移的事件(events)。工作流所实现的整个业务流程常常跨越若干企业信息系统,这给工作流带来很多集成问题。
星巴克:统一标准的咖啡需要统一标准的集成
Web若要成为可用于企业集成的技术,它就必须支持工作流——从而可靠地协调不同系统间的交互,以实现更大的业务能力。
要恰如其份地介绍工作流,就免不了讲述一大堆跟领域相关的技术细节,而这不是本文的主旨,因此,我们选择了Gregor Hohpe的星巴克工作流这个比较好理解的例子来举例说明基于Web的集成的工作原理。在这篇受到大家欢迎的博客文章里,Gregor讲述了星巴克是如何形成一个解耦合的(decoupled)盈利生产线的:
“跟大部分餐饮企业一样,星巴克也主要致力于将订单处理的吞吐量最大化。顾客订单越多,收入就越多。为此,他们采取了异步处理的办法。你在点单时,收银员取出一只咖啡杯,在上面作上记号表明你点的是什么,然后把这个杯子放到队列里去。这里的队列指的是在咖啡机前排成一列的咖啡杯。正是这个队列将收银员与咖啡师解耦开,从而,即便在咖啡师一时忙不过来的时候,收银员仍然可以为顾客点单。他们可以在繁忙时段安排多个咖啡师,就像竞争消费者模式(Competing Consumer)里那样。”
Gregor是采用EAI技术(如面向消息的中间件)来讲解星巴克案例的,而我们将采用Web资源(支持统一接口的可寻址实体)来讲解同一案例。实际上,我们将展示Web技术何以能够具有跟传统EAI工具一样的可靠性,以及何以不仅仅是请求/响应协议之上的XML消息传递!
首先,我们很抱歉擅自设想了星巴克的工作流程,因为我们的目的并不是精确无误地描述星巴克,而是用基于Web的服务来讲解工作流。好的,既然讲清楚了这一点,那么我们现在开始吧。
简明陈述
因为我们在讲工作流,所以我们有必要理解构成工作流的状态(states)以及将工作流从一个状态转移到另一个状态的事件(events)。我们的例子里有两个工作流,我们把它们用状态机(state machines)表达出来了。这两个工作流是并行执行的。一个反映了顾客与星巴克服务之间的交互(如图1),另一个刻画了由咖啡师执行的一系列动作(如图2)。
在顾客工作流里,顾客为了得到某种口味的咖啡而与星巴克服务进行交互。我们假定该工作流里包含以下动作:顾客点单,付款,然后等待饮品。在点单与付款之间,顾客通常可以修改菜单,比方说请求改用半脱脂牛奶。 /p>
图1 顾客状态机
尽管顾客看不见咖啡师,但咖啡师也有自己的状态机;这个状态机是服务实现私有的。如图2所示,咖啡师在周而复始地等待下一个订单,制作饮品,然后收取费用。当一个订单被加入到咖啡师的队列中时,一次循环实例就开始了。当咖啡师完成订单并把饮品交付给顾客时,工作流就结束了。
图2 咖啡师的状态机
尽管这些看似跟基于Web的集成毫不相干,但这两个状态机里的每一个状态迁移,都代表着与Web资源的一次交互。每一次迁移,就是通过URI对资源实施HTTP操作,从而导致状态的改变。
GET和HEAD属于特例,因为它们不引起状态迁移。它们的作用是用于查看资源的当前状态。
我们节奏稍快了点。理解状态机和Web,不是那么容易一口吃个胖子的。所以,让我们在Web的背景下,来从头回顾一下整个场景,逐步慢慢深入。
顾客视角
我们将从一张简单的故事卡片开始,它启动整个流程:
这个故事里涉及一些有用的角色与实体。首先,里面有“顾客(Customer)”角色。显然,它是(隐含的)星巴克服务(Starbucks Service)的消费者。其次,里面有两个重要的实体(“咖啡”和“订单”),以及一个重要的交互(“点单”)——我们的工作流正是由它启动的。
要把订单提交给星巴克,我们只要把订单的表示(representation)POST给下面这个众所周知的星巴克点单URI即可: http://starbucks.example.org/order
。
图3 点一杯咖啡
图3显示了向星巴克点单的交互过程。星巴克采用自己的XML格式来表达有关实体;需要关注的是,这个格式允许客户往里嵌入信息,以便进行点单——稍后我们会看到。实际提交的数据如图4所示。
在面向人类的Web(human Web)上,消费者和服务使用HTML作为表示格式(representation format)。HTML有自己特定的语义,所有浏览器都理解并接受这些语义,比如:代表“一个链接到其他文档或本文档内部某个书签的锚(anchor)”。消费者应用——浏览器——只是呈现HTML,状态机(也就是你!)用
GET
和POST
跟随链接。对于基于Web的集成也一样,只不过服务和消费者不仅要就交互协议达成一致,还要就表示的格式与语义统一意见。
图4 POST饮品订单
星巴克服务创建一个订单资源,然后把这个新资源的位置放在HTTP报头Location
里返回给消费者。为方便起见,服务还要把这个新创建的订单资源的表示(representation)也放在响应里。发给消费者的响应如下所示。
图5 创建好了订单,等待付款
201 Created
状态表明星巴克已经成功接受了订单。Location
报头给出了新创建订单的URI。响应主体里的表示(representation)包含了所点饮品及其价格。另外,这个表示里还包含另一个资源的URI——星巴克希望我们与这个URI交互,以完成顾客工作流;我们稍后将用到它。
注意,该URI是放在
标签、而不是标签里的。这里的
在顾客工作流里是具有特定含义的,其语义是事先定义好的。
我们已经知道
201 Created
状态代码表示“成功创建资源”的意思。对于这个例子以及一般的基于Web的集成,我们还需要其他一些有用的代码:
200 OK
——它的意思是:一切正常;继续执行。
201 Created
——我们刚刚创建了一个资源,一切正常。
202 Accepted
——服务已经接受了我们的请求,并请我们对Location响应报头里的URI进行轮询(poll)。这在异步处理中相当有用。
303 See Other
——我们需要跟另一个资源交互,应该不会出错。
400 Bad Request
——我们的请求格式有问题,应重新格式化后再提交。
404 Not Found
——服务因为偷懒(或者保密)没有告知请求失败的真实原因,但不管什么原因,我们都得应付它。
409 Conflict
——服务器拒绝了我们更新资源状态的请求。我们需要获取资源的当前状态(要么检查响应实体主体,要么做一次GET操作),然后再作打算。
412 Precondition Failed
——请求未被处理,因为Etag、If-Match或类似的“哨兵(guard)”报头的值不满足条件。我们需要考虑下一步怎么走。
417 Expectation Failed
——幸亏核查一下,服务器不将接受你的请求,所以别真正发送那个请求。
500 Internal Server Error
——最偷懒的响应。服务器出错了,而且什么原因都没说。祝你不要碰见它。
更新订单
星巴克很不错的一点就是,你可以按无数种不同的方式来定制自己的饮品。其实,考虑到某些高端客户极高的要求,也许让他们按化学公式来点单更好。但我们别那么贪心——至少开始的时候。我们来看另一张故事卡片:
回顾图4,显然我们在那里犯了一个错误:真正爱喝咖啡的人是不喜欢往浓咖啡里放太多热牛奶的。我们要改正那个问题。幸运地是,Web(或更确切地说,HTTP)以及我们的服务均为这样的改变提供了支持。
首先,我们要确认我们仍然可以修改订单。有时咖啡师动作很快,在我们想修改订单之前,他们就已经把咖啡做好了——于是,我们只有慢慢享用这杯热咖啡风味的牛奶了。不过,有时咖啡师会比较慢,这样我们就可以在订单得到咖啡师处理之前修改它了。为了知道我们是否还能修改订单,我们通过HTTP动词OPTIONS来向订单资源查询它接受哪些操作(如图6)。
请求 | 响应 |
OPTIONS /order/1234 HTTP 1.1 Host: starbucks.example.org |
200 OK Allow: GET, PUT |
图6 看看有哪些选择(OPTIONS)
从图6我们可以知道,订单资源既是可读的(支持GET)、也是可更新的(支持PUT)。作为好网民,我们可以拿我们的新表示来做一次试验性的PUT操作,在真正PUT之前先用Expect
报头来试一试(如图7)。
请求 | 响应 |
PUT /order/1234 HTTP 1.1 Host: starbucks.example.com Expect: 100-Continue |
100 Continue |
图7 看好再做(Look before you leap)
若我们不能修改订单了,那么对图7所示请求的响应将是417 Expectation Failed
。不过,假定我们现在得到的响应是100 Continue
,也就是说,我们可以用PUT
来更新订单资源(如图8)。用PUT
方法来提交更新后的资源表示(representation),实际上就相当于修改现有资源。在这个例子中,PUT请求里的新描述包含一个
元素,其中包含我们的更新,即外加一杯浓咖啡。
尽管部分更新(partial updates)属于REST社区里比较难懂的理念争论之一,但这里我们采取一种实用的做法,我们假定:增加一杯浓咖啡的请求,是在现有资源状态的上下文中被处理的。因此,我们没必要在网络上传送整个资源表示,我们只要传送变化的部分即可。
图8 更新资源状态
如果我们能够成功提交(PUT
)更新,那么我们会从服务器得到响应代码200
,如图9所示。
图9 成功更新资源状态
检查OPTIONS
和采用Expect
报头并不能令我们避免碰到“后续的修改请求失败”的情况。因此,我们并不强制使用它。作为好网民,我们会以某种方式来应付405
和409
响应。
OPTIONS和
Expect
报头的使用应当被视为可选步骤。
尽管我们明智地使用Expect
和OPTIONS
,但有时PUT
仍将失败;毕竟咖啡师也在一刻不停地工作——有时他们动作很敏捷!
若我们落后于咖啡师,我们在试图用PUT
操作把更新提交给资源时会被告知。图10显示的就是一个常见的更新失败的响应。409 Conflict
状态代码表明,若接受更新,将导致资源处于不一致的状态,所以没有进行更新。响应主体里显示出了我们试图PUT
的表示(representation)与服务端资源状态之间的差异。按咖啡制作的话说,加得太晚了——咖啡师已经把热牛奶倒进去了。
图10 慢了一步
我们已经讲述了使用Expect
和OPTIONS
来尽量防止竞争条件。除此以外,我们还可以给我们的PUT
请求加上If-Unmodified-Since
或If-Match
报头,以表达我们对服务的期望条件。If-Unmodified-Since
采用时间戳,而If-Match
采用原始订单的ETag1 。若订单状态自从被我们创建以来还没有改变过——也就是说,咖啡师还没有开始制作我们的咖啡——那么更新可以处理。若订单状态已经发生改变,那么我们会得到412 Precondition Failed
响应。虽然我们因为慢了咖啡师一步而只能享用牛奶咖啡,但至少我们没有把资源转移到不一致的状态。
用Web进行一致的状态更新可以采取很多种模式。HTTP PUT是幂等的(idempotent),这样我们在进行状态更新时就用不着处理一些复杂事务了,不过仍有一些选择需要我们决定。下面是正确进行状态更新的一些方法:
1. 通过发送
OPTIONS
请求,查询服务是否接受PUT
操作。这一步是可选的。它可以告知客户端,此刻服务器允许对该资源做哪些操作,不过这无法保证服务器将永远支持那些操作。2. 使用
If-Unmodified-Since
或If-Match
报头,以避免服务器执行不必要的PUT
操作。假如PUT
后来失败了,那么你会得到412 Precondition Failed
。此方法要求:要么资源是缓慢更新的,要么支持ETag;对于前者就用If-Unmodified-Since
,对于后者就用If-Match
。3. 立即用
PUT
操作提交更新,并应付可能出现的409 Conflict
响应。就算我们使用了(1)和(2),我们可能仍得应付这些响应,因为我们的“哨兵”和检查本质上都是乐观的。关于检测和处理不一致的更新,W3C有一个非规范性文档,该文档推荐采用ETag。ETags也是我们推荐采用的方法。
在完成那些更新咖啡订单的艰苦工作之后,按理说我们应当得到额外那杯浓咖啡了。所以我们现在假定已设法得到了额外那杯浓咖啡。当然,我们要付过款后星巴克才会把咖啡递给我们(其实他们也已经暗示过了!),所以我们还需要一张故事卡片:
还记得最初那个针对原始订单的响应吗?其中有个
元素。星巴克在订单资源的表示里面嵌入了有关另一个资源的信息。我们前面看过那个标签,但当时因为顾于修改订单就没有具体讲。现在我们应该进一步探讨它了:
关于next
元素,有几点是值得指出的。首先,它处于一个不同的名称空间之下,因为状态迁移并不是只有星巴克需要。在这里,我们决定把这种用于状态迁移的URI放在一个公共的名称空间里,以便于重用(或甚至最终的标准化)。
其次,rel
属性里嵌入了一则语义信息(你乐意的话,也可以称之为一种私有的微格式)。能够理解http://starbucks.example.org/payment
这串文字的消费者,可以使用由uri
属性标识的资源转移到工作流里的下一状态(付款)。
元素里的uri
指向的是一个付款资源。根据type
属性,我们已经知道预期的资源表示(representation)是XML格式的。我们可以向这个付款资源发送OPTIONS
请求,看看它支持哪些HTTP操作。
微格式(microformat)是一种在现有文档里嵌入结构化、语义丰富的数据的方式。微格式在人类可读的Web上相当常见,它们用于往网页里增加结构化信息(如日程表)的 表示(representations)。不过,它们同样也可以方便地被用于集成。微格式术语是在微格式社区里达成一致的,不过我们也可以自由创建自己的 私有微格式,用于特定领域的语义标记。
尽管它们看上去没多大用,但如图10里那样的简单链接正是REST社区所呼吁的“将超媒体作为应用状态的引擎(hypermedia as the engine of application state)”的关键。更简单地说,URI代表了状态机里的状态迁移。正如我们在文章开始时所看到的,客户端是通过跟随链接的方式来操作应用程序的状态 机的。
如果你一时不能理解,不要感到奇怪。这一模型的最不可思议之处在于:状态机和工作流不是像WS-BPEL或WS-CDL那样事先描述好的,而是在你 经历各个状态的过程中逐步得到描述的。不过,一旦你的想明白了,你就会发现,跟随链接(following links)这种方式使得我们可以在应用的各种状态下向前推进。每次状态迁移时,当前资源的表示里都包含了指向可能的下一状态的链接以及它们所代表的状 态。另外,由于这些代表下一状态的资源是Web资源,所以我们知道如何使用它们。
在顾客工作流里,我们下一步要做的是为咖啡付款。我们可以由订单里的
元素得知总金额,但在我们向星巴克付款之前,我们想向付款资源查询一下我们应当如何与之交互(如图11)。
消费者需要事先掌握多少关于一个服务的知识呢?我们已经说过了,服务和消费者在交互之前需要就它们将会交换的表示(representations)的语 义达成一致。可以将这些表示格式(representation formats)看成一组可能的状态和迁移。在消费者与服务交互时,服务选择可用的状态和迁移,并构造下一个表示。步向目标的过程是 动态发现的,而把这一过程中的各个部分串起来的方式是事先达成一致的。
在设计与开发过程中,消费者会就表示和迁移的语义与服务器达成一致。但谁也不能保证服务在其演化过程中会不会采用一种客户端预期之外的表示和迁移 (不过客户端还是知道如何处理它的)——那是Web松耦合的本质特性。尽管如此,在这些情况下就资源格式和表示达成一致超出了本文的范围。
我们下一步要做的是为咖啡付款。我们可以由订单表示的
元素得知总金额,所以我们要做的就是付款给星巴克,然后咖啡师把饮品交给我们。首先,我们向付款资源查询我们应当如何与之交互(如图11)。
请求 | 响应 |
OPTIONS/payment/order/1234 HTTP 1.1 Host: starbucks.example.com |
Allow: GET, PUT |
图11 获知如何付款
服务器返回的响应告诉我们,我们既可以读取付款(通过GET
)、也可以更新它(通过PUT
)。既然知道了金额,那么接下来,我们就把款项PUT
给那个由付款链接标识的资源。当然,付款金额属于秘密信息,所以我们将通过认证2来保护该资源。
请求 |
PUT /payment/order/1234 HTTP 1.1 |
响应 |
201 Created |
图12 付款
为成功完成付款,我们只需按图12进行交互即可。一旦经认证的PUT
返回一个201 Created
响应,我们就可以庆祝付款成功、并拿到我们的饮品了。
不过事情也有出错的时候。当资金处于危险状态时,我们希望要么没出错、要么可以挽救错误3。付款时可能出现很多种容易想象的出错情况:
- 由于服务器宕机或其他原因,我们无法连接上服务器了;
- 在交互过程中,与服务器的连接被切断了;
- 服务器返回一个
4xx
或5xx
范围的错误状态。
幸运地是,Web可以帮助我们应付以上这些情况。对前两种情况(假定连接问题是瞬间的),我们可以反复做PUT
请求,直至我们收到成功响应为止。如果前次PUT
操作已经得到了成功处理,那么我们将收到一个200
响应(本质上是一个来自服务器的空操作确认);如果本次PUT
操作成功完成了付款,那么我们将收到一个201
响应。在第三种情况中,如果服务器返回的响应代码是500
、503
或504
,那么也可以做同样处理。
4xx
范围的状态代码比较难处理,不过它们仍然指出了下一步怎么办。例如,400
响应表明我们通过PUT
请求提交的内容无法被服务器所理解,我们需要纠正后重新发送PUT
请求。403
响应则相反,它表明服务器能够理解我们的请求,但不知道如何履行(fulfil)它,而且服务器希望我们不要重试。对于这些情况,我们得在响应的有效负载(payload)里寻找其他的状态迁移(链接),换其他推进状态的路线。
在这个例子中,我们已经多次使用状态代码来指引客户端步向下一个交互了。状态代码是具有丰富语义的确认信息。让服务返回有意义状态代码,并且令客户 端懂得如何处理状态代码,这样一来,我们便给HTTP简单的请求响应机制增加了一层协调协议,从而提高了分布式系统的健壮性和可靠性。
一旦我们为自己的饮品买了单,我们这个工作流就算完成了,有关顾客的故事也就到此结束了。不过整个故事还没有完。现在我们进入到服务里面,看看星巴克的内部实现。
咖啡师视角
作为顾客,我们乐于把自己放在咖啡世界的中央,不过我们并不是咖啡服务的唯一消费者。从与咖啡师的“时间竞赛”中我们已经得知,咖啡服务还为包括咖啡师在内的其他一些相关方面提供服务。按照我们循序渐进的介绍方式,现在该推出另一张故事卡片了。
用Web的格式与协议来描述饮品列表是件很容易的事。用Atom提要(feeds)来表达列表之类的东西是相当不错的选择,它几乎可描述任何列表(比如未完成的咖啡订单),所以这里我们可以也采用它。咖啡师可以通过向该Atom提要的URI发送GET
请求来访问它,对于未完成的订单,URI是http://starbucks.example.org/orders
(如图13)。
图13 待制作饮品的Atom提要
星巴克是家相当繁忙的店,位于/orders
的Atom提要更新相当频繁,所以咖啡师要不断轮询这个提要才能保证掌握最新信息。轮询通常被认为可伸缩性很差;但是,Web支持可伸缩性极强的轮询机制——我们稍后会看到。另外,由于星巴克每分钟要制作很多咖啡,所以承受住负荷是个重要问题。
这里我们有两个相抵触的需求。一方面,我们希望咖啡师通过经常轮询订单提要,以不断掌握最新信息;另一方面,我们又不希望给服务增添负担、或者徒然增加网 络流量。为防止我们的服务因过载而崩溃,我们将在我们服务之外,用一个逆向代理(reverse proxy)来缓存并提供被频繁访问的资源表示(如图14所示)。
图14 通过缓存提升可伸缩性
对于大多数资源(尤其是那些会被很多人访问的资源,如返回饮品列表的Atom提要),在宿主服务之外缓存它们是合理的。这样可以降低服务器负 载,提升可伸缩性。我们在架构里增设了Web缓存(逆向代理),再加上有缓存元数据,这样客户端获取资源时就不会给原服务器增添很大负担了。
缓存的有利一面是,它屏蔽掉了服务器的间隙性故障,并通过提高资源可用率来帮助灾难恢复。也就是说,即便星巴克服务出现了故障,咖啡师仍然可以继续工 作,因为订单信息是被代理缓存起来的。而且,假如咖啡师漏了某个订单的话(错误),恢复也很容易进行,因为订单具有很高的可用率。
是的,缓存可以把旧订单多保留一段时间,但对于像星巴克这样吞吐量很高的商户而言,这是不太理想的。为了把太旧的订单从缓存中清除,星巴克服务用
Expires
报头来声明一个响应可以被缓存多久。任何介于消费者与服务之间的缓存都应当服从这一指示,拒绝提供过期订单4,而是把请求转发到星巴克服务上,以获取最新的订单信息。
图13所示的响应对Atom提要的Expires
报头进行了相应的设置,令饮品列表在10秒钟后过期。由于这 种缓存行为,服务器每分钟最多只要响应6次请求,其余请求将由缓存机制代劳。即便对于性能比较糟糕的服务,每分钟6个请求也属于容易处理的工作量了。在最 愉快的情况下(对星巴克服务来说),咖啡师的轮询请求是由本地缓存响应的,这样就不会给增加网络活动或服务器负荷了。
在我们的例子中,我们只设置了一个缓存来帮助提升主咖啡列表的可伸缩性。然而,在真实的基于Web的场景中,我们可以从多层缓存中受益。要在大规模环境中提升可伸缩性,利用现有Web缓存的优点是至关重要的。
Web以延迟换取了高度的可伸缩性。假如你的问题对延迟很敏感的话(比如外汇交易),那么就不太适合采用基于Web的方案了。但是,假如你可以接受“秒”数量级上的延迟,那么Web也许是个不错的平台。
既然我们已经成功解决了可伸缩性问题,那么我们继续来实现更多的功能。当咖啡师开始为你制作咖啡时,应当修改订单状态,以达到禁止更新的目的。从顾客的角度来看,这相当于我们无法再对我们的订单执行PUT
操作了(如图6、7、8、9、10所示)。
幸运地是,我们可以利用一个已经定义好的协议——Atom发布协议(Atom Publishing Protocol,简称APP或AtomPub)——来实现这一目标。AtomPub是一个以Web中心(基于URI)的协议,用于管理Atom提要里的 条目(entries)。我们来仔细看看Atom提要(/orders
)里代表咖啡的条目。
图15 咖啡订单对应的Atom条目
在图15所示的XML里,有几点值得注意。首先,它将我们的订单与Atom提要里的其他订单区分开了。其次,其中包含订单本身,即咖啡师制作咖啡所需的全部信息——包括我们要求增加一杯浓咖啡的重要信息!该订单对应的entry
元素里有个link
元素,它声明了本条目(entry
)的编辑URI(edit)。这个编辑URI指向的是一个可以通过HTTP编辑的订单资源。(这里,可编辑资源的地址刚好跟订单资源本身的地址一样,不过这不是必须的。)
如果咖啡师要锁定订单资源、禁止它被修改,就可以通过该编辑URI来改变订单资源的状态。具体地讲,咖啡师可以用PUT
请求把经修改的资源状态提交给这个编辑URI(如图16所示)。
图16 通过AtomPub设置订单状态
服务器一旦处理了如图16所示的PUT
请求,它就会拒绝对位于/orders/1234
的订单资源做除GET
以外的操作。
现在订单处于稳定状态了,咖啡师可以毫无顾虑地继续制作咖啡了。当然,咖啡师只有知道我们已经付过款才会把咖啡给我们,所以咖啡师还要查询我们是否已经完 成付款。在真实的星巴克里,情况会略有不同:一般来说,我们是点单后立即付款的;然后,其他顾客站在周围,以免你拿走别人点的饮品。但在我们计算机化的版 本里,增加这一检查并不麻烦,所以我们来看倒数第二张故事卡片:
咖啡师只要向付款资源(该资源的URI在订单表示里给出了)发送GET
请求,即可查询付款状态。
这里,顾客和咖啡师是通过订单表示里给出的链接得知付款资源的URI的。但有时,通过URI模版来访问资源也很方便。
URI模版(URI template)是一种描述知名URI的格式。它允许消费者通过修改URI里的部分字符来访问不同的资源。
Amazon的S3存储服务就是基于URI模版的。用户可以对由以下模版生成的URIs进行HTTP操作,从而对已保存的制品进行操作:
http://s3.amazonaws.com/{bucket_name}/{key_name}
。为方便咖啡师(或其他经授权的星巴克系统)不用遍历所有订单即可访问各个付款资源,我们可以在我们的模型里设计一个类似的URL模版方案:
http://starbucks.example.org/payment/order/{order_id}
。URI模版就像与消费者订立的契约,服务提供者须在服务演化过程中注意维持它们的稳定。由于这一潜在的耦合,有些Web集成工作者会有意避免采用URI模版。我们的建议是,仅当可推断的URIs(inferable URIs)很有帮助而且不会改变时才使用。
对于我们的例子,另一种办法是在
/payments
处暴露一个提要,用它提供包含指向各个付款资源的(不可推断的)链接。该提要只有经授权的系统才能读取。最终,URI模版是不是一个相对超媒体来说安全而有效的捷径,要由服务设计者来决定。我们的建议是:要保守地使用URI模版!
当然,不是人人都可以查看付款信息的。我们不想让咖啡社区里会动歪脑筋的人查看他人的信用卡详细信息,因此,跟其他敏感的Web系统一样,我们利用请求认证来保护敏感资源。
如有未认证的用户或系统试图获取一个具体的付款信息,那么服务器会质询(challenge)它、要求它提供证书。(如图17)
请求 | 响应 |
GET /payment/order/1234 HTTP 1.1 Host: starbucks.example.org |
401 Unauthorized WWW-Authenticate: Digest realm="starbucks.example.org", qop="auth", nonce="ab656...", opaque="b6a9..." |
图17 对付款资源的非授权访问受到质询
401
状态(及其认证元数据)告诉我们,我们应当在请求里附上正确的证书、然后重新发送请求。重新用正确的证书发送请求(图18)后,我们得到了付款信息,并将之与代表订单总金额的资源http://starbucks.example.org/total/order/1234
进行比较。
请求 | 响应 |
GET /payment/order/1234 HTTP 1.1 Host: starbucks.example.org Authorization: Digest username="barista joe" realm="starbucks.example.org“ nonce="..." uri="payment/order/1234" qop=auth nc=00000001 cnonce="..." reponse="..." opaque="..." |
200 OK |
图18 授权访问付款资源
一旦咖啡师制作好、交出咖啡并完成收款,他们就要在待处理饮品列表中删除相应的订单。如同前面一样,我们采用一个故事来讲解这个回合:
因为订单提要里的各个条目(entry
)都标识着一个可编辑资源,而且有自己的URI,所以我们可以对各个订单资源做HTTP操作。如图19所示,咖啡师只要对相关条目(entry
)所引用的资源做DELETE
操作即可将它从列表中删除。
请求 | 响应 |
DELETE /order/1234 HTTP 1.1 Host: starbucks.example.org |
200 OK |
图19 删除已完成的订单
在条目被删除(DELETE
)之后,再对订单提要做GET
操作的话,返回的表示里将不再包含已删除(DELETE
)的资源。假定我们的缓存工作正常、且我们已经设置了合理的缓存过期元数据的话,那么当你试图获取(GET
)那个订单条目时将直接得到404 Not Found
响应。
也许你已经注意到了,Atom发布协议可以满足我们对星巴克这个问题的大部分需求。如果我们想直接把位于/orders
的Atom提要暴露给顾客的话,顾客就可以用Atom发布协议来向该提要发布饮品订单、甚至修改订单了。
演化:Web上的现实情况
因为我们的咖啡店是基于自描述的状态机(state machines)构建起来的,所以我们可以方便地根据业务需要改造我们的工作流。例如,星巴克也许会提供一种免费的网上促销活动:
- 7月——我们的星巴克店开业,并提供标准的工作流以及我们前面提到的状态迁移和表示(representation)。消费者知道用这些格式与表示跟我们的服务进行交互。
- 8月——星巴克新推出了一种免费网上促销的表示(representation)。我们的咖啡工作流将进行更新,以包含指向该网上促销资源的链接。由于URI的特性,链接可以是指向第三方的——这跟指向星巴克内部的资源一样简单。
- 因为表示里仍然包含原来的迁移点,所以现有消费者仍然可以实现它们的目标,只不过它们可能无法享受促销而已,因为这部分还没有写进它们的代码里去。
- 9月——消费者应用和服务都进行了有关升级,以便能够理解并使用免费的网上促销。
成功进行演化的关键在于,服务的消费者们要能够预料到改变。在每一步,服务不是直接跟资源绑定(例如通过URI模版),而是提供指向具名资源(named resources)的URIs,以便消费者与之交互。这些具名资源,有些是消费者不认识的、将被忽略的,有些是消费者已知的、想采用的状态迁移点。不管 采用哪种方式,这种方案使得服务可以优雅地演化,同时还能维持与消费者兼容。
你将使用的是一个相当热门的技术
交付咖啡是我们工作流的最后一步。我们已经点了单、修改了订单(也可能无法修改)、付过款并最终拿到了我们的咖啡。在柜台另一侧,星巴克也已经同样完成了收款和订单处理。
我们可以用Web来描述所有必需的交互。我们可以利用现有的Web模型处理一些简单的不愉快的事(例如无法修改处理中或已处理完毕的订单),而不必 自己发明新的异常或错误处理机制——我们所需的一切都是HTTP现成提供的。而且,即便发生了那些不愉快的事,客户端仍然可以向它们的目标迈进。
HTTP提供的特性起初看来是无关紧要的。但这个协议现在已经取得广泛的一致、并得到广泛的部署了,而且所有的软件与硬件都能一定程度上理解它。当 我们看到其他分布式计算技术(如WS-*)处于割据状态的格局时,我们意识到了HTTP享有的巨大成功,以及它在系统间集成方面的潜力。
甚至在非功能性方面,Web也是有益的。在我们碰到临时故障时,HTTP操作(GET
、PUT
和DELETE
)的幂等性质令我们可以进行安全的重试;内在的缓存机制既屏蔽了故障,又有助于灾难恢复(通过增强的可用率);HTTPS和HTTP认证有助于基本的安全需求。
尽管我们的问题域是人为制造的,但我们所强调的技术同样可以应用于分布式计算环境。我们不会伪称Web很简单(除非你是天才),Web可以解决一切问题 (除非你是超级乐观的人,或受到REST信仰的感染),但事实上,在局部、企业级和Internet级进行系统集成,Web是个健壮的框架。
致谢
本文作者要向英国卡迪夫大学(Cardiff University)的Andrew Harrison表示感谢,是他启发了我们就Web上的“对话描述”进行讨论。
About the Authors
Jim Webber博士是ThoughtWorks公司的专业服务主管,他的工作是为全球客户进行可靠的分布式系统架构设计。此 前,Jim担任英国E-Science计划高级研究员,从事将Web服务实践及可靠面向服务计算的架构模式应用于网格计算的战略设计工作,他在Web及 Web服务架构与 开发方面具有广泛的经验。Jim还担任过惠普公司和Arjuna公司的架构师,他是业界首个Web服务事务方案的首席开发者。Jim是一位活跃的演说家, 他经常受邀出席 国际会议并发言。他还是一位活跃的作家,除了《Developing Enterprise Web Services - An Architect's Guide》这本书外,目前他正在撰写一本关于基于Web的集成的新书。Jim获得英国纽卡斯尔大学(University of Newcastle)的计算机科学学士学位和并行计算博士学位。他的博客地址是:http://jim.webber.name。
Savas Parastatidis是一位软件思想家,他的思考领域涉及系统和软件。他研究技术在eResearch里的运用,他尤其对云计算、知识表示与管理、社会网络感兴趣。他目前任职于微软研究院合作研究部。Savas喜欢在http://savas.parastatidis.name上写博客。
Ian Robinson帮助客户们创建可持续的面向服务的能力,令业务与IT从开始到实际运营始终保持齐合。他为微软公司写过关于采用微软技术实现面向服务系统的指南,还发表过文章讲述消费者驱动的服务契约及其在软件开发生命周期中的作用——该文章可以在《ThoughtWorks文集(The ThoughtWorks Anthology)》(Pragmatic Programmers,2008)及InfoQ中文站上找到。他经常在会议上做有关REST式企业开发及面向服务交付的测试驱动基础的讲演。
查看英文原文:How to GET a Cup of Coffee。
RESTful API 设计指南
网络应用程序,分为前端和后端两个部分。当前的发展趋势,就是前端设备层出不穷(手机、平板、桌面电脑、其他专用设备......)。
因此,必须有一种统一的机制,方便不同的前端设备与后端进行通信。这导致API构架的流行,甚至出现"API First"的设计思想。RESTful API是目前比较成熟的一套互联网应用程序的API设计理论。我以前写过一篇《理解RESTful架构》,探讨如何理解这个概念。
今天,我将介绍RESTful API的设计细节,探讨如何设计一套合理、好用的API。我的主要参考了两篇文章(1,2)。
一、协议
API与用户的通信协议,总是使用HTTPs协议。
二、域名
应该尽量将API部署在专用域名之下。
https://api.example.com
如果确定API很简单,不会有进一步扩展,可以考虑放在主域名下。
https://example.org/api/
三、版本(Versioning)
应该将API的版本号放入URL。
https://api.example.com/v1/
另一种做法是,将版本号放在HTTP头信息中,但不如放入URL方便和直观。Github采用这种做法。
四、路径(Endpoint)
路径又称"终点"(endpoint),表示API的具体网址。
在RESTful架构中,每个网址代表一种资源(resource),所以网址中不能有动词,只能有名词,而且所用的名词往往与数据库的表格名对应。一般来说,数据库中的表都是同种记录的"集合"(collection),所以API中的名词也应该使用复数。
举例来说,有一个API提供动物园(zoo)的信息,还包括各种动物和雇员的信息,则它的路径应该设计成下面这样。
- https://api.example.com/v1/zoos
- https://api.example.com/v1/animals
- https://api.example.com/v1/employees
五、HTTP动词
对于资源的具体操作类型,由HTTP动词表示。
常用的HTTP动词有下面五个(括号里是对应的SQL命令)。
- GET(SELECT):从服务器取出资源(一项或多项)。
- POST(CREATE):在服务器新建一个资源。
- PUT(UPDATE):在服务器更新资源(客户端提供改变后的完整资源)。
- PATCH(UPDATE):在服务器更新资源(客户端提供改变的属性)。
- DELETE(DELETE):从服务器删除资源。
还有两个不常用的HTTP动词。
- HEAD:获取资源的元数据。
- OPTIONS:获取信息,关于资源的哪些属性是客户端可以改变的。
下面是一些例子。
- GET /zoos:列出所有动物园
- POST /zoos:新建一个动物园
- GET /zoos/ID:获取某个指定动物园的信息
- PUT /zoos/ID:更新某个指定动物园的信息(提供该动物园的全部信息)
- PATCH /zoos/ID:更新某个指定动物园的信息(提供该动物园的部分信息)
- DELETE /zoos/ID:删除某个动物园
- GET /zoos/ID/animals:列出某个指定动物园的所有动物
- DELETE /zoos/ID/animals/ID:删除某个指定动物园的指定动物
六、过滤信息(Filtering)
如果记录数量很多,服务器不可能都将它们返回给用户。API应该提供参数,过滤返回结果。
下面是一些常见的参数。
- ?limit=10:指定返回记录的数量
- ?offset=10:指定返回记录的开始位置。
- ?page=2&per_page=100:指定第几页,以及每页的记录数。
- ?sortby=name&order=asc:指定返回结果按照哪个属性排序,以及排序顺序。
- ?animal_type_id=1:指定筛选条件
参数的设计允许存在冗余,即允许API路径和URL参数偶尔有重复。比如,GET /zoo/ID/animals 与 GET /animals?zoo_id=ID 的含义是相同的。
七、状态码(Status Codes)
服务器向用户返回的状态码和提示信息,常见的有以下一些(方括号中是该状态码对应的HTTP动词)。
- 200 OK - [GET]:服务器成功返回用户请求的数据,该操作是幂等的(Idempotent)。
- 201 CREATED - [POST/PUT/PATCH]:用户新建或修改数据成功。
- 202 Accepted - [*]:表示一个请求已经进入后台排队(异步任务)
- 204 NO CONTENT - [DELETE]:用户删除数据成功。
- 400 INVALID REQUEST - [POST/PUT/PATCH]:用户发出的请求有错误,服务器没有进行新建或修改数据的操作,该操作是幂等的。
- 401 Unauthorized - [*]:表示用户没有权限(令牌、用户名、密码错误)。
- 403 Forbidden - [*] 表示用户得到授权(与401错误相对),但是访问是被禁止的。
- 404 NOT FOUND - [*]:用户发出的请求针对的是不存在的记录,服务器没有进行操作,该操作是幂等的。
- 406 Not Acceptable - [GET]:用户请求的格式不可得(比如用户请求JSON格式,但是只有XML格式)。
- 410 Gone -[GET]:用户请求的资源被永久删除,且不会再得到的。
- 422 Unprocesable entity - [POST/PUT/PATCH] 当创建一个对象时,发生一个验证错误。
- 500 INTERNAL SERVER ERROR - [*]:服务器发生错误,用户将无法判断发出的请求是否成功。
状态码的完全列表参见这里。
八、错误处理(Error handling)
如果状态码是4xx,就应该向用户返回出错信息。一般来说,返回的信息中将error作为键名,出错信息作为键值即可。
{ error: "Invalid API key" }
九、返回结果
针对不同操作,服务器向用户返回的结果应该符合以下规范。
- GET /collection:返回资源对象的列表(数组)
- GET /collection/resource:返回单个资源对象
- POST /collection:返回新生成的资源对象
- PUT /collection/resource:返回完整的资源对象
- PATCH /collection/resource:返回完整的资源对象
- DELETE /collection/resource:返回一个空文档
十、Hypermedia API
RESTful API最好做到Hypermedia,即返回结果中提供链接,连向其他API方法,使得用户不查文档,也知道下一步应该做什么。
比如,当用户向api.example.com的根目录发出请求,会得到这样一个文档。
{"link": { "rel": "collection https://www.example.com/zoos", "href": "https://api.example.com/zoos", "title": "List of zoos", "type": "application/vnd.yourformat+json" }}
上面代码表示,文档中有一个link属性,用户读取这个属性就知道下一步该调用什么API了。rel表示这个API与当前网址的关系(collection关系,并给出该collection的网址),href表示API的路径,title表示API的标题,type表示返回类型。
Hypermedia API的设计被称为HATEOAS。Github的API就是这种设计,访问api.github.com会得到一个所有可用API的网址列表。
{ "current_user_url": "https://api.github.com/user", "authorizations_url": "https://api.github.com/authorizations", // ... }
从上面可以看到,如果想获取当前用户的信息,应该去访问api.github.com/user,然后就得到了下面结果。
{ "message": "Requires authentication", "documentation_url": "https://developer.github.com/v3" }
上面代码表示,服务器给出了提示信息,以及文档的网址。
十一、其他
(1)API的身份认证应该使用OAuth 2.0框架。
(2)服务器返回的数据格式,应该尽量使用JSON,避免使用XML。
(完)
Linux下如何使用命令同步时钟
linux的系统时钟在很多地方都要用到,要是不准,就会出现一些奇怪的问题;
在Linux中,用于时钟查看和设置的命令主要有date、hwclock和clock。Linux时钟分为系统时钟(System Clock)和硬件(Real Time Clock,简称RTC)时钟。系统时钟: 是指当前Linux Kernel中的时钟,硬件时钟: 是主板上由电池供电的时钟,这个硬件时钟可以在BIOS中进行设置。
当Linux启动时,硬件时钟会去读取系统时钟的设置,然后系统时钟就会独立于硬件运作。
Linux 中的所有命令(包括函数)都是采用的系统时钟设置。在Linux中,用于时钟查看和设置的命令主要有date、hwclock和clock。其中,clock和hwclock用法相近,只用一个就行,只不过clock命令除了支持x86硬件体系外,还支持Alpha硬件体系。
1、 date
查看系统时间
# date
设置系统时间
# date –set “07/07/06 10:19″ //(月/日/年时:分:秒)
2、hwclock/clock
查看硬件时间
# hwclock –show //或者
# clock –show
设置硬件时间
# hwclock –set –date=”07/07/06 10:19″ (月/日/年 时:分:秒) 或者
# clock –set –date=”07/07/06 10:19″ (月/日/年 时:分:秒)
3、硬件时间和系统时间的同步
按照前面的说法,重新启动系统,硬件时间会读取系统时间,实现同步,
但是在不重新启动的时候,需要用hwclock或clock命令实现同步。
硬件时钟与系统时钟同步:
# hwclock –hctosys // (hc代表硬件时间,sys代表系统时间)或者
# clock –hctosys
系统时钟和硬件时钟同步:
# hwclock –systohc // 或者
# clock –systohc
4. 和外部的NTP时间服务器同步
$ service ntpd stop
这一步是必须的,否则出出现:
25 Nov 18:10:34 ntpdate[2106]: the NTP socket is in use, exiting
的失败提示;
$ ntpdate ntp.sjtu.edu.cn
正常返回如下:
25 Nov 18:14:34 ntpdate[2164]: adjust time server 202.120.2.101 offset -0.006107 sec
错误返回如:
25 Nov 18:13:44 ntpdate[2158]: no server suitable for synchronization found
$ service ntpd start
$ chkconfig ntpd on
$ clock -w
还可以写进定时任务中,以做定时的时钟同步:
$ crontab -e
05 * * * * /usr/sbin/ntpdate ntp.sjtu.edu.cn 》 /dev/null 2》&1
05 17 * * * /sbin/clock -w
附上中国大概能用的NTP时间服务器地址
server 133.100.11.8 prefer
server 210.72.145.44
server 203.117.180.36
server 131.107.1.10
server time.asia.apple.com
server 64.236.96.53
server 130.149.17.21
server 66.92.68.246
server www.freebsd.org
server 18.145.0.30
server clock.via.net
server 137.92.140.80
server 133.100.9.2
server 128.118.46.3
server ntp.nasa.gov
server 129.7.1.66
server ntp-sop.inria.frserver 210.72.145.44(中国国家授时中心服务器IP地址)
server ntp.sjtu.edu.cn(上海交通大学网络中心NTP服务器地址)
上面就是使用命令同步Linux时钟的方法介绍了,一般使用data、hwclock和clock命令,而data命令是比较常用的命令,如果你的系统时钟不同步,那就赶紧改过来吧。