[《Twisted网络编程必备》翻译] 第2章

来源:互联网 发布:淘客软件下载 编辑:程序博客网 时间:2024/04/30 04:13
2. 建立简单的客户端和服务器

使用Twisted进行开发,需要学习如何使用新的类和对象。这些类是Twisted的核心,你将会在你的应用中使用这些类。它们提供了平滑的学习曲线,理解如何使用他们,将会使得使用Twisted进行开发更加简便。
这一章展示了如何编写简单的客户端和服务器。并介绍Twisted简单的类和工作流程,并展示如何使用它们。



2.1 启动Twisted的事件循环

Twisted是事件驱动的框架。这意味着不再使用特定序列来处理程序逻辑,而是通过被动调用一些函数来实现。例如,GUI程序里面使用的"button pressed"事件。设计者不需要事先确定事件何时发生,只需要编写事件的响应函数即可。这就是所谓的事件处理器(event handler)。
每个事件驱动的框架都包含了一个特殊的函数叫做事件循环(event loop)。每次启动,事件循环都会立即运行。这个函数运行时等待事件的发生。当事件发生时,事件循环函数会自动触发相关的事件处理函数。
使用事件循环需要改变一些顺序编程的习惯。一旦开始了事件循环,你将无法控制程序的执行顺序,只会自动响应事件。所以,你需要为程序设计事件处理器。也就是事件发生时的响应。
在Twisted中,有一种特殊的对象用于实现事件循环。这个对象叫做reactor。可以把反应器(reactor)想象为Twisted程序的中枢神经。除了分发事件循环之外,反应器还做很多重要的工作:定时任务、线程、建立网络连接、监听连接。为了让反应器可以正常工作,需要启动事件循环。

2.1.1 下面如何做

启动反应器是很简单的,从twisted.internet模块导入reactor对象。然后调用reactor.run()来启动反应器的事件循环。下例展示了代码:
from twisted.internet import reactor
print 'Running the reactor ...'
reactor.run()
print 'Reactor stopped.'
反应器将会一直运行,直到接到停止的通知,可以按下Ctrl+C来退出事件循环,并终止程序:
$ python runreactor.py
Running the reactor ...
<ctrl-c>
^CReactor stopped.
这是一个简洁的例子。尽管反应器运行着,但是没有做任何事情。下面的例子提供了更多有趣的事情,介绍了反应器的callLater方法。reactor.callLater方法用于设置定时事件。这个方法在用于定时在未来执行。比如一个函数已经是一个事件处理器了,这种事件会在一定时间之后启动。下面的例子,函数调用了一定时间之后被调用:
from twisted.internet import reactor
import time
def printTime():
    print 'Current time is',time.strftime("%H:%M:%S")
def stopReactor():
    print "Stopping reactor"
    reactor.stop()
reactor.callLater(1,printTime)
reactor.callLater(2,printTime)
reactor.callLater(3,printTime)
reactor.callLater(4,printTime)
reactor.callLater(5,stopReactor)
print 'Running the reactor ...'
reactor.run()
print 'Reactor stopped.'
运行这个程序,可以看到如下结果:
$ python calllater.py
Running the reactor ...
Current time is 10:33:44
Current time is 10:33:45
Current time is 10:33:46
Current time is 10:33:47
Stopping reactor
Reactor stopped.

2.1.2 它是如何工作的

例子2-1简介了导入与执行reactor.run()来启动事件循环。反应器将会一直保持执行直到按下Ctrl-C,尽管这一段时间什么都不做。在这时,反应器停止了,程序执行到代码的最后一行,并打印出反应器已经停止的消息。
第二个例子使用reactor.callLater函数定时执行函数。reactor.callLater函数包含两个必须参数,等待的秒数,和需要调用的函数。在设置了定时函数调用之后,控制就回到了反应器的事件循环reactor.run()中了。
Tip:你可以传递附加的参数(by gashero)和键值对到reactor.callLater中,这些用于调用指定的函数。例如,reactor.callLater(1,func,True,x="Hello"),将会最终调用func(True,x="Hello"),在一秒钟之后。
第一个定时函数是printTime(),简单显示了当前时间。第五个定时函数是stopReactor(),其中调用了reactor.stop(),导致了反应器退出了事件循环。这也是为什么不需要按下Ctrl-C而自动退出了事件循环的原因。
Tip:在这种情况下仍然可以按下Ctrl-C来手动停止事件循环。
注意事件的发生顺序,反应器按照给定的时间来调用指定的函数。一旦反应器开始运行,反应器就会控制事件循环,并在指定时间调用函数。反应器在被告知停止之前会一直运行,直到reactor.stop()调用。一旦反应器停止了,程序将继续处理最后一行,显示反应器停止的消息。
Tip:在实际应用中,reactor.callLater是常用于超时处理和定时事件。可以设置函数按照指定的时间间隔来执行关闭非活动连接或者保存内存数据到硬盘。

