使用统一接口
HTTP是一种应用层协议,它定义了客户端与服务器之间的转移操作的表述形式。在此协议中,诸如GET,POST和DELETE之类的方法是对资源的操作。有了它,无须创造createOrder,getOrder,updateOrder等应用程序特定的操作了。 作为应用协议,HTTP的设计目标是在客户端和服务器之间保持对库、服务器、代理、缓存和其他工具的可见性。可见性是HTTP的一个核心特征。 一旦识别并设计资源,就可以使用GET方法获取资源的表述,使用PUT方法更新资源,使用DELETE方法删除资源,以及使用POST方法执行各种不安全和非幂等的操作。可以添加适当的HTTP标头来描述请求和相应。 以下特性完全取决于保持请求和相应的可见性:
- 缓存:缓存响应内容,并在资源修改时使缓存自动失效。
- 乐观并发控制:检测并发写入,并在操作过期的表述时防止资源发生变更。
- 内容协商:在给定资源的多个可用表述中,选择合适的表述。
- 安全性和幂等性:确保客户端可以重复或重试特定的HTTP请求。
HTTP通过以下途径来实现可见性:
- HTTP的交互是无状态的,任何HTTP中介都可以推断出给定请求和响应的意义,而无须关联过去和将来的请求和响应。
- HTTP使用一个统一接口,包括有OPTIONS,GET,HEAD,POST,DELETE和TRACE方法。接口中的每一个方法操作一个且仅一个资源。每个方法的语法和含义不会因应用程序和资源的不同而发生改变。
- HTTP使用一种与MIME类似的信封格式进行表述编码。这种格式明确区分标头和内容。标头是可见的,除了创建、处理消息的部分,软件的其他部分都可以不用关心消息的内容。
保持可见性的另一方面是使用适当的状态码和状态消息,以便代理、缓冲和客户端可以决定请求的结果。 在某些情况下,可能需要权衡其他特性,如网络效率、客户端的便利性以及分离关注点,为此放弃可见性。当进行这种权衡时,应仔细分析对缓存、幂等性、安全性等特性的影响。 当有多个共享数据的资源,或一个操作修改多个资源时,需要权衡是否降低可见性(例如是否禁止缓存)以便获得更好的信息抽象、更松散的耦合程度、更好地网络效率、更好地资源粒度,或纯粹为了方便客户端使用。 可以通过带有应用程序状态的URI链接来保持应用程序状态而无需依赖服务器中内存中的会话。 安全性和幂等性是服务器要实现的HTTP方法的特征。当客户端发送GET、HEAD、OPTIONS、PUT或DELETE请求时,如果没有使用并发条件限制时,确保服务器提供相同响应。
|方法|是否安全?|是否幂等? |—– |GET|是|是 |HEAD|是|是 |OPTIONS|是|是 |PUT|否|是 |DELETE|否|是 |POST|否|否
客户端通过下列方法实现幂等的/安全的HTTP请求:
- 将GET、OPTIONS和HEAD视为只读操作,可按需随时可发送请求。
- 在网络或软件异常的情况下,通过If-Unmodified-Since/If-Match条件标头重发GET、PUT和DELETE请求。
- 不要重发POST请求,除非客户端(通过服务器文档)知道对特定资源的POST实现是幂等的。
Web基础设施严重依赖于GET方法的幂等性和安全性。客户端期望能够重复发起GET请求,而不必担心造成副作用。缓存依赖于不需访问源服务器就能提供已缓存表述的能力。 不要把GET方法用于不安全和非幂等操作。因为这样做可能造成永久性的、意想不到的、不符合需要的资源改变。 可以使用POST方法或PUT方法创建新资源。只有在客户端可以决定资源的URI时才使用PUT方法创建新资源;否则使用POST,由服务器决定新创建资源的URI(客户端请求可以使用Slug头建议新资源的URI)。 在以下场合中使用POST方法:
- 创建新的资源,把资源作为一个工厂
- 通过一个控制器资源来修改一个或多个资源
- 执行需要大数据输入的查询
- 在其他HTTP方法看上去不合适时,执行不安全或非幂等的操作。(缓存不会缓存这一方法的响应)
使用POST方式实现异步任务:服务器在接受到POST请求时,返回状态码202(Accepted),并包含一个让客户端可以跟踪异步任务状态的资源表述和客户端稍后检查状态的建议时间(ping-after)。 客户端使用GET请求查询异步任务状态,如服务器还在执行中,返回响应码200(OK)及包含当前状态的任务资源表述;如服务器成功完成,返回响应码303(SeeOther)以及包含新资源URL的Location头;如服务器任务失败,返回响应码200(OK)及任务失败的表述。 使用DELETE方法实现异步请求:服务器在收到DELETE请求,返回状态码202(Accepted),并包含一个让客户端可以跟踪异步任务状态的资源表述和客户端稍后检查状态的建议时间(ping-after)。 客户端使用GET请求查询异步任务状态,服务器返回响应码200(OK)及包含当前状态的任务资源表述。 避免使用非标准的自定义HTTP方法。当前比较有名的自定义方法包括WebDAV定义的方法、PATCH和MERGE。 HTTP服务器可能会使用自定义HTTP标头,比较有名的自定义HTTP包括X-Powered-By、X-Cache、X-Pingback、X-Forwarded-For及X-HTTP-Method-Override。实现客户端和服务器时,要让他们在没有发现需要的自定义标头时也不会失败。避免使用自定义HTTP标头改变HTTP方法的行为。
识别资源
从领域名词中识别资源。 直接将领域实体映射为资源可能导致资源效率低下且难以使用,可以通过网络效率、表述的多少以及客户端的应用程度来帮助确定资源的粒度。 粗粒度设计便于富客户端应用程序,更精细的资源颗粒可以更好地忙族缓存的要求。因此,应从客户端和网络的角度确定资源的粒度。下列原书可能会进一步影响资源粒度:
- 可缓存性
- 修改频率
- 可变性
仔细设计资源粒度,以确保使用更多缓存,减少修改频率,或将不可变数据从使用缓存较少、修改频率更高或可变数据分离出来,这样可以改善客户端和服务器端的效率。 基于应用程序特有的条件来识别相似的资源(例如共享同一数据库schema的资源,有相同特性或属性的资源),可以将这些有共性的资源组织成为集合。 基于客户端的使用模式、性能和延时要求,确定一些新的聚合其他资源的复合资源,来减少客户端与服务器的交互。 符合资源降低了统一接口的可见性,应为它们的表述中包含了和其他资源相重叠的资源。因此,在提供复合资源前,需要考虑一下几点:
- 如果在应用程序的请求很少,那么它可能不是一个好的选择。依赖缓存代理,从缓存中获取这些资源,也许能让客户端收益匪浅。
- 另一个因素是网络开销–客户端与服务器之间的网络开销,服务区和后端服务或他所依赖的数据存储之间的网络开销。如果后者开销很大,那获取大量数据并在服务器上将他们组合成复合资源可能会增加客户端的延时,降低服务器的吞吐量。
- 想要改善延时,可以在客户端和服务器之间增加一个缓存层,并避免复合资源,进行一些负载测试来验证复合资源是否能起到改善作用。
最后,为每个客户端创建特定目标的复合资源并非是注重实效的做法。选择对Web服务最重要的客户端,设计复合资源来满足它们的需要。 像计算两地距离、行车路线、信用卡验证之类的计算或处理函数可被当作资源处理,并使用带有查询参数的HTTP GET获取函数输出表述。 当需要原子性修改多个资源时,可以为每个不同的操作指派一个控制器。客户端通过HTTP POST方法提交请求触发操作。如果操作结果是创建一个新资源,返回响应码201(Created)并在Location头里包含新资源的URL。如果操作结果是对一个或多个已有资源的修改,返回响应码303(See Other)并在Location头里包含客户端可用户获取修改表述的URL。如果服务器无法提供所有修改资源的单个URI,返回状态码200(OK)并在消息体内包含客户选可以用于了解操作结果的表述。 在RESTful Web服务中,控制器有助于对服务器和客户端之间进行关注分离,增进网络效率,让服务器端原子性地实现复杂操作。
设计表述
在HTTP设计中,发送发可以用一些名为实体头的标头来描述表述正文(也成为实体正文或消息正文)。有了这些标头,接收方可能在无须查看正文的情况下决定如何处理正文,还可以将解析正文所需要提前了解及猜测的内容尖刀最小程度。 使用以下标头来注解包含消息正文的表述:
标头 | 用途 | 解析处理 | ||||||||||||||||||||||||||||||
---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
Content-Type | 用于描述表述类型,即通常所说的media-type或MIME类型,包含charset参数或其他针对该媒体类型而定义的参数。
这个标头告诉接收方如何解析数据。例如,如果标头是application/xml或其他以+xml结尾的值,就可以用XML解析器来解析消息。如果是application/json,就可以用JSON解析器。没有此标头时,就只能解析正文的格式了。 | 当收到一个不太Content-Type的表述,避免猜测表述的类型。当客户端发送不带该标头的请求时,返回错误码400(Bad Request)。当从服务器接收到一个不带标头的响应时,将其视为不正确的响应。 | ||||||||||||||||||||||||||||||
Content-Length | 最早从HTTP 1.0中被引入,用于指定表述正文的大小。 发送方需要在写正文前计算出表述的大小并设置该标头,接收方用它来判断自己是否从连接中读取了正确的字节数。 HTTP 1.1支持一种名为分块传输编码的更有效机制,这让Content-Length头变的有点多余。如果客户端不支持HTTP 1.1,需要包含Content-Length。此外对于POST和PUT请求就算使用分块传输编码,也要在客户端应用程序的请求中同时包含Content-Length头,因为有些代理会拒绝缺失Content-Length和Transfer-Encoding: chuncked的POST和PUT请求。 HTTP/1.1 200 OK Last-Modified: Thu, 02 Apr 2009 02:32:28 GMT Content-Type: application/xml;charset=UTF-8 Transfer-Encoding: chunked FF [some bytes here] | 在没有确定接收到表述不带Transfer-Encoding: chunked前,不要检查Content-Length头是否存在。 | ||||||||||||||||||||||||||||||
Content-Language | 如果使用某种语言对表述进行本地化,使用该标头来指定语言。值是两个字母的RFC5646语言标签,还可以在谋面带上连字符(-)和任意两个字母的国家代码。如en-US或kr。 | 如果存在该标头,读取并存储它的值,记录下使用的语言。 | ||||||||||||||||||||||||||||||
Content-MD5 | 工具/软件在处理或存储表述时可能存在错误,需要提供一致性校验来验证实体正文的完整性,用该标头的值是表述正文(在进行内容压缩编码之后,分块传输编码之间计算)的MD5摘要。请注意,TCP使用checksum在传输层提供一致性校验,因此此标头对非可靠网络发送或接受大的表述时非常有用。 | |||||||||||||||||||||||||||||||
Content-Encoding | 当使用gzip、compress或deflate对表述正文进行编码时,使用该标头。接收方在解析正文前需要先解压缩消息。客户端可以用Accept-Encoding头来表明自己偏好的Content-Encoding。然而,并没有一个标准的方式让客户端了解到服务器是否可以处理用给定编码压缩过的表述。 | 让网络库代码来解压这些压缩过的表述。 | ||||||||||||||||||||||||||||||
Last-Modified | 仅用在响应上的标头,值是一个时间戳,表示服务器最后修改表述或资源的时间。 |
大多数情况下,客户端应用程序只需检查Content-Type头和字符编码,以此决定如何解析表述的正文。 一定要基于Content-Type、Content-Language和Content-Encoding头的值来处理响应的表述。 在发送表述时,如果媒体类型允许使用charset参数,则包含一个带字符编码值的charset参数,该参数值将被用于将字符转为字节。当接收到一个表述时,如果带有支持charset参数的媒体类型,使用其指定编码将表述正文的字节构造成字符流;如果收到一个不带charset参数的XML、JSON或HTML表述,让解析器通过相应格式规范的算法检查头几个字节来确定字符集。 JSON媒体类型application/json不指定charset参数,而是使用UTF-8作为默认编码。 另一个引入字符编码不匹配的常见途径是在XML表述的Content-Type头中给定一个编码,正文却又给定另一个编码。这时需要使用charset参数而不是中文中的编码。 还要比避免对XML格式的表述使用text/xml媒体类型,text/xml的默认字符是us-ascii,而application/xml使用UTF-8。 如果需要设计新的格式和媒体类型,考虑以下指导方针:
- 如果媒体类型是基于XML的,使用+xml结尾的子类型。
- 如果媒体类型是私有的,使用vnd.开头的子类型,例如application/vnd.example.org.user+xml。
- 如果打算使媒体类型公共,按照RFC 4288向IANA注册新的媒体类型。
尽管自定义的媒体类型能改善协议级可见性,但现有的用于监控、国旅、路由HTTP流量的协议级工具可能不太关注,甚至不关注媒体类型。因此,没有必要仅仅为了协议层面的可见性而使用自定义媒体类型。 在XML(JSON)表述中,包含一个指向资源自身的self链接。对于那些组成资源的应用程序领域实体,在表述中包含它们的标识符。如果表述中的某个部分包含自然语言文本,添加xml:lang属性(增加一个属性),表示元素的内容用的是本地化语言。 在集合表述中包含以下内容:
- 一个指向集合资源的self链接。
- 如果集合是分页的,包含指向可能的上一页和下一页的链接。
设计集合的表述,以使集合成员在结构和语法上类似(同构)。 除了文本对最终用户的表述有意义之外,避免使用语言、区域或国家特定的格式或格式识别符。而是使用下列可移植的格式:
- 使用W3C XML模式中定义的decimal、float和double数据类型格式化包含汇率的数字。
- 使用ISO 3166中的国际和地区代码
- 使用ISO 4217中的货币代码
- 使用RFC 3339中的日期和时间值
- 使用BCP 47的语言标识符标签
- 使用Olson时区信息数据库中的时区标识符
对于RESTful Web服务,URI是资源的唯一标识符。然而,应用程序代码经常必须处理领域实体标识符(比如数据库中的ID)。应用程序领域实体标识符可作为资源表述中的URN加入。 有些表述可能需要在文本表述中包含二进制数据,可以使用如下的多部分媒体类型。避免对文本格式内的二进制数据使用Base64编码。
|媒体类型|用途 |—– |multipart/form-data|对数据的名值对及混合多部分任意媒体类数据进行编码,用于通过HTML表单上传文件。 |multipart/mixed|对多部分任意媒体类型进行打包。例如作为application/xml的视频元数据和作为video/mpeg的视频二进制数据被组合进入一个单独的HTTP消息内。 |multipart/alternative|当对相同资源使用不同媒体类型传送替代表述时使用。例如使用text/plain的普通文本和text/html的HTML格式发送邮件 |multipart/related|当各部分互相挂念并需要一同处理时使用。第一个部分是首部分,且通过Content-ID头引用其他部分。
下例展示用户上传file1.txt和file2.gif
Content-type: multipart/form-data, boundary=AaB03x
--AaB03x
content-disposition: form-data; name="field1"
Joe Blow
--AaB03x
content-disposition: form-data; name="pics"
Content-type: multipart/mixed, boundary=BbC04y
--BbC04y
Content-disposition: attachment; filename="file1.txt"
Content-Type: text/plain
... file1.txt 的内容...
--BbC04y
Content-disposition: attachment; filename="file2.gif"
Content-type: image/gif
Content-Transfer-Encoding: binary
... file2.gif的内容...
--BbC04y--
--AaB03x--
对于那些希望能被最终用户使用的资源,应该为他们提供HTML表述。避免为机器客户端设计HTML表述。以HTML文档的相识提供部分或全部表述时,考虑使用为格式或RDFa来注解HTML,可以让Web爬虫和同类软件从HTML文档中提取信息,而无须依赖文档的结构。 对于那些由客户端输入造成的错误,返回带4xx状态码的表述。对那些由于服务器实现或其当前状态造成的错误,则返回带5xx状态码的表述。这两种情况下,都要包含一个Date头,以表示错误发生时间。 除非请求的方法是HEAD,否则都应该在表述中包含一段正文,使用内容协商或是和阅读的HTML或村文本对其进行格式化和本地化。 如果能以独立的、适合阅读的文档形式来提供纠正或调试错误的信息,就包含一个指向该文档的链接,可以使用Link头,也可以使用正文中的链接。 如果为了后期最终或分析,在服务器上记录了错误日志,应该提供一个可以找到该错误的标识符或链接。 响应正文要具有描述性,但不应该包含注入错误堆栈、数据库连接错误之类的详细信息。如果可以的话,说明客户端可以采取的后续措施。
|错误码|描述|客户端解决方案 |—– |400(Bad Request)|当服务器由于语法错误无法解读请求时,返回该错误码。|查看错误表述的正文,了解问题的根本原因。 |401(Unauthorized)|当客户端无权访问资源,但在身份验证后可以获得访问权限时返回错误码。如果服务器就算是在身份验证后也不允许客户访问资源,那应该返回403(Forbidden)错误码。返回该错误码时,应该包含一个带有身份验证方法的WWW-Authenticate头。通常使用的方法是Basic和Digest。|如果客户端是面向用户的,提示用户提供身份信息。其他情况下,获取必要的安全身份信息。用带有Authorization头的请求进行重试,其中包含了身份信息。 |403(Forbidden)|当服务器不让客户端获得资源的访问权限,就算通过身份验证也没用时,返回该错误码。|这个错误意味着禁止客户端用这个请求方法来访问资源,不要重复引起该错误的请求。 |404(Not Found)|当没有找到资源时返回该错误码。如有可能,在消息体中说明原因。|资源在服务器端已经不存在了。如果客户端保存了资源的数据,清楚数据或将其标记为已删除。 |405(Not Allowed)|当资源不允许使用某个HTTP方法时返回该错误码。返回一个Allow头,其中带有该资源的有效HTTP方法。|查看Allow头来寻找适用于该资源的方法,然后做适当的代码变更,只用那些方法来访问资源。 |406(Not Acceptable)|内容协商失败|调整Accept-*头 |409(Conflict)|当请求与资源的当前状态有冲突时返回该错误码,并包含一段正文解释原因。|查看PUT的表述正文中列出的冲突。 |410(Gone)|资源以前存在,但今后不会再存在,返回该错误码。除非记录了被删除的资源,否则不能返回这个错误码。如果没有在服务器记录被删除的资源,应该用404(Not Found)取代。|将其等同于404(Not Found)。 |412(Precondition Failed)|条件请求失败|HTTP1.1允许客户端修改到期缓存、使用包含Cache-Control:no-Cache和Pragma:no-cache的无条件GET请求获取新鲜表述。之后进行后继处理。 |413(Request Entity Too Large)|当POST或PUT请求的消息体过大时返回该错误码。如有可能,在正文中说明允许哪些内容,提供一个备选方案。|在响应正文里寻找有效长度的提示。 |414(Unsupported Media Type)|当客户端用一种服务器不理解的格式来发送消息体时返回该错误。|在响应正文里了解请求支持的媒体类型。 |500(Internal Server Error)|由于某些实现上的问题,代码在服务器端失败时返回该错误码是最好的选择。|记录该错误日志,随后通知服务器开发者。 |503(Service Unavailable)|服务器在某个特定间隔或一段不确定的时间内无法完成请求时,返回该错误码。抛出该错误的两个常见场合是后端服务器失败(例如数据库连接失败)或是客户端请求达到了某个服务器设定的频率上限。如有可能,包含一个带日期或秒数的Retry-After响应头,用它的值来做提示。|如果响应中有Retry-After头,在到达时间之后进行重试。
设计URI
在设计URI时遵循常用惯例具有下列优势:
- 遵循惯例的URI一般容易调试和管理。
- 服务器可以集中从请求URI中提取数据的代码。
- 可以避免花费宝贵的设计与实现时间来发明处理URI的新惯例和规则。
- 通过跨域、子域和路径来对服务器的URI进行分区,以实现负载分配、监控、路由和安全方面的操作灵活性。
针对本地化、分布式、强化多种监控及安全策略等方面的需求,可以使用域及子域对资源进行合理的分组或划分。(例如基于本地化或客户端进行子域划分)
- 在URI的路径部分使用斜杠分隔符(/)来表示资源之间的层次关系。
- 在URI的路径部分使用逗号(,)和分号(;)来表示非层次元素。分号通常用于表示矩阵参数。
- 使用连字符(-)和下划线(_)来改善长路径中名称的可读性。
- 在URI的查询部分使用“与”符号(&)来分隔参数。
- 在URI中避免出现文件扩展名(例如.php、.aspx和.jsp)。
- 慎用空格和大写字符,以免造成问题。空格是有效的URI字符,但是RFC 3986会将其编码为%20,而applicaton/x-www-form-urlencoded媒体类型会将其编码为+。RFC 3986定义URI除了协议和主机之外的其他部分是大小写敏感,而基于Windows的Web服务器却大小写的影响。
将URI视为不透明的标识符,及需要确保每个资源具有唯一的URI。然而,这可能造成超负荷的URI。在这种情况下,URI可能会变成未定信息和操作的通用网关。这会导致不正确的缓存响应,甚至没有适当鉴权不应该共享的安全数据被泄露。
- 仅使用URI来判断处理请求的资源。例如不要使用定制HTTP头来判断资源。
- 不要重复的状态变化封装入使用相同URI或定制头的POST隧道以造成URI超载。定制头仅用于信息告知用途。
为了让客户端将URI视为不透明的标识符,尽可能在运行时表述消息体和标头中提供URI,这有助于降低服务器和客户端之间的耦合。如果不能提供可能要用到的URI全集,考虑使用URI模板(半耦合)或建立带外规则(紧耦合)以便客户端能够以编程的方式构建URI。 URI应该设计用于很长时间。客户端可能将URI存储到数据库或配置文件,甚至在代码中硬编码。服务器改变了URI,可能会导致客户端无法正常工作。 应该基于稳定概念、标识符和信息来设计URI。如果URI必须改变,来在原有URI的请求可以通过301 (Moved Permanently)转移到新的URI。如果URI被废除,使用401 (Gone)表示其不再有效。
web链接
HTML、XHTML及Atom建立了在表述中包含链接的规则。理解这些格式语义的客户端可以发现表述中的链接。然而,XML是通用格式,应该由服务器负责设计在XML格式表述中包含链接并将其设计文档给客户端。 XML表述可以使用Atom中定义的link元素。该元素在http://www.w3.org/2005/Atom命名空间定义且具有下列属性:
|属性|介绍 |—– |href|包含链接的RUI |rel|指示链接的类型 |title(可选)|人可读的链接标题。 |type(可选)|服务器为链接URI返回表述的媒体类型 |hreflang(可选)|服务器为链接URI返回表述的内容语言 |length(可选)|服务器为链接URI返回表述的内容长度
href既可以是绝对URI也可以是相对URI(需要在link元素中包含xml:base属性)。 对应Atom的link定义,在JSON表述中可以使用link(或links)特性。 Link标头提供了一种格式无关的方式来传送链接。除了使用表述体内的嵌入链接,也可以使用link标头。Link标头适用于下列场景:
- 表述使用二进制格式,例如图像、富文本文档、表单等。
- 表述的格式不容易很容易发现链接(例如普通文本文档)。
- 当客户端/服务器软件需要不解析表述体添加或读取链接。
对链接中的URI没有分配有意义的语义,链接自身就没有什么左右。链接关系类型传送了链接的角色和目的。一旦客户端与服务器对这些类型的含义达成一致,客户端就能发现并使用链接中的URI了。有两种方式为链接关系类型分配值。
- 当链接目的与下列表中的标准类型匹配,使用该值。
名称 用途 self 使用此类型链接资源的推荐URI alternate 使用此类型提供相同资源替换版本的URI链接(例如某pdf文档的英文版和中文版) appendix 使用此类型提供作为资源集合附录的资源URI链接 bookmark 在博客系统使用此类型提供摘要URI链接 chapter、section和subsection 使用这些类型用于链接资源集合中的章、节和子节URI contents 使用此类型用于链接资源集合的目录URI copyright 使用此类型用于链接资源的著作权声明URI current 使用此类型用于链接资源集合中最近条目URI describedby 使用此类型用于链接描述链接上下文的URI edit 使用此类型链接客户端能编辑资源的URI edit-media 使用此类型用于与媒体类型关联的Atom条目文档 enclosure 使用此类型链接可能很大的相关资源URI(例如视频预览片段里提供完全版本视频的链接) first、last、next、next-archive、prev、previous、prev-archive和start 使用这些类型提供用于滚动浏览资源有序序列的链接 glossary 使用此类型链接术语表URI help 使用此类型链接帮助文档URI index 使用此类型链接索引URI license 使用此类型链接许可权URI payment 使用此类型链接购买或支付的URI related 使用此类型链接有关资源 replies 使用此类型链接回复本链接的URI service 使用此类型链接Atom种子的服务文档URI stylesheet 使用此类型链接表单URI up 客户端使用此类型来进入上一层资源的链接 URI via 使用此类型标识消息源的资源URI - 当链接的目的无法与标准类型匹配,使用下列惯例定义一个扩展链接关系类型。
- 将链接关系类型表达成URI,例如http://www.example.org/rels/create-po。
- 对该URI提供HTML文档方式的信息通告资源,描述链接关系类型语义和支持的HTTP方法、对请求和响应的支持表述格式及商业规则等细节。
- 如果链接关系类型用于公共使用,向IANA注册链接类型。
超媒体链接的一个关键应用能够是客户端摆脱学习服务区用于管理应用流程的商业逻辑规则。服务器能提供包含应用状态的链接,因此使用超媒体作为应用状态的引擎。 对每个表述进行设计,使其仅包含客户端可能转移到的下一步的链接。 Web完整性是基于永久URI的,然而有时URI会是临时的。例如,一个URI可能仅对单个用户有效或在特定时间后到期终止。下面列举了几种依赖临时URI的场景:
- Web服务向客户端提供安全令牌。客户端使用该令牌能在短时间内访问某个资源。
- 保险报价Web服务生成报价,每个报价针对特定用户且仅在72小时内有效。报价到期终止后,客户端必须重新获取新报价。
- 当用户在网上注册,服务器通过邮件发送安全令牌给用户并期望用户用之验证邮箱地址。
为了支持短期存活的URI,需要在链接内交互短期URI。为这些链接分配扩展关系类型,并将URI有效期和到期终止后客户端所需操作文档化。当客户端对到期终止URI发送请求,返回适当的4xx错误并在消息体内指示客户端能够采取的操作。 当服务区无法为链接提供用于生成有效且完整URI信息时,服务器可以向客户端提供URI模板。URI模板是在符号外括上大括号的字符串。 为了让符号易于匹配和替换,符号仅限用于URI的下列部分:
- 路径段,例如http://example.org/segment1/{token1}/segment2
- 查询参数的值,例如http://example.org/path?param1={p1}¶m2={p2}
- 矩阵参数的值,例如http://example.org/path?param1={p1};param2={p2}
为了在表述中包含URI模板,使用下列方式:
- 对于XML表述,使用自己的应用程序XML命名空间内定义的link-template元素
- 对于JSON表述,使用link-template或link-templates特性。
Web浏览器是客服端使用链接进行浏览的最好例子。 为了支持服务器提供的URI和URI模板,基于已知链接关系类型从链接中抽取URI和URI模板。这些链接及其他资源数据构成了应用程序的当前状态。 如果应用程序长时间运行,将URI和关系类型同其他表述数据一同存储。 基于链接存在与否决定程序流程。 检查链接关系文档以学习任何关联的商业规则、鉴权、URI长期性、支持的方法和媒体类型等等。
Atom和AtomPub
Atom聚合格式(RFC 4287)和Atom发布协议(也称为AtomPub,RFC5023)定义了条目和种子等资源及其表述和操作协议。Atom主要用于基于文本的、意图让人们去阅读的博客、讨论论坛、评论系统等资源。AtomPub描述了允许客户端创建和修改Atom格式资源的语义,并引入有助于应用程序发现的服务和分类资源。 Atom和AtomPub被用于很多应用场景。尽管Atom通常用于博客种子,也能进行格式扩展以用于用户简介、搜索结果、相簿等应用数据。 下面列举了Atom条目和种子内的一些元素。Atom条目和种子都是可扩展的,也可以引入新的属性和元素。
|元素|描述 |—– |atom:author|存在于atom:feed和atom:entry内,表现创建条目/种子的作者,包含至少一个atom:name及可选的atom:uri和atom:email子元素 |atom:content|存在于atom:entry内,提供普通文本、HTML或XHTML条目内容或带媒体类型的其他内容,使用src和type属性链接到任意媒体 |atom:summary|存在于atom:entry内,提供条目摘要或描述。与atom:tile相似,提供type属性。 |atom:id|存在于atom:entry内,包含条目的URN格式的全局唯一标识符(例如urn:guid:550e8400-e29b-41d4-a716-446655440123)。其值在条目/种子更新或移动后必须改变。 |atom:link|存在于atom:feed和atom:entry内,每个条目/种子必须包含一个rel值为self的atom:link元素,可以包含relf值为alternate的多个type和hreflang属性唯一的atom:link元素组合,也可以包含链接关联资源的其他atom:link元素。 |atom:title|存在于atom:feed、atom:entry和atom:source内,包含条目/种子的文本标题表述。支持type属性,值为text(默认)、thml或xhtml。 |atom:update|存在于atom:feed和atom:entry内,包含条目/种子的最新更新时间。 |atom:category|存在于atom:feed和atom:entry内,对条目和种子进行分类。 |atom:contributor|每个Atom条目可以包含一个或多个atom:contributor元素。 |atom:generator|存在于atom:entry和atom:source内,指示生成种子的软件或条目来源。 |atom:icon|存在于atom:feed内,每个种子可以包含一个atom:icon元素。 |atom:logo|存在于atom:feed内,每个种子可以包含一个atom:logo元素。 |atom:published|存在于atom:entry内,每个条目可以包含一个atom:published元素,用于指示条目第一次发布的时间。 |atom:rights|存在于atom:entry内,每个条目可以包含一个atom:rights元素,描述权利例如著作权。 |atom:subtitle|存在于atom:feed和atom:source内,每个条目/源可以包含一个atom:subtitle元素。
使用Atom的好处在于互通性。为了使用Atom,将资源建模成条目,集合建模成种子。这些元素在http://www.w3.org/2005/Atom命名空间下定义,该命名空间常用的前缀为atom。 Atom种子和条目的默认内容模型包括文本、HTML或XHTML内容和摘要、标识符、链接、作者、分类等。该内容模型最适合发布和聚合作为种子的信息片。然而,由于其格式获取的基本概念对大多数应用程序有益,可被用于各种场景而不是仅仅用于内容种子。 当资源的信息模型或元数据与Atom种子和条目的语法和语义自然匹配时使用Atom。即使资源的信息模型无法匹配Atom,考虑为其提供由短文本、HTML或XHTML资源摘要和链接。用户可以通过种子阅读器等工具了解资源。 AtomPub引入了服务文档和媒体资源等额外资源,服务文档有助于客户端发现Web服务提供的集合。服务器能够使用媒体资源将语音、视频、图像媒体或任意文档与Atom条目进行关联。 使用服务文档资源将集合汇入工作空间。该资源表述是XML文档,定义在http://www.w3.org/2007/app命名空间的service是文档的根节点。该命名空间常用的前缀为app。表述的媒体类型是application/atomsvc+xml。服务(app:service)包含一个或多个工作空间(app:workspace)。每个工作空间包含多个的集合(app:collection),列举了所有种子URI、可接受媒体类型(app:accept)和分类(app:category)。 分类资源列举了集合内资源的分类,表述是category作为根节点的XML文档,有atom:category元素组成。表述的媒体类型是application/atomcat+xml。 AtomPub是修改Atom文档的应用协议。它描述如何创建、更新和删除Atom条目,也支持编辑诸如图片、打包文件等关联的非文本媒体。如果正在使用Atom格式发布可编辑资源,考虑支持AtomPub。 允许客户端通过提交消息体为Atom条目文档的POST请求来创建新资源。客户端可以接下来对edit关系类型的链接用PUT方法修改或用DELETE方法删除资源。 当表述是Atom条目文档时在媒体类型上添加参数type=entry。 AtomPub引入的资源类型之一是媒体资源。媒体资源是除了Atom条目文档之外的其他资源,可用于表现文档、图片、音频和视频文件等。由于媒体资源不是Atom条目文档且可能是二进制资源,AtomPub对每个媒体资源关联一个媒体链接资源(描述并链接媒体资源的Atom条目)。 客户端通过发送POST请求来创建媒体资源。服务器创建媒体资源和媒体链接资源,并在响应的通过Location头返回媒体链接资源的URI。在媒体链接资源表述中,通过atom:conteng元素的src属性提供新创建的媒体资源URI。
内容协商
内容协商有时也称以为conneg,是当多种表述(/变体)可用时为客户端选择资源的最佳表述。尽管内容协商经常与指示媒体类型优先级相关,它也能用于指示语言本地化、字符编码和压缩编码的优先级。HTTP指定了两种内容协商:服务器驱动协商和代理驱动协商。服务器驱动协商使用request头选择一种变体,代理驱动协商为每一种变体使用不同URI。 当实现一个客户端时,对客户端来说向服务器指示自身能够处理的表述格式、语言、字符编码和压缩编码偏好和能力是非常重要的。即使能够通过带外了解响应中上诉信息,清楚指示客户端的偏好和能力有助于客户端面对变化。否则,当服务器决定提供资源的替换表述,HTTP库的任何默认偏好可能提示服务器返回了不同的表述并中断客户端。 在发送请求时,添加一个Accept头,包含逗号分隔的媒体类型优先级列表。如果媒体类型优先级不一样,对每个媒体类型添加一个q参数,以表示相关优先级(1.0~0.0,优先级越高值越大)。如果客户端仅能处理特定格式,在Accept头添加*;q=0.0以表明无法处理Accept头媒体列表之外的媒体。 如果客户端仅能处理特定字符编码,添加带有偏好字符集的Accept-Charset头,否则避免添加Accept-Charset头。为表述的偏好语言添加Accept-Language头。如果客户端能够解压缩诸如gzip、compress或deflate编码的表述,添加带有支持的压缩编码的Accept-Encoding头,否则,不要使用该头。
# Request headers
Accept: application/atom+xml;q=1.0, application/xml;q=0.6, **;q=0.0
# Response
HTTP/1.1 200 OK
Content-Language: en
Vary: Accept-Language
...
# Request for German representation
GET /status HTTP/1.1
Host: www.example.org
Accept-Language: de;q=1.0,**;q=0.0
当服务器无法满足客户端偏好且客户端显式包含**;q=0.8
。这样很难在浏览器获得内容协商的表述。
代理驱动协商当客户端无法使用Accept-*
头来表示偏好时很有效,它通过为每个变体提供不同URI,客户端使用URI来选择期望的表述。在代理驱动协商中,客户端通过从服务器获得的带外信息判断要使用的URI。如果表述存在,服务器返回表述,否则,返回404(Not Found)状态码。尽管所有Accept-*头内要协商的信息都可在代理驱动协商中实现,通常用于媒体类型和语言类型。下面是代理驱动协商的常用做法:
- 查询参数,例如http://www.example.org/status?format=json和http://www.example.org/status?format=xml
- URI扩展,例如http://www.example.org/status.json和http://www.example.org/status.xml
- 子域,例如en.wikipedia.org和de.wikipedia.org
内容协商并不总是适合的,需要考虑Web服务支持多种格式的代价。当客户端需要多种变体或每个变体包含相同信息时,支持多种变体,否则为每个信息使用不同的URI。 在考虑为每个资源支持多种表述之前,需要考虑:
- 应用程序流可能对每种表述格式都不同。
- 内容协商仅在开发框架支持的时候代价才会很小,并不是所有代发框架都支持通过带有多个媒体类型及不同q参数的Accept头返回表述变体的。
- 在某些情况下,法律和商业需求可能是区域性的,代理驱动语言协商可能是更好的途径。
- 缓存可能无法很多好地处理内容协商响应。一些缓存可能会忽略或限制任意给定资源的存储变体个数。
查询
查询信息是HTTP GET方法的一种常见应用,查询通常涉及三个组成部分,即过滤(filtering)、排序(sorting)和投影(projection)。过滤是基于一些过滤条件选择实体的一个子集的过程。排序会影响服务器是如何排列响应中结果的。投影是选择实体中哪些字段将被包含到结果的过程。只要关注URI和表述,查询设计还是相对简单的。客户端负责运行查询,服务器的职责包括设计URI来支持过滤、排序和投影,设计表述,设置合适的缓存头。 使用查询参数来设计查询是一种常用惯例,根据自己的用例,可能需要支持以下一种或全部情况的查询参数:
- 从可用资源中选择数据
- 指定排序条件
- 罗列要包含在响应中资源的字段
http://www.example.org/book/978-0374292881/reviews?after=2009-08-15&sortbyAsc=date&fields=title
# view参数值是一个预定义的查询,可在服务器优化常用查询,提供更快的响应速度。
http://www.example.org/book/978-0374292881/reviews?after=2009-08-15&view=summary
# 获取所有标题包含“war”,发行于2000年后、至少有100条评论的电影,按年份排序
http://www.example.org/movies$contains('war')$compare(year>2000)$compare(count(comments)>100)?$sortby=year
# 将查询参数值作为SQL WHERE子句的一部分
http://www.example.org/movies?query='.title like 'war' and year > 2000 order by year'
# 使用XPath表达式选择电影标题
http://www.example.org/movies[year>2000&genre='war']/title
后三个特定查询(ad hoc query)对客户端来说很灵活,但是削弱了服务器优化数据存储和后端缓存的能力,从而降低了性能,并可能造成URI和数据存储方式的紧耦合。所以要避免使用通用查询语言(例如SQL或XPath)的特定查询。 HTTP头中一般断点下载时才用到Range和Content-Range实体头进行字节范围请求(Range:bytes=1102-1311)。有一些服务器在查询上也使用范围请求(Range: query:after=2009-08-15&sortByAsc=date),缓存可能会忽略这种非字节范围请求,应该避免使用,而查询参数则更容易实现与支持。 服务器将查询响应的表述设计为集合资源,当没有查询到任何匹配的资源,返回一个空集合。 尽管HTTP没有限制URI的长度,但它的一些实现处于安全原因对此进行限制,以避免缓存溢出,阻止用户将大量过滤条件编码到URI。使用HTTP POST来支持大查询。使用POST处理查询削弱了HTTP的统一接口。根据定义,GET才是用于安全、幂等地获取消息的。而且缓存把HOST方法的响应当成是不可缓存的,其后果是丧失了缓存能力,尤其增加了分页查询时的时延。然而在遇到实际限制时,这种权衡也是必不可少的。 服务器可以使用查询存储让那些使用POST方式发送的查询变得可以缓存。当客户端使用POST发起一个查询请求时,服务器创建一个包含查询条件的新查询资源,返回一个带有(指向新查询资源)Location头的响应码201(Created)。客户端对新查询资源发起GET请求,返回查询结果。如果客户端再用POST发起相同查询请求,服务器会找到匹配该请求的查询资源,客户端被重定向到该资源的URI上。存储查询弥补了使用POST方式处理查询的一些局限,缺点是不得不将查询永久保存为一个资源。此外,如果查询数量很大,服务器最终很有可能积聚大量不频繁使用的查询,需要频繁清理这些查询。而大量查询也会造成缓存命中率低下,缓存被很快占满,废弃很多不太使用的URI。
web缓存
当缓存能在不联系原服务器并能提供响应时,缓存会非常有效率地工作。有到期机制的缓存用于降低原服务器接受请求个数并降低应用所耗带宽。有到期机制的缓存基于Cache-Control和Expires头。这些头指导客户端和缓存在一定时间段内保留服务器返回的表述副本。缓存在时间窗内甚至在时间窗外不联系原服务器使用缓存的表述副本服务后继请求。 基于更新频率,决定缓存到期时间。此时间段后,缓存将认为缓存的表述是陈旧的。Cache-Control头是HTTP 1.1头,其max-value值是以秒为单位的新鲜生命期。为了支持遗留的HTTP 1.0缓存,也要包含Expires头及到期时间。如果决定缓存不应保留副本,使用值为no-cache的Cache-Control头。为了支持支持遗留的HTTP 1.0缓存,也要包含Pragma:no-cache头。 下面列举了Cache-Control指令:
|指令|应用 |—– |public|默认值。当请求是鉴权过的但仍希望允许共享缓存提供缓存响应服务,也可以用此指令 |private|当响应对客户端或用户私有或基于鉴权时使用。当此指令存在时,客户端缓存(例如浏览器缓存和转发代理)可以缓存表述,但服务器上或网络中的共享缓存不能进行缓存。 |no-cache和no-store|此指令防止任何缓存存储或提供缓存的表述。 |max-age|此指令是以秒为单位的新鲜生命期。 |s-maxage|此指令类似于max-age但仅用于共享缓存。当原服务器同事设置了max-age和s-maxage,缓存使用那个s-maxage。实践中,单设max-age就够了。 |must-revalidate|使用此指令请求缓存在提供陈旧表述之前检查原服务器。 |proxy-revalidate|此指令类似于must-revalidate除了它仅作用于共享缓存。
# Response
HTTP/1.1 200 OK
Date: Sun, 09 Aug 2009 00:56:14 GMT
Last-Modified: Sun, 09 Aug 2009 00:56:14 GMT
Expires: Sun, 09 Aug 2009 01:56:14 GMT
Cache-Control: max-age=3600,must-revalidate
Content-Type: application/xml; charset=UTF-8
像Squid之类的Cache为Cache-Control头提供了两个扩展指令stale-if-error和stale-if-revalidate。服务器使用stale-if-error告知缓存在max-age超时后仍可是使用一段时间的陈旧表述。服务器使用stale-if-revalidate告知缓存在max-age超时后在异步检查服务器响应的同时仍可是使用一段时间的陈旧表述。 并不是所有HTTP响应都被缓存。关于HTTP 1.1,GET、HEAD和POSt方法的响应可以缓存,但缓存认为该方法不可被缓存。对GET和HEAD请求的带有成功状态码的响应设置到期缓存头。无需对其他方法设置到期缓存头。除了带有200 (OK)状态码的成功响应设置到期缓存头,也可以考虑下面的3xx和4xx响应码。这有助于减少来自客户端的错误触发流量。这称之为消极缓存。
|状态码|介绍 |—– |300 (Multiple Choices)|带有这个状态码的表述可能很少频繁改变。将此响应缓存可以降低服务器负载。 |301 (Move Permanently)|当资源永久搬移,将URI存储在数据库的客户端肯能不会更新。在这种情况下,缓存转发响应可以不联系原服务器。 |400 (Bad Request)|当服务器返回此状态码,假定客户端就不会重发请求了。但有些客户端由于软件bug或者故意会重发请求。 |403 (Forbidden)|如果服务器永久拒绝服务此资源时添加。 |404 (Not Found)|资源不存在时添加 |405 (Method Not Allowed)|客户端可能由于软件bug重发请求。 |410 (Gone)|资源不再存在,因此缓存应尽可能为此返回错误响应。
除非使用线程软件,否则避免在客户端应用程序支持到期缓存,而是在客户端网络部署转发代理缓存,并且避免在客户端代码实现自己的缓存层(工作量大、维护复杂且耦合度高)。 复合资源中有一些数据是不经常改变的,而有一些数据可能是频繁改变的。对到期缓存处理和过期头设置基于最易于改变数据的最强新鲜需求制定。 支持缓存的一个挑战是在客户端没有发送请求时保持缓存新鲜(数据最新)且温暖(缓存不空)。当客户端上传一个新资源,所有缓存都没有这个资源,因此服务器必须为请求生成表述。一个新部署的缓存,必然是空的,只有随着客户端开始请求后才能进行填充。温暖的缓存避免冷启动问题。尽可能将超时与更新频率同步。如果不可能,实现监控数据库等新、定时通过无条件GET请求更新缓存的后台进程。如果使用Squid,使用HTTP缓存通道扩展将资源更新复制到缓存。
条件请求
HTTP条件请求有助于解决两个问题。对于GET请求,条件请求帮助客户端和缓存检验缓存的表述是否新鲜。对于PUT、POST和DELETE等不安全的请求,条件请求提供了并发控制。不支持条件GET请求会将降低性能。但对于并发,不支持条件POST、PUT和DELETE请求不安全而且可能影响应用程序完整性。缺乏足够的并发控制检查,服务器容易“丢失更新”或“陈旧删除”。当客户端它基于自认为的资源当前状态提交请求修改或删除资源,但在并发条件下,资源的当前状态并不是静态的,服务器(通过后端方式)或其他客户端都有可能已经修改或删除了资源。 并发控制确保客户端对数据的并发操作被正确处理。有两种并发控制实现方式:
- 悲观并发控制:锁机制。
- 乐观并发控制:此模式下,客户端首先获得令牌,之后在写请求中携带此令牌,如果令牌有效则操作成功,否则操作失败。HTTP以此模式工作。
服务器使用Last-Modified和ETag响应头驱动条件请求。客户端使用If-Modified-Since和If-None-Match验证缓存表述,使用If-Unmodified-Since和If-Match进行并发控制预处理。 如果对存储资源的数据存储区能够控制,修改每个资源的schema以包含用来跟踪版本的修改时戳或序列号。如果数据存储区是数据库,使用触发器在数据修改时自动更新上述字段。如果无法修改存储schema或数据存储区不允许维护时戳或序列号,使用资源数据生成ETag头数值,并存储到单独的表或存储区。如果表述不是很大,使用表述体生成MD5哈希值或每次随资源变动的某些字段用于ETag。Last-Modified是一种弱验证,ETag是一种强验证,两者不必同时使用。 如果客户端在本地存储表述体,可以将响应的Last-Modified和ETag头一同存储。客户端基于上次请求响应的Last-Modified和ETag头发送带有If-Modified-Since或If-Non-Match的条件GET请求,服务器发现表述没有改变,可以省去发送表述体仅发送状态码304 (Not Modified),否则发送包含新ETag或Last-Modified头的最新表述体。
# 客户端在一小时后发出第三个相同请求
GET /person/joe HTTP/1.1
Host: www.example.org
# 缓存向原服务器发送请求
GET /person/joe HTTP/1.1
Host: www.example.org
If-Modified-Since: Sun, 09 Aug 2009 00:40:14 GMT
If-None-Match: "3f4a74db207d0447d46710a64971e777"
# 服务器产生的响应
HTTP/1.1 304 Not Modified
Date: Sun, 09 Aug 2009 01:54:14 GMT
Last-Modified: Sun, 09 Aug 2009 00:56:14 GMT
Expires: Sun, 09 Aug 2009 02:54:14 GMT
Cache-Control: max-age=3600,must-revalidate
E-Tag: "3f4a74db207d0447d46710a64971e777"
Content-Type: application/xml; charset=UTF-8
# 缓存返回的响应
HTTP/1.1 200 OK
Date: Sun, 09 Aug 2009 00:54:14 GMT
Last-Modified: Sun, 09 Aug 2009 00:40:14 GMT
Expires: Sun, 09 Aug 2009 01:44:14 GMT
Cache-Control: max-age=3600,must-revalidate
...
服务器处理条件PUT请求 服务器处理条件DELETE请求 当客户端使用条件GET请求查询失败,获得412 (Precondition Failed)状态码。HTTP 1.1允许客户端修改到期缓存、使用包含Cache-Control:no-Cache和Pragma:no-cache的无条件GET请求获取新鲜表述。 当客户端使用PUT创建新资源时,或服务器在同一资源的上一个GET或PUT请求响应中没有返回Last-Modified或ETag头,客户端像和平时一样发送PUT请求。 如果客户端拥有之前对资源请求响应的Last-Modified和ETag头,客户端基于上次请求响应的Last-Modified和ETag头发送带有If-Unmodified-Since或If-Match的条件PUT/DELETE请求。如果服务器返回412 (Precondition Failed)状态码,客户端可通过无条件GET请求获取新鲜表述,决定是否通过重发PUT/DELETE实现自己的需求。 与PUT或DELETE不同,对一个资源的POST请求可能不会对请求URI中的资源造成任何改变。服务器可能会创建一个新资源(状态码201)或使用不同URI(状态码303)标识输出。因此,客户端不会在本地存储表述和条件头。为了让服务器检测和防止客户端重复发送POST请求,可以通过一次性URI实现条件或不可重复的POST请求。 客户端实现通过GET请求获取包含token链接,如果URI的目的是创建新资源,基于序列号、时戳和随机数的连接串生成token。如果URI的目的是修改资源,基于这些资源的实体标签和标识符生成token。 客户端发送服务器响应中提供的一次性URI进行POST请求,服务器查看token是否已经在服务器的事务日志中存在,以检测POST请求有效性,如果存在返回状态码403 (Forbidden)并解释原因,否则根据输出返回状态码201 (Created)或303 (See Other)。
杂项
为了复制资源而不泄漏服务器实现细节,可以设计一个用于复制的控制器资源。客户端向控制器发送POST请求复制资源。为了实现条件POST,可以提供一次性URI。控制器创建副本后,返回状态码201 (Created)及Location头带有副本URI。 为了合并两个或多个资源,可以设计一个用于合并的应用程序特定控制器资源。客户端想控制器发送GET请求,其查询参数包含待合并资源的URI或标识符。服务器返回Last-Modified、ETag头和包含待合并资源摘要的表述体。ETag由时戳和随机数连接串构成。为了验证摘要,客户端向同一地址发送带有If-Unmodified-Since和If-Match头的POST请求发起合并。服务器合并后在事务日志中保留If-Match头的值并返回状态码201 (Created)及含有合并后资源URI的Location头。如果客户端再发送相同If-Match头的POST请求,服务器返回状态码412 (Preconditon Failed)。 为了移动资源,服务器会提供负责移动资源控制器的链接或链接模板以使客户端可以发送POST请求,并在完成请求后根据输出返回状态码201 (Created)或303 (See Other)。 WebDAV (RFC 4918)是用于资源分布式创作和版本管理的HTTP扩展,它扩展了一些HTTP方法和头用于管理文件和文档。当Web服务是内容创作应用且服务器支持WebDAV时使用WebDAV特定方法,避免对其他类型应用使用WebDAV。
|方法|介绍 |—– |PROPFIND|WebDAV中的文档具有属性,客户端可用此方法获得属性 |PROPPATCH|客户端用此方法设置、添加或修改资源属性 |MKCOL|WebDAV可以将文档放入集合(文件夹),客户端可用此方法创建集合 |COPY|客户端可用此方法复制资源 |MOVE|客户端可用此方法移动资源 |LOCK|客户端可用此方法对给定文档加锁,以支持悲观并发控制 |UNLOCK|客户端可用此方法对给定文档去锁
为了支持跨服务器边界的操作(例如,将用户配置从一个应用移植到另一个应用,将文档从草稿服务器发布到生产服务器),需要服务器之间彼此就数据格式、后台接口、并发控制、数据加载、范式化和存储等方面协作、设计和实现设计跨服务器操作。 wiki的网页都会维护当前和过去的修订历史,以便客户可以获取、比较和评估页面改变。为了支持资源以往历史快照,服务器在收到客户端PUT请求更新资源时,在更新资源之前会默认创建快照(资源副本),并在更新后的资源表述中包含快照链接,快照表述中包含更新后资源链接。当用户发送DELETE请求,删除资源及所有快照。 提供用于撤销操作的控制器资源。当客户端发送POST请求进行撤销操作,在事务日志中记录资源当前状态以用于审计。服务器将资源状态恢复到上一快照并将客户端重定向到资源URI。 当资源很大而改动很小时,发送GET请求获取整个表述、进行小的修改、发送PUT请求将整个表述传回服务器进行更新很费时费带宽。为了支持对资源进行部分更新,可以将可修改的资源部分封装为一个新资源。客户端通过PUT请求更新该新资源,等效于部分更新原来的资源。 HTTP PUT方法用于对资源的整个更新或替换,PATCH方法(RFC 5789)用于支持部分更新。PATCH方法不是安全和幂等的,请求体是一系列对资源进行改变的表述。当收到请求,服务器将整个补丁原子性地施加于资源,并返回响应码 200 (OK)或204 (No Content)。如果服务器无法将整体补丁施加于资源,就不会做任何局部修改。可以通过请求中包含If-Unmodified-Since和/或If-Match头支持条件PATCH请求,如果不匹配则返回状态码412 (Precondition Failed)。建议在OPTIONS响应的Allow头支持PATCH,并在PTACH方法包含Accept-Patch头,其值为支持的媒体类型。 当客户端需要为不同资源提交若干类似请求时,只要对每个资源的操作是相同的且资源是类似的,可以将这些操作组合成一个针对集合资源的单个操作。使用POST请求和集合资源一次性批量创建若干资源。服务器为集合资源分配一个URI,并使用状态码303 (See Other)重定向到该集合资源,集合资源表述包含所有新创建资源的链接。使用PUT请求更新或DELETE请求删除若干资源与创建过程类似,以上操作必须是原子化的。 客户端需要执行批量作业的用例不是少数。例如,为前一天销售订单做汇总、将一个或多个文档打包、批准选择的购买订单集合等等都需要批量执行。服务器需要设计一个控制器资源用于执行批量操作。如果客户端需要跟踪操作或客户端需要提交大量用于操作的数据,返回状态码202 (Accepted)以进行异步操作,否则返回200 (OK)或204 (No Content)。 将几个HTTP组合成一个HTTP请求以支持批量处理的用例不是少数。下面列举了一些通常使用的技术实现:
- 客户端将几个HTTP请求序列化到一个JSON对象、或一个XML文档、或multipart/mixed消息的一部分。
- 客户端创建一个信封跟是将多个请求组合进入一个消息。
- 客户端向服务器的分批终点(batch end point)资源发送POST请求。
- 服务器接收到消息,打开信封,重构多个HTTP请求并分发到服务器的相关URI。或者服务器绕过HTTP将请求直接派发到能处理这种请求的代码。
- 服务器收集每个请求的响应并序列化为一个消息返回到客户端。
- 客户端打开信封并处理每个响应消息。
避免这种将多个HTTP请求封装入一个POST请求隧道的做法。因为通常隧道方案有以下不利之处:
|特性|介绍 |—– |并发|HTTP通过Last-Modified和ETag头来实现乐观并发检查。将多个HTTP请求封装进一个HTTP请求隧道的批量操作使并发检查变得困难,因为服务器需要为批量操作中每一个任务进行并发检查。 |原子性|HTTP请求是原子性的。每个请求执行单个任务,服务器在错误发生时确保数据的原子性和一致性。将多个任务混入一个请求、尤其是某些操作依赖于同一请求的前一操作是否成功的批量操作使Web服务很难确保原子性和进行错误恢复。 |可见性|将多个操作封装到一个HTTP请求隧道使中间节点无法对批量处理内的操作响应可见。此外检测请求防止拒绝攻击的典型安全方法几乎不可能捕捉到批量操作中的可疑请求,因此可能导致拒绝服务攻击。 |错误处理|用于批量操作的错误处理和报告更为复杂。单个批量请求的结构可能混杂成功和失败响应。 |可扩展性|一般用于批量操作的理由依赖批量处理比执行每个单个请求更可扩展这样的假设。当单个服务器收到非常多的批量处理,请求会降低服务器的响应能力。发送很多批量客户端处理到单个服务器的应用比不支持批量处理的相同应用性能可能更低。
分析导致促成使用隧道技术的用例,设计应用特有的控制器资源支持相同需求。由于请求使用的是处理请求资源的单个URI,所以请求可见。由于仅返回一个状态码,所以响应可见。 RESTful web服务在下列场景可能会需要处理事务:
- 客户端执行操作流的一些列步骤。客户端在取消操作流时要撤销所有已完成的数据变动。
- 客户端同若干服务器顺序交互以实现应用操作流,客户端可能希望恢复任何状态改变或持久化存储状态。
可以提供对数据进行原子化操作的资源。将未提交状态视为应用状态(并将其加入URI中)。如果服务器需要允许客户端撤销操作,使用适当的PUT、DELETE或POST抵消已有的改变。
安全
系统安全可能需要:
- 确保仅认证过的用户访问资源。
- 确保信息从采集到存储及之后展现给授权实体或用户过程中信息的可靠性和完整性。
- 防止未授权或恶意客户端滥用资源和数据。
- 维持私密性并符合当地安全法规。
认证协议,如基本认证和摘要认证,使用一种挑战-应答机制的协议。当客户端访问受限资源,服务器使用WWW-Authenticate头挑战客户端请求其应答,应答是客户端和服务器之间共享的密钥功能。认证可用于两类场景:客户端代表自己访问受限资源、客户端代表用于访问受限资源。 基本认证(RFC 2617)中客户端会通过标识符和共享密钥来向服务器认证请求。当服务器收到客户端访问受限资源的请求,返回状态码401 (Authorization Required)及WWW-Authenticate头,WWW-Authenticate:Basic realm=“some name”。客户端会将客户端标识符(例如用户名)和共享密钥(例如密码)连接成:并通过Base64编码到Authorization头,Authorization: Basic 。服务器会对文本进行解码并验证密钥是否一致。如果客户端提前知道服务器对某资源需要基本认证,可以在请求中加入Authorization头以免收到401 (Unauthorized)状态码及WWW-Authoricate头。服务器文档可包含认证需求以帮助客户端开发人员了解这些信息。 摘要认证(RFC 2617)同基本认证类似,客户端向服务器发送的是证书摘要而不是共享密钥。摘要认证也提供了防止重放攻击的机制。当服务器收到客户端访问受限资源的请求,返回状态码401 (Authorization Required)及WWW-Authenticate头,摘要认证方案、必要的realm和nonce指令及其他指令。nonce是仅一次或有限次数使用的数字或token。客户端会将客户端或用户标识符摘要、realm和共享密钥放入到Authorization头。服务器会将请求中的摘要与存储在服务器的证书摘要验证,并在响应中包含Authentication-Info头(Authorization头在服务器侧的等同体)。默认客户端使用MD5计算摘要,不同于基本认证,这种技术不会交换未加密的共享密钥。
# Request
GET /photos HTTP/1.1
Host: www.example.org
# Response
401 Unauthorized
WWW-Authenticate: Digest realm="Sample app", nonce="6cf093043215da528d7b5039ed4694d3",
qop="auth"
Content-Type: application/xml;charset=UTF-8
Unauthorized.
# Request
GET /photos HTTP/1.1
Host: www.example.org
Authorization: Digest username="photoapp.001", realm="Sample app",
12.2 How to Use Digest Authentication to Authenticate Clients | 221
nonce="6cf093043215da528d7b5039ed4694d3",
uri="/photos", response="89fba5bf5e5f9dd69865258c21860956",
cnonce="c019e396409afe784ae9f203b8dfdf7e", nc=00000001, qop="auth"
# Response
HTTP/1.1 200 OK
Content-Type: application/xml;charset-UTF8
...
OAuth(http://oauth.net)是2007年开发的一种代理认证协议。使用该协议,用户可以不用泄漏自己的证书,让客户端访问其在服务器上的数据。OAuth认证协议由于协议中包含三种角色,所以称为三方认证:服务提供者(例如服务器)、OAuth消费者(例如客户端)和用户。 OAuth依赖服务器向客户端发布的三套令牌和密钥。
- 消费者键值和消费者密钥:消费者键值是客户端的唯一标识符。客户端使用消费者密钥签署获得请求令牌的请求。
- 请求令牌和令牌密钥:请求令牌是服务器发布的一次性临时标识符,用于请求用户向客户端授予权限。令牌密钥是用于签署获得访问令牌的请求。
- 访问令牌和令牌密钥:访问令牌是客户端用于访问用户资源的标识符。拥有访问令牌的客户端能在令牌有效时访问用户资源。服务器可以由于令牌到期或用户撤销权限而随时撤销访问令牌。令牌密钥用于签署访问受限用户资源的请求。
使用三方OAuth涉及以下步骤以获得访问令牌和密钥。服务器可能会授予对特定用户资源一段时间或一定访问次数的访问令牌。
- 客户端带外向服务器请求消费者键值和消费者密钥。
- 客户端使用消费者键值获得请求令牌和密钥。
- 客户端重定向用户到服务器获得让客户端访问用户资源的权限,该过程产生认证过的请求令牌。
- 客户端请求服务器提供访问令牌和密钥。
- 当客户端发送请求访问受限资源时,客户端请求包含Authorization头(或查询参数),其含有消费者键值、访问令牌、签名方法和签名、时戳、nonce和可选的OAuth协议版本号。
由于OAuth是HTTP层之上的协议,服务器文档应该提供:获得请求令牌的URI、鉴权服务器的URI和获得访问令牌的URI。OAuth建议使用POST获得请求和访问令牌。 两方OAuth与客户端通过使用基本或摘要认证的Authorization头向服务器提供认证相类似,没有引入代理。注意OAuth协议没有指定这种认证方式,但是被广泛用于客户端与服务器之间的认证。 使用三方OAuth涉及以下步骤:
- 客户端带外向服务器请求消费者键值和消费者密钥。消费者键值是客户端的标识符。消费者密钥是客户端和服务器之间共享的密钥。
- 当客户端发送请求访问受限资源时,客户端请求包含Authorization头,其含有消费者键值、访问令牌、签名方法和签名、时戳、nonce和可选的OAuth协议版本号。
- 服务器在授予资源访问权限之前验证签名。
服务器可能将应用程序状态编码到URI。在有些情况下,这些状态可能是敏感的。当URI被通过网络传输时使用TLS有助于状态的完整性,但是服务器无法控制客户端如何管理URI。在这种情况下,服务器需要确保URI不会被篡改且URI中的消息是可靠的。为了检测URI篡改,使用HMAC-SHA1和RSA-SHA1之类的机制对URI的数据计算摘要签名,将签名作为查询参数加入资源URI中。如果URI中数据是机密的,使用AES、Blowfish、DES、Triple DES、Serpent、Twofish等机制加密数据。确保在将其加入到URI之前使用对加密结果Base64进行编码。 为了维持资源表述的可靠性和完整性,使用TLS,仅基于HTTPS的请求可以访问服务器受限资源。
可扩展性与版本控制
在任何分布式CS环境管理变化都是困难的。在这些环境中,客户端依赖服务器来遵守契约,RESTfulweb服务也不例外。对于web服务,契约包含URI、资源、表述的结构和内容、格式及对每个资源所用的HTTP方法。任何对服务器的改变在后向兼容之前似乎都是良性的。当变化是后向兼容的,当修改服务器时无需升级客户端,客户端除了在服务器升级时宕机之外能够继续按照以往的方式使用服务器。当多个客户端和服务器在不同时刻升级,另一个比较重要的兼容性是前向兼容。在某些情况下,一些新客户端可能会和老服务器交互。前向兼容的目的是确保新客户端即使在老服务器功能缺失时能够继续使用老服务器。应用是否需要考虑后向兼容和前向兼容取决于操作环境,维护兼容性的手段是可扩展性。可扩展性是解决未来变化的设计过程。作为传输协议,HTTP是可扩展的,可以添加新方法或标头来扩展HTTP(须谨慎使用),但不意味着HTTP之上的应用也自动地可扩展。尽管维护兼容性是值得的,但并不总是有可能实现。 为了使URI变化跟现有客户端兼容,必须保持URI持久不变。将相同查询参数但排序不同的请求URI视为相同。客户端必须能够对不同查询参数排序获得相同的结果。当对URI添加新参数,继续使用现有参数并将新参数视为可选。当改变查询参数数据格式,继续使用现有格式。如果行不通,通过新的查询参数或新URI引入格式变化。默认情况下,将URI中的查询参数视为可选,除非这些参数基于并发或安全使用。 为了使XML/JSON表述变化跟现有客户端兼容,设计XML/JSON格式保持子节点无序。当对XML/JSON进行改动,保持现有分层结构以使客户端能够继续使用相同结构提取数据。使请求中新创建的数据节点可选以同现有客户端兼容。如果客户端发送新数据字段,服务器也必须能继续处理。不要删除或重命名响应体中表述的任何数据字段。 Atom格式被设计成支持未来的扩展。Atom格式中的所有元素允许外来的XML元素和属性。可以通过下列方式扩展Atom:
- 添加链接关系类型,例如"种子分页和打包"扩展(RFC5005)引入了fist、last、previous和next链接关系类型。
- 在诸如atom:entry、atom:feed和atom:link之类的Atom元素内增加新元素。例如"Atom跟帖扩展"(RFC4685)引入了in-reply-to和total元素。
- 在atom:content元素内嵌套外来的XML或其他文本内容。
对atom:feed和atom:entry元素添加子元素或属性进行扩展,只要这些扩展不破坏客户端自身的功能且其他软件不了解这些扩展。当在atom:content元素下添加外来内容,在atom:summary元素下添加可读文本或XHTML。 为了使链接变化跟现有客户端兼容,避免删除链接,不要改变链接的rel和href属性值。当引入新资源,使用链接向客户端提供资源的URI。 当服务器作出兼容性改变,为了使客户端不操作失败,可让客户端解析表述体,仅寻找已知数据。不要假设从服务器接收到的表述是固定媒体类型、字符编码、内容语言或内容编码。 当服务器无法维持兼容性时或某些客户端需要与其他客户端不同的行为或功能时考虑对某些或所有资源版本化以对客户端隔离改变。当对RESTfulweb服务版本化时,当资源行为或表述所含信息发生变化时使用新URI添加新资源,在子域名、路径片段或查询参数中使用例如v1或v2这样易于检测的模式分辨URI。避免对同一资源使用不同媒体类型的新表述视为一个版本。
服务发现
当创建RESTful web服务时,需要解决设计时可发现性和运行时可发现性这两类可发现性。设计时可发现性有助于其他设计和创建客户端,它描述了客户端开发组和管理员用于构建和启动客户端的基本信息。运行时可发现性有助于维护客户端和服务器之间的松散耦合并使能即插即用式自动化。运行时可发现性解决了HTTP统一接口、媒体类型、链接和链接关系类型。本章是关于设计时可发现性的。 设计时发现简单来说就是将web服务用散文描述,不管这些散文诗有工具生成还是设计者手工写作。客户端开发者可以查询散文以理解资源语义、媒体类型、链接类型等信息以实现客户端。 下面这些信息需要在RESTful web服务文档中描述:
- 所有资源及每个资源支持的方法
- 请求和响应中资源的媒体类型和表述格式
- 使用的链接关系、商业意义、所用的HTTP方法及链接标识的资源
- 所用不通过链接提供的固定URI
- 用于所有固定URI的查询参数
- URI模板和符号替换规则
- 访问资源的认证和安全证书
对于XML表述,如果客户端和服务器支持XML schema,使用schema语言描述用于请求和响应中表述的XML结构。对于其他格式,使用散文描述表述。 通过支持HTTP OPTIONS方法,有助于工具了解web服务中的资源。在服务器端,实现OPTIONS方法通过Allow头返回支持方法列表。当资源支持PATCH方法,添加Accept-Patch头列举支持PATCH请求的媒体类型。可选择性添加含有资源描述文档的链接的Link头。