返回顶部

01 | tornado 入门必备

Tornado 是一个基于Python的Web服务框架和 异步网络库, 最早开发与 FriendFeed 公司. 通过利用非阻塞网络 I/O, Tornado 可以承载成千上万的活动连接, 完美的实现了 长连接WebSockets, 和其他对于每一位用户来说需要长连接的程序

由此可以知道Tornado 适用于高并发和长连接的场景

它是如何实现高并发的?

  1 异步非阻塞IO

  2 基于epoll 的时间循环

  3 协程提高了代码的可读性

注意:在tornado中不要写同步的代码,因为tornado是基于协程(单线程)调度的,如果在一个地方存在了同步的耗时,就会造成其他协成的阻塞。

官方文档

https://tornado-zh-cn.readthedocs.io/zh_CN/latest/

安装tornado

pip install -i https://pypi.douban.com/simple tornado

Tornado 可以被分为以下四个主要部分:  

  • Web 框架 (包括用来创建 Web 应用程序的 RequestHandler 类, 还有很多其它支持的类).

  • HTTP 客户端和服务器的实现 (HTTPServer 和 AsyncHTTPClient).

  • 异步网络库 (IOLoop 和 IOStream), 对 HTTP 的实现提供构建模块, 还可以用来实现其他协议.

  • 协程库 (tornado.gen) 让用户通过更直接的方法来实现异步编程, 而不是通过回调的方式

 同步异步,阻塞和非阻塞

阻塞和非阻塞关注的是当前线程的状态

阻塞是指调用函数时候当前线程被挂起

非阻塞是指调用函数时候当前线程不会被挂起,而是立即返回

同步和异步关注的是获取结果的方式

同步指代码调用IO操作时,获取结果后才能执行下一步操作

异步是指代码调用IO操作时,不必等待IO操作完成就返回的调用方式

现在存在的一些事实

  1 cpu 的速度远高于io速度

  2 IO包括网络访问和本地文件访问,比如requests,urllib等传统的库都是同步的io

  3 网络IO大部分的时间都是处于等待的状态,在等待的时候cpu是空闲的,但是又不能执行其他操作

 

阻塞IO request 模块

import requests
html = requests.get("http://www.baidu.com").text
#1. 三次握手建立tcp连接,
# 2. 等待服务器响应
print(html)

在上面的例子中,在和服务器建立3次握手的时候,应用程序是处于阻塞的,那么怎么能利用这个阻塞的时间呢

request模块内部封装了urllib,urllib内部封装了socket,我们可以使用socket为我们提供非阻塞的方式,使cpu利用起来

通过socket直接获取html(阻塞)

import socket
client = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
host = "www.baidu.com"
client.connect((host, 80))  #阻塞io, 意味着这个时候cpu是空闲的
client.send("GET {} HTTP/1.1\r\nHost:{}\r\nConnection:close\r\n\r\n".format("/", host).encode("utf8"))

data = b""
while 1:
    d = client.recv(1024) #阻塞直到有數據
    if d:
        data += d
    else:
        break

data = data.decode("utf8")
print(data)

 

通过socket直接获取html(非阻塞)  

import socket

import socket
client = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
client.setblocking(False)
host = "www.baidu.com"

try:
    client.connect((host, 80))  #阻塞io, 意味着这个时候cpu是空闲的
except BlockingIOError as e:
    #做一些其他事
    pass

while 1:
    try:
        client.send("GET {} HTTP/1.1\r\nHost:{}\r\nConnection:close\r\n\r\n".format("/", host).encode("utf8"))
        print("send success")
        break
    except OSError as e:
        pass

data = b""
while 1:
    try:
        d = client.recv(1024) #阻塞直到有數據
    except BlockingIOError as e:
        continue
    if d:
        data += d
    else:
        break

data = data.decode("utf8")
print(data)

 

在上面的代码中虽然请求的时间和阻塞的没有区别,但是在三次握手的时候我们不需要在等待了,并不是不建立3次连接,而是把它交给操作系统去完成,而我们的应用不在去管它,从而防止CPU 存在空闲时间

 select poll 和 epoll

虽然我们通过socket 可以实现非阻塞的IO,但是程序还是会不停的进行循环尝试,操作系统为我们提供了一些方法可以监听socket状态,一旦socket 准备就绪(读写就绪),就会通知应用程序进行响应的读写操作。

