编写服务器

概述

本文档介绍如何使用 Twisted 实现 TCP 服务器的网络协议解析和处理(相同的代码可用于 SSL 和 Unix 套接字服务器)。有一个单独的文档介绍 UDP。

您的协议处理类通常会继承自twisted.internet.protocol.Protocol。大多数协议处理程序继承自此类或其便利子类之一。协议类的实例按需为每个连接实例化,并在连接完成时消失。这意味着持久配置不会保存在 Protocol 中。

持久配置保存在 Factory 类中,该类通常继承自twisted.internet.protocol.FactoryFactorybuildProtocol 方法用于为每个新连接创建 Protocol

通常,能够在多个端口或网络地址上提供相同的服务非常有用。这就是为什么 Factory 不监听连接,实际上也不了解网络。有关更多信息,请参阅端点文档,或IReactorTCP.listenTCP 以及其他 IReactor*.listen* API,这些 API 是端点基于的更低级 API。

本文档将解释每一步。

协议

如上所述,这是大多数代码所在的地方,以及辅助类和函数。Twisted 协议以异步方式处理数据。协议在从网络接收事件时响应,这些事件以对协议上的方法的调用形式到达。

以下是一个简单的示例

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("An apple a day keeps the doctor away\r\n")
        self.transport.loseConnection()

此协议使用一个众所周知的引语响应初始连接,然后终止连接。

connectionMade 事件通常是连接对象设置发生的地方,以及任何初始问候(如上面的 QOTD 协议,它实际上基于RFC 865)。 connectionLost 事件是拆除任何连接特定对象的地方。以下是一个示例

from twisted.internet.protocol import Protocol

class Echo(Protocol):

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

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

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

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

这里, connectionMadeconnectionLost 协同工作,在一个共享对象(工厂)中维护活动协议的计数。在创建新实例时,必须将工厂传递给 Echo.__init__。工厂用于共享超出任何给定连接生命周期的状态。您将在下一节中了解为什么这个对象被称为“工厂”。

loseConnection() 和 abortConnection()

在上面的代码中, loseConnection 在写入传输后立即被调用。 loseConnection 调用仅在 Twisted 将所有数据写入操作系统后才会关闭连接,因此在这种情况下安全使用,无需担心传输写入丢失。如果生产者正在与传输一起使用, loseConnection 仅在生产者注销后才会关闭连接。

在某些情况下,等待所有数据写入并非我们想要的。由于网络故障,或连接另一端存在的错误或恶意行为,写入传输的数据可能无法传递,因此即使调用了 loseConnection,连接也不会断开。在这些情况下,可以使用 abortConnection:它会立即关闭连接,而不管传输中未写入的缓冲数据,或仍注册的生产者。请注意, abortConnection 仅在 Twisted 11.1 及更高版本中可用。

使用协议

在本节中,您将学习如何运行使用您的 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()

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

endpoint.listen() 告诉反应器使用特定协议处理到端点地址的连接,但反应器需要运行才能执行任何操作。 reactor.run() 启动反应器,然后无限期地等待连接到达您指定的端口。 您可以通过在终端中按 Ctrl+C 或调用 reactor.stop() 来停止反应器。

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

辅助协议

许多协议建立在类似的底层抽象之上。

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

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

以下是一个简单使用行接收器的示例

from twisted.protocols.basic import LineReceiver

class Answer(LineReceiver):

    answers = {'How are you?': 'Fine', None: "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])

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

存在其他几个辅助程序,例如 netstring based protocolprefixed-message-length protocols

状态机

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

  • 不要编写大型状态机。 优先编写一次处理一个抽象级别的状态机。

  • 不要将应用程序特定代码与协议处理代码混合。 当协议处理程序必须进行应用程序特定调用时,将其保留为方法调用。

工厂

更简单的协议创建

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

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 + '\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 'An apple a day keeps the doctor away'

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

如果您只需要一个简单的工厂来构建协议而没有任何其他行为,Twisted 13.1 添加了 Factory.forProtocol,这是一种更简单的方法。

工厂启动和关闭

工厂有两个方法来执行应用程序特定的构建和拆卸(由于工厂经常被持久化,因此在 __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 + '\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()

将所有内容放在一起

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

chat.py

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


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(f"Welcome, {name.decode('utf-8')}!".encode("utf-8"))
        self.name = name
        self.users[name] = self
        self.state = "CHAT"

    def handle_CHAT(self, message):
        message = f"<{self.name.decode('utf-8')}> {message.decode('utf-8')}".encode(
            "utf-8"
        )
        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)


reactor.listenTCP(8123, ChatFactory())
reactor.run()

您可能不熟悉的唯一 API 是 listenTCPlistenTCP 是将 Factory 连接到网络的方法。 这是 端点 为您包装的更低级 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