twisted学习一(protocol和factory)

twisted的中文资料都是翻译的诗歌服务器,剩下的都是抄来抄去,搜索半天找不到一个能解决我问题的。主要是我还比较小白。下面的内容是twisted官方文档的一部分,再加上我自己的理解,我希望我能尽量阐述的直白些,适合python的初学者去理解twisted的服务器部分,如果能帮到你那就更好了,我有说的不对的地方也希望读者能够指出来,先谢谢啦。

这一段是吐槽,可以直接跳过。这里我先假设你已经自己搜索过一些资料,知道twisted是一个异步的东西,对那个转圈圈的监听事件已经有个大概的了解了。说实话,很多文章都是写到转圈监听,然后就没有了,稍微涉及到具体怎么实现就开始抄那个诗歌服务器的例子了。我承认那个例子应该是很经典的,但是能不能写写自己的理解,通篇看的都是翻译腔,很烦。要不就是已经懂了大神说个只言片语,让我等也无法理解。好了吐槽完毕,正文开始

正文开始,本文基本是按照Writing Servers - Twisted 15.1.0 documentation这个文档讲的。我有什么说的不清楚的地方你可以去直接看原文,当然我相信你不会的,如果会的话,你就直接看英文文档啦~哈哈。

这篇文档就是讲怎么编一些程序(这里叫做网络协议)来处理服务器接口送来的消息。我理解它这里说的protocol说白了就是你自己编的程序,怎么处理过来的数据啦,返回什么值,不是指的HTTP1.1协议什么的。然后呢,我们都知道,twisted的那个循环,就是那个圈圈,不会一直等我们处理数据,它还要继续监听端口看有没有新的请求,但是我们自己写的程序运行完毕了后需要告诉循环(圈圈)你该处理我的返回值了(或者其他的什么),所以怎么通知它呢,twisted告诉你说这个你不用管,只要你在编写自己的代码的时候(也就是protocol)继承twisted给你规定的一些类。换句话说,就是你不用管怎么样通知循环来处理你运行完毕的代码,但是你要继承twisted的一些规则,这些规则会完成这些工作。我也不知道是别人没写清楚还是我理解力太差,看了半天才理清楚这些概念。

接下来我们来看自己要编写的处理请求的代码(也就是protocol,下文我就直接用protocol了),要写成一个类,你这个类呢,应该是twisted.internet.protocol.Protocol的子类,说白了就是要让你继承twisted.internet.protocol.Protocol这个类,或者是继承他的一些子类,你看,不让你随便乱写。每一个连接都对应一个protocol的实例,在新的连接到达的时候创建,断开的时候销毁。而protocol只是一个协议,规定一些规则。比如从端口过来一个请求,程序会根据这个协议专门创建专属于这个请求的protocol实例来处理这个请求。如果你清楚类这个概念的话,我这么解释就已经很清楚了,如果不清楚,那我得再写一篇文章来解释类(逃)。

下面我们就谈谈这个protocol(协议)

如前文所述,通过继承一些类和方法,我们自己写的代码就可以异步的来处理数据了。protocol会在网络的数据到达的时候调用一些方法。这样咱们就能处理数据了。举个例子:

from twisted.internet.protocol import Protocol

class Echo(Protocol):

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

这是个简单的例子(嗯,我喜欢)

我们自己写的Echo类继承了Protocol这个类,Protocol类里面有很多方法,这里举了个简单的。dataReceived这个方法表示一旦有数据到达,就如何如何。函数名字不能改......因为他是重写,特定的名字对应特定的功能,要么你记住,要么你多查几次,就记住了......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()

这个例子除了返回了一句话,调用了一个loseConnection()方法用来断开连接。我们再看一个例子:

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)