2.1.3 建立(establishing)一个TCP连接

所有的网络应用程序都必须做一个简单的步骤,开启一个连接。接着可以发送邮件、传递文件、看电影等等,但是在所有这些之前,必须在电脑之间建立一个连接。本节讲解了如何使用reactor开启TCP连接。

2.1.4 下面如何做

调用reactor.connectTCP()方法打开一个TCP连接,传递一个ClientFactory对象作为第三个参数。ClientFactory对象等待连接被建立,然后创建一个Protocol对象来管理连接中的数据流。下面的例子2-3展示了如何在电脑和Internet之间建立一个连接(www.google.com):
from twisted.internet import reactor,protocol
class QuickDisconnectedProtocol(protocol.Protocol):
    def connectionMade(self):
        print "Connected to %s."%self.transport.getPeer().host
        self.transport.loseConnection()
class BasicClientFactory(protocol.ClientFactory):
    protocol=QuickDisconnectProtocol
    def clientConnectionLost(self,connector,reason):
        print 'Lost connection: %s'%reason.getErrorMessage()
        reactor.stop()
    def clientConnectionFailed(self,connector,reason):
        print 'Connection failed: %s'%reason.getErrorMessage()
        reactor.stop()
reactor.connectTCP('www.google.com',80,BasicClientFactory())
reactor.run()
当运行这个例子的时候,可以看到如下输出:
$ python connection.py
Connected to www.google.com
Lost connection: Connection was closed cleanly.
除非你的电脑没有在线。在这种情况下,会看到如下错误信息:
$ python connection.py
Connection failed: DNS lookup failed: address 'www.google.com' not found.

2.1.5 它是如何工作的

这里有两个主要的类用于作为客户端工作,ClientFactory和Protocol。这些类被设计成处理连接中所有可能运到的事件:成功建立连接、连接失败、连接断开、数据传送等等。
ClientFactory和Protocol有严格的不同。ClientFactory的工作是管理连接事件,并且创建Protocol对象处理每一个成功的连接。一旦连接建立,Protocol对象就接管下面的工作了,包括收发数据和决定是否关闭连接。
Tip:名字"Factory"在ClientFactory是来自于Protocol的请求,响应每一个成功的连接。
例子2-3定义了自定义的Protocol叫做QuickDisconnectProtocol,继承自protocol.Protocol。它重载了一个方法connectMade。这个方法连接成功时运行,在reactor刚刚成功建立了连接,然后ClientFactory创建了QuickDisconnectProtocol的实例时。有如他的名字,QuickDisconnectProtocol对象在打印信息之后就马上关闭了。
Tip:Protocol对象有一个属性叫做transport,包含了当前活动连接对象。
BasicClientFactory是继承自protocol.ClientFactory的类。它首先设置了类变量protocol为QuickDisconnectProtocol。这个类的实例被创建用于管理成功的连接。
BasicClientFactory重载了ClientFactory的两个方法,clientConnectionLost和clientConnectionFailed。这两个方法是事件处理器。clientConnectionFailed在反应器无法建立连接时被调用。clientConnectionLost在建立的连接被关闭或断开时调用。
告知反应器建立TCP连接,按照例子2-3的方法是调用reactor.connectTCP:
reactor.connectTCP('www.google.com',80,BasicClientFactory())
这一行告知反应器建立一个TCP连接到服务器www.google.com的80端口,通过BasicClientFactory来管理连接。





2.2 使用非同步的方式工作的结果

