python requests库流程简析

0.072字数 1815阅读 751

今天重新梳理网络编程的时候,想到对于部分应用,他们的数据流是按照http协议,中间经过其他协议层,最后通过底层的物理层到达服务器的,这使我想到了Requests库。Requests库就像我们常用的浏览器,搞清楚了Requests的实现,就基本搞清楚了网络编程客户端方向的数据处理流程,以及经常提到的网络协议在编程过程中的具体体现和代表。

现在就以requests库为例,TCP/IP协议和socket,梳理一下数据流从应用层经过哪些函数,最后到达链路层,并最后通过链路层到达服务器的。
代码如下:

import requests
res = request.get("www.baidu.com" )

上面简单的两句代码就完成了我们通过浏览器访问www.baidu.com的功能。既然requests库的重要功能之一就是作为HTTP客户端,那么它传输的数据应该是一个按照HTTP协议组织的数据,这个数据如何生成,其实就是一个Request数据结构。
request.get其实就是调用api模块中的get方法,get方法是调用api模块中的request方法,而request方法调用的是session模块中Session对象中的request方法,而request方法中会根据request.get方法传进来的参数生成一个Request对象,然后将Request对象作为参数并调用Session对象中的prepare_request函数进一步处理生成请求需要的request对象,这时候的request对象可以理解为一个基础的数据存储,后面会根据具体的请求协议再次封装。

对于上面的例子,下一步就是选用合适的适配器,依据request封装的请求数据并按照对应的访问协议,将request对象数据再次封装成相应协议的数据格式,本例中就是http协议。在requests库中的体现如下。

在Session完成基本request封装之后,就开始调用自身的send函数开始请求,请求的真正操作放是依据请求协议调用对应的适配器,请求协议体现在哪儿呢?其实就是我们在使用request.get("www.baidu.com" )时指定了,因此,这里就是http协议,因此Session中获取的适配器就是HTTPAdapter类,所以Session对象中的send函数中调用的adapter.send()其实就是HTTPAdapter的send函数,HTTPAdapter.send函数中就已经开始真正向服务器发起连接,然后提交请求数据了。

HTTPAdapter.send函数提交请求,提交之前必须连接服务器,因此调用了urllib3的PoolManager,之所以采用PoolManager,其实跟常用的线程池的思想一样,减少socket的创建连接,提高socket的复用。urllib3库底层其实是调用了socket库进行连接,我们知道socket只是tcp/ip协议的实现,所以,到这里,数据已经从应用层开始流到了网络层。

前面说道HTTPAdapter.send会先去调用HTTPAdapter.get_connection从连接池获取连接,这里的连接池就是urllib3的PoolManager。因此HTTPAdapter.get_connection通过调用PoolManager.connection_from_url函数从requests连接池中获取对应协议的连接池对象。这里的连接池对象其实就是PoolManager调用了urllib3中的connectionpool模块中的HTTPConnectionPool创建的对象,所以最后后面的conn.urlopen其实就是HTTPConnectionPool类中的urlopen。这个连接池中的连接其实是根据ConnectionCls(也就是连接类型)去建立连接的,这里是HTTPConnection对象,请求完成后将HTTPConnection对象放入pool中以便下次复用。

查看requests中的urllib3的connection模块,发现最后中连接就是调用socket库建立socket连接。对于网络请求,我们通常提交的都是域名,而socket需要的是IP,而且我们也知道,浏览器在请求服务器的时候也要经过dns查询,获取到服务器的IP,那么对于requests库,哪一步的哪个函数做了这部分的事情呢?答案是socket库中的getaddrinfo函数。
看看getaddrinfo函数说明

socket.getaddrinfo(host,  port, family=0, socktype=0, proto=0, flags=0)    
#根据给定的参数host/port,相应的转换成一个包含用于创建socket对象的五元组,    
#参数host为域名,以字符串形式给出代表一个IPV4/IPV6地址或者None.    
#参数port如果字符串形式就代表一个服务名,比如“http”"ftp""email"等,或者为数字,或者为None    
#参数family为地主族,可以为AF_INET  ,AF_INET6 ,AF_UNIX.    
#参数socketype可以为SOCK_STREAM(TCP)或者SOCK_DGRAM(UDP)    
#参数proto通常为0可以直接忽略    
#参数flags为AI_*的组合,比如AI_NUMERICHOST,它会影响函数的返回值    
#附注:给参数host,port传递None时建立在C基础,通过传递NULL。    
#该函数返回一个五元组(family, socktype, proto, canonname, sockaddr),同时第五个参数sockaddr也是一个二元组(address, port)  

到这里,也就是说HTTPAdapter.get_connection从requests的连接池中取出对应的连接池对象,这里是HTTPConnectionPool对象,然后这个HTTPConnectionPool连接池对象里面有一个pool对象,其实是一个deque,保存着前面请求时使用的socket连接对象。

在requests的代码调用中,HTTPAdapter.send函数里面调用的conn.urlopen()访问服务器其实就是HTTPConnectionPool类的urlopen,因此,接下来关注HTTPConnectionPool类urlopen即可。(简单的总结下来,adapter.send函数先根据协议从连接池中获取对应的连接池对象,再通过连接池对象的urlopen去访问服务器,而连接池对象中有对应的socket连接池。)

而通过分析requests库中urllib3的connectionpool模块中的HTTPConnectionPool类的urlopen对象发现,这个urlopen函数调用了HTTPConnectionPool类的_make_request函数,这个函数会调用HTTPConnectionPool对象连接池中的HTTPConnection对象的request函数,而HTTPConnection对象其实就是标准的httplib库中的HTTPConnection类,这个从requests的urllib3/connection模块的开头的import语句也能得到验证(from httplib import HTTPConnection as _HTTPConnection)

总结:
requests.get的内部调用流程:
requests.get -> request() -> Session.request -> Session.send ->adapter.send -> HTTPConnectionPool -> HTTPConnection
其实就是requests库是封装了urllib3库,urllib3自己封装了连接池,并调用httplib库(这个库的关键就是HTTPConnection,这个类包含了底层的socket连接和request请求方法),httplib库封装了底层的socket库。 dns的解析其实就是socket库中的getaddrinfo函数。

socket:最基础的socket编程库,是TCP/IP协议最直接的实现,实现端对端的网络数据传输
httplib:基于socket库,是最基础最底层的http库,主要是将数据按照http协议组织,然后创建socket连接,将封装的数据发往服务端。
urllib:基于httplib库,主要对url的解析和编码做进一步的处理,比如代理的处理,url编码函数urlencode,同时增加retrival函数实现下载,并缓存,如果在同一个进程中多次请求相同的url,会直接返回缓存,缓存只来源于retrieval函数,urlopen不缓存。
urllib3:基于httplib库,他比urllib更高级的地方在于用PoolManager实现了socket连接复用和线程安全,提高了了效率
requests:基于urllib3,但是比urllib3更高级的是实现了session对象,用session对象保存一些数据状态,进一步提高效率

推荐阅读更多精彩内容