与端点建立连接

简介

在网络中,可以将任何给定的连接视为一根长线,连接着两个点。在这根线的长度上可能会发生很多事情 - 路由器、交换机、网络地址转换等等,但这些通常对跨越它传递数据的应用程序来说是不可见的。Twisted 努力使“线”的本质尽可能透明,使用高度抽象的接口来传递和接收数据,例如 ITransportIProtocol.

但是,应用程序不能完全忽略这条线。特别是,它必须做一些事情来启动连接,为此,它必须识别线的端点。端点角色有不同的名称 - “发起者”和“响应者”、“连接器”和“监听器”,或“客户端”和“服务器” - 但共同的主题是连接的一端等待某人连接到它,而另一端则进行连接。

在 Twisted 10.1 中,引入了几个新的接口来描述面向流连接的每个角色:IStreamServerEndpointIStreamClientEndpoint。在这种情况下,“流”指的是将连接视为连续的字节流而不是一系列离散数据报的端点:TCP 是“流”协议,而 UDP 是“数据报”协议。

构建和使用端点

编写服务器编写客户端 中,我们都介绍了端点的基本用法;您构建一个合适的服务器或客户端端点类型,然后调用 listen(对于服务器)或 connect(对于客户端)。

在这两个教程中,我们都直接构建了特定类型的端点。但是,在大多数程序中,您希望允许用户指定监听或连接的位置,以一种允许用户请求不同的策略的方式,而无需调整您的程序。为了允许这样做,您应该使用 clientFromStringserverFromString.

没什么大不了的

每种类型的端点只是一个接口,只有一个方法,该方法接受一个参数。 serverEndpoint.listen(factory) 将使用您的协议工厂开始在该端点上监听,而 clientEndpoint.connect(factory) 将启动一次连接尝试。但是,这些 API 中的每一个都返回一个值,这可能很重要。

但是,如果您还没有,您应该非常熟悉 Deferred,因为它们是由 connectlisten 方法返回的,以指示连接何时已连接或监听端口何时已启动并运行。

服务器和停止

IStreamServerEndpoint.listen 返回一个 Deferred,它会使用一个 IListeningPort 来触发。请注意,此 Deferred 可能会出现错误。这种错误的最常见原因是另一个程序已经在使用请求的端口号,但确切的原因可能会因您正在监听的端点类型而异。如果您收到此类错误,则意味着您的应用程序实际上并未监听,并且不会接收任何传入连接。在这种情况下,尤其是在您只有一个监听端口的情况下,务必以某种方式提醒服务器管理员!

还要注意,一旦成功,它将永远继续监听。如果您需要出于某种原因停止监听,以响应除完全服务器关闭(reactor.stop 和/或 twistd 通常会为您处理这种情况)以外的任何内容,请确保您保留对该监听端口对象的引用,以便您可以调用 IListeningPort.stopListening。最后,请记住,stopListening 本身返回一个 Deferred,并且端口可能尚未完全停止监听,直到该 Deferred 触发。

大多数服务器应用程序不需要担心这些细节。需要关注所有这些事件的一个示例是,非 PASV FTP 协议的实现,其中需要为特定操作的整个生命周期绑定新的监听端口,然后将其处理掉。

客户端和取消

connectProtocolProtocol 实例连接到给定的 IStreamClientEndpoint。它返回一个 Deferred,该 Deferred 在连接建立后会使用 Protocol 来触发。连接尝试可能会失败,因此该 Deferred 也可能会出现错误。如果出现错误,您将不得不再次尝试;不会进行进一步的尝试。有关示例用法,请参阅 客户端文档

connectProtocol 是对底层 API 的封装:IStreamClientEndpoint.connect 将使用协议工厂来尝试建立新的出站连接。它返回一个 Deferred,该 Deferred 会在工厂的 buildProtocol 方法返回 IProtocol 时触发,或者在连接失败时触发错误回调。