select,poll,epoll都是IO多路复用的机制。IO多路复用就是通过一种机制,一个进程可以监听多个描述符(socket),一旦文件描述符 准备就绪(读写就绪),就会通知应用程序进行响应的读写操作。但是select,poll,epoll本质上还是同步IO,因为在读写事件准备就绪后需要自己读写,也就是说,这个读写过程是阻塞的。

而异步IO在无需自己读写,异步IO的实现会负责被数据从内核拷贝到用户空间(现在操作系统的异步IO比O多路复用目前来说还没达到明显的提升),所以目前使用最多的还是IO多路复用epoll

selct

select函数监视的文件描述符分为三类:writefds, readfds,exceptfds。调用select后,select函数会阻塞,直到有描述符就绪(有数据可读、可写、或者有except),或者超时(timeout指定等待时间,如果立即返回,设为null即可)函数返回,这个时候程序就会系统调用,将数据从kernel复制到进程缓冲区。。当select函数返回后,可以遍历fdset来找到就绪的描述符。

select目前几乎在所有的平台上支持,其良好夸平台支持也是它的一个优点。

select的缺点

1、根据fd_size的定义,它的大小为32个整数大小(32位机器为32*32,所有共有1024bits可以记录fd),每个fd一个bit,所以最大只能同时处理1024个fd。

2、每一次呼叫 select( ) 都需要先从 user space把 FD_SET复制到 kernel(约线性时间成本)

为什么 select 不能像epoll一样,只做一次复制就好呢?

每一次呼叫 select()前,FD_SET都可能更动,而 epoll 提供了共享记忆存储结构,所以不需要有 kernel 與 user之间的数据沟通。

3、每次要判断【有哪些event发生】这件事的成本很高,也就是遍历fd_set,因为select(polling也是)采取主动轮询机制.

poll

poll的原理与select非常相似,差别如下:

  • 描述fd集合的方式不同,poll使用 pollfd 结构而不是select结构fd_set结构,所以poll是链式的,没有最大连接数的限制

  • poll有一个特点是水平触发,也就是通知程序fd就绪后,这次没有被处理,那么下次poll的时候会再次通知同个fd已经就绪。

epoll

epoll 提供了三个函数:

1、int epoll_create(int size);

建立一個 epoll 对象,并传回它的id

2、int epoll_ctl(int epfd, int op, int fd, struct epoll_event *event);

事件注册函数,将需要监听的事件和需要监听的fd交给epoll对象

3、int epoll_wait(int epfd, struct epoll_event *events, int maxevents, int timeout);

等待注册的事件被触发或者timeout发生

epoll解决的问题:

  • epoll没有fd数量限制

  • epoll没有这个限制,我们知道每个epoll监听一个fd,所以最大数量与能打开的fd数量有关,一个g的内存的机器上,能打开10万个左右

  • epoll不需要每次都从user space 将fd set复制到内核kernel

  • epoll在用epoll_ctl函数进行事件注册的时候,已经将fd复制到内核中,所以不需要每次都重新复制一次

  • select 和 poll 都是主動輪詢機制,需要拜訪每一個 FD;epoll是被动触发方式,给fd注册了相应事件的时候,我们为每一个fd指定了一个回调函数,当数据准备好之后,就会把就绪的fd加入一个就绪的队列中,epoll_wait的工作方式实际上就是在这个就绪队列中查看有没有就绪的fd,如果有,就唤醒就绪队列上的等待者,然后调用回调函数。

  • 虽然epoll。poll。epoll都需要查看是否有fd就绪,但是epoll之所以是被动触发,就在于它只要去查找就绪队列中有没有fd,就绪的fd是主动加到队列中,epoll不需要一个个轮询确认。

  • 换一句话讲,就是select和poll只能通知有fd已经就绪了,但不能知道究竟是哪个fd就绪,所以select和poll就要去主动轮询一遍找到就绪的fd。而epoll则是不但可以知道有fd可以就绪,而且还具体可以知道就绪fd的编号,所以直接找到就可以,不用轮询。

 

自己去实现事件循环

以下代码在linux上运行,在Windows上可能会报错

import socket
from selectors import DefaultSelector, EVENT_WRITE, EVENT_READ

selector = DefaultSelector()