这里增加了一些新的东西,我们先看这三个方法: connectionMade()是连接建立时调用的,也就是一旦有网络连接建立,就会调用这个方法。self.factory.numProtocols()我们先不管factory这个东西,这个方法看起来是统计连接数量的,所以加一。然后还是返回一个字符串,显示当前的的连接数量是多少。connectionLost()这个就是断开连接时候把连接的数量减一。dataReceived()就是处理连接有数据发送过来时候调用的函数。我的理解是没数据的话就不调用dataReceived(),而 connectionMade()是不论有没有数据,一旦有连接建立就会调用。这里的factory我们暂时先把它看成一个普通的类,咱们调用了一些factory类的方法。暂时不影响我们对这段代码的理解,其实我现在也理解的不是很到位,马上我们会讨论这个factory()。

loseConnection() 和 abortConnection()的区别:loseConnection() 是等数据下载或者上传完毕后断开连接。abortConnection()是立刻断开连接,目的是为了防止程序出现问题无法用loseConnection() 正常的断开连接时,强制断开。

接下来,我们就要使用自己Protocol来建立一个服务器了。我们用第二个例子的QOTD来建立服务器:

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

class QOTD(Protocol):

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


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()

8007就是你要监听的端口

我把上面例子的代码也贴进来了,应该可以直接运行吧(我有九成把握),你可以试试,我没试过(逃)

这里我们创建了一个协议工厂(protocol Factory),就是这个class QOTDFactory(Factory),他的作用就是使用buildProtocol这个方法(方法名也是重写的,所以自己不能改,所以要记住......或者用的时候查,上面例子中的方法名也是,基本都是固定的)创建QOTD()的实例,并且返回它。为什么这么做呢,先不管,继续往下看。TCP4ServerEndpoint是用来把端口和reactor绑定在一起的,就是把循环的圆圈和8007这个端口绑定在一起了,reactor开始监听8007这个端口了。endpoint.listen()方法告诉reactor(就是那个循环)有连接过来的话要使用QOTDFactory()这个特定的协议来处理。reactor.run()就是启动循环,程序开始运行。想要终止的话使用Ctrl+c或者调用reactor.stop()方法。

休息一下:写的我好累,写到这里的时间基本上是我看完原文档的时间,我都想半途而废了,但那岂不是变成了我吐槽过的那些只介绍皮毛的人了,不能放弃~

接下来是对于protocol一点拓展。

很多protocols 都是由一些低层次的、近似的抽象概念构成的。例如很多流行的网络通信协议是一行一行的字符串(或者文本,我理解的意思),然后里面用一些换行符来分割不同的内容,而不是直接把要传输的数据放进去。还有很多协议是用换行符分隔的字符串数据和原始数据混在一起传输的。比如HTTP/1.1 and the Freenet protocol。然后咱们有一个LineReceiver 的protocol可以处理这种情况,他有两种方法分别是lineReceived() 和rawDataReceived()来处理这些数据。默认是启用lineReceived() 方法,一旦setRawMode 被调用,就启用了rawDataReceived()方法。然后可以使用setLineMode切换回 lineReceived() 方法。还有一个sendLine()方法,可以把发送的数据用分隔符分隔。下面是例子

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 self.answers.has_key(line):
            self.sendLine(self.answers[line])
        else:
            self.sendLine(self.answers[None])

这个例子和我们要讲的关系不大,比较细节了,我就不多说了。有时间的话我好好研究下。

Factories

我们来说这个Factories(工厂),这个是重点和难点,说实话,我自己的理解也不是很清晰,大家边看自己边思考,我说的也不一定对。

我先简单的把原文档的话说一遍:如果我们只是单纯的使用factory的将protocol实例化这个功能,那么直接使用buildProtocol()这个函数就好了。buildProtocol()函数会调用factory的protocol属性,来创建一个Protocol实例,然后在这个实例上设置一个叫做factory 的属性,用来指向factory自己。这样就使得所有被创建Protocol实例都可以访问或者修改factory 中的配置。我们也可以不使用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()

我觉得我翻译的已经很明白了,只要对着我的句子然后看下代码就能看懂了。看不懂也不要紧,我一句句的解释。

