第二部分:slow poetry and the apocalypse
原文:http://krondo.com/blog/?p=1247
作者:dave
译者:notedit
时间:2011.05.19
我的假设
我假设你已经有一些基本的能力去写python程序,并且直到一些socket编程方面的额知识,如果你没有接触过socket请移步这里 socket module documention, 这个系列中的代码例子都是运行在python2.5 和twisted 8.2.0,程序如果不能正确运行请检查你的python和twisted 的版本.
获取例子代码
你可以在public git repository获取例子代码,如果你可以用git 或者其他的版本管理软件,你可以直接clone 出一份代码:
git clone git://github.com/jdavisp3/twisted-intro.git
缓慢的诗
尽管cpu比网络要快很多,但是网络还是仍旧会比你的脑袋或者眼睛快.所以你要看到cpu-network是怎样运行几乎是不可能的.我们需要的是一个slow server,可以让我们的眼睛看到变化.既然是一个server,那就让我们的server来生产两首颇具诗意的诗吧,在代码仓库的子目录中有John Donne, W.B. Yeats, 和 Edgar Allen Poe 的三首小诗,当然如果换成你自己写的,that will be nice
最基本的”poetry server” 的实现在 blocking-server/slowpoetry.py,你可以让他跑起来:
python blocking-server/slowpoetry.py poetry/ecstasy.txt
上面这个命令会开启一个阻塞的服务器,你可以打开这个阻塞服务器的源码看一下,你可以看到我们并没有用到twisted,只有一些简单的socket 操作.它可以在每个固定的延迟后发出固定的数量的字节,默认的它会每0.1s 发出10个字节.你可以通过–num-bytes 和 –delay 来控制时间和字节数,比如下面:
python blocking-server/slowpoetry.py --num-bytes 50 --delay 5 poetry/ecstasy.txt
当这个服务开始运行的时候,它会打印出它监听的端口.默认的,这个是一个随机的端口,你可以通过修改配置让它监听固定的端口,你可以指定它监听的端口如下:
python blocking-server/slowpoetry.py --port 10000 poetry/ecstasy.txt
如果你有netcat 命令的话,你可以测试这个server 通过一下的命令:
netcat localhost 10000
如果一切正常的话,你会看到一条小诗被每次输出一些字节,一旦这首小诗被完全生产出来,服务器会断开连接.
如果你看源码的话你会发现,poetry server 不仅仅是每次输出一些字节,如果你再用其他的client去连接server 的话,其他的client 必须等到第一个client被处理完才会被poetry server 处理. 现在的poetry server 确实很慢啊,慢的我蛋痛.
阻塞的client
在我们的例子中我们的客户端也是阻塞的,它可以从多个server 上一个接一个的读取内容,现在我们给我们的客户端三个任务去执行,就想图片一中的那样.首先我们先启动三个服务器,为三个不同的客户端来服务,如下:
python blocking-server/slowpoetry.py --port 10000 poetry/ecstasy.txt --num-bytes 30
python blocking-server/slowpoetry.py --port 10001 poetry/fascination.txt
python blocking-server/slowpoetry.py --port 10002 poetry/science.txt
接下来我们可以开启阻塞的客户端blocking-client/get-poetry.py来接收小诗了:
python blocking-client/get-poetry.py 10000 10001 10002
客户端会按照顺序的去接收小诗,只有接收完一个才会去接收下一个.你会看到一下的输出:
Task 1: get poetry from: 127.0.0.1:10000
Task 1: got 3003 bytes of poetry from 127.0.0.1:10000 in 0:00:10.126361
Task 2: get poetry from: 127.0.0.1:10001
Task 2: got 623 bytes of poetry from 127.0.0.1:10001 in 0:00:06.321777
Task 3: get poetry from: 127.0.0.1:10002
Task 3: got 653 bytes of poetry from 127.0.0.1:10002 in 0:00:06.617523
Got 3 poems in 0:00:23.065661
基本上就是图片一的文字版本.你可以看一下源代码去定位一下在接收和发送字节的时候那些地方会产生阻塞.
异步的client
下面让我们看看一个简单的异步的client,没有用twisted.先让我们运行一下, async-client/get-poetry.py:
python async-client/get-poetry.py 10000 10001 10002
你将会得到如下的输出:
Task 1: got 30 bytes of poetry from 127.0.0.1:10000
Task 2: got 10 bytes of poetry from 127.0.0.1:10001
Task 3: got 10 bytes of poetry from 127.0.0.1:10002
Task 1: got 30 bytes of poetry from 127.0.0.1:10000
Task 2: got 10 bytes of poetry from 127.0.0.1:10001
...
Task 1: 3003 bytes of poetry
Task 2: 623 bytes of poetry
Task 3: 653 bytes of poetry
Got 3 poems in 0:00:10.133169
这一次的输出结果会比较长因为每当异步的client 每一次从任何一个服务器上获取一些东西都会输出.注意,这个异步的client 会交错的执行三个任务,就像在图片三中描述的那样.
需要注意的是,异步客户端完成任务用了差不多10s,而同步的客户端需要差不多23s,现在回想一下图片三和图片四的区别.通过减少阻塞的时间,我们的异步客户端需要更少的时间.我们的异步的客户端也是会有一些阻塞,但通过交错任务来执行可以减少很多阻塞的时间.
严格的来说,我们的异步client也在执行阻塞操作:它会向stdout输出一些内容.但对我们的例子还没有太大的影响.终端其实一直在准备接收更多的print 的输出,所以print 并不会阻塞,和我们的slow 服务器来说,print语句很快.如果我们的异步程序是一个进程中管道的一部分,在处理标准的输入和输出的时候应该考虑用异步的I/O.twisted 已经提供了在处理标准输入输出时异步I/O 的支持,为了kiss 原则,这里先不用
更进一步的
让我们看一下异步client 的代码,注意异步client代码和同步client代码的区别:
- 异步的client会一次连接多个server,而同步的client一次只连接一个server
- 异步的client中的socket 通过setblocking(0)设置为了非阻塞的
- select 模块的selsect可以用来监听是否有socket可以提供给client 的数据
- 从服务器读数据的时候,异步client会尽可能的多读数据直到这个socket被阻塞,然后如果有其他的socket是可读的话就转到下一个,这意味着我们必须要记录每一首小诗传输状态
这个异步client 中最核心的部分是get_poetry 函数的上层的事件循环,这个循环要经过一下的过程:
- 用select监听所有打开的sockets,直到其中的一个socket有数据流可以读
- 如果有一个socket有数据可读,读取它
- 重复循环,直到所有的socket都关闭
同步的client在main 函数中也有一个循环,但是每次遍历的时候同步客户端只下载同一首小诗,直到这首小诗读完.而异步的client在每次循环的时候会读取多个小诗的片段,我们不确定在一次循环中他会读取几个小诗,或者每次读取多少字节, 这完全依赖于服务器的速度以及网络的速度.我们让select 告诉我们哪一个socket有数据可以读,然后我们就可以在不产生阻塞的情况下尽可能多的去读数据了.
假如一个同步的client一直连在一个相同的服务器上(让我们假设是三号服务器),它现在根本不需要一个外部的循环,因为get_poety函数是阻塞的,客户端会一直连在同一个server上,直到获取完整个的一首诗.而异步的客户端没有一个事件循环的话则无法进行下去,异步的客户端需要在一开始的就监听全部的socket,然后在每一次的循环中处理所能收受的所有数据.
这种用一个循环去等待事件的发生,然后去处理事件的模式我们很常见,已经形成了一种设计模式叫做:reactor pattern(反应堆模式),下面这张图比较直观的描述了反应堆模式
这个循环就是一个反应器因为他在不断的等待事件的发生然后响应他们,因为这个原因也被叫做事件循环.因为”反应器”系统总是等待I/O,这些循环有时也被叫做 select loops,select call 也经常等待I/O. 在一个select loop 中,一个事件就是其中一个socket 变为可读的或者可写的.记住 select 不是等待I/O 的唯一的方法,还有其他的方法被用做等待I/O(比如epoll),他们甚至比select 的性能要好,抛开性能不说,他们都用来做同一件事情:监听一些端口,然后等待其中的一些端口可以读或者可以写.
用select来监测文件描述符来实现一个简单的非阻塞程序是可能的,在" 反应器系统中"执行一些不涉及I/O的操作也是可以的.但是在一个真正的反应器系统中,所有的工作都是I/O相关的
严格的来,咱们的异步client 不是” 反应堆模式”,因为循环的逻辑和也业务逻辑没有完全分开,他们交错在一起.一个真正的反应器模式应该让事件循环作为一个抽象来实现以下两点:
- 1,接收一些你要处理I/O的文件描述符
- 2,当任何一个文件可以读或者可以写的时候,独立的告诉你
一个非常好的反应堆模式还应实现:
- 可以处理各种操作系统各种奇怪的实现
- 提供一个非常好的抽象的实现去帮助你很容易的去应用reactor
- 提供各种公开的协议的实现
好吧,其实我们想说的就是twisted,一个具有鲁棒性很强,跨平台的,包罗万象的反应器模式的实现.第三部分我们将开始写一些简单的twisted 代码了.ARE YOU READY?