最近开始用Tornado做开发了,究其原因,主要是Tornado基于Python,一来代码量少开发速度快,二来采用epoll方式,能够承载的并发量很高。在我的i5台式机上用ab测试,不连接数据库的情况下,单用get生成页面,大概平均的并发量在7900左右。这比php或者java能够承载并发量都高很多很多。三来Python代码可维护性相对来说比php好很多,语法结构清晰。四来,tornado的框架设计的很黄很暴力,以HTTP请求方式作为方法名称,通常情况下,用户写一个页面只需要有get和post两种方式的方法定义就够了。

在学习的过程中遇到一些比较重要的问题,记录下来以后备查,在学习的过程中遇到不少问题,基本都是靠翻墙解决,百度实在是令人痛苦不堪。记录比较零散一些,可能不仅限于tornado,也会包括python的一些知识。由于我也还在学习过程中,所以有些东西不一定详尽或者理解到位,tornado高人勿拍。

tornado入门不是很难,只要理解了他处理的方式就很好做了。tornado在处理网页的时候,针对于URL的连接,实际就是对class类的一个路由映射。而类中的方法通常无非就两种,处理连接请求的get或者post。所以tornado的页面编写很简单。比如,这是一个用作验证登录用户的类,逐行解释一下:

class SigninHandler(BaseHandler): #引入BaseHandler
    def post(self): #HTTP的POST方法,是GET渲染的form中的post method所对应
        username = self.get_argument('username') #获取form中username的值
        password = self.get_argument('password') #获取form中password的值
        conn = MySQLdb.connect('localhost', user = 'root', passwd = '', db = 'datacenter', charset = 'utf8', cursorclass = MySQLdb.cursors.DictCursor) #连接数据库,指定cursorclass的目的是要让返回结果以字典的形式呈现,如果不写,是以元组形式返回
        cursor= conn.cursor() #定义数据库指针
 
        sql = 'SELECT * FROM dc_users WHERE username=%s AND password=password(%s)' #写sql,为何这样写后面再说
        cursor.execute(sql, (username, password,)) #执行SQL
        row = cursor.fetchone() #获取一条,返回值为dict,因为前面连接数据库时定义了cursorclass = MySQLdb.cursors.DictCursor,当然,你需要import MySQLdb.cursors的包
        if row: #如果存在记录
            self.set_secure_cookie('id', str(row['id']).encode('unicode_escape'),  expires_days=None) #设置安全cookie,防止xsrf跨域
            self.set_secure_cookie('username', row['username'].encode('unicode_escape'),  expires_days=None) #same
            self.set_secure_cookie('role', row['role'].encode('unicode_escape'),  expires_days=None) #same
            ip = self.request.remote_ip #获取来访者IP
            sql = 'UPDATE dc_users SET last_access = NOW(), last_ip=%s WHERE id = %s' #认证审计变更的SQL
            cursor.execute(sql, (ip, row['id'],)) #执行SQL
            conn.commit() #提交执行
            cursor.close() #关闭指针
            conn.close() #关闭数据库连接
            self.redirect('/') #转入首页
            return #返回,按照官方文档的要求,在redirect之后需要写空的return,否则可能会有问题,实测确实会有问题
        else: #如果不存在记录
            self.redirect('/Signin') #跳转回登录页面
            return
    def get(self): #HTTP GET方式
        self.render('users/login_form.html') #渲染登录框HTML

login_form.html内容如下

{% include 'header.html' %} <!--引入header文件,需要跟login_form在同一路径下,否则写相对路径,如 {% include '../header.html' %} -->
<div class="container">
    <h2><script>document.write(language.Title + ' ' + language.Version + ' - ' + language.Codename)</script></h2>
    <form class="form-horizontal" method="post" action="/Signin"> <!--这里的action对应的上面Python代码中SigninHandler的post方法-->
        {% module xsrf_form_html() %} <!--防跨域cookie模块-->
        <div class="form-group">
            <label class="col-sm-2 control-label"><script>document.write(language.Username + language.Colon)</script></label>
            <div class="col-sm-4"><input class="form-control" type="text" name="username" placeholder="Username"></div>
        </div>
        <div class="form-group">
            <label class="col-sm-2 control-label"><script>document.write(language.Password + language.Colon)</script></label>
            <div class="col-sm-4"><input class="form-control" type="password" name="password" placeholder="Password"></div>
        </div>
        <div class="form-group">
            <div class="col-sm-2"></div>
            <div class="col-sm-4">
                <button type="submit" class="col-sm-4 btn btn-info"><script>document.write(language.Signin)</script></button>
            </div>
        </div>
    </form>