class Fetcher:
    def connected(self, key): # key 文件描述符
        selector.unregister(key.fd)
        self.client.send("GET {} HTTP/1.1\r\nHost:{}\r\nConnection:close\r\n\r\n".format("/", self.host).encode("utf8"))
        selector.register(self.client.fileno(), EVENT_READ, self.readble)

    def readble(self, key):
        d = self.client.recv(1024)
        if d:
            self.data += d
        else:
            selector.unregister(key.fd)
            data = self.data.decode("utf8")
            print(data)

    def get_url(self, url):
        self.client = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
        self.client.setblocking(False)
        self.data = b""
        self.host = "www.baidu.com"

        try:
            self.client.connect((self.host, 80))  # 阻塞io, 意味着这个时候cpu是空闲的
        except BlockingIOError as e:
            # 做一些其他事
            pass

        selector.register(self.client.fileno(), EVENT_WRITE, self.connected)

def loop_forever():
    #事件循环
    while 1:
        ready = selector.select()
        for key, mask in ready:
            call_back = key.data
            call_back(key)

if __name__ == "__main__":
    fetcher = Fetcher()
    url = "http://www.baidu.com"
    fetcher.get_url(url)
    loop_forever()

 

输出结果如下

 以上select实例是基于方法回调模式的实现,它一下几个缺点:

1、回调过深,造成代码维护非常困难。

2、栈撕裂,造成异常无法抛出(比如connected、readble方法出现异常是无法抛出的)

要解决以上问题,就需要利用下一小节中介绍的协程。

协程

python 3.5中协程的写法 ,使用async关键字标记协程,await关键字调用协程并获取协程的结果

from tornado.gen import coroutine

async def yield_test():
    yield 1
    yield 2
    yield 3

async def main():
    result = await yield_test()
    result = await yield_test2()

async def main2():
    await yield_test()

my_yield = yield_test()
for item in my_yield:
    print(item)

  

 上面的伪代码async表示的是把一个函数标记成一个协程,函数中的 await 表示要获取协程的结果调用非阻塞的IO向epoll中注册,同时切换到其它的协程执行,epoll会监听socket状态,等到可以读的时候程序在向下执行,达到了协程间的切换和调度。

 

AsyncHttpClient(tornado提供的异步非阻塞的包)异步http请求

from tornado import httpclient

async def f():
    http_client = httpclient.AsyncHTTPClient()
    try:
        response = await http_client.fetch("http://www.tornadoweb.org/en/stable/")
    except Exception as e:
        print("Error: %s" % e)
    else:
        print(response.body.decode("utf8"))

if __name__ == "__main__":
    import tornado
    io_loop = tornado.ioloop.IOLoop.current()

    #run_sync方法可以在运行完某个协程之后停止事件循环
    io_loop.run_sync(f)

 

tornado实现高并发的爬虫

from urllib.parse import urljoin

from bs4 import BeautifulSoup
from tornado import gen, httpclient, ioloop, queues

base_url = "http://www.tornadoweb.org/en/stable/"
concurrency = 3

async def get_url_links(url):
    response = await httpclient.AsyncHTTPClient().fetch("http://www.tornadoweb.org/en/stable/")
    html = response.body.decode("utf8")
    soup = BeautifulSoup(html)
    links = [urljoin(base_url, a.get("href")) for a in soup.find_all("a", href=True)]
    return links

async def main():
    seen_set = set()
    q = queues.Queue()

    async def fetch_url(current_url):
        #生产者
        if current_url in seen_set:
            return

        print("获取: {}".format(current_url))
        seen_set.add(current_url)
        next_urls = await get_url_links(current_url)
        for new_url in next_urls:
            if new_url.startswith(base_url):
                #放入队列,
                await q.put(new_url)

    async def worker():
        async for url in q:
            if url is None:
                return
            try:
                await fetch_url(url)
            except Exception as e:
                print("excepiton")
            finally:
                q.task_done()

    #放入初始url到队列
    await q.put(base_url)

    #启动协程
    workers = gen.multi([worker() for _ in range(concurrency)])
    await q.join()

    for _ in range(concurrency):
        await q.put(None)

    await workers


if __name__ == "__main__":
    io_loop = ioloop.IOLoop.current()
    io_loop.run_sync(main)

 

posted @ 2018-12-26 20:48  Crazymagic  阅读(317)  评论(0编辑  收藏  举报