除了反应器reactor之外,Deferred可能是最有用的Twisted对象。你可能在Twisted程序中多次用到Deferred,所有有必要了解它是如何工作的。Deferred可能在开始的时候引起困惑,但是它的目的是简单的:保持对非同步活动的跟踪,并且获得活动结束时的结果。
Deferred可以按照这种方式说明:(by gashero)可能你在饭店中遇到过这个问题,如果你在等待自己喜欢的桌子时,在一旁哼哼小曲。带个寻呼机是个好主意,它可以让你在等待的时候不至于孤零零的站在那里而感到无聊。你可以在这段时间出去走走,到隔壁买点东西。当桌子可用时,寻呼机响了,这时你就可以回到饭店去你的位置了。
一个Deferred类似于这个寻呼机。它提供了让程序查找非同步任务完成的一种方式,而在这时还可以做其他事情。当函数返回一个Deferred对象时,说明获得结果之前还需要一定时间。为了在任务完成时获得结果,可以为Deferred指定一个事件处理器。

2.2.1 下面如何做?

当编写一个启动非同步操作的函数时,返回一个Deferred对象。当操作完成时,调用Deferred的callback方法来返回值。如果操作失败,调用Deferred.errback函数来跑出异常。例子2-4展示了程序使用Deferred使用非同步操作的连接例子,连接一个服务器的端口。
当调用一个可以返回Deferred的函数时,使用Deferred.addCallback方法指定返回结果时调用的函数。使用Deferred.addErrback方法指定执行发生异常时调用的函数。
from twisted.internet import reactor,defer,protocol
class CallbackAndDisconnectProtocol(protocol.Protocol):
    def connectionMade(self):
        self.factory.deferred.callback("Connected!")
        self.transport.loasConnection()
class ConnectionTestFactory(protocol.ClientFactory):
    protocol=CallbackAndDisconnectProtocol
    def __init__(self):
        self.deferred=defer.Deferred()
    def clientConnectionFailed(self,connector,reason):
        self.deferred.errback(reason)
def testConnect(host,port):
    testFactory=ConnectionTestFactory()
    reactor.connectTCP(host,port,testFactory)
    return testFactory.deferred
def handleSuccess(result,port):
    print "Connect to port %i"%port
    reactor.stop()
def handleFailure(failure,port):
    print "Error connecting to port %i: %s"%(
        port,failure.getErrorMessage())
    reactor.stop()
if __name__=="__main__":
    import sys
    if not len(sys.argv)==3:
        print "Usage: connectiontest.py host port"
        sys.exit(1)
    host=sys.argv[1]
    port=int(sys.argv[2])
    connecting=testConnect(host,port)
    connecting.addCallback(handleSuccess,port)
    connecting.addErrback(handleFailure,port)
    reactor.run()
运行这个脚本并加上服务器名和端口这两个参数会得到如下输出:
$ python connectingtest.py oreilly.com 80
Connecting to port 80.
或者,如果连接的端口是关闭的:
$ python connectiontest.py oreilly.com 81
Error connecting to port 81: Connection was refused by other side: 22: Invalid argument.
或者连接的服务器并不存在:
$ python connectiongtest.py fakesite 80
Error connecting to port 80: DNS lookup failed: address 'fakesite' not found.

2.2.2 它是如何工作的?

类ConnectionTestFactory是ClientFactory的子类,并且含有Deferred对象作为属性。当连接完成时,CallbackAndDisconnectProtocol的connectionMade方法会被调用。connectionMade会调用self.factory.deferred.callbakc加上任意值标识调用成功。如果连接失败,ConnectionTestFactory的clientConnectionFailed方法会被调用。第二个参数clientConnectionFailed的reason是twisted.python.failure.Failure对象,封装了异常,并描述了异常发生的原因。clientConnectionFailed传递了Failure对象到self.deferred.errback来标识操作失败。
testConnecn函数需要两个参数host和port。用于创建叫做testFactory的ConnectionTestFactory的类实例,并且传递给reactor.connectTCP参数host和port。当连接成功时返回testFactory.deferred属性,就是用于跟踪的Deferred对象。
例子2-4夜展示了两个事件处理器函数:handleSuccess和handleFailure。当从命令行运行时,它们需要host和port两个参数来调用testConnect,指定了结果Deferred到变量connecting。此时可以使用connecting.addCallback和connecting.addErrback来设置事件处理器函数。在每一种情况下,传递port作为附加参数。因为扩展的参数或者关键字参数将会由addCallback或addErrback传递给事件处理器,这将会在调用handleSuccess和handleFailure时得到端口参数作为第二个参数。
Tip:当函数返回Deferred时,可以确定事件调用了Deferred的callback或者errback方法。否则代码将永远等待结果。
在调用了testConnect和设置事件处理器之后,控制劝将会交给reactor.run()。依赖于testConnect的执行成功与否,将会调用handleSuccess或者handleFailure,打印西那个关的信息并停止反应器。

