本帖最后由 阳 于 2018-11-06 08:21:17 编辑。

Writing Servers

Overview (概述)

本文档解释了如何使用Twisted来实现TCP服务器的网络协议解析和处理(SSL和Unix套接字服务器可以重用相同的代码). 有一个单独的文档涉及UDP。

您的协议处理类通常是 twisted.internet.protocol.Protocol 的子类。大多数协议处理程序要么从这个类继承,要么从它的子类继承。协议类的实例根据需要在每个连接中实例化,并在连接完成时消失。这意味着在协议(Protocol)中不保存持久配置。

持久性配置保存在 Factory 类中,Factory 类通常继承自 twisted.internet.protocol.Factory 。Factory 的 buildProtocol 方法用于为每个新连接创建一个协议(Protocol)。

在多个端口或网络地址上提供相同的服务是经常用到的。这就是为什么 Factory 不监听连接,实际上它对网络一无所知。有关更多信息,请参见端点文档,或 IReactorTCP.listenTCP 和其他 IReactor*.listen* Api (基于端点的底层 api)。

本文档将解释这个方式的每个步骤。

Protocols (协议)

如上所述,协议与辅助类和函数一起,是大部分代码的地方。Twisted protocol 以异步方式处理数据。协议响应网络上到达的事件,事件到达后会调用协议上的方法。

这里有一个简单的例子:

from twisted.internet.protocol import Protocol

class Echo(Protocol):

    def dataReceived(self, data):
        self.transport.write(data)

这是最简单的协议之一。它只是回写所写的内容,不响应所有事件。

这是一个协议响应一个事件的例子:

from twisted.internet.protocol import Protocol

class QOTD(Protocol):

    def connectionMade(self):
        self.transport.write(b"An apple a day keeps the doctor away\r\n")
        self.transport.loseConnection()

此协议以一个的谚语响应初始连接,然后终止连接。

connectionMade 事件通常发生在设置连接对象时,也发生在任何初始问候语发生的地方(如上面的QOTD协议,它实际上基于 RFC 865)。

connectionLost 事件发生在对任何特定的连接的对象进行 tearing down 时。这是一个例子:

from twisted.internet.protocol import Protocol

class Echo(Protocol):

    def __init__(self, factory):
        self.factory = factory

    def connectionMade(self):
        self.factory.numProtocols += 1
        self.transport.write(
            b"Welcome! There are currently %d open connections.\n" %
            (self.factory.numProtocols,))

    def connectionLost(self, reason):
        self.factory.numProtocols -= 1

    def dataReceived(self, data):
        self.transport.write(data)

在这里,connectionMadeconnectionLost 合作保存共享工厂对象中的活动协议计数。创建 Echo 协议实例时工厂必须传给 Echo.__init__ 。工厂用于共享存在于任何给定连接的生命周期之外的状态。在下一节中,您将看到为什么这个对象被称为“工厂”。

loseConnection() 和 abortConnection()

在上面的代码中,loseConnection 在写入到传输之后立即调用。loseConnection 调用仅在将所有数据通过 Twisted 写到操作系统时才会关闭连接,因此在这种情况下,可以安全地使用它,而不必担心传输写入丢失。如果一个生产者(producer)与传输(transport)一起使用,那么一旦生产者未注册,loseConnection 会关闭连接。

在某些情况下,我们不希望等到所有的数据都写出来。由于网络故障或连接另一端的错误或恶意,写到传输的数据可能无法交付,因此即使调用 loseConnection ,也不会关闭连接。在这些情况下,可以使用 abortConnection: 它可以立即关闭连接,而不考虑仍然在传输中未写的缓冲数据,或者仍在注册的生产者。注意,abortConnection 仅在 Twisted 11.1 和更新版本中可用。

Using the Protocol (使用协议)

在本节中,您将学习如何运行使用您的协议的服务器。

下面的代码将运行前面讨论的QOTD服务器:

from twisted.internet.protocol import Factory
from twisted.internet.endpoints import TCP4ServerEndpoint
from twisted.internet import reactor

class QOTDFactory(Factory):
    def buildProtocol(self, addr):
        return QOTD()

# 8007 is the port you want to run under. Choose something >1024
endpoint = TCP4ServerEndpoint(reactor, 8007)
endpoint.listen(QOTDFactory())
reactor.run()

在本例中,我创建了一个协议工厂。我想告诉这个工厂它的工作是构建 QOTD 协议实例,所以我设置了它的 buildProtocol 方法来返回 QOTD 类的实例。然后,我想监听 TCP 端口,因此我创建了一个 TCP4ServerEndpoint 来标识我想绑定到的端口,然后将我刚创建的工厂传递给它的 listen 方法。

endpoint.listen()告诉 reactor(反应器) 使用特定的协议处理端点地址的连接,但是反应器需要运行以便它能做任何事情。reactor.run() 启动反应器,然后永远等待连接到达您指定的端口。您可以通过在终端中单击 Control-C 或调用 reactor.stop() 来停止反应器。

有关侦听传入连接的不同方式的更多信息,请参阅端点API的文档。有关使用反应器的更多信息,请参见反应器概述

Helper Protocols (协议助手)

许多协议建立在类似的低级抽象之上。

例如,许多流行的 internet 协议都是基于行的,包含以换行符 ( 通常是 CR-LF ) 终止的文本数据,而不是包含直接的原始数据。然而,有相当多的协议是混合的——它们有基于行的部分,然后是原始数据部分。例如 HTTP/1.1 和 Freenet 协议。

