使用 Twisted Web 客户端¶
概述¶
本文档介绍如何使用 Twisted Web 中包含的 HTTP 客户端。阅读完本文档后,您应该能够使用 Twisted Web 发出 HTTP 和 HTTPS 请求。您将能够指定请求方法、标头和主体,并且您将能够检索响应代码、标头和主体。
本文档还解释了一些更高级的功能,包括代理、自动内容编码协商和 Cookie 处理。
先决条件¶
本文档假设您熟悉 Deferreds 和 Failures 以及 生产者和消费者 。它还假设您熟悉 HTTP 的基本概念,例如请求和响应、方法、标头和消息主体。本文档的 HTTPS 部分还假设您对 SSL 有所了解,并且已经阅读了有关 在 Twisted 中使用 SSL 的内容。
代理¶
发出请求¶
twisted.web.client.Agent
类是客户端 API 的入口点。使用 request
方法发出请求,该方法将请求方法、请求 URI、请求标头和可以生成请求主体(如果有)的对象作为参数。代理负责连接设置。因此,它需要一个反应堆作为其初始化程序的参数。创建代理并使用它发出请求的示例可能如下所示
from twisted.internet import reactor
from twisted.web.client import Agent
from twisted.web.http_headers import Headers
agent = Agent(reactor)
d = agent.request(
b"GET",
b"http://httpbin.com/anything",
Headers({"User-Agent": ["Twisted Web Client Example"]}),
None,
)
def cbResponse(ignored):
print("Response received")
d.addCallback(cbResponse)
def cbShutdown(ignored):
reactor.stop()
d.addBoth(cbShutdown)
reactor.run()
可能很明显,这会向 example.com
上的 Web 服务器发出针对 /
的新的 GET 请求。 Agent
负责将主机名解析为 IP 地址,并在端口 80(对于 HTTP URI)、端口 443(对于 HTTPS URI)或 URI 本身中指定的端口号上连接到它。它还负责在之后清理连接。此代码发送一个请求,该请求包含一个自定义标头 User-Agent 。传递给 Agent.request
的最后一个参数是 None
,因此请求没有主体。
发送包含主体的请求需要将提供 twisted.web.iweb.IBodyProducer
的对象传递给 Agent.request
。此接口扩展了更通用的 IPushProducer
,通过添加一个新的 length
属性,并对生产者和消费者交互的方式添加一些约束。
length 属性必须是非负整数或常量
twisted.web.iweb.UNKNOWN_LENGTH
。如果长度已知,它将用于指定请求中 Content-Length 标头的值。如果长度未知,则应将属性设置为UNKNOWN_LENGTH
。由于更多服务器支持 Content-Length ,因此如果可以提供长度,则应提供。IBodyProducer
实现需要一个额外的方法:startProducing
。此方法用于将消费者与生产者关联。它应该返回一个Deferred
,该Deferred
在所有数据都已生成时触发。IBodyProducer
实现永远不应该调用消费者的unregisterProducer
方法。相反,当它生成完所有要生成的数据时,它应该只触发startProducing
返回的Deferred
。
有关 IBodyProducer
实现要求的更多详细信息,请参阅 API 文档。
这是一个将内存中的字符串写入消费者的简单 IBodyProducer
实现
from zope.interface import implementer
from twisted.internet.defer import succeed
from twisted.web.iweb import IBodyProducer
@implementer(IBodyProducer)
class BytesProducer:
def __init__(self, body):
self.body = body
self.length = len(body)
def startProducing(self, consumer):
consumer.write(self.body)
return succeed(None)
def pauseProducing(self):
pass
def stopProducing(self):
pass
此生产者可用于发出包含主体的请求
from bytesprod import BytesProducer
from twisted.internet import reactor
from twisted.web.client import Agent
from twisted.web.http_headers import Headers
agent = Agent(reactor)
body = BytesProducer(b"hello, world")
d = agent.request(
b"POST",
b"http://httpbin.org/post",
Headers(
{
"User-Agent": ["Twisted Web Client Example"],
"Content-Type": ["text/x-greeting"],
}
),
body,
)
def cbResponse(ignored):
print("Response received")
d.addCallback(cbResponse)
def cbShutdown(ignored):
reactor.stop()
d.addBoth(cbShutdown)
reactor.run()
如果您想上传文件或只是在字符串中有一些数据,则不必复制 StringProducer
。相反,您可以使用 FileBodyProducer
。此 IBodyProducer
实现适用于任何类似文件的对象(因此如果您的上传数据已在内存中作为字符串,请使用 StringIO
与它一起使用);其想法与上一个示例中的 StringProducer
相同,但有一些额外的代码来仅以服务器能接受的速度发送数据。
from io import BytesIO
from twisted.internet import reactor
from twisted.web.client import Agent, FileBodyProducer
from twisted.web.http_headers import Headers
agent = Agent(reactor)
body = FileBodyProducer(BytesIO(b"hello, world"))
d = agent.request(
b"GET",
b"http://example.com/",
Headers(
{
"User-Agent": ["Twisted Web Client Example"],
"Content-Type": ["text/x-greeting"],
}
),
body,
)
def cbResponse(ignored):
print("Response received")
d.addCallback(cbResponse)
def cbShutdown(ignored):
reactor.stop()
d.addBoth(cbShutdown)
reactor.run()
FileBodyProducer
在不再需要它时关闭文件。
如果连接或请求花费的时间过长,您可以取消 Agent.request
方法返回的 Deferred
。这将中止连接,并且 Deferred
将使用 CancelledError
错误回调。
接收响应¶
到目前为止,这些示例演示了如何发出请求。但是,它们忽略了响应,除了表明它是一个 Deferred
,似乎在收到响应时触发。接下来,我们将介绍该响应是什么以及如何解释它。
Agent.request
与大多数返回 Deferred
的 API 一样,可以返回一个 Deferred
,该 Deferred
使用 Failure
触发。如果请求以某种方式失败,这将通过错误反映出来。这可能是由于查找主机 IP 地址时出现问题,或者可能是因为 HTTP 服务器没有接受连接,或者可能是因为解析响应时出现问题,或者可能是由于导致无法接收响应的任何其他问题。它不包括具有错误状态的响应。
但是,如果请求成功,则 Deferred
将使用 Response
触发。这发生在收到所有响应标头后。它发生在处理任何响应主体(如果有)之前。 Response
对象具有几个属性,用于提供响应信息:它的代码、版本、短语和标头,以及要期望的主体长度。除了这些之外, Response
还包含对它所响应的 request
的引用;请求上一个特别有用的属性是 absoluteURI
:发出请求的绝对 URI。 Response
对象有一个方法可以使响应主体可用: deliverBody
。使用响应对象的属性和此方法,以下是一个显示对请求的部分响应的示例
from pprint import pformat
from twisted.internet import reactor
from twisted.internet.defer import Deferred
from twisted.internet.protocol import Protocol
from twisted.web.client import Agent
from twisted.web.http_headers import Headers
class BeginningPrinter(Protocol):
def __init__(self, finished):
self.finished = finished
self.remaining = 1024 * 10
def dataReceived(self, bytes):
if self.remaining:
display = bytes[: self.remaining]
print("Some data received:")
print(display)
self.remaining -= len(display)
def connectionLost(self, reason):
print("Finished receiving body:", reason.getErrorMessage())
self.finished.callback(None)
agent = Agent(reactor)
d = agent.request(
b"GET",
b"http://httpbin.com/anything/",
Headers({"User-Agent": ["Twisted Web Client Example"]}),
None,
)
def cbRequest(response):
print("Response version:", response.version)
print("Response code:", response.code)
print("Response phrase:", response.phrase)
print("Response headers:")
print(pformat(list(response.headers.getAllRawHeaders())))
finished = Deferred()
response.deliverBody(BeginningPrinter(finished))
return finished
d.addCallback(cbRequest)
def cbShutdown(ignored):
reactor.stop()
d.addBoth(cbShutdown)
reactor.run()
本示例中的 BeginningPrinter
协议被传递给 Response.deliverBody
,然后响应主体在到达时被传递到其 dataReceived
方法。当主体完全传递后,协议的 connectionLost
方法被调用。检查传递给 connectionLost
的 Failure
很重要。如果响应主体已完全接收,则故障将包装一个 twisted.web.client.ResponseDone
异常。这表明已知所有数据都已接收。故障也可能包装一个 twisted.web.http.PotentialDataLoss
异常:这表明服务器对响应进行了分帧,因此无法知道何时已接收整个响应主体。只有 HTTP/1.0 服务器应该以这种方式运行。最后,异常可能是其他类型,表明由于某种原因(连接丢失、内存错误等)保证数据丢失。
就像与 TCP 连接关联的协议被赋予一个传输一样,传递给 deliverBody
的协议也将被赋予一个传输。但是,由于在这个请求阶段向连接写入更多数据毫无意义,因此传输仅提供 IPushProducer
。这允许协议控制响应数据的流:调用传输的 pauseProducing
方法将暂停传递;稍后调用 resumeProducing
将恢复传递。如果决定不再需要响应主体的其余部分,则可以使用 stopProducing
永久停止传递;在此之后,协议的 connectionLost
方法将被调用。
需要牢记的一件重要的事情是,只有在调用 Response.deliverBody
之后才会从连接中读取主体。这也意味着连接将保持打开状态,直到完成此操作(并读取主体)。因此,一般来说,任何带有主体的响应都必须使用 deliverBody
读取该主体。如果应用程序对主体不感兴趣,它应该发出HEAD请求或使用立即在其传输上调用 stopProducing
的协议。
如果响应主体不会被增量使用,那么可以使用 readBody
将主体作为字节字符串获取。此函数返回一个 Deferred
,该函数在请求完成后使用主体触发;取消此 Deferred
将立即关闭与 HTTP 服务器的连接。
from pprint import pformat
from sys import argv
from twisted.internet.task import react
from twisted.web.client import Agent, readBody
from twisted.web.http_headers import Headers
def cbRequest(response):
print("Response version:", response.version)
print("Response code:", response.code)
print("Response phrase:", response.phrase)
print("Response headers:")
print(pformat(list(response.headers.getAllRawHeaders())))
d = readBody(response)
d.addCallback(cbBody)
return d
def cbBody(body):
print("Response body:")
print(body)
def main(reactor, url=b"http://httpbin.org/get"):
agent = Agent(reactor)
d = agent.request(
b"GET", url, Headers({"User-Agent": ["Twisted Web Client Example"]}), None
)
d.addCallback(cbRequest)
return d
react(main, argv[1:])
自定义您的 HTTPS 配置¶
无论请求 URI 的方案是HTTP还是HTTPS,您到目前为止阅读的所有内容都适用。
从 15.0.0 版本开始,Twisted 的 Agent
HTTP 客户端默认情况下会根据您的平台信任存储验证 https
URL。
注意
如果您使用的是早期版本,请升级,因为这是一个关键的安全功能!
注意
只有 Agent
以及使用它的东西会验证 HTTPS。不要使用 getPage
检索 HTTPS URL;虽然我们还没有删除所有使用它的示例,但我们不鼓励使用它。
您应该 pip install twisted[tls]
才能获得正确执行 TLS 所需的所有依赖项。 安装说明 提供了有关可选依赖项以及如何安装它们的更多详细信息,这些信息可能会有用。
对于某些用途,您可能需要自定义 Agent 对 HTTPS 的使用;例如,提供客户端证书,或为内部网络使用自定义证书颁发机构。
在这里,我们只向您展示如何将相关的 TLS 配置注入 Agent,因此我们将使用最简单的示例,而不是更实用但更复杂的示例。
Agent
的构造函数接受一个可选的第二个参数,它允许您自定义其相对于 HTTPS 的行为。此处传递的对象必须提供 twisted.web.iweb.IPolicyForHTTPS
接口。
使用非常有用的 badssl.com
Web API,我们将构建一个无法验证的请求,因为证书中的主机名不正确。以下 Python 程序在作为 python example.py https://wrong.host.badssl.com/
运行时应该会产生验证错误。
import sys
from twisted.internet.task import react
from twisted.web.client import Agent, ResponseFailed
@react
def main(reactor):
agent = Agent(reactor)
requested = agent.request(b"GET", sys.argv[1].encode("ascii"))
def gotResponse(response):
print(response.code)
def noResponse(failure):
failure.trap(ResponseFailed)
print(failure.value.reasons[0].getTraceback())
return requested.addCallbacks(gotResponse, noResponse)
在运行上述程序时看到的验证错误是由于“wrong.host.badssl.com
”(从 URL 派生的预期主机名)与“badssl.com
”(服务器提供的证书中包含的主机名)不匹配造成的。在我们假定的场景中,我们希望构建一个 Agent
,它通常验证 HTTPS 证书,_除了_ 此主机。对于 "wrong.host.badssl.com"
,我们希望手动提供正确的主机名来检查证书,以解决此问题。为此,我们将提供我们自己的策略,该策略根据主机名创建不同的 TLS 配置,如下所示
import sys
from zope.interface import implementer
from twisted.internet.task import react
from twisted.internet.ssl import optionsForClientTLS
from twisted.web.iweb import IPolicyForHTTPS
from twisted.web.client import Agent, ResponseFailed, BrowserLikePolicyForHTTPS
@implementer(IPolicyForHTTPS)
class OneHostnameWorkaroundPolicy(object):
def __init__(self):
self._normalPolicy = BrowserLikePolicyForHTTPS()
def creatorForNetloc(self, hostname, port):
if hostname == b"wrong.host.badssl.com":
hostname = b"badssl.com"
return self._normalPolicy.creatorForNetloc(hostname, port)
@react
def main(reactor):
agent = Agent(reactor, OneHostnameWorkaroundPolicy())
requested = agent.request(b"GET", sys.argv[1].encode("ascii"))
def gotResponse(response):
print(response.code)
def noResponse(failure):
failure.trap(ResponseFailed)
print(failure.value.reasons[0].getTraceback())
return requested.addCallbacks(gotResponse, noResponse)
现在,调用 python example.py https://wrong.host.badssl.com/
将愉快地为我们提供一个 200
状态代码;但是,使用 https://expired.badssl.com/
或 https://self-signed.badssl.com/
或任何其他错误主机名运行它仍然应该会产生错误。
注意
上面介绍的技术不会忽略不匹配的主机名,而是专门提供错误的主机名,以便预期到错误配置。Twisted 没有记录任何禁用验证的功能,因为这会使 TLS 变得毫无用处;相反,我们强烈建议您弄清楚您需要连接的哪些属性并验证这些属性。
使用此 TLS 策略机制,您可以自定义 Agent 以使用 Twisted 支持的任何 TLS 功能,包括上面给出的示例;客户端证书、备用信任根以及其他功能。有关 Twisted 中客户端 TLS 配置的更多详细信息,请查看 optionsForClientTLS
API 的文档以及本文档的 在 Twisted 中使用 SSL 章节。
HTTP 持久连接¶
HTTP 持久连接使用相同的 TCP 连接来发送和接收多个 HTTP 请求/响应。这减少了延迟和 TCP 连接建立开销。
twisted.web.client.Agent
的构造函数接受一个可选参数池,该池应该是 HTTPConnectionPool
的实例,它将用于管理连接。如果池使用参数 persistent
设置为 True
(默认值)创建,它不会在请求完成后关闭连接,而是将它们保存在其缓存中以供重复使用。
以下是一个通过持久连接发送请求的示例
from twisted.internet import reactor
from twisted.internet.defer import Deferred, DeferredList
from twisted.internet.protocol import Protocol
from twisted.web.client import Agent, HTTPConnectionPool
class IgnoreBody(Protocol):
def __init__(self, deferred):
self.deferred = deferred
def dataReceived(self, bytes):
pass
def connectionLost(self, reason):
self.deferred.callback(None)
def cbRequest(response):
print('Response code:', response.code)
finished = Deferred()
response.deliverBody(IgnoreBody(finished))
return finished
pool = HTTPConnectionPool(reactor)
agent = Agent(reactor, pool=pool)
def requestGet(url):
d = agent.request('GET', url)
d.addCallback(cbRequest)
return d
# Two requests to the same host:
d = requestGet('http://localhost:8080/foo').addCallback(
lambda ign: requestGet("http://localhost:8080/bar"))
def cbShutdown(ignored):
reactor.stop()
d.addCallback(cbShutdown)
reactor.run()
在这里,两个请求都发送到同一个主机,一个接一个。在大多数情况下,将使用相同的连接进行第二个请求,而不是在使用非持久池时使用两个不同的连接。
对同一服务器的多个连接¶
twisted.web.client.HTTPConnectionPool
实例具有一个名为 maxPersistentPerHost
的属性,它限制了对同一服务器的缓存持久连接的数量。默认值为 2。这仅在 persistent
选项为 True 时有效。您可以像下面这样更改值
from twisted.web.client import HTTPConnectionPool
pool = HTTPConnectionPool(reactor, persistent=True)
pool.maxPersistentPerHost = 1
使用默认值 2,池最多保留两个对同一主机的连接。最终,缓存的持久连接将被关闭,默认情况下在 240 秒后;您可以使用池的 cachedConnectionTimeout
属性更改此超时值。要强制关闭所有连接,请使用 closeCachedConnections
方法。
自动重试¶
如果请求在没有收到响应的情况下失败,并且该请求是希望可以重试而不会产生任何副作用的请求(例如,使用 GET 方法的请求),那么在通过先前缓存的持久连接发送请求时,它将自动重试。您可以通过将 retryAutomatically
设置为 False
来禁用此行为。请注意,每个请求只会重试一次。
跟随重定向¶
Agent
本身不会跟随 HTTP 重定向(状态码为 301、302、303、307 且具有 location
标头字段的响应)。您需要使用 twisted.web.client.RedirectAgent
类来实现。它实现了 RFC 的严格行为,这意味着它只会在 GET
和 HEAD
请求上将 301 和 302 重定向为 307。
以下示例展示了如何使用支持重定向的代理。
from twisted.python.log import err
from twisted.web.client import Agent, RedirectAgent
from twisted.internet import reactor
def display(response):
print("Received response")
print(response)
def main():
agent = RedirectAgent(Agent(reactor))
d = agent.request("GET", "http://example.com/")
d.addCallbacks(display, err)
d.addCallback(lambda ignored: reactor.stop())
reactor.run()
if __name__ == "__main__":
main()
相反,twisted.web.client.BrowserLikeRedirectAgent
实现了更宽松的行为,它与 Web 浏览器非常相似;换句话说,301 和 302 POST
重定向被视为 303,这意味着在进行重定向请求之前,方法将更改为 GET
。
如前所述,Response
包含对它所响应的 request
以及先前接收到的 response
的引用,可以通过 previousResponse
访问。在大多数情况下,不会有先前的响应,但在 RedirectAgent
的情况下,可以通过从响应到响应地跟踪先前的响应来获取响应历史记录。
使用 HTTP 代理¶
为了能够使用代理与代理一起使用,您可以使用 twisted.web.client.ProxyAgent
类。它支持与 Agent
相同的接口,但将代理的端点作为初始化参数。这专门用于与实现 HTTP 协议代理变体的服务器通信;对于其他类型的代理,您需要使用 Agent.usingEndpointFactory
(请参阅下面的文档)。
以下示例演示了如何使用在 localhost:8000 上运行的 HTTP 代理。
from twisted.python.log import err
from twisted.web.client import ProxyAgent
from twisted.internet import reactor
from twisted.internet.endpoints import TCP4ClientEndpoint
def display(response):
print("Received response")
print(response)
def main():
endpoint = TCP4ClientEndpoint(reactor, "localhost", 8000)
agent = ProxyAgent(endpoint)
d = agent.request("GET", "https://example.com/")
d.addCallbacks(display, err)
d.addCallback(lambda ignored: reactor.stop())
reactor.run()
if __name__ == "__main__":
main()
有关它们的工作原理以及 twisted.internet.endpoints
API 文档的更多信息,请参阅 端点文档,以了解其他类型的端点。
自动内容编码协商¶
twisted.web.client.ContentDecoderAgent
添加了对发送 Accept-Encoding 请求标头和解释 Content-Encoding 响应标头的支持。这些标头允许服务器以某种方式对响应主体进行编码,通常使用某种压缩方案来节省传输成本。 ContentDecoderAgent
将此功能作为现有代理实例的包装器提供。与一个或多个解码器对象(例如 twisted.web.client.GzipDecoder
)一起,此包装器会自动协商要使用的编码并相应地解码响应主体。对于使用此类代理的应用程序代码,传递的数据没有明显的区别。
from twisted.internet import reactor
from twisted.internet.defer import Deferred
from twisted.internet.protocol import Protocol
from twisted.python import log
from twisted.web.client import Agent, ContentDecoderAgent, GzipDecoder
class BeginningPrinter(Protocol):
def __init__(self, finished):
self.finished = finished
self.remaining = 1024 * 10
def dataReceived(self, bytes):
if self.remaining:
display = bytes[: self.remaining]
print("Some data received:")
print(display)
self.remaining -= len(display)
def connectionLost(self, reason):
print("Finished receiving body:", reason.type, reason.value)
self.finished.callback(None)
def printBody(response):
finished = Deferred()
response.deliverBody(BeginningPrinter(finished))
return finished
def main():
agent = ContentDecoderAgent(Agent(reactor), [(b"gzip", GzipDecoder)])
d = agent.request(b"GET", b"http://httpbin.org/gzip")
d.addCallback(printBody)
d.addErrback(log.err)
d.addCallback(lambda ignored: reactor.stop())
reactor.run()
if __name__ == "__main__":
main()
实现对新内容编码的支持就像编写一个新的类(如 GzipDecoder
)一样简单,该类可以使用新编码来解码响应。由于广泛使用的内容编码并不多,因此 gzip 是 Twisted 本身支持的唯一编码。
连接到非标准目标¶
通常,您希望您的 HTTP 客户端直接与 Web 服务器打开 TCP 连接。但是,有时能够以其他方式连接非常有用,例如通过 SOCKS 代理连接发出 HTTP 请求或连接到侦听 UNIX 套接字的服务器。为此,有一个名为 Agent.usingEndpointFactory
的备用构造函数,它接受一个 endpointFactory
参数。此参数必须提供 twisted.web.iweb.IAgentEndpointFactory
接口。请注意,在与 HTTP 代理(即实现 HTTP 代理特定变体的服务器)通信时,您应该使用 ProxyAgent
- 请参阅上面的文档。
# Copyright (c) Twisted Matrix Laboratories.
# See LICENSE for details.
"""
Send a HTTP request to Docker over a Unix socket.
Will probably need to be run as root.
Usage:
$ sudo python endpointconstructor.py [<docker API path>]
"""
from sys import argv
from zope.interface import implementer
from twisted.internet.endpoints import UNIXClientEndpoint
from twisted.internet.task import react
from twisted.web.client import Agent, readBody
from twisted.web.iweb import IAgentEndpointFactory
@implementer(IAgentEndpointFactory)
class DockerEndpointFactory:
"""
Connect to Docker's Unix socket.
"""
def __init__(self, reactor):
self.reactor = reactor
def endpointForURI(self, uri):
return UNIXClientEndpoint(self.reactor, b"/var/run/docker.sock")
def main(reactor, path=b"/containers/json?all=1"):
agent = Agent.usingEndpointFactory(reactor, DockerEndpointFactory(reactor))
d = agent.request(b"GET", b"unix://localhost" + path)
d.addCallback(readBody)
d.addCallback(print)
return d
react(main, argv[1:])
结论¶
您现在应该了解 Twisted Web HTTP 客户端的基础知识。特别是,您应该了解
如何使用任意方法、标头和主体发出请求。
如何访问响应版本、代码、短语、标头和主体。
如何存储、发送和跟踪 Cookie。
如何控制响应主体的流式传输。
如何启用 HTTP 持久连接,以及如何控制连接数量。