2.2.3 关于

是否需要保持一串的Deferred呢?有时确实需要同时保持多个非同步任务,且并非同时完成。例如,可能需要建立一个端口扫描器运行testConnect函数来对应一个范围的端口。为了实现这个,使用DeferredList对象,作为例子2-5,来挂历众多的Deferred。
from twisted.internet import reactor,defer
from connectiontester import testConnect
def handleAllResules(results,ports):
    for port,resultInfo in zip(ports,results):
        success,result=resultInfo
        if success:
            print 'Connected to port %i' % port
    reactor.stop()
import sys
host=sys.argv[1]
ports=range(1,201)
testers=[testConnect(host,port) for port in ports]
defer.DeferredList(testers,consumeErrors=True).addCallback(handleAllResults,ports)
reactor.run()
运行portscan.py脚本并传递host参数。将会自动扫描1-200端口并报告结果。
$ python portscan.py localhost
Connected to port 22
Connected to port 23
Connected to port 25
... ...
例子2-5使用了Python的列表(list)的形式来创建一个列表存储由testConnect()返回的Deferred对象的列表。每一个testConnect()使用host参数,并测试1-200端口。
testers=[testConnect(host,port) for port in ports]
例子2-5使用列表方式包装了Deferred对象成为DeferredList对象。DeferredList将会跟踪所有的Deferred对象的结果并传递作为首参数。当它们都完成了的时候,将会回调按照(success,result)的格式。在Deferred完成时,第一个回传值是TRue和第二个参数是Deferred传回结果。如果执行失败,则第一个参数是False且第二个参数是Failure对象包装的异常。consumeErrors键设置为True时告知DeferredList完成了吸收Deferred的所有错误。否则将会看到弹出的错误信息。
当DeferredList完成时,results将会被传给handleAllResults函数,列表中的每个元素都会被扫描。handleAllResults使用了zip函数来匹配每个结果的端口。对每一个端口如果有成功的连接则打印信息。最终反应器会结束并结束程序。





2.3 发送和接收数据

一旦TCP连接被建立,就可以用于通讯。程序可以发送数据到另一台计算机,或者接收数据。

2.3.1 下面如何做?

使用Protocol的子类可以发送和接收数据。重载dataReceived方法用于控制数据的接收,仅在接收到数据时才被调用。使用self.transport.write来发送数据。
例子2-6包括了DataForwardingProtocol的类,可以将接收到的数据写入self.output。这个实现创建了简单的应用程序,类似于netcat,将收到的数据传递到标准输出,用于打印接收的数据到标准输出。
from twisted.internet import stdio,reactor,protocol
from twisted.protocols import basic
import re
class DataForwardingProtocol(protocol.Protocol):
    def __init__(self):
        self.output=None
        self.normalizeNewLines=False
    def dataReceived(self,data):
        if self.normalizeNewLines:
            data=re.sub(r"(/r/n|/n)","/r/n",data)
        if self.output:
            self.output.write(data) #by gashero
class StdioProxyProtocol(DataForwardingProtocol):
    def connectionMade(self):
        inputForwarder=DataForwardingProtocol()
        inputForwarder.output=self.transport
        inputForwarer.normalizeNewLines=True
        stdioWarpper=stdio.StandardIO(inputForwarder)
        self.output=stdioWarpper
        print "Connected to server. Press Ctrl-C to close connection."
class StdioProxyFactory(protocol.ClientFactory):
    protocol=StdioProxyProtocol
    def clientConnectionLost(self,transport,reason):
        reactor.stop()
    def clientConnectionFailed(self,transport,reason):
        print reason.getErrorMessage()
        reactor.stop()
