最近开始用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上最讨厌。