前言
Django本身是同步框架,采用wsgi协议与web服务器进行交互,而web服务器都是基于多线程去处理多个请求,这种情况下如果要处理高并发就很难应对。
Tornado为了解决高并发的性能问题,使用异步非阻塞的处理方式。其原理是tornado的核心io循环模块,底层封装了Linux的I/O复用模型epoll。
概述
tornado三大组件
- tornado.web
RequestHandler-封装了处理请求的所有方法,get,post等方法监听同名方法的请求,write方法为响应请求内容。
Application-作为服务器的入口组件,初始化参数中配置请求路由地址。
- tornado.ioloop
current方法返回当前IOLoop实例,start方法打开服务器监听。
- tornado.httpserver
HTTPServer方法可以传入Application实例,该方法为tornadohttp服务器的实现。
Demo项目
tornado 4.2+jinja2+pymysql/peewee/sqlachemy
部署环境 centos 6.2+nginx
简述:
该项目采用jinja2作为模板引擎(django模板使用方式非常接近),数据库交互方式采用pymysql非orm框架,当然也可以选用peewee,sqlachemy轻量级的orm框架,如果考虑到性能,pymysql作为首选。
一、项目目录结构
handlers目录为处理请求的主目录
1.db
config.py连接数据库配置
# -*- coding:utf-8 -*- from utils.aesimp import decrypt_aes # Mysql config mysql_db_config = { 'db':'tornado', 'host':'localhost', 'port':3306, 'user':'root', # 数据库密码采用非明文的方式 'password': decrypt_aes('SPivfEfVD9cr9/g9nxY3rw=='), 'charset': 'utf8' }
connections.py管理数据库连接,这里采用单连接的方式,如果需要可以拓展成连接池的方式
# -*- coding:utf-8 -*- import pymysql from contextlib import contextmanager from config import mysql_db_config # 该上下文装饰器用于自动管理数据连接的创建和关闭,不需每次手动去执行 @contextmanager def connect(cursor_type=pymysql.cursors.DictCursor): conn = pymysql.connect(**mysql_db_config) cursor = conn.cursor(cursor=cursor_type) try: yield cursor finally: conn.commit() conn.close() cursor.close()
2.handlers
indexhandler.py 处理首页请求,没有业务逻辑,渲染一个页面给前端
# -*- coding:utf-8 -*- from tornado.web import RequestHandler import uuid class IndexHandler(RequestHandler): def get(self, *args, **kwargs): page_dict = dict() page_dict['order_forms'] = [ {'name':'order_id','value':str(uuid.uuid4()).replace("-", "")[:24],'lable':u'订单号'}, {'name':'order_amt','value':'','lable':u'订单金额'}, {'name':'order_curr','value':'156','lable':u'订单币种'}, ] self.render("index.html",**page_dict)
其他业务处理通用,可以自己根据业务需求进行设计
3.static用于存放css,js,image等静态文件
4.template用于存放html模板
ps:jinja2模板与django类似,支持模板的继承和子模块的重写,为了提高复用,这里拆分出来几个模板。
{% block name %}{% endblock %}name不能使用'-'符号,这是一个坑
- base.html-模板的基本骨架
<!DOCTYPE html> <html> <head> <meta charset="utf-8"> <meta name="test" content="width=device-width, initial-scale=1.0"> <meta http-equiv="X-UA-Compatible" content="IE=edge"> <title>{% block title %}{% endblock %}</title> <!-- 引入通用样式--> <!-- static_url动态转义成静态文件static目录地址--> <link href="{{ static_url('css/bootstrap.min.css') }}" rel="stylesheet"> <link href="{{ static_url('font-awesome/css/font-awesome.css') }}" rel="stylesheet"> <link href="{{ static_url('css/style.css') }}" rel="stylesheet"> <!--子模板中可以引入额外的样式--> {% block extrastyle %}{% endblock %} </head> <body> <div id="wrapper"> <!--左侧导航栏,这里直接引入了模板,可以继承或者重载--> {% block leftsidebar %} {% include 'left-nav.html' %} {% endblock %} <div id="page-wrapper" class="gray-bg dashbard-1"> {% block breadcrumb %} {% include 'breadcrumb.html' %} {% endblock %} <!--显示内容的区域--> {% block content %}{% endblock %} </div> </div> <!-- Mainly scripts --> <!-- js放置页面底部引入可以加速页面渲染 --> <script src="/static/js/jquery-2.1.1.js"></script> <script src="/static/js/bootstrap.min.js"></script> {% block extrascript %}{% endblock %} </body> </html>
- index.html
{# 支持继承通用模板 #} {% extends 'base.html' %} {% block content %} {# macro为自定义函数,支持传递参数 #} {% macro input(name,value,label) %} <div class="col-sm-4"> <div class="form-group"> <label class="control-label" for="{{ label | replace(' ','_') | lower }}">{{ label }}</label> <input type="text" id="{{ name }}" name="{{ name }}" value="{{ value }}" class="form-control" > </div> </div> {% endmacro %} <div class="wrapper wrapper-content animated fadeInRight ecommerce"> <div class="ibox-content m-b-sm border-bottom"> <div class="row"> {# 该标签支持server端响应内容 #} {% for feild in order_forms %} {{ input(feild.name,feild.value,feild.lable) }} {% endfor %} </div> <div class="row"> <div class="ibox-content"> <button id="submit" class="btn btn-primary pull-right"> <i class="fa fa-shopping-cart"></i> 提交订单 </button> </div> </div> </div> </div> {% endblock %}
5.application.py
控制handlers的一个集合容器,初始化时还可以以dict形式配置httpserver的参数
# -*- coding:utf-8 -*- import tornado.web import os from urls import * from utils.jinjaloader import JinjaLoader settings = { "cookie_secret": "bZJc2sWbQLKos6GkHn/VB9oXwQt8S0R0kRvJ5/xJ89E=", "xsrf_cookies": True, # 这里为引入jinja2模板引擎的关键配置 "template_loader": JinjaLoader(os.path.join(os.path.dirname(__file__), 'templates/')), "template_path": os.path.join(os.path.dirname(__file__), "templates"), "static_path": os.path.join(os.path.dirname(__file__), "static"), "debug": True, "autoreload": True # debug为True模式下,允许自动重载 } application = tornado.web.Application(handlers=urls, **settings)
6.urls.py
映射路由地址与handler
# -*- coding:utf-8 -*- from handlers import * from tornado.web import url, RedirectHandler urls = [ # 支持使用url进行配置别名name,Application.reverse_url可以转义出实际路由地址 url(r"/test/order/", OrderHandler, name='order'), url(r"^/$", IndexHandler, name='index'), # 支持路由地址参数化,这里使用了tornado自带重定向handler,需要传递url这个参数 (r"/redirect/P?<url>.*", RedirectHandler, dict(url='/test/order/')), ]
7.server.py
项目启动程序,ioloop启动服务监听
# -*- coding:utf-8 -*- import tornado.ioloop import tornado.options import tornado.httpserver from application import application from tornado.options import define,options define("port",default=8123,help="run on th given port",type=int) def main(): tornado.options.parse_command_line() http_server = tornado.httpserver.HTTPServer(application) # http_server.listen(options.port) http_server.bind(options.port) # start()传入参数可以启动子进程数目,如果<=0,启动数量和cpu核数 http_server.start(0) print('Development server is running at http://127.0.0.1:%s/' % options.port) print('Quit the server with Control-C') tornado.ioloop.IOLoop.current().start() if __name__ == "__main__": main()
二、tornado引入Jinja2模板
方法一、重写basehandler的render方法,目前大多数都是采用这种方式,但该方法侵入性太强。
# -*- coding:utf-8 -*- import tornado.web from jinja2 import Environment, FileSystemLoader, TemplateNotFound class TemplateRender(object): """ A simple class to hold methods for rendering templates. """ def render_template(self, template_name, **kwargs): template_dirs = [] if self.settings.get('template_path', ''): template_dirs.append(self.settings['template_path']) env = Environment(loader=FileSystemLoader(template_dirs)) try: template = env.get_template(template_name) except TemplateNotFound: raise TemplateNotFound(template_name) content = template.render(kwargs) return content class BaseHandler(tornado.web.RequestHandler, TemplateRender): """ Tornado RequestHandler subclass. """ def initialize(self): pass def get_current_user(self): user = self.get_secure_cookie('user') return user if user else None def render_html(self, template_name, **kwargs): kwargs.update({ 'settings': self.settings, 'static_path': self.settings.get('static_url_prefix', '/static/'), 'request': self.request, 'current_user': self.current_user, 'xsrf_token': self.xsrf_token, 'xsrf_form_html': self.xsrf_form_html, }) content = self.render_template(template_name, **kwargs) self.write(content)
方法二、在tornado的application中配置模板引擎,前面已经引入,下面为jinjia2的模板加载器
# -*- coding:utf-8 -*- import threading from tornado import template, web import jinja2 class TTemplate(object): def __init__(self, template_instance): self.template_instance = template_instance def generate(self, **kwargs): return self.template_instance.render(**kwargs) class JinjaLoader(template.BaseLoader): def __init__(self, root_directory, **kwargs): super(JinjaLoader,self).__init__(root_directory, **kwargs) self.jinja_env = jinja2.Environment( loader=jinja2.FileSystemLoader(root_directory), extensions=['jinja2.ext.i18n'], **kwargs ) self.templates = {} self.lock = threading.RLock() def resolve_path(self, name, parent_path=None): return name def _create_template(self, name): template_instance = TTemplate(self.jinja_env.get_template(name)) return template_instance
三、tornado.ioloop原理图
四、nginx部署+supervisor监控
简述:nginx作为高效的负载均衡服务器,作为反向代理可以转发到不同的tornado服务器中处理。
Supervisor是基于Python实现的监控进程的工具。
nginx配置
upstream tornados{
server 127.0.0.1:8001; # 分发到不同端口的tornado服务器中
server 127.0.0.1:8002;
server 127.0.0.1:8003;
}
proxy_next_upstream error;
server {
listen 80; #还可以支持ssl方式
server_name www.tornado.com;
# 静态文件直接由Nginx处理
location /static/{
alias /project_path/static/;
expires 24h;
}
location /{
proxy_pass_header Server;
proxy_set_header Host $http_host;
proxy_redirect off;
proxy_set_header X-Real-IP $remote_addr;
# 把请求方向代理传给tornado服务器,负载均衡
proxy_pass http://tornados;
}
}
Supervisor配置
# 为了方便管理,增加一个tornado组
[group:tornados]
programs=tornado-0,tornado-1,tornado-2
# 分别定义三个tornado的进程配置
[program:tornado-0]
# 进程要执行的命令
command=python /project_path/server.py --port=8020
directory=/project_path/
user=tornado
# 自动重启
autorestart=true
redirect_stderr=true
# 日志路径
stdout_logfile=/home/user/tornado0.log
loglevel=info
[program:tornado-1]
command=python /project_path/server.py --port=8021
directory=/project_path/
user=tornado
autorestart=true
redirect_stderr=true
stdout_logfile=/home/user/tornado1.log
loglevel=info
[program:tornado-2]
command=python /project_path/server.py --port=8022
directory=/project_path/
user=tornado
autorestart=true
redirect_stderr=true
stdout_logfile=/home/user/tornado2.log
loglevel=info