1.传输编码
理解HTTP传输编码与内容编码之间的区别是至关重要的。前者只是一个用于将资源转换为HTTP响应体的机制。显然,传输编码方式的选择不会对客户端获得的资源有任何影响。例如不管服务器发送的响应是通过Content-Length还是区块编码来封帧的,客户端接收到的文档或图片都是一样的。发送资源时,可以使用原始字节,也可以为了加快传输分速度,使用压缩后的字节,但是最终的资源内容是相同的。传输编码只是一种用于数据传输的封装方式,并不会修改真正的数据。
现代网络浏览器支持多种传输编码,但是在程序员中最流行的还是gzip。能够接受这种传输编码的客户端必须在Accept-Encoding头中进行声明,并且检查响应中的Transfer-Encoding头,缺人服务器是否使用了客户端要求的传输编码。
urllib库不支持这一机制。因此如果要使用压缩形式的传输编码,就需要在自己的代码中检查这些头信息,然后自己对响应体进行解压缩。
Requests库自动为Accept-Encoding头声明了gzip和deflate两种传输编码。如果服务器发送的响应使用了合适了传输编码,Requests就会自动解压缩响应消息体。Requests库不仅可以对使用服务器支持的传输编码方式传输的信息进行自动解压缩,还能向用户隐藏这一过程。
2.内容协商
内容类型和内容编码与传输编码不同,它们对终端用户或发送HTTP请求的客户端程序是完全可见的。它们决定了要使用哪种文件格式来表示给定的资源,如果选择的格式是文本的话,它们还决定了要使用哪种文件格式来表示给定的资源,如果选择的格式是文本的话,它们还决定了要使用哪种编码方式将文本代码转化为字节。
通过这些HTTP头,不支持最新PNG图片的旧版浏览器可以声明优先使用GIF和JPG格式。用户也可以向网络浏览器指定一种资源传输使用的首选语言。下面例子展示了由一个现代网络浏览器生成的HTTP头。
GET / HTTP/1.1
Accept: text/html;q=0.9,text/plain,image/jpg,*/*;q=0.8
Accept-Charset: unicode-1-1;q=0.8
Accept-Language:en-US,en;q=0.8,ru;q=0.6
User-Agent: Mozilla/5.0 (X11; Linux i686) AppleWebKit/537.36 (KHTML)
HTTP头中先列出的类型和语言优先级最高,权重为1.0,其后列出的类型和语言优先级权重通常会将会q=0.9或q=0.8,以确保服务器知道后面列出的类型和语言不是第一选择。许多简单的HTTP服务和网站会直接忽略这些HTTP头,它们会为资源的每个版本都分配一个独立的URL。例如,如果一个网站支持英语和法语的话,它的首页可能有两个版本。然而HTTP设计的意图并非如此。HTTP旨在为每个资源提供一个路径。无论可能使用多少种不同的机器格式或人类语言来生成资源,每个资源都只应有一个路径。服务器使用上述这些用于内容协商的HTTP头来选择资源。
3.内容类型
一旦服务器从客户端检测到了多个Accept头信息并决定了要返回的资源的表达方式,就会相应地在响应消息中设置Content-Type头。
作为email消息的一部分,多媒体可以通过多种MIME类型表示。内容类型便是从这些MIME类型中选择出来的。text/plain和text/html都是普通类型,image/gif、image/jpg等则是图像类型。文档以包括application/pdf在内的形式进行传输。application/ octet-stream用于传输原始的字节流,服务器不会进一步对这些字节流进行解释。
在处理通过HTTP传输的Content-Type时,有一个复杂之处。如果主要类型(斜杠之前的单词)是text,则有多种编码方式可供服务器选择。要指定将文本传输给客户端所用的编码方式,只需在Content-Type头后面加上一个分号,之后再接上字符编码方式。 Content-Type: text/ktml; charset =utf-8。
4.HTTP认证
发送HTTP请求时有一个内置认证过程,来确认发送请求的机器或是用户的身份。
当服务器无法通过协议验证用户身份或是认证用户没有权限查看请求的特定资源时,服务器会返回错误码401 Not Authorized。许多现实世界中的HTTP服务器是完全为人类用户而设计的,因此它们实际上从来都不会返回401错误码。向这些服务器请求资源时,如果没有通过认证,那么服务器可能会返回303 See Other状态码,并将页面转至登录界面。这对于人类用户来说是很有帮助的但是对于Python程序就不尽然。我们需要在程序中区分由于认证失败引起的303 See Other和正常请求访问资源时重定向引起的303 See Other。
每个HTTP请求都是独立的,与任何其他请求都不相关,因此即使是同一套接字处理的几个连接请求,也需要对认证信息进行单独传输。这种独立性使得代理服务器和负载均衡器在任意数量的服务器之间分配HTTP请求时得以安全运行,即使所有请求都发送到同一套接字也不会影响安全性。
服务器涉及的第一个认证机制是基本认证,使用该机制的服务器在返回的401 Not Authorized头信息中包含一个叫做realm的字符串表示认证域。浏览器保存了用户密码和认证域之间的对应关系,因此认证域字符串使得一台服务器能够通过不同的密码来保护文档树的不同部分。客户端在收到返回的401码后重新发送请求,并在Authorization头中指定与认证域对应的用户名和密码(通过base-64)进行编码。理想情况下此时就可以获得200响应。
GET / HTTP/1.1
...
HTTP/1.1 401 Unauthorized
WWW-Authenticate:Basic realm ="engineering team"
...
GET / HTTP/1.1
Authorization:Basic YgndskkjlSFklsdhfkljHSFK==
...
HTTP/1.1 200 OK
...
直接使用明文传输用户名和密码在今天听起来相当不合理。随着协议的发展,出于安全考虑,出现了摘要访问认证机制。采用摘要认证的服务器会返回一个随机数,然后客户端发送根据随机数和用户密码生成的MD5散列值。然而这一机制仍然是不安全的,即使使用了摘要认证,用户名依然是可见的。所有通过表单提交的数据和网站返回的资源也都是可见的。
为此协议设计者们为了创建HTTPS连接,发明了SSL以及后续一系列我们今天仍在用的TLS版本。加入TLS后,使用基本认证在原则上已经没有任何问题了。许多简单的通过HTTPS保护的API和网络应用程序现在都使用基本认证。要在urllib中使用基本认证较为复杂,而Requests则直接通过一个关键字参数来支持基本认证
r = requests.get("http://example.com/api/",auth='brandon','atigdngnatwwal')
使用Requests时,同样可以事先定义一个Session并进行认证,以避免每次调用get()或post()时都进行重复认证需要注意的是,无论是Requests还是其他现代库,都没有实现完整的协议。事先设置的用户名和密码都没有绑定到任何特定的认证域。用户名和密码只是单向绑定至请求的,过程中并没有事先检测服务器是否需要用户名和密码,因此服务器不会返回401响应,更不会提供认证域。无论是auth关键字还是Session设置,都只是用来帮助用户在无需自己进行base-64编码的前提下设置Authorization头的。
相对于实现完整的基于认证域的协议,现代开发者更偏爱这种简单的方法。通常来说,他们唯一的目的就是根据发起请求的用户或应用程序的身份,对一个面向开发者的API提供的GET或POST请求进行独立认证。一个支持单向认证的HTTP头就I足以完成这一任务。这种方法还有一个优势:当用户已经有足够的理由确信此次请求需要密码时,就不会再浪费时间和带宽来获取初始401响应。
如果需要进行交互的是一个历史遗留系统,需要对同一服务器上的不通认证域使用不同密码的话,Requests库就无能为力了。开发者需要提供正确URL和正确的密码。这也是为数不多的urllib支持而Requests不支持的有用功能,但是这种认证协商方式已经非常罕见了。
5.cookie
对于使用网络浏览器访问的资源来说,使用HTTP认证最后其实被证明是一个失败的主张。通常来说,网站的设计者都希望使用他们自己的方式来进行认证。他们喜欢在给出了网站的用户操作指南后提供一个友好的自定义登陆界面。而使用协议内置HTTP认证,浏览器会跳出一个小小的弹窗提示输入用户名和密码,破坏了用户体验。且只要输入错误,弹窗就会反复弹出,而用户却不知道错在哪里,从而不知道如何修改。
因此cookie就出现了。从客户端角度看,cookie是一个很难懂的键值对。任何从服务器发送至客户端的成功响应中都可以传输cookie。
GET /login HTTP/1.1
...
HTTP/1.1 OK
Set-Cookie:session-id=d9h9j0h7ffh90jkd; Path=/
...
在这之后,如果客户端还要向该服务器发送任何请求的话,就将接收到的cookie键值对添加到cookie头中、
GET /login HTTP/1.1
Cookie:session-id=d9h9j0h7ffh90jkd
...
这使得用户可以通过网站生成的登陆页面来完成身份认证。当提交的登录表单中包含非法的认证信息时,服务器可以要求用户重新填写登陆表单,并根据需要给出许多有用的提示或支持链接,而所有这些信息的样式风格都与网站的其余部分保持统一。一旦表单正确提交,服务器就可以进行授权,为该客户端生成一个特有的cookie。在之后的所有请求中,客户端都可以使用这个cookie来通过服务器的身份验证。
更为巧妙的是,如果登陆界面没有使用真正的web表单而是使用了Ajax,在同一界面内进行登录操作,只要调用的API属于同一主机,那么仍然可以使用cookie。当进行登陆的API调用验证了用户名和密码,并返回了200 OK及Cookie头后,所有后续的发送至同一网站的请求(不仅是API调用,还包括对页面、图片和数据的请求)都可以使用cookie来进行身份认证。
需要注意的是,需要将cookie设计成人类无法理解的串。可以在服务器端生成随机的UUID串,指向存储真正用户名的数据库记录;也可以不使用数据库,把cookie设计成一个加密的字符串,直接在服务器端进行解密并验证用户身份。如果用户可以解析cookie的话,一些聪明的用户就可以自己编辑cookie,提交一些伪造的值来模拟其他用户。
现实世界的Set-Cookie头比上述例子复杂的多,其中有一个secure属性。该属性告诉HTTP客户端,向网站发送非加密请求时不要传输cookie。如果没有这个属性的话,cookie就可能会暴露。使用咖啡店无线网络的其他任何人只要得到了某用户的cookie值就可以模拟该用户。有些网站给用户提供cookie只是为了记录用户的访问信息。它们通过cookie来追踪用户在该网站的访问行为。在用户浏览时,收集到的访问历史就已经被应用到了定向广告中。如果用户之后使用用户名进行了登陆,网站就会将浏览历史信息保存到永久的账户历史中。
许多用户定制的HTTp服务必须使用cookie来跟踪用户的身份,并保证用户通过认证才能成功运行。在Requests中,如果创建并始终使用Session对象,那么cookie跟踪是自动进行的。
6.连接、Keep-Alive和httplib
要打开一个TCP连接,需要经过三次握手。如果连接已经打开,就可以避免三次连接的过程。这甚至促使早起的HTTP允许在浏览器先后下载HTTP资源、Javascript以及CSS和图片的过程中时钟保持打开连接。当TLS出现并称为所有HTTP连接的最佳实践后,建立新连接的花销就变得更大了,这也增加了连接复用带来的好处。
HTTP/1.1版本的协议在默认设置下会在请求完成后保持HTTP连接处于打开状态。客户端和服务器都可以指定Connection: close,在一起请求完成后关闭连接;否则,就可以使用单个TCP连接,根据客户端的需要,不断从服务器获取资源。网络浏览器经常会对一个网站同时建立4个或更多TCP连接,这样就可以并行下载一个页面及其所有支持文件和图像,尽快将页面呈现在用户眼前。
不幸的是,urllib并没有提供对连接复用的支持。要使用标准库在同一套接字上进行两次请求,只能使用更底层的httplib模块。与urllib不同,Requests库的Session对象使用了第三方的urllib3包,它会维护一个连接池,保存与最近通信的HTTP服务器的处于打开状态的连接。这样一来,在向同一网站请求其他资源时,就可以自动重用连接池中保存的连接了。