MVC模式是一个非常好的软件架构模式,在网站开发中,也常常要求遵守这个模式。
MVC模式(Model-View-Controller)是软件工程中的一种软件架构模式,把软件系统分为三个基本部分:模型(Model)、视图(View)和控制器(Controller)。
MVC模式最早由Trygve Reenskaug在1978年提出,是施乐帕罗奥多研究中心(Xerox PARC)在20世纪80年代为程序语言Smalltalk发明的一种软件设计模式。MVC模式的目的是实现一种动态的程式设计,使后续对程序的修改和扩展简化,并且使程序某一部分的重复利用成为可能。除此之外,此模式通过对复杂度的简化,使程序结构更加直观。软件系统通过对自身基本部分分离的同时也赋予了各个基本部分应有的功能。专业人员可以通过自身的专长分组:
- (控制器 Controller)- 负责转发请求,对请求进行处理。
- (视图 View) - 界面设计人员进行图形界面设计。 -(模型 Model) - 程序员编写程序应有的功能(实现算法等等)、数据库专家进行数据管理和数据库设计(可以实现具体的功能)。
所谓前端就是指用浏览器打开之后看到的那部分,它是呈现网站传过来的信息的界面,也是用户和网站之间进行信息交互的界面。前端开发,一般使用HTML/CSS/JS,当然,非要用python也不是不可以,但这势必造成以后维护困难。
前端所实现的功能主要有:
这里所说的后端,对应着MVC中的Controller和Model的部分或者全部功能,因为在我们的图中,“后端”是一个狭隘的概念,没有把数据库放在其内。
主要任务就是根据需要处理由前端发过来的各种请求,根据请求的处理结果,一方面操作数据库(对数据库进行增删改查),另外一方面把请求的处理结果反馈给前端。
工作比较单一,就是面对后端的python程序,任其增删改查。
/.
|
handlers
|
methods
|
statics
|
templates
|
application.py
|
server.py
|
url.py
依次说明上面的架势中每个目录和文件的作用:
url.py文件:
#!/usr/bin/env python
# coding=utf-8
import sys #utf-8,兼容汉字
reload(sys)
sys.setdefaultencoding("utf-8")
from handlers.index import IndexHandler #假设已经有了
url = [
(r'/', IndexHandler),
]
/*
url.py文件主要是设置网站的目录结构。from handlers.index import IndexHandler,虽然在handlers文件夹还没有什么东西,为了演示如何建立网站的目录结构,假设在handlers文件夹里面已经有了一个文件index.py,它里面还有一个类IndexHandler。在url.py文件中,将其引用过来。
变量url指向一个列表,在列表中列出所有目录和对应的处理类。比如(r'/', IndexHandler),,就是约定网站根目录的处理类是IndexHandler,即来自这个目录的get()或者post()请求,均有IndexHandler类中相应方法来处理。
*/
application.py文件:
#!/usr/bin/env python
# coding=utf-8
from url import url
import tornado.web
import os
settings = dict(
template_path = os.path.join(os.path.dirname(__file__), "templates"),
static_path = os.path.join(os.path.dirname(__file__), "statics")
)
application = tornado.web.Application(
handlers = url,
**settings
)
/*
从内容中可以看出,这个文件完成了对网站系统的基本配置,建立网站的请求处理集合。
from url import url是将url.py中设定的目录引用过来。
setting引用了一个字典对象,里面约定了模板和静态文件的路径,即声明已经建立的文件夹"templates"和"statics"分别为模板目录和静态文件目录。
接下来的application就是一个请求处理集合对象。请注意tornado.web.Application()的参数设置:
tornado.web.Application(handlers=None, default_host='', transforms=None, **settings)
关于settings的设置,不仅仅是文件中的两个,还有其它,比如,如果填上debug = True就表示出于调试模式。调试模式的好处就在于有利于开发调试,但是,在正式部署的时候,最好不要用调试模式。
*/
server.py文件:
#!/usr/bin/env python
# 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 = 8000, help = "run on the given port", type = int)
def main():
tornado.options.parse_command_line()
http_server = tornado.httpserver.HTTPServer(application)
http_server.listen(options.port)
print "Development server is running at http://127.0.0.1:%s" % options.port
print "Quit the server with Control-C"
tornado.ioloop.IOLoop.instance().start()
if __name__ == "__main__":
main()
#!/usr/bin/env python
# coding=utf-8
import pymysql
conn = pymysql.connect(host="localhost", user="root", passwd="123123", db="qiwsirtest", port=3306, charset="utf8") #连接对象
cur = conn.cursor() #游标对象
# 1、登陆功能的url配置:(url.py文件中)
from handlers.index import IndexHandler
url = [
(r'/', IndexHandler),
]
# 2、登陆状态处理:(在handlers里面建立index.py文件:
#!/usr/bin/env python
# coding=utf-8
import tornado.web
class IndexHandler(tornado.web.RequestHandler):
def get(self):
self.render("index.html") # 去往登陆页面
当访问根目录的时候,不论输入localhost:8000
,还是http://127.0.0.1:8000
,或者网站域名,都会将相应的请求交给handlers目录中的index.py文件中的IndexHandler类的get()方法来处理,它的处理结果是呈现index.html模板内容。
render()
函数的功能在于向请求者反馈网页模板,并且可以向模板中传递数值。
特别注意,在handlers目录中,不要缺少了__init__.py
文件,因为这里面的文件要在别处被当做模块引用.
3.1 运行服务
找到server.py文件,运行它:
$ python server.py
Development server is running at http://127.0.0.1:8000
Quit the server with Control-C
3.2 访问
打开浏览器,输入http://localhost:8000
或者http://127.0.0.1:8000
在已经建立了前端表单之后,就要实现前端和后端之间的数据传递。在工程中,常用到一个被称之为ajax()的方法。
4.1 前端使用ajax()进行数据传输,示例如下:
$(document).ready(function(){
$("#login").click(function(){
var user = $("#username").val(); // 从前端捕获到数据1
var pwd = $("#password").val(); // 从前端捕获到数据2
var pd = {"username":user, "password":pwd}; // 数据json格式
$.ajax({
type:"post", // 请求方式
url:"/", // 请求地址
data:pd, // 传输数据内容
cache:false,
success:function(data){
alert(data);
},
error:function(){
alert("error!");
},
});
});
});
4.2 后端接受数据:
前端通过ajax技术,将数据以 json格式 传给了后端,并且指明了对象目录"/"
,这个目录在url.py文件中已经做了配置,是由handlers目录的index.py文件的IndexHandler类来处理。因为是用post方法传的数据,那么在这个类中就要有post方法来接收数据。所以,要在IndexHandler类中增加post()方法,增加之后的完善代码是:
#!/usr/bin/env python
# coding=utf-8
import tornado.web
class IndexHandler(tornado.web.RequestHandler):
def get(self):
self.render("index.html")
def post(self):
username = self.get_argument("username") # 获取前端传来的数据1
password = self.get_argument("password") # 获取前端传来的数据2
self.write(username) # 向前端返回数据
get_argument(name, default=[], strip=True)
# 它能够获取name的值。(name就是从前端传到后端的那个json对象的键的名字)
# 如果获取不到name的值,就返回default的值,但是这个值默认是没有的,如果真的没有就会抛出HTTP 400。# # 要想获取多个值,可以使用get_arguments(name, strip=true)。
self.write(username)
# 是后端向前端返回数据。这里返回的实际上是一个字符串,也可返回json字符串。
4.3 验证数据(用户名和密码)
按照流程,用户在前端输入了用户名和密码,并通过ajax提交到了后端,后端借助于get_argument()方法得到了所提交的数据(用户名和密码)。下面要做的事情就是验证这个用户名和密码是否合法,其体现在:
这个验证工作完成之后,才能允许用户登录,登录之后才能继续做某些事情。
首先,在methods目录中创建一个db.py文件,用于数据库操作:
# 该方法实现从数据库中查询,并返回查询结果
def select_table(table, column, condition, value ):
sql = "select " + column + " from " + table + " where " + condition + "='" + value + "'"
cur.execute(sql)
lines = cur.fetchall()
return lines
有了这段代码之后,就进一步改写index.py中的post()方法:
def post(self):
username = self.get_argument("username")
password = self.get_argument("password")
user_infos = mrd.select_table(
table="users",column="*",condition="username",value=username)
if user_infos:
db_pwd = user_infos[0][2]
if db_pwd == password:
self.write("welcome you: " + username)
else:
self.write("your password was not right.")
else:
self.write("There is no thi user.")
特别注意,在methods目录中,不要缺少了__init__.py
文件,因为这里面的文件要在别处被当做模块引用,才能在index.py中实现import methods.db
4.4 重复(步骤3)测试功能
五、模板使用
模板主要针对前端页面的html来说,因为前端页面要显示从后端读取出来的数据,在前端页面中获取数据的位置,用变量或标签代替,就实现了模板功能。tornado提供比较好用的前端模板(tornado.template)。通过使用模板,能够让前端开发者的编写不受后端的限制。
# 模板中的变量:
1、使用变量的语法:{{ 变量名 }}
2、从变量(列表、元组、字典)中取值,用索引或键的方式
# 模板中的标签:
for循环:
{% for 变量 in 列表 | 元组 | 字典 %}
{% endfor %}
# 允许使用 for 提供的内置变量 - forloop
1、forloop.counter # 记录当前循环的次数,从1开始
2、forloop.first # 是否是第一次循环(第一项)
3、forloop.last # 是否是最后一次循环(最后一项)
if条件语句:
1、
{% if 条件 %}
满足条件要运行的内容
{% endif %}
2、
{% if %}
满足条件要运行的内容
{% else %}
不满足条件要运行的内容
{% endif %}
3、
{% if 条件1 %}
满足条件1要运行的内容
{% elif 条件2 %}
满足条件2要运行的内容
{% elif 条件3 %}
满足条件3要运行的内容
{% else %}
不满足条件要运行的内容
{% endif %}
# 条件中允许使用 (比较运算符> < >= <= == !=)
# (逻辑运算符 not and or)
# 但是:and 和 or 不能同时出现
示例功能:用户正确登录之后,跳转到另外一个页面,并且在那个页面中显示出用户的完整信息
1、先修改url.py文件,在其中增加一些内容。完整代码如下:
#!/usr/bin/env python
# coding=utf-8
import sys
reload(sys)
sys.setdefaultencoding("utf-8")
from handlers.index import IndexHandler
from handlers.user import UserHandler
url = [
(r'/', IndexHandler),
(r'/user', UserHandler), // 新的url地址
]
2、然后就建立handlers/user.py文件,内容如下:
#!/usr/bin/env python
# coding=utf-8
import tornado.web
import methods.readdb as mrd
class UserHandler(tornado.web.RequestHandler):
def get(self):
username = self.get_argument("user")
user_infos = mrd.select_table(
table="users",column="*",condition="username",value=username)
self.render("user.html", users = user_infos) # 将查询到的用户信息返回给前端
注意:上述的user.py代码为了简单突出本将要说明的,没有对user_infos的结果进行判断。在实际的编程中,这要进行判断或者使用try...except。
3、前端的user.html模板文件,注意传入的引用对象user_infos不是一个字符串了,是一个元组。内容如下:
Learning Python
Your informations are:
{% for one in users %} # 使用for循环遍历传入的对象
- username:{{one[1]}}
# 索引获取元素
- password:{{one[2]}}
- email:{{one[3]}}
{% end %}
实际工作中有很多模板都有相同的部分内容,在tornado的模板中有一种“继承”的机制,它的作用之一就是能够让代码重用。
6.1 先建立一个文件,命名为base.html,代码如下:
Learning Python
{% block header %}
{% end %}
{% block body %}
{% end %}
6.2 接下来就以base.html为父模板,依次改写已经有的index.html和user.html模板
index.html代码如下:
{% extends "base.html" %}
{% block header %}
登录页面
用用户名为:{{user}}登录
{% end %}
{% block body %}
{% end %}
user.html的代码如下:
{% extends "base.html" %}
{% block header %}
Your informations are:
{% end %}
{% block body %}
{% for one in users %}
- username:{{one[1]}}
- password:{{one[2]}}
- email:{{one[3]}}
{% end %}
{% end %}
因为HTTP协议是无状态的,即服务器不知道用户上一次做了什么,这严重阻碍了交互式Web应用程序的实现。
典型场景1:在网上购物中,用户浏览了几个页面,买了一盒饼干和两瓶饮料。最后结帐时,由于HTTP的无状态性,不通过额外的手段,服务器并不知道用户到底买了什么。 所以Cookie就是用来绕开HTTP的无状态性的“额外手段”之一。服务器可以设置或读取Cookies中包含信息,借此维护用户跟服务器会话中的状态。
在刚才的购物场景中,当用户选购了第一项商品,服务器在向用户发送网页的同时,还发送了一段Cookie,记录着那项商品的信息。当用户访问另一个页面,浏览器会把Cookie发送给服务器,于是服务器知道他之前选购了什么。用户继续选购饮料,服务器就在原来那段Cookie里追加新的商品信息。结帐时,服务器读取发送来的Cookie就行了。
典型场景2:当登录一个网站时,网站往往会请求用户输入用户名和密码,并且用户可以勾选“下次自动登录”。如果勾选了,那么下次访问同一网站时,用户会发现没输入用户名和密码就已经登录了。这正是因为前一次登录时,服务器发送了包含登录凭据(用户名加密码的某种加密形式)的Cookie到用户的硬盘上。第二次登录时,(如果该Cookie尚未到期)浏览器会发送该Cookie,服务器验证凭据,于是不必输入用户名和密码就让用户登录了。
和任何别的事物一样,cookie也有缺陷:
对于用户来讲,可以通过改变浏览器设置,来禁用cookie,也可以删除历史的cookie。但就目前而言,禁用cookie的可能不多了,因为你总要在网上买点东西吧。
Cookie最让人担心的还是由于它存储了用户的个人信息,并且最终这些信息要发给服务器,那么它就会成为某些人的目标或者工具,比如有cookie盗贼,就是搜集用户cookie,然后利用这些信息进入用户账号,达到个人的某种不可告人之目的;还有被称之为cookie投毒的说法,是利用客户端的cookie传给服务器的机会,修改传回去的值。这些行为常常是通过一种被称为“跨站指令脚本(Cross site scripting)”(或者跨站指令码)的行为方式实现的。
跨网站脚本(Cross-site scripting,通常简称为XSS或跨站脚本或跨站脚本攻击)是一种网站应用程序的安全漏洞攻击,是代码注入的一种。它允许恶意用户将代码注入到网页上,其他用户在观看网页时就会受到影响。这类攻击通常包含了HTML以及用户端脚本语言。
XSS攻击通常指的是通过利用网页开发时留下的漏洞,通过巧妙的方法注入恶意指令代码到网页,使用户加载并执行攻击者恶意制造的网页程序。这些恶意网页程序通常是JavaScript,但实际上也可以包括Java, VBScript, ActiveX, Flash 或者甚至是普通的HTML。攻击成功后,攻击者可能得到更高的权限(如执行一些操作)、私密网页内容、会话和cookie等各种内容。
cookie是好的,被普遍使用。在tornado中,也提供对cookie的读写函数。
set_cookie() # 写入cookie值
get_cookie() # 获取cookie值
# 是tornado默认提供的两个方法,但是它是明文不加密传输的。
在index.py文件的IndexHandler类的post()方法中,当用户登录,验证用户名和密码后,将用户名和密码存入cookie,代码如下:
def post(self):
username = self.get_argument("username")
password = self.get_argument("password")
user_infos = mrd.select_table(
table="users",column="*",condition="username",value=username)
if user_infos:
db_pwd = user_infos[0][2]
if db_pwd == password:
self.set_cookie(username,db_pwd) #将用户信息存入cookie
self.write(username)
else:
self.write("your password was not right.")
else:
self.write("There is no thi user.")
tornado提供另外一种(非绝对)安全的方法:
set_secure_cookie() # 写入cookie信息(加密)
get_secure_cookie() # 获取cookie信息(加密)
# 这种方法称其为安全cookie,是因为它以明文加密方式传输。
跟set_cookie()的区别还在于, set_secure_cookie()执行后的cookie保存在磁盘中,直到它过期为止。也是因为这个原因,即使关闭浏览器,在失效时间之间,cookie都一直存在。
要是用set_secure_cookie()方法设置cookie,要先在application.py文件的setting中进行如下配置:
setting = dict(
template_path = os.path.join(os.path.dirname(__file__), "templates"),
static_path = os.path.join(os.path.dirname(__file__), "statics"),
cookie_secret = "bZJc2sWbQLKos6GkHn/VB9oXwQt8S0R0kRvJ5/xJ89E=",
)
其中cookie_secret = "bZJc2sWbQLKos6GkHn/VB9oXwQt8S0R0kRvJ5/xJ89E="
是新增加的,但是,它并不是真正的加密,仅仅是一个障眼法罢了。
因为tornado会将cookie值编码为Base-64字符串,并增加一个时间戳和一个cookie内容的HMAC签名。所以,cookie_secret的值,常常用下面的方式生成(这是一个随机的字符串):
>>> import base64, uuid
>>> base64.b64encode(uuid.uuid4().bytes)
'w8yZud+kRHiP9uABEXaQiA=='
如果嫌弃上面的签名短,可以用base64.b64encode(uuid.uuid4().bytes + uuid.uuid4().bytes)
获取。这里得到的是一个随机字符串,用它作为 cookie_secret值。然后修改index.py中设置cookie那句话,变成:
self.set_secure_cookie(username, db_pwd, httponly=True, secure=True)
如果要获取此cookie,用 self.get_secure_cookie(username) 即可。
这种对网站的攻击方式跟上面的跨站脚本(XSS)似乎相像,但攻击方式不一样。XSS利用站点内的信任用户,而XSRF则通过伪装来自受信任用户的请求来利用受信任的网站。与XSS攻击相比,XSRF攻击往往不大流行(因此对其 进行防范的资源也相当稀少)和难以防范,所以被认为比XSS更具危险性。
对于防范XSRF的方法,上面推荐阅读的文章中有明确的描述。还有一点需要提醒读者,就是在开发应用时需要深谋远虑。任何会产生副作用的HTTP请求,比如点击购买按钮、编辑账户设置、改变密码或删除文档,都应该使用post()方法。这是良好的RESTful做法。
在tornado中,提供了XSRF保护的方法。在application.py文件中,使用xsrf_cookies参数开启XSRF保护。
setting = dict(
template_path = os.path.join(os.path.dirname(__file__), "templates"),
static_path = os.path.join(os.path.dirname(__file__), "statics"),
cookie_secret = "bZJc2sWbQLKos6GkHn/VB9oXwQt8S0R0kRvJ5/xJ89E=",
xsrf_cookies = True,
)
这样设置之后,Tornado将拒绝请求参数中不包含正确的 _xsrf 值的 post/put/delete 请求。tornado会在后面悄悄地处理xsrf_cookies,所以,在表单中也要包含XSRF令牌以却表请求合法。比如 index.html 的表单,修改如下:
{% extends "base.html" %}
{% block header %}
登录页面
用用户名为:{{user}}登录
{% end %}
{% block body %}
{% end %}
{% raw xsrf_form_html() %} -- 是新增的,目的就在于实现上面所说的授权给前端以合法请求。
前端向后端发送的请求是通过ajax(),所以,在ajax请求中,需要一个_xsrf参数。以下是script.js的代码
function getCookie(name){
var x = document.cookie.match("\\b" + name + "=([^;]*)\\b");
return x ? x[1]:undefined;
}
$(document).ready(function(){
$("#login").click(function(){
var user = $("#username").val();
var pwd = $("#password").val();
var pd = {"username":user, "password":pwd, "_xsrf":getCookie("_xsrf")};
$.ajax({
type:"post",
url:"/",
data:pd,
cache:false,
success:function(data){
window.location.href = "/user?user="+data;
},
error:function(){
alert("error!");
},
});
});
});
函数 getCookie() 的作用是得到cookie值,然后将这个值放到向后端post的数据中。
这是tornado提供的XSRF防护方法。是不是这样做就高枕无忧了呢?没这么简单。要做好一个网站,需要考虑的事情还很多。
所谓同步,就是在发出一个“请求”时,在没有得到结果之前,该“请求”就不返回。但是一旦请求返回,就得到返回值了。 换句话说,就是由“请求发起者”主动等待这个“请求”的结果。
而异步则是相反,“请求”在发出之后,这个请求就直接返回了,但没有返回结果。换句话说,当一个异步过程请求发出后,请求者不会立刻得到结果。而是在“请求”发出后,“被请求者”通过状态、通知来通知请求者,或通过回调函数处理这个请求。
阻塞和非阻塞
“阻塞和非阻塞”与“同步和异步”常常被换为一谈,其实它们之间还是有差别的。
阻塞和非阻塞关注的是程序在等待调用结果(消息,返回值)时的状态.
阻塞调用是指调用结果返回之前,当前线程会被挂起。调用线程只有在得到结果之后才会返回。非阻塞调用指在不能立刻得到结果之前,该调用不会阻塞当前线程。
按照这个说明,发短信就是显然的非阻塞,发出去一条短信之后,你利用手机还可以干别的。
10.1 tornado的同步
此前,在tornado基础上已经完成的web,就是同步的、阻塞的。
10.2 tornado的异步
tornado提供了一套异步机制,就是异步装饰器1: @tornado.web.asynchronous + 回调函数
#!/usr/bin/env python
# coding=utf-8
import tornado.web
from base import BaseHandler
import time
class SleepHandler(BaseHandler):
@tornado.web.asynchronous
def get(self):
tornado.ioloop.IOLoop.instance().add_timeout(
time.sleep(17), callback=self.on_response)
def on_response(self): # 回调函数
self.render("sleep.html")
self.finish()
@tornado.web.asynchronous :它的作用在于将tornado服务器本身默认的设置 _auto_fininsh 值修改为 false。如果不用这个装饰器,客户端访问服务器的 get() 方法并得到返回值之后,两只之间的连接就断开了,但是用了 @tornado.web.asynchronous 之后,这个连接就不关闭,直到执行了 self.finish() 才关闭这个连接。
异步装饰器2: @tornado.gen.coroutine + 生成器
import time
class SleepHandler(tornado.web.RequestHandler):
@tornado.gen.coroutine
def get(self):
yield tornado.gen.Task(
tornado.ioloop.IOLoop.instance().add_timeout, time.sleep(17))
self.render("sleep.html")
yield tornado.gen.Task(tornado.ioloop.IOLoop.instance().add_timeout, time.sleep(17)) 中的 tornado.gen.Task() 方法,其作用是“Adapts a callback-based asynchronous function for use in coroutines.”。返回后,最后使用yield得到了一个生成器,先把流程挂起,等完全完毕,再唤醒继续执行。要提醒读者,生成器都是异步的。
10.3 实践中的异步
如果在tornado中按照之前的方式只用它们,以下各项同步(阻塞)的,就会削减tornado的非阻塞、异步优势:
除了以上,或许在编程实践中还会遇到其他的同步、阻塞问题,怎么解决?聪明的大牛程序员帮我们做了扩展模块,专门用来实现异步/非阻塞的。
tornado.gen.sleep()
或者tornado.ioloop.IOLoop.instance().add_timeout
,这在前面代码已经显示了。其它的解决方法,只能看到问题具体说了,甚至没有很好的解决方法。不过,这里有一个列表,列出了足够多的库,供使用者选择:Async Client Libraries built on tornado.ioloop,同时这个页面里面还有很多别的链接,都是很好的资源,建议读者多看看。
初级学习到这里就结束了,不过还有很多实际问题等着我们,加油!!