【异步编程】
1. 简介
2. 异步设计问题
3. 使用反射(Reflection)
【简介】
编写网络程序有很多方法。下面是三种主要方式:
1. 为每个连接建立一个单独的处理进程。
2. 为每个连接建立一个单独的处理线程(脚注1)。 (线程池)
3. 在一个线程中使用“非阻塞”的系统调用来处理所有的连接。 (回调函数)
当使用一个线程来处理多个连接时,由应用程序而不是操作系统负责调度。通常通过“为每个已经准备好执行读/写操作的连接调用一个事先已经注册过的函数”这种方法来实现--这种方式常常被称为“异步事件驱动”或是“基于回调机制(callback-based)”的编程。
多线程编程是十分灵活的,但是Python中的“全局解释器锁”机制(Global Interpreter Lock)和高阶的抽象能力限制了其通过多线程编程所获得的潜在性能收益。而Fock操作对Python进程来说也存在许多的不足,例如:Python的后台的引用计数机制不能很好的处理“写时复制”(copy-on-write)和一些共享状态的问题。于是,事件驱动的框架便成为Python网络编程的一个最佳选择。使用事件驱动的框架编程的一个好处是可以用其它的事件驱动的框架来取代程序中的主循环,这直接导致服务器端与客户端代码的同质化,于是编写p2p应用成为可能。
事件驱动的编程也有其灵活的方面。由于每个回调函数都必须尽快返回,因此我们不能在函数级的局部变量中保存持久性信息。此外,像递归等编程技巧也不能使用,因为这会使Python中“递归下降解析”的协议处理器失效。由于需要频繁的编写状态机处理程序(state machines),事件驱动编程向来有难于使用的坏名声。但是Twisted事件驱动编程框架建立在“恰当的使用库,事件驱动的编程要比多线程编程更容易”这样一个假设上。
值得注意的是,如果你真的需要使用多线程编程,那么Twisted仍然给予你使用的自由。这常用来编写一些新代码与旧时的“使用同步机制编写的代码”之间的接口部分。
【异步设计问题】
在Python中,代码通常可以分解为一些可覆写的(override)基类方法调用,这些方法可以由不同的子类实现。在这种情况下,或其它类似的情形下,编程的重点就转换为如何编写不同的子类方法的实现。如果可以预知一个实现中的某个类方法会执行很长时间(例如一些由于cpu或是网络问题导致延迟),则该方法就应该设计为异步执行的方式。这通常意味着要将这个方法转换为基于回调的实现方式。在Twisted中,这也就意味着该方法要返回一个Deferred。
由于每个方法都会迅速返回,因此持久性状态不能保存在局部变量中,而要保存在类的实例变量(instance variables)中。在一些必须使用递归的情况下,要手工维护递归产生的堆栈信息,此时常常使用Python中list类的append和pop方法。由于状态机处理程序非常复杂,将他们分层是一个较好的方式,这样每个状态机都只做一件事--将一个抽象级别的事件转换为下一个更高抽象级别的事件。这使代码十分清晰,并且易于调试。
【使用反射(Reflection)】
使用回调方式编程的一个直接产物就是需要为一些小代码段命名。咋看起来这是一些无关轻重的小事儿,但是如果恰当的处理就会极大的提高编程效率。如果使用严格一致的命名规则,就可以避免大量使用常见的“if-else”或是case语句。例如,在SMTP的客户端代码中,一个类的实例变量会通知服务器它想要做什么,当它接收到SMTP服务器的应答的时候,它只要调用 "do_%s_%s" % (self.state, responseCode) 就可以实现所有的事件驱动操作。这种方法使我们无需再事先注册回调函数,或是在代码中加入大段的“if-else”语句。此外,还可以通过子类来覆写(override)或修改每个应答所对应的操作,而无需编写那些“与应用逻辑不相关”(harness)的代码。SMTP客户端的实现可以在代码twisted/protocols/smtp.py中找到。
(函数数组指针)
【脚注】
1. 还存在这个方法的一些变种实现,例如:可以使用一个限制大小的线程池来处理所有的连接。它对该方法做了一些优化,但是本质上仍是相同的。
【Reactor概述】
1. Reactor基础
2. 使用Reactor对象
这个HOWTO文档介绍了Twisted中的reactor类,描述了reactor的基础以及一些相关内容。
【Reactor基础】
reactor是Twisted中事件循环的核心,这个循环用于驱动使用Twisted编写的应用程序。reactor为各种服务(包括网络通讯、线程和事件分派)都提供了基本的接口。
要了解有关reactor和Twsited事件循环的更多信息,请看
HOWTO中关于事件分派的部分:调度和使用Deferred
HOWTO中关于网络通讯的部分:TCP服务器、TCP客户端、UDP网络、使用process
使用thread
reactor有多个变种实现。在默认实现的基础上,每个reactor的变种实现都为支持一些特殊应用进行了相应的优化。关于这些变种实现的更多信息和如何选择一个特定的reactor实现可以参考“选择一个reactor”这一部分。
可以使用“twisted.application.service 中提供的接口”替代“单纯的reactor代码”来配置和运行基于Twisted的application。具体的信息可以参考“使用application”中对application的介绍。
【使用reactor对象】
通过下面的代码引入reactor对象:
-----------------------------------------
| from twisted.internet import reactor |
-----------------------------------------
每个reactor的实现都实现了一组常用接口,但是根据所选择的reactor实现的不同,每个reactor实现所支持的接口也会略有不同。下面列出了一些常用的接口:
--------------------------------------------------------------------------
IReactorCore|实现了reactor的核心功能
IReactorFDSet|使用文件描述符对象
IReactorProcess |进程管理(参考“使用Process”)
IReactorSSL |提供了SSL网络支持
IReactorTCP |提供TCP网络支持(参考“编写TCP服务器”和“编写TCP客户端”)
IReactorThreads |提供线程的使用与管理支持。(参考“Twisted中的线程”)
IReactorTime|提供调度支持(参考“调度任务”)
IReactorUDP |提供UDP网络支持(参考“UDP网络”)
IReactorUNIX|提供UNIX socket支持
---------------------------------------------------------------------------
【编写服务器】
1. 概述
2. 协议
o 使用Protocol
o 一些辅助Protocol
o 状态机
3. 工厂
o 把所有的东西都放里
【概述】
Twisted是一个设计灵活的框架,你可以利用Twisted编写功能强大的服务器。为了保证灵活性,Twisted中使用了分层实现的机制,你用Twisted所编写的服务器也要遵循这种分层的机制。
这份文档描述了Protocol层,在Protocol层中,你可以自己实现对特定协议解析与处理。如果正在实现一个application,那么你应该先看看“为Twisted编写插件”中关于如何开始编写你的Twisted application的介绍,然后在看这份文档。本文只涉及TCP、SSL和UNIX socket服务器,对于UDP相关的问题会用单独的一章来描述。
你的协议处理类通常要继承 twisted.internet.protocol.Protocol。大多数的协议处理类要么继承这个基类,要么继承该基类的某个更适合它使用的子类。依据需要,可以为每个连接都初始化一个协议类的实例,当连接断开的时候就释放这个实例。这意味着“要求持久性保存的配置信息”不能保存在Protocol类中。
持久性的配置信息通常保存在一个工厂类中,这个类应该从 twisted.internet.protocol.Factory 类继承。默认工厂类的实现只是实例化每一个Protocol类,并为每个Protocol类的实例加一个指向它自己的名为factory的属性。这种方式使每个Protocol类都可以通过这个factory访问或是修改保存在工厂类中的持久性的信息。
能够在多个网络地址或端口上提供相同的服务使十分有用的,这就是为什么工厂类不能监听连接,事实上它对网络环境一无所知。(参考 twisted.internet.interfaces.IReactorTCP.listenTCP 和其它的 IReactor*.listen* API函数)
本文将具体解释上述内容。
【协议】
正如上面所说,protocol和一些辅助的类包含了大部分的代码。一个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("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 connectionMade(self): |
| self.factory.numProtocols = self.factory.numProtocols + 1|
| if self.factory.numProtocols > 100: |
| self.transport.write("Too many user, try again later!\r\n")|
| self.transport.loseConnection() |
|def connectionLost(self, reason):|
| self.factory.numProtocols = self.factory.numProtocols - 1|
|def dataReceived(self, data):|
| self.transport.write(data) |
--------------------------------------------------------------------------
这里connectionMade和connectionLost相互配合来维护factory中保存的当前激活连接的个数。如果连接数过多,connectionMade事件处理程序就断开连接。
【使用Protocol】
在这个部分中将解释如何方便的测试你的协议。(注意:如果需要编写“产品级”的Twisted服务器,最好去看HOWTO中“为Twisted编写插件”的部分。)
下面的代码将运行上面提过的QOTD服务器。
--------------------------------------------------------------------------
| 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()|
| |
| # 下面的代码令人惊奇 |
| factory = Factory() |
| factory.protocol = QOTD |
| |
| # 服务将在8007端口运行,也可以选择其它大于1024的端口|
| reactor.listenTCP(8007, factory) |
| reactor.run() |
--------------------------------------------------------------------------
不要担心最后6行看似神奇代码--本文后面会解释他们的作用。
【辅助Protocol】
许多协议建立在类似的低级抽象上。大多数流行的internet协议是基于行的协议。使用CR-LF的组合标识一行的终结。
然而,也有一些协议是混和型的--他们既有基于行的部分也有基于原始数据的部分,例如HTTP/1.1协议和Freenet协议。
大多数情况下,都可以使用LineReceiver协议。这个协议会分派两个不同的事件处理器--lineReceived和rawDataReceived。默认会对每个行调用lineReceived方法。但是如果调用了setRawMode方法将协议设置为接受原始数据的方式,该协议就会调用rawDataReceived方法来处理到来的数据。直到再次调用setLineMode方法来切换处理函数为止。