if __name__=='__main__':
    import sys
    if not len(sys.argv)==3:
        print "Usage: %s host port "__file__
        sys.exit(1)
reactor.connectTCP(sys.argv[1],int(sys.argv[2]),StdioProxyFactory())
reactor.run()
运行dataforward.py并传入host和port两个参数。一旦连接就可以将所有来自服务器的消息送回服务器,所有接收的数据也同时显示在屏幕上。例如可以手动连接一个HTTP服务器,并发送HTTP请求到oreilly.com:
$ python dataforward.py oreilly.com 80
Connected to server.  Press Ctrl-C to close connection.
HEAD / HTTP/1.0
Host: oreilly.com
                           <--空行
HTTP/1.1 200 OK
......

2.3.2 它们是如何工作的?

例子2-6开始于定义类DataForwardingProtocol。这个协议用于接受数据并存入self.output,这个属性可以用write方法访问,如同self.output.write。DataForwardingOutputProtocol包含一个叫做normalizeNewLines的属性。如果这个属性设置为True,将会由Unix风格的/n换行改变为/r/n这种常见的网络换行方式。
作为DataForwardingProtocol的子类StdioProxyProtocol类具体负责工作。一旦连接被建立,将会创建一个叫做inputForwarder的DataForwardingProtocol实例,并设置输出为self.transport。然后包装twisted.internet.stdio.StandardIO的实例inputForwarder,以标准IO的方式代替网络连接。这一步对所有的通过StdioProxyProtocol方式的网络连接都有效。最终设置StdioProxyProtocol的output属性到值stdioWarpper,所以数据的接收工作定义到了标准输出。
协议定义之后,很容易定义StdioProxyFactory,将其protocol属性设置为StdioProxyProtocol,并且处理反应器的停止和连接、失败工作。调用reactor.connectTCP连接,然后依靠reactor.run()来控制事件循环。






2.4 接受客户端的连接

前面的实验都是讲解如何作为客户端进行连接。Twisted也同样适合于编写网络服务器,用于等待客户端连接。下面的实验将会展示如何编写Twisted服务器来接受客户端连接。

2.4.1 下面如何做?

创建一个Protocol对象来定义服务器行为。创建一个ServerFactory对象并引用Protocol,并传递给reactor.listenTCP。例子2-7展示了(by gashero)简单的echo服务器,简单的返回客户端发来的信息。
from twisted.internet import reactor,protocol
from twisted.protocols import baisc
class EchoProtocol(basic.LineReceiver):
    def lineReceived(self,line):
        if line=='quit':
            self.sendLine("Goodbye.")
            self.transport.loseConnection()
        else:
            self.sendLine("You said: "+line)
class EchoServerFactory(protocol.ServerFactory):
    protocol=EchoProtocol
if __name__=="__main__":
    port=5001
    reactor.listenTCP(port,EchoServerFactory())
    reactor.run()
当这个例子运行时,将会在5001端口监听,并报告已经建立的连接。
$python echoserver.py
Server running, Press Ctrl-C to stop.
Connection from  127.0.0.1
Connection from  127.0.0.1
在另一个终端,使用netcat、telnet或者dataforward.py(例子2-6)来连接服务器。将会返回键入的例子。输入quit来关闭连接。
$ python dataforward.py localhost 5001
Connected to server.  Press Ctrl-C to close connection.
hello
You said: hello
twisted is fun
You said: twisted is fun
quit
Goodbye.

2.4.2 它们是如何工作的?
Twisted服务器使用相同的Protocol类作为客户端。为了复用,EchoProtocol继承了twisted.protocols.basic.LineReceiver,作为Protocol.LineReceiver的轻量级实现,可以自动按照获得的行来产生处理。当EchoProtocol接收到一个行时,就会返回收到的行,除非遇到了'quit'将退出。
下一步,定义了EchoServerFactory类。EchoServerFactory继承自ServerFactory,作为ClientFactory的服务器端,并设置了EchoProtocol为protocol属性。EchoServerFactory的实例作为第二个参数传递给reactor.listenTCP,第一个参数是端口号。



 
原创粉丝点击