基于Tornado开发一个web框架

  • 2018-06-15
  • 0
  • 0

基于Tornado开发一个web框架

上次在公司做了一次关于python的技术分享.特别说了这个框架,今天以教程的形式,记录下来

始于 2018年05月20日13:45:09

结束于 2018年06月15日23:13:54

完结语:终于写完这篇博客了.期间也在不停的学习tornado.花了接近一个月时间。期间去了趟大连游玩.很是开心.之后我会一直维护这个框架。

作者信息:

姓名:黄志成

博客: 博客地址

[TOC]

Tornado 介绍

Tornado is a Python web framework and asynchronous networking library, originally developed at FriendFeed. By using non-blocking network I/O, Tornado can scale to tens of thousands of open connections, making it ideal for long polling, WebSockets, and other applications that require a long-lived connection to each user.

这是复制官方文档的介绍.

Tornado的特点:

完备的web框架: 与Django,Flask 提供了路由映射,Request上下文,基于模板的页面渲染技术

它还是一个高效的网络库,提供了异步IO支持.可以用来做网关,游戏服务后台。

它自己也是一个高效的HTTP服务器.

总结就是:这是一个高性能的web框架.

安装Tornado

可以直接通过 PIP 安装

pip3 install tornado

开发环境:

Mac OS 10.12.6

Python3.6

Tornado4.5

先来一个Hello World的例子吧

import tornado.ioloop
import tornado.web

class MainHandler(tornado.web.RequestHandler):
    def get(self):
        self.write("Hello, world")

def make_app():
    return tornado.web.Application([
        (r"/", MainHandler),
    ])

if __name__ == "__main__":
    app = make_app()
    app.listen(8888)
    tornado.ioloop.IOLoop.current().start()

让我们来解读一下这段代码

1> 首先通过import语句导入tornado包中的 ioloop 和 web 类, 这两个是Tornado程序的基础.

2> 实现一个tornado.web.RequestHandler的子类,并且重载它get方法,该方法就是负责RequestHandler请求处理的.

3> 定义一个make_app函数.返回一个web.Application对象,用于Tornado程序的路由定义.

4> web.Application.listen 监听8888 端口

5> tornado.ioloop.IOLoop.current().start() 启动ioloop,该函数一直运行,用于处理所有访问请求

下面让它运行起来

python3 http_server.py

在浏览器中打开 http://localhost:8888

image

输出了我们的hello,world.

这里只是一个简单的介绍.

Tornado 可以被划分为四大部分:

Web 框架 (包括创建 Web 应用的 RequestHandler 类,还有很多其他支持的类);

HTTP的客户端和服务端实现 (HTTPServer and AsyncHTTPClient);

异步网络库 (IOLoop and IOStream), 为HTTP组件提供构建模块,也可以用来实现其他协议;

协程库 (tornado.gen) 允许异步代码写得更直接而不用链式回调的方式。...

(以上内容选自 掘金 — 一个帮助开发者成长的社区)

代码的架构

了解了简单的Hello,World程序,我们要开始做大工程.不能把所有代码写在一个文件中.

├── app
│   ├── controller // 业务逻辑文件夹
│   ├── router // 路由文件夹
│   └── view // 视图文件夹
├── config // 配置项文件
├── core // 核心文件夹
├── log // 日志文件夹
├── main.py// 入口文件
├── public // 静态资源文件夹
└── route.py // 路由文件

上面是一个简单的代码组织,当然可以根据自己来设置.

框架实现代码

首先看一下main.py吧

#! /usr/bin/python3
# -*- coding:utf-8 -*-

import tornado.ioloop
import tornado.web
import os
from tornado.options import define, options
from core.url_handle import include, url_wrapper


class Application(tornado.web.Application):
    def __init__(self):
        # 定义 Tornado 服务器的配置项,如 static/templates 目录位置、debug 级别等
        settings = dict(
            debug=True,
            static_path=os.path.join(os.path.dirname(__file__), "public"),
            template_path=os.path.join(os.path.dirname(__file__), "app/view")
        )

        handlers = url_wrapper(include('route'))
        tornado.web.Application.__init__(self, handlers, **settings)


if __name__ == '__main__':
    print("Tornado server is start\r")
    define('port', default=8000, help="run on the given port", type=int)
    tornado.options.parse_command_line()
    Application().listen(options.port, xheaders=True)
    try:
        tornado.ioloop.IOLoop.instance().start()
    except KeyboardInterrupt:
        tornado.ioloop.IOLoop.instance().stop()

    print('Tornado server is stop')

与我们最初的Hello,World有很大的区别.

main.py是整个框架的入口.通过执行python main.py 来启动服务.

首先编写这块框架.是为了方便web服务开发.我们需要将代码划分层级.定义路由的放在一个py文件中.各种配置项放在一个文件中。

下面我们看下我们如何去定义路由的.首先一个原则是 我们不希望使用框架的人去修改框架的核心文件.比如这个main.py文件.

所以我们需要将定义路由的文件写在其他文件中.通过模块载入的方式去定义路由.

from core.url_handle import include, url_wrapper

