Python 通过cookie注入状态,Cookie的引入改变了客户端和服务器之间的整体关系,使之状态化了,然而没有涉及对HTTP协议本身的修改。状态信息通过请求和响应的报头进行通信。服务器会在响应报头中向用户代理发送cookie。用户代理会在请求报头中保存并使用cookie进行响应。
用户代理或浏览器需要保留cookie值的缓存,将它作为响应的一部分来提供,并在后续的请求中包含相应的cookie。Web服务器会在请求报头中查找cookie,并在响应报头中提供更新了的cookie。这样做的效果是Web服务器变为无状态的,而状态的改变只在客户端发生。服务器将cookie视为请求中的额外参数,并会在响应中提供一些附加的细节信息。
Cookie可以包含任何内容。通常会将它们加密以避免Web服务器的详细信息暴露给客户端计算机上运行的其他应用程序。传输巨大的cookie会拖慢处理速度。优化这种状态处理的最佳方法是使用现有框架。我们将忽略cookie和会话管理的细节。
会话的概念是Web服务器的一个特性,而不是HTTP协议。通常将会话定义为具有相同cookie的一系列请求。在发出初始请求时,由于没有可用的cookie,因此会创建一个新的会话cookie。每个后续请求都将包含该cookie。一个登陆用户的会话cookie中会包含其他细节信息。只要服务器还继续接收该cookie,会话便可以一直存续,即cookie可以永久有效,或者在几分钟后失效。
Web服务的REST方法不依赖会话或cookie。由于每个REST请求都不同,因此相比使用cookie来简化用户交互的交互式网站,它不是那么用户友好。
本教程关注基于REST的Web服务是因为它们非常符合函数式设计模式。
使用无会话REST进程的一个结果是每个单独的REST请求都会被单独验证。如果需要身份认证,则意味着REST通信必须使用SSL(secured socket layer,安全套接字层)协议。可以使用https
方案将凭证从客户端安全地传输到服务器。
函数式设计的服务器考量
HTTP背后的一个核心思想是服务器响应是对请求的函数。从概念上讲,Web服务应该具有顶层实现,概括如下:
response = httpd(request)
然而这样做是不切实际的。事实证明,HTTP请求并不是简单的、单一的数据结构,它包含一些必要的内容和可选的内容。请求可能含有报头、方法和路径,也可能有附件。这里的附件可能包括表单或上传的文件,或两者兼有。
如果情况再复杂一些,那么可以将浏览器的表单数据作为GET
请求路径中的查询字符串来发送,或者将其作为附件发送给POST
请求。尽管可能会造成混淆,但大部分Web应用程序框架都会创建HTML表单标签,通过<form>
标签中的method=POST
参数提供数据,随后表单数据会以附件形式包含在请求中。
深入研究函数式视图
HTTP响应和请求都有独立于主体的报头。请求还可以包含一些附加的表单数据或者其他上传的文件,因此可以把一个Web服务器视为如下形式:
headers, content = httpd(headers, request, [form or uploads])
请求报头可能包含cookie值,可以将其视为额外添加的参数。此外,Web服务器通常依赖所运行的操作系统环境,因此也可以将该操作系统环境数据看作请求中提供的附加参数。
对于内容,有一个范围虽广但十分合理的定义。MIME(multipurpose internet mail extensions,多用途互联网邮件扩展)类型定义了Web服务可能返回的内容类型。其中包括纯文本、HTML、JSON、XML,或者网站提供的非文本形式的各种媒体。
当深入研究针对HTTP请求构建响应所需的处理时,会发现一些我们希望复用的公共特性。可复用元素的思想是创建从简单到复杂的Web服务框架。函数式设计的方式允许我们复用函数,这表明函数式方法有助于构建Web服务。
下面介绍如何为服务响应中的各个元素创建管道,来讲解Web服务的函数式设计。我们将通过嵌套负责处理请求的函数来实现这一点,这样外部元素产生的一般开销就不会影响内部元素了。这也使得可以把外部元素作为过滤器,为无效的请求生成错误响应,并让内部函数只关注应用程序本身的处理。
嵌套服务
可以将Web请求处理看作许多嵌套的上下文。例如外层上下文可能涉及会话管理,即检查请求以确定这是现有会话还是新会话中的一个请求;内层上下文可能会为用于检测CSRF(Cross-Site Request Forgeries,跨站请求伪造)的表单处理提供令牌;其他上下文可能会处理会话中的用户身份认证。
对于上面解释的功能,其概念性的视图如下所示:
response = content(
authentication(
csrf(
session(headers, request, forms)
)
)
)
这里的思想是每个函数都可以建立在前一个函数的结果之上。每个函数既可以扩充请求,也可以因请求无效而将其拒绝。例如函数session()
可以使用报头来确定这是一个现有会话还是一个新会话。由于CSRF处理要求会话有效,因此函数csrf()
会检查表单输入以确保使用的是合适的令牌。对于缺少有效凭证的会话,函数authentication()
会返回一个错误响应,而当存在有效凭证时,它可以使用用户信息来扩充请求。
函数content()
无须担心会话、伪造和未经身份认证的用户。它可以专注于解析路径,以确定应当提供哪类内容。在更复杂的应用程序中,函数content()
可能包含极其复杂的映射,用于将路径元素映射到能确定合适内容的函数。
然而嵌套的函数视图仍然不够准确,其问题在于每个嵌套的上下文可能还需要调整响应,而不是或者不仅是调整请求。
理想的情况如下所示:
def session(headers, request, forms):
pre-process: determine session
content = csrf(headers, request, forms)
post-processes the content
return the content
def csrf(headers, request, forms):
pre-process: validate csrf tokens
content = authenticate(headers, request, forms)
post-processes the content
return the content
这一概念体现出一种函数式设计思想:可以使用支持扩充输入或输出的嵌套函数集合来创建Web内容。更巧妙的做法是,应当定义可供不同函数使用的简单标准接口。一旦将该接口标准化了,就能以不同的方式组合函数并添加功能了,从而实现函数式编程的目标,用简单明了的程序提供Web内容。