与端点建立连接¶
简介¶
在网络中,可以将任何给定的连接视为一根长线,连接着两个点。在这根线的长度上可能会发生很多事情 - 路由器、交换机、网络地址转换等等,但这些通常对跨越它传递数据的应用程序来说是不可见的。Twisted 努力使“线”的本质尽可能透明,使用高度抽象的接口来传递和接收数据,例如 ITransport
和 IProtocol
.
但是,应用程序不能完全忽略这条线。特别是,它必须做一些事情来启动连接,为此,它必须识别线的端点。端点角色有不同的名称 - “发起者”和“响应者”、“连接器”和“监听器”,或“客户端”和“服务器” - 但共同的主题是连接的一端等待某人连接到它,而另一端则进行连接。
在 Twisted 10.1 中,引入了几个新的接口来描述面向流连接的每个角色:IStreamServerEndpoint
和 IStreamClientEndpoint
。在这种情况下,“流”指的是将连接视为连续的字节流而不是一系列离散数据报的端点:TCP 是“流”协议,而 UDP 是“数据报”协议。
构建和使用端点¶
在 编写服务器 和 编写客户端 中,我们都介绍了端点的基本用法;您构建一个合适的服务器或客户端端点类型,然后调用 listen
(对于服务器)或 connect
(对于客户端)。
在这两个教程中,我们都直接构建了特定类型的端点。但是,在大多数程序中,您希望允许用户指定监听或连接的位置,以一种允许用户请求不同的策略的方式,而无需调整您的程序。为了允许这样做,您应该使用 clientFromString
或 serverFromString
.
没什么大不了的¶
每种类型的端点只是一个接口,只有一个方法,该方法接受一个参数。 serverEndpoint.listen(factory)
将使用您的协议工厂开始在该端点上监听,而 clientEndpoint.connect(factory)
将启动一次连接尝试。但是,这些 API 中的每一个都返回一个值,这可能很重要。
但是,如果您还没有,您应该非常熟悉 Deferred,因为它们是由 connect
和 listen
方法返回的,以指示连接何时已连接或监听端口何时已启动并运行。
服务器和停止¶
IStreamServerEndpoint.listen
返回一个 Deferred
,它会使用一个 IListeningPort
来触发。请注意,此 Deferred 可能会出现错误。这种错误的最常见原因是另一个程序已经在使用请求的端口号,但确切的原因可能会因您正在监听的端点类型而异。如果您收到此类错误,则意味着您的应用程序实际上并未监听,并且不会接收任何传入连接。在这种情况下,尤其是在您只有一个监听端口的情况下,务必以某种方式提醒服务器管理员!
还要注意,一旦成功,它将永远继续监听。如果您需要出于某种原因停止监听,以响应除完全服务器关闭(reactor.stop
和/或 twistd
通常会为您处理这种情况)以外的任何内容,请确保您保留对该监听端口对象的引用,以便您可以调用 IListeningPort.stopListening
。最后,请记住,stopListening
本身返回一个 Deferred
,并且端口可能尚未完全停止监听,直到该 Deferred
触发。
大多数服务器应用程序不需要担心这些细节。需要关注所有这些事件的一个示例是,非 PASV
FTP 协议的实现,其中需要为特定操作的整个生命周期绑定新的监听端口,然后将其处理掉。
客户端和取消¶
connectProtocol
将 Protocol
实例连接到给定的 IStreamClientEndpoint
。它返回一个 Deferred
,该 Deferred
在连接建立后会使用 Protocol
来触发。连接尝试可能会失败,因此该 Deferred
也可能会出现错误。如果出现错误,您将不得不再次尝试;不会进行进一步的尝试。有关示例用法,请参阅 客户端文档。
connectProtocol
是对底层 API 的封装:IStreamClientEndpoint.connect
将使用协议工厂来尝试建立新的出站连接。它返回一个 Deferred
,该 Deferred
会在工厂的 buildProtocol
方法返回 IProtocol
时触发,或者在连接失败时触发错误回调。
连接尝试也可能需要很长时间,您的用户可能会感到厌烦并离开。如果发生这种情况,并且您的代码出于任何原因决定您已经等待连接太久,您可以调用 Deferred.cancel
在从 connect
或 connectProtocol
返回的 Deferred
上,底层机制应该放弃连接。这应该会导致 Deferred
触发错误回调,通常是 CancelledError
;但是,您应该查阅您特定端点类型的文档,以查看它是否可能执行其他操作。
虽然某些端点类型可能暗示内置超时,但接口不保证超时。如果您没有办法让应用程序取消一个失控的连接尝试,该尝试可能会一直等待下去。例如,一个非常简单的 30 秒超时可以这样实现
attempt = connectProtocol(myEndpoint, myProtocol)
reactor.callLater(30, attempt.cancel)
注意
如果您以前使用过 ClientFactory
,请记住 connect
方法接受一个 Factory
,而不是一个 ClientFactory
。即使您将 ClientFactory
传递给 endpoint.connect
,它的 clientConnectionFailed
和 clientConnectionLost
方法不会被调用。特别是,扩展 ReconnectingClientFactory
的客户端不会重新连接。下一节将介绍如何在端点上设置重新连接的客户端。
持久客户端连接¶
twisted.application.internet.ClientService
可以维护与服务器的持久出站连接,该连接可以与您的应用程序一起启动和停止。
维护与 IRC 的长期客户端连接的一种流行协议,因此,作为 ClientService
的示例,以下是如何建立与 IRC 服务器的长期加密连接(其他细节,例如如何进行身份验证,为了简洁起见省略了)
from twisted.internet.protocol import Factory
from twisted.internet.endpoints import clientFromString
from twisted.words.protocols.irc import IRCClient
from twisted.application.internet import ClientService
from twisted.internet import reactor
myEndpoint = clientFromString(reactor, "tls:example.com:6997")
myFactory = Factory.forProtocol(IRCClient)
myReconnectingService = ClientService(myEndpoint, myFactory)
如果您已经有一个父服务,您可以将重新连接的服务添加为子服务
parentService.addService(myReconnectingService)
如果您没有父服务,您可以使用其 startService
和 stopService
方法启动和停止重新连接的服务。
ClientService.stopService
返回一个 Deferred
,该 Deferred
在当前连接关闭或当前连接尝试被取消时触发。
获取活动客户端¶
在维护长期连接时,能够获取当前连接(如果连接处于活动状态)或等待下一个连接(如果当前正在进行连接尝试)通常很有用。例如,我们可能希望将前面的示例中的 ClientService
传递给一些可以发送 IRC 通知以响应某些外部事件的代码。该 ClientService.whenConnected
方法返回一个 Deferred
,该 Deferred
会在下一个可用的 Protocol
实例时触发。您可以像这样使用它
waitForConnection = myReconnectingService.whenConnected()
def connectedNow(clientForIRC):
clientForIRC.say("#bot-test", "hello, world!")
waitForConnection.addCallback(connectedNow)
请记住,您可能需要为您的特定应用程序包装它,因为当没有现有的连接可用时,回调会在连接建立后立即执行。例如,那个小片段有点过于简化:在运行 connectedNow
时,机器人还没有进行身份验证或加入频道,因此它的消息会被拒绝。一个真实的 IRC 机器人需要有自己的方法来等待连接完全准备好聊天,然后再聊天。
报告初始失败¶
通常,第一次连接尝试的失败是特殊的。它可能表明存在一个问题,仅仅通过更努力地尝试是无法解决的。该服务可能配置了错误的主机名,或者用户可能根本没有互联网连接(也许他们忘记打开他们的 Wi-Fi 适配器)。
应用程序可以要求 whenConnected
使其 Deferred
失败,如果该服务连续进行了一次或多次连接尝试而没有成功。您可以将 failAfterFailures
参数传递给 ClientService
来设置此阈值。
通过在服务首次启动时(就在 startService
之前或之后)调用 whenConnected(failAfterFailures=1)
,您的应用程序将收到初始连接失败的通知。
将其设置为 1 表示在一次连接失败后失败。将其设置为 2 表示它将尝试一次,等待一段时间,再次尝试,然后根据第二次连接尝试的结果成功或失败。如果您特别有耐心,也可以使用 3 或更多。默认值为 None
表示它将永远等待成功连接。
无论 failAfterFailures
如何,如果在建立连接之前停止服务,Deferred
将始终使用 CancelledError
失败。
waitForConnection = myReconnectingService.whenConnected(failAfterFailures=1)
def connectedNow(clientForIRC):
clientForIRC.say("#bot-test", "hello, world!")
def failed(f):
print("initial connection failed: %s" % (f,))
# now you should stop the service and report the error upwards
waitForConnection.addCallbacks(connectedNow, failed)
重试策略¶
ClientService
在调用 startService
时将立即尝试建立出站连接。如果该连接尝试由于任何原因失败(名称解析、连接被拒绝、网络不可达等),它将根据 retryPolicy
构造函数参数中指定的策略进行重试。默认情况下,ClientService
将使用指数退避算法,最小延迟为 1 秒,最大延迟为 1 分钟,抖动最多为 1 秒,以防止踩踏式性能级联。这是一个很好的默认值,如果您没有高度专业化的需求,您可能希望使用它。如果您需要调整这些参数,您有两个选择
您可以将自己的超时策略传递给
ClientService
的构造函数。超时策略是一个可调用对象,它接受失败尝试的次数,并计算到下一次连接尝试的延迟。因此,例如,如果您非常非常确定您希望在您正在与之通信的服务出现故障时每秒重新连接一次,您可以这样做myReconnectingService = ClientService(myEndpoint, myFactory, retryPolicy=lambda ignored: 1)
当然,除非您只有一个客户端和一个服务器,并且它们都在 localhost 上,否则这种策略很可能会在您的服务器出现故障时导致巨大的性能下降和雷鸣般的资源争用,因此您可能希望选择第二个选项…
您可以通过将
twisted.application.internet.backoffPolicy()
的结果传递给retryPolicy
参数来调整默认的指数退避策略。例如,如果您想让它将尝试之间的延迟增加三倍,但从更快的连接间隔(半秒而不是一秒)开始,您可以这样做myReconnectingService = ClientService( myEndpoint, myFactory, retryPolicy=backoffPolicy(initialDelay=0.5, factor=3.0) )
注意
在端点出现之前,重新连接的客户端是作为 ReconnectingClientFactory
的子类创建的。这些子类需要调用 resetDelay
。使用端点的众多优势之一是,这些特殊的子类不再需要。ClientService
接受普通的 IProtocolFactory
提供者。
最大化您的端点投资回报¶
在您的应用程序中直接构造端点很少是最佳选择,因为它将您的应用程序绑定到特定类型的传输。端点 API 的优势在于将端点的构造(确定连接或监听的位置)与其激活(实际连接或监听)分开。
如果您正在实现一个需要监听连接或建立出站连接的库,在可能的情况下,您应该编写代码以接受客户端和服务器端点作为函数的参数或作为您对象构造函数的参数。这样,调用您的库的应用程序代码就可以提供任何合适的端点。
如果您正在编写一个应用程序,并且您需要自己构造端点,您可以允许用户使用 clientFromString
和 serverFromString
API 来指定由字符串描述的任意端点。由于这些 API 只接受字符串,因此它们提供了灵活性:如果 Twisted 添加了对新类型端点的支持(例如,IPv6 端点或 WebSocket 端点),您的应用程序将能够在没有任何代码更改的情况下自动利用它们。
端点并不总是答案¶
对于许多用例,尤其是常见的 twistd
插件用例,它运行一个长时间运行的服务器,只绑定一个简单的端口,你可能不想直接使用端点 API。相反,你可能想要使用 IService
,使用 strports.service
,它将完美地融入 twistd 插件 API 的所需结构。这不会给你的应用程序带来太多控制 - 端口在启动时开始监听,并在关闭时停止监听 - 但它确实在你的应用程序将支持的服务器端点类型方面提供了相同的灵活性。
然而,几乎总是优先使用端点,而不是直接调用更底层的 API,例如 connectTCP
、listenTCP
等。通过接受任意端点,而不是要求特定的反应器接口,你为你的应用程序留下了未来有趣的传输层扩展性。
Twisted 中包含的端点类型¶
由 clientFromString
和 serverFromString
使用的解析器可以通过第三方插件扩展,因此你系统上可用的端点取决于你安装了哪些包。但是,Twisted 本身包含一组始终可用的基本端点。
客户端¶
- TCP
支持的参数:
host
、port
、timeout
。timeout
是可选的。例如,
tcp:host=twistedmatrix.com:port=80:timeout=15
。- TLS
必需参数:
host
、port
。可选参数:
timeout
、bindAddress
、certificate
、privateKey
、trustRoots
、endpoint
。host
是要连接到的(UTF-8 编码)主机名,也是要验证的主机名。port
是要连接到的数字端口号。timeout
和bindAddress
与 TCP 客户端的timeout
和bindAddress
具有相同的含义。certificate
是要用于客户端的证书;它应该是包含证书的 PEM 文件的路径名,其中privateKey
是私钥。privateKey
是客户端的私钥,与certificate
指定的证书匹配。它应该是包含 X.509 客户端证书的 PEM 文件的路径名。如果指定了certificate
但未指定privateKey
,Twisted 将在certificate
指定的同一文件中查找证书。trustRoots
指定 PEM 编码证书文件目录的路径。如果你未指定此项,Twisted 将尽力使用平台默认的信任根集,这应该是默认的 WebTrust 集。可选的
endpoint
参数会稍微改变tls:
端点的含义。与默认的通过 TCP 连接并使用相同的主机名进行验证不同,你可以通过任何端点类型连接。如果你在此处指定端点,则host
和port
仅用于证书验证目的。请记住,你需要在此处对端点描述中的冒号进行反斜杠转义。
此客户端连接到提供的主机名,将服务器的主机名与提供的主机名进行验证,然后在验证成功后立即升级到 TLS。
最简单的示例是:
tls:example.com:443
。如果你想连接到主机名,可以使用
endpoint:
功能与 TCP 结合使用;例如,如果你的 DNS 无法正常工作,但你知道 IP 地址 7.6.5.4 指向awesome.site.example.com
,你可以指定:tls:awesome.site.example.com:443:endpoint=tcp\:7.6.5.4\:443
。你也可以将它与任何其他端点类型结合使用;例如,如果你有一个本地 UNIX 套接字,它在
/var/run/awesome.sock
中建立了到awesome.site.example.com
的隧道,你可以改为执行tls:awesome.site.example.com:443:endpoint=unix\:/var/run/awesome.sock
。或者,从 python 代码中
wrapped = HostnameEndpoint('example.com', 443) contextFactory = optionsForClientTLS(hostname=u'example.com') endpoint = wrapClientTLS(contextFactory, wrapped) conn = endpoint.connect(Factory.forProtocol(Protocol))
- UNIX
支持的参数:
path
、timeout
、checkPID
。path
给出监听 UNIX 域套接字服务器的文件系统路径。checkPID
(可选)启用对 Twisted 基于 UNIX 域套接字服务器使用的锁文件的检查,以证明它们仍在运行。例如,
unix:path=/var/run/web.sock
。- TCP(主机名)
支持的参数:
host
、port
、timeout
。host
是要连接到的主机名。timeout
是可选的。它是一个基于名称的 TCP 端点,它返回在解析的地址中首先建立的连接。例如,
endpoint = HostnameEndpoint(reactor, "twistedmatrix.com", 80) conn = endpoint.connect(Factory.forProtocol(Protocol))
SSL(已弃用)
注意
除非你需要使用早于 16.0 的 Twisted 版本,否则你通常应该优先使用上面的“TLS”客户端端点。除其他事项外
ssl:
客户端端点要求你传递“两者”hostname=
(用于主机名验证)以及host=
(用于 TCP 连接地址)才能获得主机名验证,这是安全所必需的,而tls:
默认情况下会通过使用相同的主机名来完成正确的事情。
ssl:
客户端端点不适用于 IPv6,而tls:
端点则适用。所有 TCP 参数都受支持,此外还有:
certKey
、privateKey
、caCertsDir
。certKey
(可选)给出证书(PEM 格式)的文件系统路径。privateKey
(可选)给出私钥(PEM 格式)的文件系统路径。caCertsDir
(可选)给出包含受信任 CA 证书的目录的文件系统路径,这些证书用于验证服务器证书。例如,
ssl:host=twistedmatrix.com:port=443:caCertsDir=/etc/ssl/certs
。
服务器¶
- TCP(IPv4)
支持的参数:
port
、interface
、backlog
。interface
和backlog
是可选的。interface
是要绑定的 IP 地址(属于 IPv4 地址族)。例如,
tcp:port=80:interface=192.168.1.1
。- TCP (IPv6)
支持所有 TCP (IPv4) 参数,
interface
采用 IPv6 地址字面量。例如,
tcp6:port=80:interface=2001\:0DB8\:f00e\:eb00\:\:1
。- SSL
支持所有 TCP 参数,以及:
certKey
、privateKey
、extraCertChain
、sslmethod
和dhParameters
。certKey
(可选,默认为privateKey
的值)给出证书(PEM 格式)的文件系统路径。privateKey
给出私钥(PEM 格式)的文件系统路径。extraCertChain
给出包含一个或多个以 PEM 格式连接的证书的文件的文件系统路径,这些证书建立了从根 CA 到签署您证书的 CA 的链。sslmethod
指示要使用的 SSL/TLS 版本(类似于TLSv1_3_METHOD
的值)。dhParameters
给出包含用于 Diffie-Hellman 密钥交换的参数的文件的文件系统路径(PEM 格式)。 由于这对于提供完美前向保密 (PFS) 的DHE
系列密码是必需的,因此建议指定一个。 可以使用openssl dhparam -out dh_param_1024.pem -2 1024
创建这样的文件。 有关更多详细信息,请参阅 OpenSSL 的 dhparam 文档。例如,
ssl:port=443:privateKey=/etc/ssl/server.pem:extraCertChain=/etc/ssl/chain.pem:sslmethod=SSLv3_METHOD:dhParameters=dh_param_1024.pem
。- UNIX
支持的参数:
address
、mode
、backlog
、lockfile
。address
给出使用 UNIX 域套接字服务器监听的文件系统路径。mode
(可选)给出要应用于该套接字的文件系统权限/模式(以八进制表示)。lockfile
启用使用单独的锁文件来证明服务器仍在运行。例如,
unix:address=/var/run/web.sock:lockfile=1
。- systemd
支持的参数:
domain
、name
和index
。domain
指示继承的文件描述符所属的套接字域(例如 INET、INET6)。name
指示从 systemd 继承的文件描述符的名称。 这由 systemd 配置为套接字设置。index
指示从 systemd 继承的文件描述符数组中的偏移量。name
应该优先于index
,因为描述符的顺序可能难以预测。例如,
systemd:domain=INET6:name=my-web-server
。另请参阅 使用 systemd 部署 Twisted。
- PROXY
PROXY 协议是一个流包装器,可以通过在正常端口定义前面放置
haproxy:
来将其应用于任何其他服务器端点。例如,
haproxy:tcp:port=80:interface=192.168.1.1
或haproxy:ssl:port=443:privateKey=/etc/ssl/server.pem:extraCertChain=/etc/ssl/chain.pem:sslmethod=SSLv3_METHOD:dhParameters=dh_param_1024.pem
。PROXY 协议提供了一种方法,使负载均衡器和反向代理能够发送连接源和目标的真实 IP,而无需依赖 X-Forwarded-For 标头。 使用此端点包装器的 Twisted 服务必须在发送有效 PROXY 协议标头的服务后面运行。 有关协议的更多信息,请参阅 正式规范。 目前支持协议的版本一和版本二。