</div>
{% include 'footer.html' %}

对于主代码,应如下:

#-*- coding: utf-8 -*-
 
import sys
reload(sys)
sys.setdefaultencoding('utf-8')
import tornado.ioloop
import tornado.web
import tornado.httpserver
import tornado.autoreload
import os
 
class BaseHandler(tornado.web.RequestHandler): #BaseHandler
    def get_current_user(self):
        user = self.get_secure_cookie('username')
        return user
 
class IndexHandler(BaseHandler):
    @tornado.web.authenticated
    def get(self):
        if not self.current_user:
            self.redirect('/Signin') #如未登录,则跳转Signin,Signin的GET方法调用的就是login_form.html页面
            return
        self.render('welcome.html') #否则渲染welcome.html
 
settings = \
    {
        "cookie_secret": "HeavyMetalWillNeverDie", #Cookie secret
        "xsrf_cookies": True, #开启跨域安全
        "gzip": False, #关闭gzip输出
        "debug": False, #关闭调试模式,其实调试模式是很纠结的一事,我喜欢打开。
        "template_path": os.path.join(os.path.dirname(__file__), "./templates"), #定义模板,也就是login_form.html或header.html相对于本程序所在的位置
        "static_path": os.path.join(os.path.dirname(__file__), "./static"), #定义JS, CSS等文件相对于本程序所在的位置
        "login_url": "/Signin", #登录URL为/Signin
    }
 
application = tornado.web.Application([
    (r"/", IndexHandler), #路由设置/ 使用IndexHandler
    (r"/signin", SigninHandler) # Signin使用SigninHandler
], **settings)
 
if __name__ == "__main__": #启动tornado,配置里如果打开debug,则可以使用autoload,属于development模式,如果关闭debug,则不可以使用autoload,属于production模式。autoload的含义是当tornado监测到有任何文件发生变化,不需要重启server即可看到相应的页面变化,否则是修改了东西看不到变化。
    server = tornado.httpserver.HTTPServer(application)
    server.bind(10002) #绑定到10002端口
    server.start(0) #自动以多进程方式启动Tornado,否则需要手工启动多个进程
    tornado.ioloop.IOLoop.instance().start()

 

 

if __name__ == “__main__”: #启动tornado,配置里如果打开debug,则可以使用autoload,属于development模式,如果关闭debug,则不可以使用autoload,属于production模式。autoload的含义是当tornado监测到有任何文件发生变化,不需要重启server即可看到相应的页面变化,否则是修改了东西看不到变化。
server = tornado.httpserver.HTTPServer(application)
server.bind(10002) #绑定到10002端口
server.start(0) #自动以多进程方式启动Tornado,否则需要手工启动多个进程
tornado.ioloop.IOLoop.instance().start()

对于sql部分,执行最好写成cursor.execute(sql, (id,)),将%s的东西以元组形式传递给execute方法,这样做的目的是最大程度避免SQL注入的发生。如果直接写为 ‘select * from xxx where id = ‘ + id 或者 ‘select * from xxx where id = %s’ % id 的话,会被注入。另外,如果是sqlite3的话,需要写成 ‘select * from xxx where id=?’ ,然后execute方式一样。

另外,如果开启了禁止xsrf跨域功能的话,在每个HTML的form表单里必须加上{% module xsrf_form_html() %}否则会出现禁止访问的错误。

下篇记录一下编码格式处理,这个在python2上最讨厌。

发表评论

*
*

Required fields are marked *