连接尝试也可能需要很长时间,您的用户可能会感到厌烦并离开。如果发生这种情况,并且您的代码出于任何原因决定您已经等待连接太久,您可以调用 Deferred.cancel 在从 connectconnectProtocol 返回的 Deferred 上,底层机制应该放弃连接。这应该会导致 Deferred 触发错误回调,通常是 CancelledError;但是,您应该查阅您特定端点类型的文档,以查看它是否可能执行其他操作。

虽然某些端点类型可能暗示内置超时,但接口不保证超时。如果您没有办法让应用程序取消一个失控的连接尝试,该尝试可能会一直等待下去。例如,一个非常简单的 30 秒超时可以这样实现

attempt = connectProtocol(myEndpoint, myProtocol)
reactor.callLater(30, attempt.cancel)

注意

如果您以前使用过 ClientFactory,请记住 connect 方法接受一个 Factory,而不是一个 ClientFactory。即使您将 ClientFactory 传递给 endpoint.connect,它的 clientConnectionFailedclientConnectionLost 方法不会被调用。特别是,扩展 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)

如果您没有父服务,您可以使用其 startServicestopService 方法启动和停止重新连接的服务。

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 秒,以防止踩踏式性能级联。这是一个很好的默认值,如果您没有高度专业化的需求,您可能希望使用它。如果您需要调整这些参数,您有两个选择

  1. 您可以将自己的超时策略传递给 ClientService 的构造函数。超时策略是一个可调用对象,它接受失败尝试的次数,并计算到下一次连接尝试的延迟。因此,例如,如果您非常非常确定您希望在您正在与之通信的服务出现故障时每秒重新连接一次,您可以这样做

    myReconnectingService = ClientService(myEndpoint, myFactory, retryPolicy=lambda ignored: 1)
    

    当然,除非您只有一个客户端和一个服务器,并且它们都在 localhost 上,否则这种策略很可能会在您的服务器出现故障时导致巨大的性能下降和雷鸣般的资源争用,因此您可能希望选择第二个选项…

  2. 您可以通过将 twisted.application.internet.backoffPolicy() 的结果传递给 retryPolicy 参数来调整默认的指数退避策略。例如,如果您想让它将尝试之间的延迟增加三倍,但从更快的连接间隔(半秒而不是一秒)开始,您可以这样做

    myReconnectingService = ClientService(
        myEndpoint, myFactory,
        retryPolicy=backoffPolicy(initialDelay=0.5, factor=3.0)
    )
    

注意

在端点出现之前,重新连接的客户端是作为 ReconnectingClientFactory 的子类创建的。这些子类需要调用 resetDelay。使用端点的众多优势之一是,这些特殊的子类不再需要。ClientService 接受普通的 IProtocolFactory 提供者。

最大化您的端点投资回报

在您的应用程序中直接构造端点很少是最佳选择,因为它将您的应用程序绑定到特定类型的传输。端点 API 的优势在于将端点的构造(确定连接或监听的位置)与其激活(实际连接或监听)分开。

如果您正在实现一个需要监听连接或建立出站连接的库,在可能的情况下,您应该编写代码以接受客户端和服务器端点作为函数的参数或作为您对象构造函数的参数。这样,调用您的库的应用程序代码就可以提供任何合适的端点。

如果您正在编写一个应用程序,并且您需要自己构造端点,您可以允许用户使用 clientFromStringserverFromString API 来指定由字符串描述的任意端点。由于这些 API 只接受字符串,因此它们提供了灵活性:如果 Twisted 添加了对新类型端点的支持(例如,IPv6 端点或 WebSocket 端点),您的应用程序将能够在没有任何代码更改的情况下自动利用它们。

端点并不总是答案

对于许多用例,尤其是常见的 twistd 插件用例,它运行一个长时间运行的服务器,只绑定一个简单的端口,你可能不想直接使用端点 API。相反,你可能想要使用 IService,使用 strports.service,它将完美地融入 twistd 插件 API 的所需结构。这不会给你的应用程序带来太多控制 - 端口在启动时开始监听,并在关闭时停止监听 - 但它确实在你的应用程序将支持的服务器端点类型方面提供了相同的灵活性。

