12.2 用HTTP和REST传输对象

    超文本传输协议(Hypertext Transfer Protocol,HTTP)是由一系列的RFC(Request for Comments)文档定义的。我们不会介绍所有的细节,但是会触及其中3个高层的部分。

    HTTP协议包括请求和响应。一个请求包括一个方法、一个统一资源标识符(Uniform Resource Identifier,URI)、一些报头和可选的附件。标准中定义了许多可用方法。大多数浏览器主要使用GETPOST请求。标准的浏览器包括GETPOSTPUTDELETE请求,我们会使用这4种请求,因为它们对应着CRUD操作。我们会忽略大部分的报头而是只关注URI的路径部分。

    一个响应包含一个状态码、原因、一些报头和一些数据。有许多可能返回的状态码,但是我们只对它们其中的几个感兴趣。200通常是来自服务器的OK响应。201代表Created响应,它可能适合用来向我们展示POST请求成功并且数据已经被发布。204代表No Content响应,它可能适合被用在DELETE中。400代表Bad Request401代表Unauthorized404代表Not Found。这些状态码广泛地被用来反映某些操作无法执行或者出现错误。

    大多数2xx成功响应会包含一个(或者一些)编码过的对象,4xx错误响应可能会包含更详细的错误信息。

    HTTP是无状态协议。服务器不会保留任何之前与客户端交互的信息,有很多方法可以克服这个限制。对于交互式网站,通过cookie来追踪事务状态并且改善程序行为。但是,对于网络服务,客户端不是人,每个请求都可以包含验证凭证。这进一步强调了保证连接安全性的重要性。基于我们的目的,会假设服务器使用安全套接字层(Secure Sockets Layer,SSL),并且使用基于端口443的HTTPS连接而不是基于端口80的HTTP连接。

    12.2.1 用REST实现CRUD操作

    我们会介绍发明REST协议的3个基本想法。第1个想法是,使用任何方便对象状态的文本序列化表示。第2个想法是,可以用HTTP的URI请求来命名一个对象,一个URI包含任何级别的细节,它以统一的格式包含了模式、模块、类和对象标识符。最后,我们可以将HTTP方法映射到CRUD规则来定义在命名对象上即将执行的操作。

    用HTTP协议实现RESTful服务是在挑战HTTP请求的响应的原始定义。这意味着对于一些请求和响应的语义可能会有不同的讨论和意见。相比于介绍各有各自优点的各种选择,我们会建议用一种单一的方法。我们主要关注Python语言,而不是设计RESTful网络服务器这个更一般性的问题。一个REST服务器常常通过下面5种基本用法支持CRUD操作。

    • 创建:我们会用HTTP的POST请求创建一个新的对象,而URI在这种情况下只提供类信息。一个类似//host/app/blog/的路径可能可以为类命名。响应可以是201,并且包含最后被保存对象的备份。返回的对象信息可能包括RESTful服务器为新创建对象分配的URI或者是用于创建URI的相关键。POST请求应该被用于创建一些新的RESTful资源。
    • 获取-搜索(Retrieve-Search):用于获取多个对象的请求。我们会使用 HTTP 的 GET请求,并且包含一个提供查询条件的URI,通常条件以查询字符串的形式包含在?字符之后。可能的 URI 是//host/app/blog/?title="Travel 2012-2013"。注意,GET不会改变任何RESTful资源的状态。
    • 获取-实例(Retrieve-Instance):这是用于获取单个对象的请求。我们会使用HTTP的GET请求,并且包含一个在路径中指定了特定对象的URI。可能的URI是//host/app/blog/id/。尽管预期的响应是一个单独的对象,但是为了与搜索的响应兼容,它有可能被保存在一个列表中。由于这个响应是 GET,因此不会改变资源状态。
    • 更新:我们会用HTTP的PUT请求,并且包含一个指定了目标替代对象的URI。可能的 URI 是//host/app/blog/id/。响应可以是 200,并且包含一份更新后对象的备份。很明显,这个操作会修改RESTful资源。我们可以使用其他号码来代替200。但是,对于我们的例子,我们依然会使用200。
    • 删除:我们会用HTTP的DELETE请求,并且包含一个类似于//host/app/blog/id/的URI。响应可以是简单的204 NO CONTENT,而不用在响应中提供任何对象的细节。

    由于HTTP协议是无状态的,因此没有登录和注销的规定。每个请求都必须单独验证。我们通常会使用HTTP的Authorization头提供用户和密码这些验证信息。当选择这种做法时,我们必须也用SSL为Authorization头的内容提供安全措施。有一些更成熟的替代产品,它们会使用独立的身份管理服务器提供验证令牌而不是仅仅使用凭证。

    12.2.2 实现非CRUD操作

    一些应用程序中有一些操作无法被简单地归类为CRUD。例如,我们可能有一个用于执行复杂计算的远程过程调用(Remote Procedure Call,RPC)风格的应用程序。计算用的参数通过URI提供,所以没有改变RESTful服务器上资源的状态。

    大多数时候,这些主要执行计算任务的操作可以实现为GET请求,因为状态没有改变。但是,作为不可否认计划(non-repudiation scheme)的一部分,如果想要记录请求和响应日志,那么可能需要考虑使用POST请求。这对于使用付费服务的网站尤其重要。

    12.2.3 REST协议和ACID

    第10章“用Shelve保存和获取对象”中定义了ACID属性。这些属性是原子性、一致性、隔离性和持久性,这些是包含多个数据库操作事务的基本属性,这些属性不会自动成为REST协议的一部分。当我们确保达到ACID属性的要求时,必须考虑到HTTP是如何工作的。

    每个HTTP请求都是原子的,所以,我们应该避免把程序设计成将操作操作分到多个POST请求中。反之,我们应该想办法把所有的信息合并在一个单独的请求中。另外,我们必须认识到请求常常来自许多交叉的客户端,也没有一种简洁的方式可以在交叉的请求序列中保证隔离性。如果我们有一个合理的多层架构,我们应该将持久性委托给独立的持久化模块来完成。

    为了达到ACID属性的要求,常用的一个方法是让POSTPUT或者DELETE请求包含所有相关的信息。通过提供一个单独的复合对象,应用程序可以在单个REST请求中执行所有操作。这些大型的对象成为了文档(documents),它们可能包含多个对象并且作为更复杂事务的一部分。

    回到BlogPost的关系,我们会发现,为了创建一个新的Blog实例,我们可能希望可以处理两种HTTP POST请求。这两种请求如下。

    • 只有标题而没有其他POST对象的Blog:可以很容易地为这个对象实现ACID属性,因为它只是一个单独的对象。
    • 一个包含了Post对象集合的复合Blog对象:我们需要序列化Blog和所有相关的Post实例。这个对象需要在一个POST请求中发送。然后,可以通过创建Blog对象和相关的Post对象,并且当所有的对象都保存后返回一个201 Created状态实现ACID属性。这可能需要在支持RESTful服务器的数据库中执行一个复杂的多语句事务。

    12.2.4 选择一种表示方法——JSON、XML或者YAML

    没有理由只选择一种表示方法,相对来说,支持几种不同的表示方法也是很容易的。客户端可以请求使用某种特定的表示方法。客户端可以在下面这些地方指定想要使用的表示方法。

    这些都是非常好的选择。为了与现有的RESTful服务兼容,我们可能需要使用某种特定的格式,用来解析URI模式的框架本身可能也会建议使用某种格式。

    JSON是很多JavaScript表示层的首选。其他的表示层或者其他种类的客户端可能会倾向于其他的表示方法,例如XML或者YAML。在一些例子中,可能还有其他表示方法。例如,某个特殊的客户端应用程序可能要求使用MXML或者XAML。