Tornado 是一个 Python Web 框架和异步网络库,最初由 FriendFeed 开发。 通过使用非阻塞网络 I/O,Tornado 可以扩展到成千上万的开放连接,非常适合长时间轮询,WebSocket 和需要与每个用户建立长期连接的其他应用程序。Tornado 出现的目的在于解决高并发场景下的稳定性和性能问题。Tornado 提供了一个 HttpServer,在 Tornado 6.x 版本中,使用了最新的异步 IO 库,使该服务器的性能大大提高。它使用非阻塞网络 I/O 并解决 C10k 问题(意思就是,单机下它可以处理 10,000 多个并发连接)。
Tornado 和 Django、Flask 称为 Python Web 开发三大框架,Tornado 和其他两个框架存在明显的区别,就是它的非阻塞网络 IO。Tornado 和 Flask 存在一些共同点,首选就是它们都很“微”、很轻量,符合现在的微服务架构设计,在 Tornado 中没有默认提供模板引擎,这就意味着你更多的时候是使用 Tornado API 接口,也就是常说的 RESTfulAPI。当然你可以使用第三方的模板引擎来扩展你的应用。但我还是建议你用 Tornado 开发纯后端应用更好一些。
日常工作中我们使用 Tornado 可以开发哪些类型的应用呢?以下是笔者常用的场景:
本节将使用几个简单的小例子带你上手进行 Tornado 开发,笔者的开发环境是 Linux 系统、Python 3.6+、MySQL 5.7、Redis 6.0 。注意,本节中 Python 3.6+ 开发环境是必须的,至于开发系统使用 Mac 或 Windows 都可以。MySQL、Redis 本节暂时不需要,后边的开发中需要使用,因此可以后面再安装,注意版本号尽量一致。如果因为版本不同导致的异常,请小伙伴们自行百度或谷歌解决。
安装 Tornado
pip install tornado==6.0.4
使用 Tornado 异步 HTTP 客户端写一个小爬虫
主要代码如下:
# async_http_cilent.py
import time
from datetime import timedelta
from html.parser import HTMLParser
from urllib.parse import urljoin, urldefrag
from tornado import gen, httpclient, ioloop, queues
base_url = "http://www.tornadoweb.org/en/stable/"
concurrency = 10
async def get_links_from_url(url):
"""Download the page at `url` and parse it for links.
Returned links have had the fragment after `#` removed, and have been made
absolute so, e.g. the URL 'gen.html#tornado.gen.coroutine' becomes
'http://www.tornadoweb.org/en/stable/gen.html'.
"""
response = await httpclient.AsyncHTTPClient().fetch(url)
print("fetched %s" % url)
html = response.body.decode(errors="ignore")
return [urljoin(url, remove_fragment(new_url)) for new_url in get_links(html)]
def remove_fragment(url):
pure_url, frag = urldefrag(url)
return pure_url
def get_links(html):
class URLSeeker(HTMLParser):
def __init__(self):
HTMLParser.__init__(self)
self.urls = []
def handle_starttag(self, tag, attrs):
href = dict(attrs).get("href")
if href and tag == "a":
self.urls.append(href)
url_seeker = URLSeeker()
url_seeker.feed(html)
return url_seeker.urls
async def main():
q = queues.Queue()
start = time.time()
fetching, fetched = set(), set()
async def fetch_url(current_url):
if current_url in fetching:
return
print("fetching %s" % current_url)
fetching.add(current_url)
urls = await get_links_from_url(current_url)
fetched.add(current_url)
for new_url in urls:
# Only follow links beneath the base URL
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("Exception: %s %s" % (e, url))
finally:
q.task_done()
await q.put(base_url)
# Start workers, then wait for the work queue to be empty.
workers = gen.multi([worker() for _ in range(concurrency)])
await q.join(timeout=timedelta(seconds=300))
assert fetching == fetched
print("Done in %d seconds, fetched %s URLs." % (time.time() - start, len(fetched)))
# Signal all the workers to exit.
for _ in range(concurrency):
await q.put(None)
await workers
if __name__ == "__main__":
io_loop = ioloop.IOLoop.current()
io_loop.run_sync(main)
运行服务(注意:如果使用了虚拟环境,要在虚拟环境内运行命令):
python async_http_cilent.py
本节将使用 Tornado 的 Web 框架,编写一个小服务端应用,这也是 RESTfulAPI 接口的雏形,本节会实现一个接口,路径是 /test/ 请求该路径时,应用会返回一串 JSON 字符串,主要的 massage 是 Hello World。
# tornado_server.py
import tornado.ioloop
import tornado.web
import asyncio
import time
class BaseHandler(tornado.web.RequestHandler):
'''
请求的基类,继承自 Tornado 的 web 框架的 RequestHandler
'''
def __init__(self, application, request, **kwargs):
super().__init__(application, request, **kwargs)
print('[%s]' % (time.strftime("%Y-%m-%d %H:%M:%S")),request.version,request.remote_ip,request.method,request.uri)
class TestHandler(BaseHandler):
async def get(self, *args, **kwargs):
res_format = {
"message": "ok", "errorCode": 0, "data": {
}}
try:
await asyncio.sleep(2) # 会将程序暂停两秒
res = mul.delay(3,5)
res_format['message'] = 'Hello World'
return self.finish(res_format)
except Exception as e:
print('出现异常:%s' % str(e))
return self.finish({
"message": "出现无法预料的异常:{}".format(str(e)), "errorCode": 1, "data": {
}})
def make_app():
return tornado.web.Application([
(r"/test/", TestHandler),
], debug=True)
if __name__ == "__main__":
print("""Wellcome...
Starting development server at http://%s:%s/
Quit the server with CTRL+C.""" % ('127.0.0.1', '8080'))
app = make_app()
app.listen(8080) # 设置监听的端口号,默认监听地址是 localhost
tornado.ioloop.IOLoop.current().start()
运行服务:
python tornado_server.py
使用 curl 命令进行测试:
$ curl -H "Content-Type: application/json" http://127.0.0.1:8080/test/
$ {
"message": "Hello World", "errorCode": 0, "data": {
}}