上面这段代码其实就是把buildProtocol()函数做的事情详细的写了出来。我们的QOTDFactory类继承了Factory,所以写了protocol = QOTD,调用了factory的protocol属性,来创建一个Protocol实例。然后在这个实例上设置一个叫做factory 的属性,用来指向factory自己(设置factory 的属性这个过程应该是twisted自己完成的),这样我们在QOTD这个类里面,就可以写self.factory.quote来访问属于QOTDFactory()这个类下面的self.quote属性了。从而实现了Protocol实例访问Factory内部配置的功能。这样我们创建的很多个不同的Protocol实例就可以访问一个共享的属性,一会我们就能看到这样做的好处了。这也是其他地方经常提到的Protocol实例不能保存配置信息,而Factory能够持久的保存配置信息。这也是使用Factory的原因之一吧。

Factory 有两个方法来对特定的应用程序进行创建和销毁。下面是一个factory 的例子,这个factory 使用startFactory(),stopFactory()来在连接建立前和连接断开前执行内部的程序。

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()

LoggingProtocol()这个类里只有一个方法,当数据到达时调用lineReceived()方法,把数据写入文件。这里我想说的是,像诸如lineReceived(),startFactory(),stopFactory(),这样的函数,当满足条件时他们会自动的被调用,不需要你指明什么时候调用,也就是说startFactory()这个函数自己就能知道连接建立前需要执行你给他写在内部的程序。我当时就是怎么也想不通这一点,比如你想在连接断开前时候做一些事情,就需要查找文档去找一个符合你要求的函数,找了半天发现stopFactory()这个函数符合你的要求,那就把函数名写进来,然后在他的内部写你的代码。如果上面的例子你都看懂了,我们来看下面这个综合性的例子,先尝试着自己看,试着把它的功能弄懂,然后实现一下,看是否是和自己想的一样。当我验证自己的推测和实际运行的结果一致时,好开心~哈哈,虽然现在看来当时也是懵懵懂懂的,不过也不见得现在我就有多明白了。例子如下:

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("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("Name taken, please choose another.")
            return
        self.sendLine("Welcome, %s!" % (name,))
        self.name = name
        self.users[name] = self
        self.state = "CHAT"

    def handle_CHAT(self, message):
        message = "<%s> %s" % (self.name, message)
        for name, protocol in self.users.iteritems():
            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()

下面是我的解释版

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("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("Name taken, please choose another.")
            return
        self.sendLine("Welcome, %s!" % (name,))
        self.name = name
        self.users[name] = self#将当前的Chat保存为值,name为键
        self.state = "CHAT"#重置状态

    def handle_CHAT(self, message):
        message = "<%s> %s" % (self.name, message)
        for name, protocol in self.users.iteritems():#给其他用户发送消息
            if protocol != self:
                protocol.sendLine(message)


class ChatFactory(Factory):

    def __init__(self):
        self.users = {} #创建了一个公用的字典来保存用户名和对应的Chat实例

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


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

希望大家能够自己实现一下这个例子,实现的时候也有不少坑,我贴过来原文的实现过程。 $ 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

windows下的telnet启动后我的电脑看不到输入的文字,只有光标。我是在linux下实现的。

总之,使用factory和protocol就是找到满足你需要的函数,然后重写它,所以就要查API.另外不要浮躁,twist确实比较麻烦,刚开始我看的马马虎虎,效果不佳,很多地方是要认真抠的。希望本文能对你有帮助,同时也欢迎交流~

最后写一点我的思考吧,我不知道为什么很多分享的博客或者帖子都是同一个内容抄来抄去,这样让我在查找答案的时候很困扰,点开好几个页面都是一样的回答,对我的理解帮助很小。我觉得对于同一个事情,如果每个人都真实的把自己的理解说出来,不要担心说错了别人看不起,就说自己是怎么理解这个概念的,这样寻找答案的人可以从很多个角度来认识这个问题,对问题的理解就更加的全面和深刻了,同时如果自己发现自己理解的是错误的也可以及时发现。单纯的复制粘贴,或者机械的把英文的资料翻译过来,为了怕出错让人笑话连句子的语序都不去调整,导致句子不伦不类的,我认为这样的心态要不得。也许是我以小人之心度君子之腹了吧,建议大家逐渐学会用英文搜索,提高效率,完。