对于这些情况,有个 LineReceiver 协议。该协议分派给两个不同的事件处理程序—— lineReceivedrawDataReceived 。默认情况下,每一行只调用一次 lineReceived 。但是,如果调用了 setRawMode ,协议将调用 rawDataReceived ,直到调用 setLineMode ,它将使用 lineReceived 返回。它还提供了一个名为 sendLine 的方法,该方法将数据与分割行的分隔符一起写入传输(默认情况下是 \r\n )。

下面是一个简单的例子使用了 LineReceiver:

from twisted.protocols.basic import LineReceiver

class Answer(LineReceiver):

    answers = {b'How are you?': b'Fine', None: b"I don't know what you mean"}

    def lineReceived(self, line):
        if line in self.answers:
            self.sendLine(self.answers[line])
        else:
            self.sendLine(self.answers[None])

注意,分隔符不是该行的一部分。

还有其他几个助手,比如基于网络字符串的协议预定义消息长度的协议

State Machines (状态机)

许多Twisted 协议处理程序需要编写状态机来记录它们所在的状态。以下是一些帮助编写状态机的建议:

  • 不要编写大型状态机。编写的状态机每次处理一个抽象级别。
  • 不要将应用程序的代码与协议处理代码混合。当协议处理程序必须进行特定于应用程序的调用时,将其作为方法调用分离出来。

Factories (工厂)

Simpler Protocol Creation (创建简单的协议)

对于只实例化特定协议类实例的工厂,有一种更简单的实现工厂的方法。buildProtocol 方法的默认实现调用工厂的 protocol 属性来创建一个协议实例,然后在其上设置一个名为 factory 的属性,该属性指向工厂本身。这允许每个协议访问,并可能修改持久配置。下面是一个使用这些特性而不是重写构建协议的例子:

from twisted.internet.protocol import Factory, Protocol
from twisted.internet.endpoints import TCP4ServerEndpoint
from twisted.internet import reactor

class QOTD(Protocol):

    def connectionMade(self):
        # self.factory was set by the factory's default buildProtocol:
        self.transport.write(self.factory.quote + b'\r\n')
        self.transport.loseConnection()


class QOTDFactory(Factory):

    # This will be used by the default buildProtocol to create new protocols:
    protocol = QOTD

    def __init__(self, quote=None):
        self.quote = quote or b'An apple a day keeps the doctor away'

endpoint = TCP4ServerEndpoint(reactor, 8007)
endpoint.listen(QOTDFactory(b"configurable quote"))
reactor.run()

如果您所需要的只是一个简单的工厂,它可以构建一个协议,而不需要任何额外的行为, Twisted 13.1 增加的 Factory.forProtocol 类方法 是一种更简单的方法。

# Create a factory for the given protocol.

# It sets the protocol attribute and returns the constructed factory instance.

# Parameters	protocol	A Protocol subclass
#               args	Positional arguments for the factory.
#               kwargs	Keyword arguments for the factory.
# Returns	A Factory instance wired up to protocol.
twisted.internet.protocol.Factory.forProtocol(cls, protocol, *args, **kwargs)

Factory Startup and Shutdown

工厂有两个方法来执行应用程序的构建和分解(由于工厂经常被持久化,通常不适合在 __init____del__ 中进行,常常太早或太晚)。

这里有一个工厂的例子,它允许它的协议写入一个特殊的日志文件:

from twisted.internet.protocol import Factory
from twisted.protocols.basic import LineReceiver


class LoggingProtocol(LineReceiver):

    def lineReceived(self, line):
        self.factory.fp.write(line + b'\n')


class LogfileFactory(Factory):

    protocol = LoggingProtocol

    def __init__(self, fileName):
        self.file = fileName

    def startFactory(self):
        self.fp = open(self.file, 'a')

    def stopFactory(self):
        self.fp.close()

Putting it All Together (把它放在一起)

最后一个例子是一个简单的聊天服务器,它允许用户选择用户名,然后与其他用户进行通信。它演示了在工厂中使用共享状态、每个单独协议的状态机以及不同协议之间的通信。

# chat.py (python3)

from twisted.internet.protocol import Factory
from twisted.protocols.basic import LineReceiver
from twisted.internet import reactor


class Chat(LineReceiver):

    def __init__(self, users):
        self.users = users
        self.name = None
        self.state = "GETNAME"

    def connectionMade(self):
        self.sendLine(b"What's your name?")

    def connectionLost(self, reason):
        if self.name in self.users:
            del self.users[self.name]

    def lineReceived(self, line):
        if self.state == "GETNAME":
            self.handle_GETNAME(line)
        else:
            self.handle_CHAT(line)

    def handle_GETNAME(self, name):
        if name in self.users:
            self.sendLine(b"Name taken, please choose another.")
            return
        self.sendLine(b"Welcome, %s!" % (name,))
        self.name = name
        self.users[name] = self
        self.state = "CHAT"

    def handle_CHAT(self, message):
        message = b"<%s> %s" % (self.name, message)
        for name, protocol in self.users.items():
            if protocol != self:
                protocol.sendLine(message)


class ChatFactory(Factory):

    def __init__(self):
        self.users = {}  # maps user names to Chat instances

    def buildProtocol(self, addr):
        return Chat(self.users)


if __name__ == "__main__":
    reactor.listenTCP(8123, ChatFactory())
    reactor.run()

您可能不熟悉的唯一 API 是 listenTCPlistenTCP 是将工厂连接到网络的方法。这是端点为您包装的底层 API 。

以下是聊天会话的样本文本 (>后面的文本是用户输入的):

> telnet 127.0.0.1 8123
 Trying 127.0.0.1...
 Connected to 127.0.0.1.
 Escape character is '^]'.
 What's your name?
> test
 Name taken, please choose another.
> bob
 Welcome, bob!
> hello
 <alice> hi bob
> twisted makes writing servers so easy!
 <alice> I couldn't agree more
 <carrol> yeah, it's great
共收到 0 条回复