zoukankan      html  css  js  c++  java
  • 读书笔记_python网络编程3_(9)

    9. HTTP客户端

    9.1. Python客户端库

    9.1.1. Requests库不仅把其他第三方竞争者远远甩在了身后,还替代了urllib,在使用HTTP时的第一选择。基于urllib3的连接池逻辑。

    urllib和Requests的基本接口及其相似,都提供了可供调用的方法,用于打开HTTP连接,发起请求,等待接收响应头,将包含响应头的响应对象发送给程序员。响应体会留在socket的接收队列中,只有程序员请求时才会读取响应体。
    使用http://httpbin.org的小型测试网站来测试这两个HTTP-Cli库,可以使用pip在本地安装这个网站,然后在WSGI容器内运行它。
    在localhost的8000端口运行这个测试网站,使得无需访问公共版本的httpbin.org的情况下,就能在自己的机器上运行示例。

    $ pip install gunicorn httpbin requests
    $ gunicorn httpbin:app
    Starting gunicorn 20.0.4
    Listening at: http://127.0.0.1:8000 (3419)
    Using worker: sync
    Booting worker with pid: 3422
    

    安装完成后,就能使用urllib和Requests来获取该网站的页面了。两者的接口及其类似

    >>> import requests
    >>> r = requests.get('http://localhost:8000/headers')
    >>> print(r.text)
    {
    	"headers":{
    	"Accept":"*/*",
    	"Accept-Encoding":"gzip, deflate",
    	"Connection":"keep-alive",
    	"Host":"localhost:8000",
    	"User-Agent":"python-requests/2.18.4"
    	}
    }
    
    >>> from urllib.request import urlopen
    >>> import urllib.error
    >>> r = urlopen('http://localhost:8000/headers')
    >>> print(r.read().decode('ascii'))
    {
    	"headers":{
    	"Accept-Encoding":"identity",
    	"Connection":"close",
    	"Host":"localhost:8000",
    	"User-Agent":"Python-urllib/3.6"
    	}
    }
    
    urllib和Requests的两处区别:
    

    1)Requests一开始就声明支持gzip和deflate两种压缩格式的HTTP响应;urllib不支持
    2)Requests能自己确定正确的解码方式,将HTTP响应从原始字节转换为文本;urllib只会返回原始字节,需要自己解码

    9.1.2. 许多网站都非常复杂,只能通过全功能的浏览器才能与这些网站进行交互。原因在于,现在的表单通常只通过JS的注解/纠正来实现。许多现代风格的form甚至没有实体的提交按钮,直接通过激活一个脚本就可以完成相应的功能。

    9.2. 端口、加密与封帧

    80-port是用于纯文本HTTP会话的标准端口。
    

    有些Cli希望首先协商一个加密的TLS会话,一旦加密连接建立完成,就使用HTTP进行通信。这是超文本安全协议(HTTPS,HHypertext Transfer Protocol Secure)的一个变形
    443-port是HTTPS的标准端口
    在加密连接内部,只要像在普通的未加密socket一样,直接使用HTTP即可。
    TLS的目的不仅是保护数据在传输过程中不被窃听,也会对CLi连接的Serv身份进行验证(此外,如果Cli也提供证书的话,TLS也允许Serv对Cli身份进行验证)
    如果某个HTTPS-Cli没有检查尝试连接的Serv所提供的证书,是否与其hostname匹配的话,绝对不被使用这个Cli,所有Cli都会进行这样的检查

    9.2.1. HTTP中,Cli首先会向Serv发送一个获取文档的请求。一旦发送完整个请求(request),Cli就会进行等待,直到从Serv接收到完整的响应(response)为止。response可能会包含错误信息,也可能会提供Cli请求的文档信息。

    在流行的HTTP/1.1版本的协议中,不允许Cli在未收到上一个req的res前,就在同一个socket上发送第二个req
    

    HTTP有一种很重要的平衡---req和res采取了相同的格式化与封帧规则。如下,一对req和res:

    GET /ip HTTP/1.1
    User-Agent: curl/7.35.0
    Host: localhost:8000
    Accept: */*
    
    HTTP/1.1 200 OK
    Server: gunicorn/19.1.1
    Date: Sat, 20 Sep 2014 00:18:00 GMT
    Connection: close
    Content-Type: application/json
    Content-Length: 27
    Access-Control-Allow-Origin: *
    Access-Control-Allow-Credentials: true
    {
    	"origin": "127.0.0.1"
    }
    

    表示req的文本块以GET开始;
    res以表示版本号的HTTP/1.1开始;
    在res-head后跟了一个空行,然后是3行JSON文本。
    req和res的标准名称都的HTTP消息(message),每个消息由3部分组成:

    9.2.1.1. 在req中,第一行包含一个方法名和要请求的文档名;在res消息中,第一行包含返回码和描述信息。无论是req还是res中,第一行都以回车和换行(CR-LF, ASCII码13和10)结尾

    9.2.1.2. 第二部分包含0个/多个head-mes,每个head-mes由一个名称、一个冒号、一个值组成。HTTP头的名称是区分大小写的,可根据Cli/Serv的要求,自由使用大写字母。每个head-mes由一个CR-LF结尾。列出了所有的head-mes后,再跟上一个空行,空行由CR-LF-CR-LF四个连续字节组成。无论是否包含head-mes,都必须包含空行

    9.2.1.3. 可选的消息体,消息体紧跟head-mes后面的空行。

    第一行和head-mes通过表示结束的CR-LF进行封帧。这两部分作为一个整体,又通过空行封帧,Serv/Cli可通过调用recv()来接收这两部分信息,直到遇到CR-LF-CR-LF这4个连续字符位置。

    9.2.2. 在接收时,不会事先得到任何关于第一行和head-mes长度的警告,许多Serv根据常识,设置了第一行和head-mes的最大长度,房子有Cli连接并发送无限长度的head-mes而造成内存溢出。

    对head-mes进行封帧时,有3种不同的方法供选择:
    

    9.2.2.1. 最常见的封帧方法:提供一个Content-Length头。Content-Length的值是一个十进制整数,表示mes-body包含的字节数。Cli可以简单地在循环中不断运行recv()调用,直到接收到的字节总数与Content-Length表示的字节数相等为止。有时数据是动态生成的,无法在头信息中声明Content-Length头,只有在整个过程完成后,才能确定mes-body的长度。

    9.2.2.2. 头信息中指定Transfer-Encoding头,将其设置为chuncked,就可以激活一个更为复杂的封帧机制。该机制不会在mes-body前指定长度,而是将mes-body分成一系列较小的消息块,在每个块前使用一个前缀来指定该块的长度。每个块中包含:1)表示块长度的十六进制数、两个字符CR-LF、2)一个与给定长度相符的数据块、3)最后的两个字符CR-LF。所有块结尾,使用一个长度为0的块来表示消息的结束。长度为0的块是最小的块,包含数字0、两个字符CR-LF、最后的两个字符CR-LF

    可在块长度和CR-LF间插入一个分号,指定应用于该块的extension选项。最后一个块中,发送者可在用0表示的块长度和CR-LF间添加最后一些HTTP头
    

    9.2.2.3. Serv可指定Connection:close,就能随意发送任意大小的mes-body,发送完毕后关闭TCP-socket。该方法引入了一个危险,Cli无法判断socket是因为发送了完整的mes-body而关闭,还是Serv/网络错误而提前关闭。此外,Cli每发送一个请求,都需重连Serv,降低了协议的效率。

    标准规定,Connection:close不能由Cli来指定,否则Cli无法接收到Serv的响应。使用shutdown()来单向关闭socket,允许Cli关闭发送方向的socket,但仍然能从Serv读取发回的数据。
    

    9.3. 方法

    HTTP请求中,第一个单词指定了Cli请求Serv时使用的操作类型。GET和POST是两种最常见的方法。此外,还有许多不常见的方法,这些方法为其他想获取文档的程序,提供了完整的API(如JS,Serv本身会将JS脚本传输给浏览器)
    GET和POST两种基本方法,提供了HTTP的基本“读”和“写”操作

    9.3.1. 在浏览器中键入HTTP的URL时,使用的就是GET。请求Serv将请求路径指向的文档,作为响应发回浏览器。GET方法不包括mes-body。

    HTTP标准规定,任何情况都不允许Cli通过GET修改Serv上的数据。该限制使得Cli能在第一次请求失败时,安全地重新尝试GET,也能将GET的响应存入缓存中。此外,还能在运行网络抓取程序时,安全地访问任意数量的URL,不必担心网络抓取程序,会在遍历的网站上,创建/删除内容。
    

    像?q=python、?results=10附加到请求路径后的任何参数,都只能修改返回后的文档,而不能修改Serv上的数据。

    9.3.2. 当Cli希望向Serv提交新数据时,会使用POST方法。传统的Web表单通常使用POST来提交Cli的请求(除非直接将表单数据复制到URL中)。

    面向程序员的API同样使用POST来提交新文档、评论、数据库行。
    两次运行同一个POST,会在Serv上进行两次相同的操作(如商户重复提交两次100美元),因此,不能将POST操作的结果,存入缓存以提高后续重复操作的速度,也不能在没有接受到响应时,自动重试POST。

    9.3.3. 其余HTTP方法分为两大类:

    1)本质上类似于GET的方法
    2)本质上类似于POST的方法
    

    9.3.3.1. OPTIONS和HEAD是类似于GET的方法:

    1)OPTIONS方法: 请求与给定路径匹配的HTTP头的值
    2)HEAD方法: 请求Serv,做好一切发送资源的准备,只发送head-mes。使得Cli能在不需下载整个mes-body的前提下,检查Content-Type信息,降低了查询所用的花销。

    9.3.3.2. PUT和DELETE是类似于POST的操作,相同点在于,都可能会对存储在Serv上的内容作出不可逆转的修改

    1)PUT的目的: 向Serv发送一个新的文档,该文档上传后,会存在于请求指定的路径上。
    2)DELETE的目的: 请求Serv删除指定的路径及所有存在于该路径下的内容。
    尽管这两个方法,都请求对Serv进行“写”操作,但与POST不同的是,它们在某种意义上是安全的。它们是幂等的,Cli可进行任意次数的重试,多次运行这两个操作,与单次运行的效果一致。

    9.3.3.3. 用于调试的TRACE方法和用于将所有的协议从HTTP切换为其他协议的CONNECT方法(用于打开WebSocket):

    很少使用,且不涉及文档传输。文档传输才是HTTP协议的核心工作。
    标准库的urlopen()方法奇怪之处:隐式地选择了HTTP方法。如果调用者指定了一个数据参数,使用POST方法;否则使用GET方法。HTTP方法的正确使用,对于Cli和Serv设计安全性非常重要,所以并不是一个明智的选择。Requests库的选择好多了,为不同的基础方法都提供了get()和post()方法

    9.4. 路径与主机

    第一个版本的HTTP,只允许在req中包含方法名和路径

    Get /html/rfc7230
    

    在Web早期没有问题,当时每台Serv上只会托管一个网站。但MA希望在大型HTTP-Serv上部署几十甚至几百个网站,上述做法就行不通了。
    如果只提供路径,Serv要如何猜测用户在URL中输入的是哪个hostname?尤其是现在几乎每个Web上都存在/这样的路径

    9.4.1. 解决方法就是: 至少要强制使用Host头。现代HTTP协议也要求提供协议版本,一个req至少需要提供下述信息:

    GET /html/rfc7230 HTTP/1.1
    Host: tools.ietf.org
    

    如果Cli没有提供Host-head指出在URL中使用的hostname,许多HTTP-Serv就会发出一个Cli-error,结果通常是400 Bad Request。

    9.5. 状态码

    响应首行,以协议版本开始,与以协议版本结尾的req首行不同。协议版本后跟着一个标准状态码,最后是对状态的非正式文本描述,供用户阅读/记录log。

    9.5.1. 一切正常,状态码为200,响应首行如下:

    HTTP/1.1 200 OK
    

    跟在状态码后的文本,只是非正式的,所以Serv可以将OK改为Okay、Yippee、It Worked等/运行Serv的国家本地语言
    标准制定了二十多个返回码,覆盖了通用情况,也有一些特定情况

    9.5.2. 一般,200300的状态码表示成功,300400表示重定向,400500表示Cli的请求无法被识别/非法,500600表示Serv错误导致一些意外错误。

    9.5.2.1. 200 OK: 请求成功。POST操作,表示已经对Serv产生了预期的影响

    9.5.2.2. 301 Moved Permanently: 路径合法,但该路径已经不是所请求资源的官方路径了(曾经可能是)。Cli若要获取响应,应请求Location头中给出的URL。如果Cli希望将新URL存入缓存,所有后续的请求都会直接忽略旧URL,直接转向新URL

    9.5.2.3. 303 See Other: 通过某个路径req资源时,Cli可通过使用GET方法,对响应信息的Location头中,给出的URL进行req,以获取响应结果,但对该资源的后续req仍然需要通过当前req路径来完成。该状态码对网站的设计是至关重要的。任何使用POST正确提交的表单,都应返回303状态码,就能通过安全、幂等GET方法,获取Cli实际看到的页面了。

    9.5.2.4. 304 Not modified: 不需在响应中包含文档内容,原因在于req-head指出了Cli已经在缓存中存储了所req文档的最新版本

    9.5.2.5. 307 Temporary Redirect: 无论Cli使用GET/POST发起了什么样的res,都需使用res-Location-head中给出的另一个URL,重新发起req。对于同一资源的后续req,还需通过当前req路径来发起,该状态码允许在Serv宕机/不可用时,暂时将form提交到另一个可用的地址。

    9.5.2.6. 400 Bad Request: 请求不是一个合法的HTTP-req

    9.5.2.7. 403 Forbidden: Cli没有在req中,向Serv提供正确的密码、cookie/其他验证数据来证明Cli有访问Serv的权限

    9.5.2.8. 404 Not Found: 路径没有指向一个已经存在的资源。因为用户在req成功时,只会在屏幕上看到所req的文档,而不会看到200状态码,404是最著名的异常码

    9.5.2.9. 405 Method Not Allowed: Serv能识别方法和路径,但该方法无法用于该路径

    9.5.2.10. 500 Server Error: Serv希望完成请求,但由于某些内部错误,暂时无法请求。

    9.5.2.11. 502 Bad Gateway: 请求的Serv是一个网关/代理,无法连接到真正为该req路径提供响应的Serv

    返回码为3xx的res,不包含mes-body,返回码为4xx/5xx的res则通常包含mes-body----一般会提供一些人们可以理解的,用于err的描述。
    也有一些err页面,提供的信息很少,如,有的页面会原封不动地,将编写web-Serv所用的语言/框架层面的错误显示出来。Serv的编写者,会重新编写一些能提供更多信息的页面,来帮助user/dev了解错误恢复的方法

    9.5.3. 库是否会自动进行重定向。

    如果不提供重定向功能,就需要自行检查转态码为3xx的res的Location-head。标准库内置的底层httplib模块没有提供自动重定向功能,但urllib会根据标准,提供该功能。Requests库也提供了重定向功能,提供了一个history变量,将整个重定向链,从头到尾列了出来。

    r = urlopen('http://httpbin.org/status/301')
    Traceback (most recent call last):
      File "<input>", line 1, in <module>
    NameError: name 'urlopen' is not defined	
    # 如果直接按照书上内容,此处会报错,参考https://blog.csdn.net/hellocsz/article/details/87997693
    >>> from urllib.request import urlopen
    >>> r = urlopen('http://httpbin.org/status/301')
    >>> r.status, r.url
    (200, 'http://httpbin.org/get')
    >>> r = requests.get('http://httpbin.org/status/301')
    >>> (r.status, r.url)
    Traceback (most recent call last):
      File "<input>", line 1, in <module>
    AttributeError: 'Response' object has no attribute 'status'
    # 如果直接按照书上内容,此处会报错,按tab所弹出的提示改为status_code
    >>> (r.status_code, r.url)
    (200, 'http://httpbin.org/get')
    >>> r.history
    [<Response [301]>, <Response [302]>]
    

    此外Requests库还允许在需要时,关闭重定向功能。关闭该功能,只需一个关键字参数(urllib也可做到,但难得多)

    # 书上的google和twitter需要翻墙,最近没续费VPN,用百度和小米替代
    >>> r = requests.get('http://baidu.com/')
    >>> r.url
    'http://baidu.com/'
    >>> r = requests.get('http://www.xiaomi.com/')
    >>> r.url
    'https://www.mi.com'
    

    Google(baidu)和Twitter(xiaomi)这两个流行的网站,在决定是否要在官方hostname内包含www前缀时,采取了相反的方案。不过,这两个web都通过重定向,来强制将URL转换为官方形式,也避免了web因拥有两个不同URL而造成混乱。
    如果编写的程序不清楚网站的重定向规则,且设法避免重定向,每当请求资源时,若URL中不包含正确的hostname,就会发送两个HTTP请求

    9.5.4. 如果尝试访问的URL返回了4xx/5xx的错误状态码,Cli会采取什么方法通知?

    一旦返回了这样的错误码,标准库的urlopen()会抛出一个异常,防止代码意外以处理正常数据的方式,处理Serv返回的err页面。

    >>> from urllib.request import urlopen
    >>> urlopen('http://localhost:8000/status/500')
    Traceback (most recent call last):
    	...
    ConnectionRefusedError: [WinError 10061] 由于目标计算机积极拒绝,无法连接。
    
    如果urlopen()抛出异常并中断了程序,该如何查看res的细节?
    

    只要查看异常现场即可,异常对象的主要作用有两个:
    1)表示发生的异常;
    2)作为包含了res-head和res-body的res对象

    >>> import urllib
    >>> try:
    ...     urlopen('http://localhost:8000/status/500')
    ... except urllib.error.HTTPError as e:
    ...     print(e.status, repr(e.headers['Content-Type']))
    500 'text/html; charset=utf-8
    

    比起urlopen(),Requests库即使只请求状态码,也会直接向调用方返回一个响应对象。调用方要负责检查响应的状态码/通过手动调用raise_for_status(),在状态码为4xx/5xx时抛出异常。

    >>>r = requests.get('http://localhost:8000/status/500')
    >>>r.status_code
    500
    >>>r.raise_for_status()
    >Traceback (most recent call last):
    ...
    
    

    可以封装一个函数,来防止忘记在每次调用requests.get的时候进行状态检查。

    9.6. 缓存与验证

    防止Cli对频繁使用的资源进行重复的GET请求,HTTP采用了多种机制,在Serv将相应的head-mes加入到允许这些机制的资源时适用。

    9.6.1. Serv程序采用缓存,能减少网络流量,降低Serv负载,加快Cli程序的运行速度。

    Serv架构者在添加HTTP-head来允许缓存时,考虑一个最重要的问题:
    

    是不是只要两个req的路径完全相同,就应该返回同一个文档?有没有其他的因数导致这两个req返回不同的文档?
    如果有,该服务就需在每个res中包含Vary-head-mes,列出文档内容所依赖的其他HTTP-head。
    Vary-head中常见的选项有Host和Accept-Encoding,如果将不同的文档返回给不同的用户,Cookie也常见
    设置了Vary-head,可激活多个不同级别的缓存

    9.6.2. 可完全禁止将资源存储在Cli缓存中,可防止Cli自动复制非易失存储器中的res。目的是让user决定是否要选择“保存”,来将资源的副本保存到硬盘中。

    HTTP/1.1 200 OK
    Cache-control: no-store
    ...
    

    9.6.3. 如果允许缓存,Serv通常会希望在user每次req资源时,都返回所req资源的缓存版本(缓存过期前)。某个文档/图片的每个版本都会永久存储,且每个版本都对应一个特定的路径,Serv无需担心缓存有效期,可永远返回缓存版本的资源。

    如,设计师每次完成一个新版本的公司logo,将获取logo的URL末尾的版本号,自增/改变URL末尾的散列值,任意特定版本的logo都能被永久存储
    

    Serv可使用两种方法来避免永远向用户返回存储在Cli的缓存版本的资源。
    1)指定一个过期日期和时间,如果要在该过期日期和时间后访问资源,就必须重新向Serv发送请求。

    HTTP/1.1 200 OK
    Expires: Thu, 01 Dec 1994 16:00:00 GMT
    

    过期日期和时间的方法引入一种威胁:没有正确设置Cli的时钟,存储在缓存中的资源的有效时间就可能会过长。
    现代机制指定资源在缓存中的有效时间(s为单位)。只要Cli的时钟没有停止,就有效。

    HTTP/1.1 200 OK
    Cache-control: max-age=3600
    

    上面的两个head-mes指定Cli能在一段时间内使用缓存的资源副本,无需再向Serv发起查询。
    如果Serv希望对使用缓存版本的资源,还是从Serv返回最新版本的资源保留决定权的话,该怎么办?

    9.6.4. 此时需要Cli在每次想用某个资源时,通过一个HTTP请求向Serv进行验证。由于直接使用缓存中的副本,不需要进行任何网络操作,这种花销会更大。但如果Cli缓存中存储的副本确实已经过期,Serv仍然需要发送最新版本的资源。这种方法能节省时间。

    如果Serv希望Cli在每次req资源时,都发送测试请求,询问要使用哪个版本的资源,且尽可能地重用Cli缓存中的资源副本,有两种机制:
    

    只有在这些测试的结果表明,Cli缓存汇总的资源版本已经过期时,Serv才会发送res-body,将这些test-req称为条件(conditional)请求

    9.6.4.1. 要求Serv知道资源的最近修改时间。Cli-req的资源是存储在文件OS上文件,要获取最近修改时间,是很容易的。但,如果req的资源要从DB-TAB中查询得到,该DB-TAB又没有维护审计日期/最近修改时间,要采用这种机制,就变得非常困难,甚至不太可能实现。能获得资源的最近修改时间,则Serv就可在每个res中包含该信息

    HTTP/1.1 200 OK
    Last-Modified: True, 15 Nov 1994 12:45:25 GMT
    

    想要重用缓存中的资源副本,则Cli可将最近修改时间也存储到缓存中。下一次需要使用该资源时,将缓存中的最近修改时间,发回给Serv。Serv进行比对,检查上一次Cli收到该资源后,资源是否有改动。没有就不返回mes-body,只返回mes-head和特殊状态码304

    GET / HTTP/1.1
    If-Modified-Since: Tue, 15 Nov 1994 12:45:26 GMT
    ...
    
    HTTP/1.1 304 Not Modified
    ...
    

    9.6.4.2. 不通过修改时间来实现,通过资源ID实现。Serv需要通过一些方法,为某个资源的每个版本,创建一个唯一的标签,且保证任何时候,只要资源发生改变,该标签会更改为一个新的值。校验码/DB的UUID就可作为标签的两种信息。Serv在构造res时,需将该标签放在ETag-head中传输给Cli

    HTTP/1.1 200 OK
    ETag: "d41d8cd98f00b204e9800998ecf8427e"
    ...
    

    一旦Cli在缓存中,保存了该版本的资源副本,就可在想要重用该副本时,向Serv发送一个资源req,且在req中包含缓存的标签。如果缓存中的版本仍然是最新版本,Serv就不需再次传输该资源了

    GET / HTTP/1.1
    If-None-Match: "d41d8cd98f00b204e9800998ecf8427e"
    ...
    HTTP/1.1 304 Not Modified
    ...
    

    ETag和If-None-Match中使用了引号,说明不仅可以比较两个字符串是否相等,还可用功能更强大的str比较操作。

    9.6.5. 不管是If-Modified-Since还是If-None-Match,都是通过防止资源重复传输,来节约用于传输的时间,从而节省带宽。在Cli能使用资源前,仍然至少需要一次,从Cli到Serv的req-res往返。

    9.6.6. 缓存技术功能强大,对于现代web的性能来说极其重要。urllib和Requests两个Cli库,默认请求下,都不会进行缓存,主要工作是在需要时,进行实时Web-HTTP-req,而不是管理缓存来减少网络通信。

    需要一个封装好的库,要求其能在所req的资源,来自某种本地持久化的情况下,使用Expires-head和Cache-control-head、修改时间、Etags,来最小化Cli-req资源的网络延迟及网络流量,必须寻找别的三方库。

    9.7. 传输编码

    理解HTTP传输编码与内容编码间的区别是至关重要的。
    

    9.7.1. 传输编码(transfer encoding)只是一个用于将资源转换为HTTP-res-body的机制。显然,tra-enc的选择,不会对Cli获得的资源有任何影响。

    如,不管Serv的res是通过Content-Length还是区块编码来封帧,Cli接收到的文档/图片都是一样的。发送资源时,可使用原始bytes/为了加快传输速度,使用压缩后的bytes,但最终的资源内容是相同的。
    

    tra-enc只是一种用于数据传输的封装方式,并不会修改真正的数据。

    9.7.2. 最流行的传输编码还是gzip,能接受这种tra-enc的Cli,必须在Accept-Encoding-head中进行声明,且检查res中的Transfer-Encoding-head,确认Serv是否使用了Cli要求的tra-enc。

    GET / HTTP/1.1
    Accept-Encoding: gzip
    ...
    
    HTTP/1.1 200 OK
    Content-Length: 3913
    Transfer-Encoding:gzip
    

    urllib库不支持这一机制。如果要使用压缩形式的tra-enc,需要在自己的代码中代码中检查这些head-mes,然后自己对res-body进行解压缩。

    9.7.3. Requests自动为Accept-Encoding-head声明了gzip和deflate两种tra-enc。

    如果Serv发回的res使用了合适的tra-enc,Requests就会自动解压缩req-body,不仅可对使用Serv支持的tra-enc方式传输的mes进行自动解压缩,还能向用户隐瞒这一过程。

    9.8. 内容协商

    内容类型(content type)和内容编码(content encoding)与tra-enc不同,对终端用户/发送HTTP-req的Cli程序都是完全可见的。决定了要使用哪种文件格式来表示给定的资源,如果选择的格式是文本的话,还决定要使用哪种编码方式将文本代码转化为字节。
    通过HTTP-head,不支持最新PNG图片的旧版浏览器可以声明优先使用GIF和JPG格式,user也可向web浏览器指定一种资源传输使用的首选语言,以下为一个现代web浏览器生成的HTTP-head

    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-head中,先列出的类型和语言优先级最高,权重为1.0,后面列出的权重降为q=0.9/q=0.8,确保Serv知道后面列出的类型和语言,不是第一选择。

    9.8.1. 许多简单的HTTP服务和web会彻底忽略这些HTTP-head,会为资源的每个版本都分配一个独立的URL。

    如,一个web支持英语和法语,首页可能有两个版本,URL分别为/en/index.html和/fr/index.html
    

    RESTful-web服务的文档通常会为不同的返回格式,指定不同的URL查询参数,如?f=json和?f=xml

    9.8.2. HTTP旨在为每个资源提供一个路径。无论使用多少种不同的机器格式/人类语言来生成资源,每个资源都只有一个路径。Serv使用内容协商的HTTP-head来选择资源。

    为什么内容协商会经常被忽略?
    

    9.8.3. 内容协商会使user很难控制用户体验。同时支持英语和法语的web的例子。如果该web使用Accept-Language-head来决定显示的语言,而user却希望使用功能另一种语言,此时Serv就没什么好办法。Serv只能建议user打开web浏览器的控制面板,然后更改默认语言。

    用户使用的浏览器,可能代码写得不好、逻辑不清或很难配置,因此很多web不希望由浏览器来负责语言的选择,直接构造了多个冗余的路径,为web支持的每种语言都分配了一个路径。当接收到user-req时,会检查Accept-Language-head,并据此为浏览器自动选择最适合的语言。自动选择的语言并不是user想要的,这些站点也允许user选择其他语言。

    9.8.4. 内容协商经常被忽略的第二个原因是,HTTP的Cli-API通常难以控制Accept、Accept-Charset、Accept-Language这些HTTP-head。将控制元素放在URL中的一大优点就是,任何人只使用最原始的URL获取工具,也可通过调整URL来控制要访问的资源版本。

    9.8.5. 最后一个问题,内容协商意味着HTTP-Serv必须进行一系列选择之后,才能生成/确定要返回的内容。可假设Serv逻辑始终可以获取Accept-head-mes。不考虑内容协商,Serv的编程通常简单很多

    对于想要支持内容协商的复杂服务,内容协商可帮助减少URL的数量,还允许智能的HTTP-Cli获取根据其需要的数据格式和人类阅读的需求生成的内容。

    9.8.6. User-Agent本来不是内容协商的一部分,只是使用功能有限的特定浏览器的权宜之计。它原本只是针对特定的Cli精心设计了一些处理方法,以便让所有其它Cli都正常无误地访问页面

    两个客户端库urllib和Requests都允许将Accept-head-mes加入到req中,也都支持创建一个自动使用user首选的HTTP-head的Cli。Requests通过Session来实现
    
    >>> s = requests.Session()
    >>> s.headers.update({'Accept-Language': 'en-US,en;q=0.8'})
    

    除非使用另一个值将默认值覆盖,否则像s.get()方法的后续调用,都会使用HTTP-head的默认值
    urllib提供了自己的模式来默认处理函数,注入默认HTTP-head。但相当复杂

    9.9 内容类型

    一旦Serv从Cli检测到了多个Accept-head-mes,并决定了要返回的资源的表达方式,就会相应在res-mes中设置Content-Type-head

    9.9.1. 作为email-mes的一部分,多媒体可通过多种MIME类型表示。内容类型是从这些MIME类型中选择出来的。

    1)text/plain和text/html都是普通类型;
    2)image/git、image/jpg和image/png则是图像类型;
    3)文档以包括application/pdf在内的形式进行传输;
    4)application/octet-stream用于传输原始的字节流,Serv保证不会进一步对这些字节流进行解释

    9.9.2. 处理通过HTTP传输的Content-Type-head时,如果主要类型是text,则又多种编码方式可供Serv选择。要指定将文本传输给Cli所用的编码方式,则只需在Content-Type-head后加上一个分号,再接上用于将文本转换为字节的字符编码方式即可。

    Content-Type: text/html; charset=utf-8
    

    从一系列MIME类型中,扫描搜索特定的Content-Type时,必须先检查该Content-Type中是否包含分号。如包含,先将其分割为两部分。urllib和Requests都不提供这一功能,代码中需要检查内容类型,必须自己根据分号对Content-Type进行分割(通过req-Requests的Response对象,获取解码后的text属性时,Requests会隐式使用内容类型的字符集)

    9.9.3. WebOb库的Response会在默认情况下,对内容类型和字符集进行分割,没有像Content-Type-head的标准形式那样,提供由分号分割的内容类型和字符集,而是提供content_type和charset两个单独的属性。

    9.10. HTTP认证

    发送HTTP请求时,有一个内置认证过程,来确认发送请求的机器/用户的身份。
    当Serv无法通过协议验证用户/认证用户没有权限查看请求的特定资源时,Serv会返回401 Not Authorized,现实中实际上从来不会返回401。如果没有通过认证,Serv可能会返回303 See Other状态码,并将页面转至登录界面。对user来说很有帮助,但Python程序需要区分由于认证失败引起的303 See Other和正常请求访问资源时,重定向引起的303

    9.10.1. 每个HTTP-req都是独立的,与其他req都不相关,即使是同一socket处理的几个连续req,也需要对认证信息进行单独传输。

    这种独立性使代理Serv和和负载均衡器,在任意数量的Serv间分配HTTP-req时得以安全运行,即使所有req都发送到同一个socket,也不会影响安全性。
    

    Serv涉及的第一个认证机制是基本认证(basic Auth),使用该机制的Serv,在返回401-head-mes中,包含了一个realm的str,表示认证域。
    浏览器保存了用于密码和认证域间的对应关系,认证域字符串使得一台Serv能通过不同的密码,来保护文档树的不同部分。Cli在收到返回的401后,重新发送请求,在Authorization-head中指定与认证域对应的username和password,此时就可得到200响应。

    GET / HTTP/1.1
    ...
    HTTP/1.1 401 Unauthorized
    WWW-Authenticate: Basic reaml="engineering team"
    ...
    
    GET / HTTP/1.1
    Authorization: Basic YnJhbmRvbjphGInZG5nbmFod3dhbA==
    ...
    
    HTTP/1.1 200 OK
    

    协议设计者们为了创建HTTPS连接,发明了SSL及TLS,加上了TLS后,使用基本认证,在原则上已经没有任何问题了。要在urllib中使用基本认证,就必须构建一系列对象,并将这些对象传入URLopener中。Requests直接通过一个关键字参数来支持基本认证。

    >>> r = requests.get('http://example.com/api/', auth=('brandon', 'atigdngatwwal'))
    

    使用Requests时,可以实现定义一个Session并进行认证,避免每次调用get()/post()时都进行重复认证。

    >>> s = requests.Session()
    >>> s.auth = 'brandon', 'atigdngnatwwal'
    >>> s.get('http:httpbin.org/basic-auth/brandon/atigdngnatwwal')
    <Response [200]>
    

    无论是Requests还是其他库,都没有实现完整的协议,实现设置的username和pwd都没有绑定到任何特定的认证域。username和pwd只是单向绑定至req,过程中没有实现检测Serv是否需要username和pwd,Serv不会返回401响应,更不会提供认证域。无论是auth关键字,还是等价的Session设置,都只用来帮助user在无需自己进行base-64编码的前提下设置Authorization-head

    9.10.2. 相较于实现完整的基于认证域的协议,现代开发者唯一的目的就是,根据发起req的user/程序的身份,对一个面向DEV的API提供GET/POST-req进行独立认证。一个支持单向认证的HTTP-head足以完成这一任务。

    这一方法的另一个优势:当user已经有足够的理由,确信此次req需要密码时,不会再浪费时间和带宽来获取初始401响应。
    

    9.10.3. 需要进行交互的是一个历史遗留OS,需要对同一Serv上的不同认证域,使用不同密码,Requests库就不行了。DEV需要提供正确的密码和正确的URL。这是为数不多urllib支持,而Requests不支持的有用功能。但真正的基本认证协商已经非常罕见了。

    基于HTTP的认证已经很少使用。使用HTTP认证被证明是一个失败的主张。
    HTTP认证, 给user带来的问题:
    网站的设计者希望使用自己的方式来进行认证。使用协议内置的HTTP认证时,浏览器会跳出一个弹窗---绝对是败笔,破坏了体验。只要输入的username/pwd有误,弹窗就会反复弹出,user却不知道哪里出错,就不知道如何修改

    9.11.1. 从Cli的角度,cookie是一个很难懂的键值对。任何从Serv发送至Cli的成功响应中,都可传输cookie

    GET /login HTTP/1.1
    ...
    HTTP/1.1 200 OK
    Set-Cookie: session-id=d41d8cd98f00b204e9800998ecf8427e; Path=/
    ...
    

    之后,如果Cli还要向Serv发送任何请求,就将接收到的cookie键值对,添加到cookie-head中。

    GET /login HTTP/1.1
    Cookie: session-id=d41d8cd98f00b204e9800998ecf8427e
    

    使得用户可以通过web生成的登录页面来完成身份认证。当提交的登录表单中包含非法的认证信息时,Serv可要求用户重新填写登录表单,根据需要给出许多有用的提示/支持链接,所有这些信息的样式风格,都与网站的其余部分保持统一。
    表单正确提交,Serv就可以进行授权,为该Cli生成一个特有的cookie。之后的所有req中,Cli都可使用这个cookie来通过Serv的身份验证。

    9.11.2.如果登录页面没有真正的web表单,而是使用了Ajax,同一页面内进行登录操作,只要调用的API属于同一主机,仍然可以使用cookie。

    当进行登录的API调用,验证了username和pwd,返回了200 OK及Cookie头后,所有后续的发送至同一网站的req(不仅是API调用,还包括对页面、图片、数据的请求)都可使用cookie来进行身份验证。

    9.11.3. 应将cookie设计为人类无法理解的串。

    9.11.3.1. 可在Serv生成随机的UUID串,指向存储真正username的DB记录;

    9.11.3.2. 也可不使用DB,把cookie设计为一个加密的str,直接在Serv进行解密,并验证user身份。

    如果user可以解析cookie,就可以自己编辑cookie,生成并在之后的req中,提交一些伪造的值,来模拟其他,他们知道username/可猜出username的user。

    使用咖啡店的wifi的任何人,只要得到了某user的cookie的值,就可以模拟该user。
    

    有些web给user提供cookie,只是为了记录user的访问记录。通过cookie来追踪user在该web的访问行为。在user浏览时,收集到的访问历史,就被应用到了定向广告中。user之后使用username进行了登录,web就会将浏览历史信息,保存到永久的账户历史中。

    9.11.5. 许多user定制的HTTP服务,必须使用cookie来追踪user的身份,保证user通过认证,才能成功运行。使用urllib跟踪cookie需要用到,面向对象的思想。在Requests中,如果创建并始终使用Session对象,那么cookie追踪是自动进行的。

    9.12.连接、Keep-Alive和httplib

    9.12.1. 要打开一个TCP连接,需要经过三次握手。如果连接已经打开,就可以避免三次连接的过程。

    甚至促使早期HTTP允许在浏览器,先后下载HTTP资源、JS、CSS和图片的过程中,始终保持打开连接。

    9.12.2. 当TLS出现并成为所有HTTP连接的最佳实践后,建立新连接的花销变得更大了,增加了连接复用带来的好处。

    9.12.3. HTTP/1.1版本的协议,在默认设置下,会在req完成后,保持HTTP连接处于打开状态。

    Cli和Serv都可指定Connection:close,在一次req完成后,关闭连接;否则,就可使用单个TCP连接,根据Cli的需要,不断从Serv获取资源。
    web浏览器经常会对一个web-site同时建立4个/更多TCP连接,这样就可并行下载一个页面,及其所有支持文件和图像,尽快将页面呈现到用户眼前。
    urllib没有提供对连接复用的支持。使用标准库在同一socket进行两次req,只能使用更底层的httplib模块

    >>> import http.client
    >>> h = http.client.HTTPConnection('localhost:8000')
    >>> h.request('GET', '/ip')
    >>> r = h.getresponse()
    >>> r.status
    200
    >>> h.request('GET', '/user-agent')
    >>> r = h.getresponse()
    >>> r.status
    200
    

    使用HTTPConnection对象,进行第二次req时,不会返回err,而是隐式建立一个新的TCP连接,来代替之前建立的连接。HTTPSConnection类提供了经过TLS保护后的HTTPConnection。
    与urllib不同的是,Requests库的Session对象,使用了三方的urllib3包。会维护一个连接池,保存与最近通信的HTTP-Serv的处于打开状态的链接。在向同一web-req其他资源时,就可自动重用连接池中保存的连接了。

    9.13. 小结

    9.13.1. HTTP协议用于根据保存资源的hostname和路径来获取资源,标准库的urllib-Cli提供了在简单情况下,获取资源所需的基本功能。比起Requests,urllib功能弱很多。Requests提供了许多urllib没有的特性,是web上最热门的Python库。如果想从网上获取资源,Requests是最佳选择。

    9.13.2. HTTP运行于80-port,通过明文发送。通过TLS保护的HTTP(HTTPS)在443-port运行。Cli的req和Serv的res在传输过程中,都使用相同的基本结构:首行信息,然后是由名字+值组成的HTTP-head-mes,最后一个是空行,然后是可选的mes-body。mes-body可使用多种不同的方式进行编码和分割。Cli总是先发送请求,等待Serv返回res。

    9.13.3. 最常用的HTTP方法,是用于获取资源的GET和用于更新Serv-mes的POST。除了GET和POST,还有其他方法,本质上都与GET/POST类似。Serv在每个res中都会返回一个状态码,表示req-success、fail/需要Cli重定向载入另一个资源。

    9.13.4. HTTP的设计,采用了像同心圆一样的分层结构。可对head-mes进行缓存,将资源存储在Cli的缓存中,这样可重复使用资源,避免不必要的重复获取。这些缓存的head-mes也可避免Serv重复发送没有修改过的资源。这两种优化方法,对繁忙站点的性能至关重要。

    9.13.5. 内容协商可保证,根据Cli和user的真实偏好,来决定返回的数据格式和语言。实际应用中,内容协商会带来一些问题,使得它没有得到广泛应用。内置的HTTP认证在交互设计上很糟糕,已经被自定义的登录页面和cookie替代。在使用TLS保护的API时,有时还会用基本认证。

    9.13.6. HTTP/1.1的连接,在默认情况下,是保持打开并且可以复用的,Requests库也在需要的时候,精心提供了这一功能

  • 相关阅读:
    异常作业
    多态作业
    封装和继承作业
    类和对象作业
    多重循环、方法作业
    选择语句+循环语句作业
    数据类型和运算符作业
    初识Java作业
    C 数据结构堆
    C基础 旋转数组查找题目
  • 原文地址:https://www.cnblogs.com/wangxue533/p/12162149.html
Copyright © 2011-2022 走看看