爬虫学习(三)--Twisted网络架构
爬虫学习(三)--Twisted网络架构
Twisted是一个完整的事件驱动的网络框架,利用这个框架可以开发做出完整的一步网络应用程序。有很多著名的Python 框架是基于Twisted框架的,例如,后面的章节要讲的网络爬虫框架Scrapy就使用Twisted框架编写的。
Twisted并不是Python标准模块,所以在使用之前需要使用pip install twisted 安装twisted 模块,如果使用的是Anaconda Python 开发环境,也可以使用conda install -c anaconda twisted 安装twisted模块。
异步编程模型
学习Twisted模块之前,先要了解一下一步编程模型。可能很多读者认为,异步编程就是多线程编程,其实这两种编程模型有着本质的区别。目前常用的编程模型有三种,同步编程模型、线程编程模型,异步编程模型。
下面看看有什么区别
同步编程模型
如果所有的人物都在一个线程中完成,那么这种编程模型成为同步编程模型。线程中的任务都是顺序执行的,也就是说,只有当第一个任务执行完后,才会执行第二个任务。
很显然,同步编程模型尽管很简单,但执行效率较低,如果中间某一个任务出现阻塞现象,那么后续任务就无法执行。会无限期等待下去。
线程编程模型
如果要完成多个任务,比较有效的方式是将这些任务分解,然后启动多个进程,每个进程处理一部分任务,最后再将处理结果合并,这样做的好处是当一个任务被阻塞后,并不影响其他任务的执行。
如果是单CPU单核的计算机,那么多线程也是同步执行的,也是任何一个线程都无法长时间独占CPU的计算时间,所以多个线程会不断交替在CPU上执行,也就是说,每个线程都可能被分成若干个小的执行块,并根据某种调度算法获取CPU计算资源。但是哪个线程该执行,什么时间执行都不是由用户决定的,这通常是操作系统的底层机制决定的,所以对于应用层的程序时无法干预的,当然,对于多CPU多核这样的高性能计算机,线程是有可能同时运行的。因此,多线程执行的高低在某种程度上取决于计算机是有多颗CPU,以及每颗CPU有多少个核,不管怎样,线程编程模型在运行效率上肯定会袁媛高于同步编程模型。
异步编程模型
我们只考虑在单CPU上的异步编程模型,至于在多CPU上的异步编程模型,有一些类似于多线程编程模型,但是更复杂,这里先不做考虑,其实基本原理是相同的。
在单CPU上,如果采用同步编程模型,任务肯定会顺利执行,但是如果其中一个任务被阻塞,那么在该任务后面的所有任务都无法执行。不过要是采用异步模型,当一个任务被阻塞后,就会立刻执行另一个任务,在异步编程模型中,从一个任务切换到另一个任务,要么是这个任务被阻塞,要么是这个任务执行完毕。而且,在异步编程模型中调度任务是由程序员控制的。
从前面的描述可知,单机运行效率来看,同步编程模型是最低的,而线程编程模型是最高的,尤其是在多CPU的计算机上。异步编程模型也可以进行任务切换,但要等到任务被阻塞或者执行结束才能切换到其他任务,因此,异步编程模型的运行效率介于同步编程模型和线程编程模型之间。
既然线程编程模型最高,为什么还要使用异步编程模型呢,原因有三
- 线程模型使用起来复杂,而且不可控,所有在使用线程模型是要认为这些线程是同步执行的(尽管实际情况并非如此),因此要在代码上加上一些和线程有关的机制,例如同步,加锁,解锁等。
- 如果由一两个任务需要于用户进行交互,使用异步编程模型可以理解切换到其他任务,这一切都是可控的。
- 任务之间相互独立,以至于任务内部的相互很少。这种机制让异步编程模型比线程模型更简单,更容易操作。
Reactor(反应堆)模式
异步编程模型之所以能监视所有的任务的完成和阻塞情况,是因为通过循环用非阻塞模式模式执行完了所有的人物。例如,对于使用Socket访问多个服务器的任务。如果使用同步编程模型,会一个任务一个任务地顺序执行,而使用异步编程模型,执行的所有Socket方法都是出于非阻塞的(使用色图blocking(0)设置),也就是说,使用异步编程模型需要在循环中执行所有的非阻塞Socket任务,并利用select模块中的select方法监视所有的Socket是否有数据需要接收。
这种利用循环体来等待时间反应,然后处理发生的事件的模型被设计成一个模式:Reactor(反应堆)模式,Twisted就是使用了Reactor模型的异步网络框架。
Hello World,Twisted 框架
从这一章开始,我们开始学习如何使用Twisted 框架
由于Twisted框架是基于Reactor模式的,所以需要循环来处理所有的任务,不过这个循环并不需要我们写,Twisted框架已经为我们封装好了,只需要调用reactor模块中的run函数就可以通过Reactor模式以非阻塞方式运行所有的任务。
from twisted.internet import reactor
reactor.run()
我们什么也没做,这里调用了run函数,实际上是开启事件循环,也就是Reactor模式中的循环。
首先要了解的几点:
- Twisted的Reactor模式必须通过run函数启动
- Reactor循环是在开始的进程中运行的,也就是运行在主进程中
- 一旦启动Reactor,就会一直运行下去。Reactor会在程序的控制之下
- Reactor循环髌骨会消耗任何CPU 资源
- 并不需要显示创建Reactor循环,只要导入reactor模块就可以了。也即是说Reactor 是Singleton (单件)模式,即在一个程序中只能有一个Reactor。
twisted 可以使用不同的Reactor,但需要在导入twisted.internet.reactor之前安装他。例如,引用pollreactor的代码如下:
from twisted.internet import pollreactor
pollreactor.install()
如果在导入twisted.internet.reactor之前没有安装任何特殊的reactor,那么twisted会为用户安装selecteractor,正因此如何,习惯性做法是不要在最顶层的模块内引入reactor以避免安装默认的reactor,而是在使用reactor的区域内安装。
下面代码安装pollreactor,然后代入和运行reactor
from twisted.internet import pollreactor
# 安装pollreactor
pollreactor.install()
from twisted.internet import reactor
reactor.run()
其实什么也没做
下面开始hello world开始,学习twisted
下面代码在reactor循环开始后向终端打印一条信息
def hello():
print('Hello, How are you?')
from twisted.internet import reactor
# 执行回调函数
reactor.callWhenRunning(hello)
print('Starting the reactor')
reactor.run()
输出:
Starting the reactor
Hello, How are you?
函数回调需要了解以下几点:
- reactor 模式是单线程的
- 向twisted这种交互是模型已经实现了reactor 循环,这就意味着无需亲自 去实现它
- 仍然需要框架来调用自己的代码来完成业务逻辑
- 因为在单线程中循行,要想运行自己的代码,必须在reactor循环中调用它们
- reactor实现并不直到调用代码中的哪个函数
用twisted实现事件戳用户端
twisted框架的异步机制是整个框架的基础,可以在这个基础上实现很多基于异步编程模型的应用,我们利用twisted框架的相关API 实现一个时间戳客户端。
连接一个服务端Socket,需要调用connectTCP函数,并且通过giant函数的参数指定了host和port,以及一个工厂对象,该工厂对象对应的类必须是ClientFactory的子类,并且设置了protocol 等属性。protocol相当于一个回调类,Protocol类的子类实现的很多父类的方法都会被回调。
程序:利用twisted框架实现一个时间戳客户端程序,在Console中输入字符串,然后按回车键将字符串发送给时间戳服务端,最后时间戳服务端会返回服务端的时间和发送给服务器的字符串。
用Twisted实现时间戳服务端
用twisted 编写服务端Socket 程序于编写客户端Socket程序的步骤差不多,只是需要调用listenTCP监听端口。编写服务端Socket程序同样需要一个Factory对象,以及一个从Protocol继承的类
# -*- coding: utf-8 -*-
from twisted.internet import protocol, reactor
from time import ctime
port = 9876
class MyProtocol(protocol.Protocol):
# 当客户端连接到服务端后,调用该方法
def connectionMade(self):
# 获取客户端 IP
client = self.transport.getPeer().host
print('客户端', client, '已经连接')
def dataReceived(self, data: bytes):
# 接收到客户端发送过来的数据后,向客户端发送服务器的数据
self.transport.write(ctime().encode(encoding='utf-8') + b' ' + data)
# 创建Factory
factory = protocol.Factory()
factory.protocol = MyProtocol
print('正在等待客户端连接')
# 监听端口号,等待客户端的请求
reactor.listenTCP(port, factory)
reactor.run()
运行程序后,会一直处于等待状态,我们可以用上一节实现的时间戳客户端测试本例,也可以使用telnet或其他客户端测试本例。这里选用了selnet 进行测试。在终端执行telnet localhost 9876,运行telent,并连接端,然后输入字符串,并按回车键,重复这一操作,会看到终端中会输出了信息。

浙公网安备 33010602011771号