handlers = url_wrapper(include('route'))
tornado.web.Application.__init__(self, handlers, **settings)

上面是我们获取路由映射的方式.我们看下 core.url_handle 是如何实现的.

# -*- coding:utf-8 -*-
from importlib import import_module


def include(module):
    res = import_module(module)
    urls = getattr(res, 'urls', res)
    return urls


def url_wrapper(urls):
    '''
    拼接请求 url,调用对应的模块,如拼接 users 和 regist 成 url /users/regist,
    调用 views.users.users_views.RegistHandle 模块
    '''
    wrapper_list = []
    for url in urls:
        path, handles = url
        if isinstance(handles, (tuple, list)):
            for handle in handles:
                # 分离获取字符串(如regist)和调用类(如views.users.users_views.RegistHandle)
                pattern, handle_class = handle
                # 拼接url,新的url调用模块
                wrap = ('{0}{1}'.format(path, pattern), handle_class)
                wrapper_list.append(wrap)
        else:
            wrapper_list.append((path, handles))
    return wrapper_list

通过引用python的importlib包里的import_module函数可以导入类. 然后通过 getattr 函数获取里面的 urls 属性

getattr() 函数用于返回一个对象属性值。

下面给出一个 getattr 的使用例子

>>>class A(object):
...     bar = 1
... 
>>> a = A()
>>> getattr(a, 'bar')        # 获取属性 bar 值
1
>>> getattr(a, 'bar2')       # 属性 bar2 不存在,触发异常
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
AttributeError: 'A' object has no attribute 'bar2'
>>> getattr(a, 'bar2', 3)    # 属性 bar2 不存在,但设置了默认值
3
>>>

# 参考代码来自 http://www.runoob.com/python/python-func-getattr.html

我们在看看route.py里的内容.

# -*- coding:utf-8 -*-

from __future__ import unicode_literals
from core.url_handle import include

urls = [
    (r"/", include('app.router.index'))
]

引用 future 是为了兼容python2.7

当然这里也include('app.router.index')了。

这里是获取具体应用下的路由设置了.

在这里app 文件夹就是我们的具体项目目录.我们的业务代码就写在这里.

再次回到main.py在 我们将include到是数据传入 url_wrapper 函数.

url_wrapper 是为了拼接请求的url 和 具体调用的模块. 这里理解起来可能有点难度.我会单写一篇博文来介绍.这里就不过多讲解.

我们可以查看一下拼接好的内容

[('/', <class 'app.controller.index.IndexHandle'>)]

我们来看一下 app/controller/index.py中IndexHandle类里的内容

# -*- coding:utf-8 -*-

import tornado.web
import json


class IndexHandle(tornado.web.RequestHandler):
    def get(self):
        self.render('index/index.html')
        # self.write(json.dumps({"data": {"msg": '欢迎使用HTornado框架', "code": 200}}))

这里就是我们的业务逻辑。具体业务代码这里就不过多介绍.

这样使用框架的人就不需要改动main的任何代码.只需要在route文件里添加路由即可.

接下来就得监听端口.然后启动服务.

同样框架是给很多人使用的.我们遵循不改变main的原则.我们如何让用户设置自己的监控端口呢?

这个时候我们可以使用define方法.这是tornado为我们封装的.

from tornado.options import define, options

define('port', default=8000, help="run on the given port", type=int)
tornado.options.parse_command_line()

tornado.options.define()

用来定义options选项变量的方法,定义的变量可以在全局的tornado.options.options中获取使用,传入参数:

name 选项变量名,须保证全局唯一性,否则会报“Option 'xxx' already defined in ...”的错误;

default 选项变量的默认值,如不传默认为None;

type 选项变量的类型,从命令行或配置文件导入参数的时候tornado会根据这个类型转换输入的值,转换不成功时会报错,可以是str、float、int、datetime、timedelta中的某个,若未设置则根据default的值自动推断,若default也未设置,那么不再进行转换。可以通过利用设置type类型字段来过滤不正确的输入。

multiple 选项变量的值是否可以为多个,布尔类型,默认值为False,如果multiple为True,那么设置选项变量时值与值之间用英文逗号分隔,而选项变量则是一个list列表(若默认值和输入均未设置,则为空列表[])。

help 选项变量的帮助提示信息,在命令行启动tornado时,通过加入命令行参数 --help 可以查看所有选项变量的信息(注意,代码中需要加入tornado.options.parse_command_line())。

tornado.options.parse_command_line()

转换命令行参数,并将转换后的值对应的设置到全局options对象相关属性上。

来看下如何使用. 在启动时候:

$ python3 main.py --port=8080
Tornado server is start

我们可以输入 python3 main.py --help 来查看option

我们访问http://localhost:8080/

image

(哈哈哈) 这个欢迎页是有那么一点点丑.以后会慢慢优化.

这个框架还是一个不完善的版本.我们会后面的时间内进行优化它.它还缺少很多.比如日志,错误提示页面.等等

我会持续更新这个框架.已经开源在GITHUB中了.

地址:GITHUB地址

希望支持的朋友们.给一个star.我会更加努力的去更新维护它~

评论

还没有任何评论,你来说两句吧