然而,几乎总是优先使用端点,而不是直接调用更底层的 API,例如 connectTCPlistenTCP 等。通过接受任意端点,而不是要求特定的反应器接口,你为你的应用程序留下了未来有趣的传输层扩展性。

Twisted 中包含的端点类型

clientFromStringserverFromString 使用的解析器可以通过第三方插件扩展,因此你系统上可用的端点取决于你安装了哪些包。但是,Twisted 本身包含一组始终可用的基本端点。

客户端

TCP

支持的参数:hostporttimeouttimeout 是可选的。

例如,tcp:host=twistedmatrix.com:port=80:timeout=15

TLS

必需参数:hostport

可选参数:timeoutbindAddresscertificateprivateKeytrustRootsendpoint

  • host 是要连接到的(UTF-8 编码)主机名,也是要验证的主机名。

  • port 是要连接到的数字端口号。

  • timeoutbindAddress 与 TCP 客户端的 timeoutbindAddress 具有相同的含义。

  • certificate 是要用于客户端的证书;它应该是包含证书的 PEM 文件的路径名,其中 privateKey 是私钥。

  • privateKey 是客户端的私钥,与 certificate 指定的证书匹配。它应该是包含 X.509 客户端证书的 PEM 文件的路径名。如果指定了 certificate 但未指定 privateKey,Twisted 将在 certificate 指定的同一文件中查找证书。

  • trustRoots 指定 PEM 编码证书文件目录的路径。如果你未指定此项,Twisted 将尽力使用平台默认的信任根集,这应该是默认的 WebTrust 集。

  • 可选的 endpoint 参数会稍微改变 tls: 端点的含义。与默认的通过 TCP 连接并使用相同的主机名进行验证不同,你可以通过任何端点类型连接。如果你在此处指定端点,则 hostport 仅用于证书验证目的。请记住,你需要在此处对端点描述中的冒号进行反斜杠转义。

此客户端连接到提供的主机名,将服务器的主机名与提供的主机名进行验证,然后在验证成功后立即升级到 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

支持的参数:pathtimeoutcheckPIDpath 给出监听 UNIX 域套接字服务器的文件系统路径。 checkPID(可选)启用对 Twisted 基于 UNIX 域套接字服务器使用的锁文件的检查,以证明它们仍在运行。

例如,unix:path=/var/run/web.sock

TCP(主机名)

支持的参数:hostporttimeouthost 是要连接到的主机名。 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 参数都受支持,此外还有:certKeyprivateKeycaCertsDircertKey(可选)给出证书(PEM 格式)的文件系统路径。 privateKey(可选)给出私钥(PEM 格式)的文件系统路径。 caCertsDir(可选)给出包含受信任 CA 证书的目录的文件系统路径,这些证书用于验证服务器证书。

例如,ssl:host=twistedmatrix.com:port=443:caCertsDir=/etc/ssl/certs

服务器

TCP(IPv4)

支持的参数:portinterfacebackloginterfacebacklog 是可选的。 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 参数,以及:certKeyprivateKeyextraCertChainsslmethoddhParameterscertKey(可选,默认为 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

支持的参数:addressmodebackloglockfileaddress 给出使用 UNIX 域套接字服务器监听的文件系统路径。 mode(可选)给出要应用于该套接字的文件系统权限/模式(以八进制表示)。 lockfile 启用使用单独的锁文件来证明服务器仍在运行。

例如,unix:address=/var/run/web.sock:lockfile=1

systemd

支持的参数:domainnameindexdomain 指示继承的文件描述符所属的套接字域(例如 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.1haproxy: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 协议标头的服务后面运行。 有关协议的更多信息,请参阅 正式规范。 目前支持协议的版本一和版本二。