Bootstrap

自学Python第二十六天- Tornado 框架


Tornado 是一个基于 phthon 的 web 开发框架,类似于 Django 和 Flask 等。Tornado 是一个 高性能的框架,支持 异步Web服务,其性能达到了 Flask 的1.5倍。Tornado在设计之初就考虑到了性能因素,旨在解决C10K问题,这样的设计使得其成为一个拥有非常高性能的框架,能够处理严峻的网络流量。

由于其高性能,可以用于即时反馈及实时通信型的场景,如可扩展的社交应用、实时分析引擎(因为 Tornado 是基于长连接的,所以可以进行实时数据分析和同步)、RESTful API、WebSocket(例如在线客服、视频直播等) 等。

Tornado 中文文档
Tornado 官方英文文档

安装及基础引用

pip install tornado
from tornado.web import Application
from tornado.ioloop import IOLoop
from tornado.web import RequestHandler

创建、配置、初始化应用及简单运行服务

简单的 Tornado 应用服务包括了创建应用、配置应用、设置路由及处理器、启动服务四个主要的步骤。

创建应用

使用 Application 创建应用对象,其参数是一个元组列表,每一个元组都指定了路由及处理器。

from tornado.web import Application
# 创建应用
app = Application([('/', IndexHandler)])

对 app 进行设置

app 创建好后,需要进行设置,例如绑定端口、设置访问地址等

# 设置监听
app.listen(5000, '127.0.0.1')

另一种设置方法

tornado 支持使用 options 在命令行中添加参数的方式进行设置

# manager.py
from tornado.options import define, parse_command_line, options
# 设置命令行输入的参数
define('port', default=5000, type=int, help='bind socket port')
define('host', default='127.0.0.1', type=str, help='host ip address')
# 解析命令行的参数
parse_command_line()
# 创建 app
app = make_app()	# 使用 make_app 函数来创建app对象和注册路由及处理器
# 在监听中使用获取的参数
app.listen(options.port, options.host)
# 在服务运行前输出提示
print('starting web server at http://%s:%s' % (options.host, options.port))
# 运行服务
IOLoop.current().start()

这样就可以在命令行中看到参数提示

python manager.py --help

使用参数

python manager.py --host='127.0.0.1' --port=5000

一些其他的配置

可以在创建 app 对象和注册路由及处理器时,加入其他设置。

from tornado.web import Application

app = Application([('/', IndexHandler), ('/hello', HelloHandler)], **settings)

这里的 settings 是一个字典,内容为一些设置信息。

import os

settings = {
    # 设置模板路径
    'template_path': os.path.join(os.path.dirname(__file__), 'templates'),
    # 设置静态文件路径
    'static_path': os.path.join(os.path.dirname(__file__), "static"),
    # 设置防跨域请求,默认为 false,即不防御
    'xsrf_cookies': True,
    # 设置登陆路径,未登陆用户在操作时跳转会用到这个参数:
    # #默认为 @tornado.web.authenticated
    # 'login_url': '/login',
    # 设置调试模式,默认为 false
    'debug': True,
    # 设置cookie密钥,默认为字符串"secure cookies"
    'cookie_secret': "alen#!2ODFog45G45(*&(';//?}dfj423$%^FS",
    # 设置是否自动编码,用以兼容之前的APP,默认为未设置
    'autoescape': None,
    # 设置gzip压缩:
    'gzip': True,
    # 设置静态路径,默认是 '/static/'
    'static_url_prefix': '/static/',
    # 设置静态文件处理类,默认是 tornado.web.StaticFileHandler
    # 'static_handler_class' : tornado.web.StaticFileHandler,
    # 设置日志处理函数
    # 'log_function' : your_fun,
}

关于调试模式

调试模式有一些缺点:只感知.py文件的改变,模版的改变不会加载。有些特殊的错误,比如import的错误,就会直接让服务下线,到时候还得手动重启。还有就是调试模式和 HTTPServer 的多进程模式不兼容。在调试模式下,你必须将 HTTPServer.start 的参数设为不大于 1 的数字

设置路由处理器

在之前创建 app 对象时需要指定路由及处理器

from tornado.web import Application
# 创建应用
app = Application([('/', IndexHandler)])

路由处理器是自定义的一个继承自 RequestHandler 的类

from tornado.web import RequestHandler
# 设置路由处理器
class IndexHandler(RequestHandler):
    # 处理 get 请求,其他请求可以写对应的方法
    def get(self):
        # 向客户端发送响应数据
        self.write('<h3>Hello, Tornado!</h3>')

启动服务

Tornado 是支持多线程多进程的,其服务的启动是将应用加入 IO 事件循环中。

from tornado.ioloop import IOLoop
# 启动服务,因为会进行阻塞,所以先输出信息
print('starting http://127.0.0.1:5000')
IOLoop.current().start()	# 获取当前循环,启动服务

以 HTTPServer 启动服务

如果使用 HTTPServer 服务,例如需要使用多进程服务,则需要创建 http_server ,并添加监听(不再使用 app 进行监听)

from tornado.web import Application
from tornado.httpserver import HTTPServer
from tornado.ioloop import IOLoop

app = Application([
	('/', IndexHandler)
], **settings)
http_server = HTTPServer(app)
http_server.listen(5000, '127.0.0.1')
IOLoop.instance().start()

执行简单的 Tornado 服务

from tornado.ioloop import IOLoop
from tornado.web import Application, RequestHandler

class IndexHandler(RequestHandler):
    # 处理 get 请求,其他请求可以写相应的方法
    def get(self):
        # 向客户端发送响应数据
        self.write('<h3>Hello, Tornado!</h3>')

if __name__ == '__main__':
    # 创建应用
    app = Application([
        ('/', IndexHandler), ('/hello', IndexHandler)
    ])
    # 设置监听
    app.listen(5000, '127.0.0.1')
    # 启动服务
    print('starting http://127.0.0.1:5000')
    IOLoop.current().start()  # 获取当前循环,启动服务

代码写好后,直接执行即可。可以通过浏览器测试访问。

Tornado 应用结构

Tornado 的应用结构类似于 flask,由 manager.py 进行管理,是程序的启动入口。应用放在各 app 文件夹中,models 存放数据模型类,utils 存放其他工具。app 目录下包含 static 存放静态资源,templates 存放模板文件,views 存放视图类。另外 tornado 支持自定义 UI 模块,可以将 UI 模块独立存放在 app 目录下的一个文件或文件夹中。

请求切入点

每个请求 tornado 会创建相应的处理器对象,然后由对应的方法进行处理。在此之前,会经过初始化对象实例等方法和一些特定的方法,作为切入点。

initialize 初始化方法

每次请求时,在所有请求方法被调用之前,都会执行 initialize() 方法。

prepare 预处理方法

在初始化之后,调用处理方法之前,会调用此方法进行预处理。主要用于验证参数、权限、读取缓存等。

on_finish 完成方法

在所有方法执行完成后,会执行 on_finish() 方法。一般用于释放资源。

处理 request

当 tornado 收到一个请求,会创建相应的处理器对象,并使用相应的处理方法进行处理,所以所有的请求信息都会在此处理器对象中。通过设置断点调试可以发现,request 就是是处理器对象的一个成员,可以通过调用此成员对象获取需要的数据。

请求对象中包含哪些信息

请求对象中经常使用的有

  • arguments : 请求参数(包含了body_arguments和query_arguments中的参数),字典类型
  • body : 请求体,字节类型
  • body_arguments : 请求体的参数(正文参数),字典类型
  • cookies : cookie 信息,字典(其实是 SimpleCookie)类型
  • files : 文件信息,字典类型
  • headers :请求头,字典(其实是 HTTPHeaders)类型
  • host : 请求主机,即 ip + port
  • host_name : 主机名称,即没有端口号的 ip
  • method : 请求方法
  • path : 请求路径(不带ip、端口号、参数等)
  • protocol : 请求协议,http或https等
  • query : 查询字符串,即 url 中带的参数
  • query_arguments : 请求参数 (查询参数),字典类型
  • remote_ip : 远程IP
  • uri : 请求资源地址,path 加查询字符串

获取请求参数

如果显示不关心参数来自 get 请求还是 post 请求,可以使用 self.request.arguments 获取参数。需注意是一个字典,字典值为字节码。也可以使用方法 self.get_argument() 获取需要的参数,返回字符串。如果需要获取多个同名参数的值,可以使用 self.get_arguments() 方法,返回字符串列表。

# 使用方法获取参数
from tornado.web import RequestHandler

class HelloHandler(RequestHandler):
    def get(self):
        uid = self.get_argument('uid')		# 返回值为字符串
        name = self.get_arguments('name')[0]	# 返回值为字符串的列表
		print(uid, name)
        self.write('<h2>Hello, Tornado!</h2>')
# 从请求对象中获取参数
from tornado.web import RequestHandler

class HelloHandler(RequestHandler):
    def get(self):
		req: HTTPServerRequest = self.request	# 先获取请求对象
		args = req.arguments				# arguments 成员是一个字典,键为参数名,值为参数值,是字节型
		uid = req.arguments.get('uid')		# 返回值为字节型列表
		name = req.arguments.get('name')	# 返回值为字节型列表
		print(args, type(args))
		print(uid, name)
        self.write('<h2>Hello, Tornado!</h2>')

获取 get 参数(查询参数)

可以从 self.request.query_arguments 中获取 get 参数,需注意是一个字典,字典值为字节码。也可以使用方法 self.get_query_argument() 来获取需要的参数,返回字符串。如果需要获取多个同名参数的值,可以使用 self.get_query_arguments() 方法,返回字符串列表。

如果想从 request 对象中获取参数,可以使用 self.request.query_arguments。需注意获取的值是字节类型。

获取 post 参数(正文参数)

可以从 self.request.body_argumenst 获取 post 参数。需注意是一个字典,字典值为字节码。也可以使用方法 sefl.get_body_argument() 来获取需要的参数,返回字符串。如果需要获取多个同名参数的值,可以使用 self.get_body_arguments() 方法,返回字符串列表。

如果想从 request 对象中获取参数,可以使用 self.request.body_arguments。需注意获取的值是字节类型。

获取路径路由中的参数

将参数添加到 URL 里,例如 http://127.0.0.1:5000/page/112 ,其中 112 就可以是参数。

from tornado.web import Application, RequestHandler

class PageHandler(RequestHandler):
	# 方法中添加参数,接收路由中的参数数据
    def get(self, page, name):
        self.write('page = %s <br>name = %s' % (page, name))

# 创建app对象时,可以通过注册路由获取参数
app = Application([
	('/', IndexHandler),
    (r'/page/(\d+)/(\w+)', PageHandler)	# 注册路由时,使用正则的方式可以获得参数
], **settings)

也可以指定分组的名称,这样就会按照名称而不是顺序传值了

from tornado.web import Application, RequestHandler

class PageHandler(RequestHandler):
	# 方法中添加参数,接收路由中的参数数据
    def get(self, page, name):
        self.write('page = %s <br>name = %s' % (page, name))

# 创建app对象时,可以通过注册路由获取参数
app = Application([
	('/', IndexHandler),
    (r'/page/(?P<name>\d+)/(?P<page>\w+)', PageHandler)	# 注册路由时,使用正则的方式可以获得参数
], **settings)

需注意的是,使用了路由路径参数时,处理器的处理方法必须添加传入的参数,否则会报错。

获取 json 数据

json 数据是以请求体(正文)方式传入的,所以使用 self.request.body 来获取。注意获得的是字节型,需要转换为字典使用。

from tornado.web import RequestHandler

class ApiHandler(RequestHandler):
	def post(self):
		# 获取 json 数据
		bytes = self.request.body		# 字节类型
		# 判断数据类型
		content_type = self.request.headers.get('Content-Type')
		if content_type.startswith('application/json'):
			json_str = bytes.decode('utf-8')		# 解码成字符串
			json_data = json.loads(json_str)		# 反序列化为 json 对象(字典)
			self.write('upload 成功')
		else:
			self.write('upload data 必须是 json 格式')

读取 cookie 和 header

同获取参数数据,获取 cookie 同样可以使用 request 对象和相应方法两种方式,但是不推荐从 request 对象中获取,因为使用 self.request.cookies.get() 获取到的 cookie 是 http.cookies.Morsel 对象,而使用 self.get_cookie() 方法能够直接获取字符串。但是可以通过 request 对象获取所有 cookie。

可以使用 self.request.headers 获取所有请求头信息,使用 self.request.headers.get() 获取需要的请求头数据。

cookie 漏洞和安全 cookies

由于有很多方法都可以在浏览器中截获 cookie,并伪造 cookie 数据,所以 tornado 提供了安全 cookies 来使用加密签名验证 cookies 是否被非法篡改。

可以使用 tornado 的 set_secure_cookie()get_secure_cookie() 函数设置和获取浏览器的 cookie,来防范恶意篡改。不过为了使用这些函数,必须在设置中设置 cookie_secret 参数。需注意的是,安全 cookies 是签名而不是加密,因此其并非是绝对安全的。

读取文件信息及内容

self.request.files 包含了上传的文件信息,类型是字典类型。可以获取相应的文件对象,使用 FileStorage.save() 来保存文件

from tornado.web import RequestHandler

class UploadHandler(RequestHandler):
	def post(self):
		# 注意提交的 form 中文件字段的名称必须相同
		upload_file = self.request.files.get('upload', None)
		if not upload_file:
			self.write('没有上传文件')
		# 判断文件类型是否需要
		elif upload_file.content_type.startswith('image/'):
			self.write('上传文件只支持图片类型')
		# 符合要求,保存图片
		else:
			# 创建文件名
			filename = uuid.uuid4().hex + os.path.splitext(upload_file.filename)[-1]
			# 上传文件保存路径
			filepath = os.path.join(settings['static_url_prefix'], filename)
			# 服务端保存上传文件
			upload_file.save(filepath)
			# 返回保存成功信息
			self.write('文件保存成功')

处理 response

tornado 不像 flask 等,有多种返回 response 的方式。它只会将 write 、set_coonkie 等方法设置的对象进行返回。所以这些方法无所谓调用顺序,所有的设置都会生效。

返回简单信息

可以使用 self.write() 方法返回简单的html信息

from tornado.web import RequestHandler

class HelloHandler(RequestHandler):
    def get(self):
		# 直接返回html信息
        self.write('<h2>Hello, Tornado!</h2>')

返回模板页面

tornado 使用 self.render() 方法返回模板页面

class IndexHandler(RequestHandler):
	def get(self):
		self.render('index.html')

返回数据到模板页面

tornado 返回数据到模板页面的方法有点类似于 flask ,通过在 self.render() 方法中添加参数传递。

<!-- test.html -->
<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>test</title>
</head>
<body>
    <p>
        a = {{ aa }}<br>
        b = {{ bb }}<br>
        data = {{ dd }}
    </p>
    {% for k,v in dd.items() %}
        <p>{{ k }} = {{ v }}</p>
    {% end %}
</body>
</html>
class TestHandler(RequestHandler):
    def get(self):
        a = '数据a'
        b = 2
        data = {
            '参数1': '1111',
            '参数2': '2222',
            'aaa': 333
        }
        self.render('test.html', aa=a, bb=b, dd=data)

返回重定向

使用 self.redirect() 方法进行重定向。重定向和 self.wirte() 等方法并不冲突,实际使用中,重定向后其他方法设置的东西其实是没有用的。

返回 json

可以使用 self.write() 方法直接写入序列化的 json 对象

from tornado.web import RequestHandler
import json

class JsonHandler(RequestHandler):
    def post(self):
        data = {
            'msg': 'Hello, Tornado!',
            'status': 'OK'
        }
        self.write(json.dumps(data))	# 将字典对象序列化
        # 因为 json.dumps() 方法返回的响应,其 content-type 类型还是 text/html,所以需要重设响应头
        self.set_header('Content-Type','application/json;charset=utf-8')

自定义 cookie 和 header

可以使用 self.set_cookie(key, value) 来设置 cookie,使用 self.clear_cookie() 方法删除某条 cookie,使用 self.clear_all_cookies() 删除所有的 cookie。

使用 self.set_header(key, value) 来设置请求头。可以使用 self.clear_header() 方法删除请求头。

解决跨域请求问题

目前跨域请求有JSONP和CORS两种常用方案,最广泛使用的是CORS方式。

处理器对象继承的 RequestHandler 类中自带了一个函数 set_default_headers() 可以统一为所有请求设置响应头。此方法在路由处理方法调用完成后调用,设置完响应头后再返回 Response ,所以可以在此方法内部添加跨域 CORS 信息。

class LoginHandler(RequestHandler):
	# 所有的请求方法执行完成后,默认设置响应头信息
	def set_default_handlers(self):
		# 设置响应头以解决跨域问题
		self.set_header('Access-Control-Allow-Origin', '*')		# 设置允许的源域名(即从哪个域访问本域)
		self.set_header('Access-Control-Allow-Headers', 'Content-Type,x-requested-with')	# 允许跨域访问时重设哪些属性
		self.set_header('Access-Control-Allow-Methods', 'GET,POST,PUT,DELETE')	# 允许跨域请求的方法
		self.set_header('Access-Control-Allow-Credentials', 'true')		# 允许跨域传输 cookie
		self.set_header('Access-Control-Expose-Headers', "cos1,cos2")	# 允许请求端接收自定义的响应头
		self.set_header('Access-Control-Max-Age', 10)	# 设置预检缓存时间,时间内发送请求无需预检(option)
	
	# 跨域请求时,会被客户端请求,用来表示服务器是否支持跨域请求
	def options(self):
		# 默认会返回200,所以可以不写这条语句,但是必须有此方法
		self.set_status(200)

模板语言

tornado 和 flask 一样支持 jinja2 模板语言,不过还是有些小变化

变量数据

tornado 使用双花括号填充变量及表达式

<!-- 后端传入数据
	name = '张三',
	value = 447
	age = 18 -->

<p>
	姓: {{ name[0:1] }}<br>
	名: {{ name[1:] }}<br>
	年龄: {{ age }}<br>
	得分: {{ value }}<br>
</p>

也可以使用一些复杂的表达式、函数、方法。需要注意的是最终输出的是字符串。

对于字典、对象等变量的使用

和 flask 不同的是,tornado 不支持"点"语法,而是使用 python 访问字典的语法(dict[key])来使用字典变量,而对于对象还是使用“点”来获取其属性。

管道符过滤器

需注意的是 tornado 不支持管道符过滤器,需要的操作可以使用对应的函数及方法处理。

控制流

控制流语句放在 {% %} 内部,可以像对应的 python 语句一样,支持 if 、for、while、和 try 语句。不过和 flask 等支持的 jinja2 不同的是,结束语句是 {% end %} ,而不是 {% endif %} 或 {% endfor %} 之类的。

在控制流语句中,甚至可以使用 {% set 变量名 = 值 %} 来设置变量,不过大多数情况下,最后使用 UI 模块来做更复杂的划分。

另外使用 if 来判断某个变量是否有值时需注意,如果没有传入该变量, flask 等 jinja2 的模板语法中默认认为是空值,而 tornado 则会报错。所以前端使用的变量在后端必须传入,哪怕值为 None。

模板中使用函数

除了能在流语句中使用很多 python 函数和方法外,tornado 还提供了一些便利的模板函数

  • escape(s) : 替换字符串 s 中的 &、<、>、等特殊字符为对应的 HTML 字符
  • url_escape(s) : 使用 urllib.quote_plus 替换字符串 s 中的字符为 URL 编码形式
  • json_encode(val) : 将 val 编码成 JSON 格式(调用 json.dumps() 方法)
  • squeeze(s) : 过滤字符串 s ,把连续的多个空白字符替换成一个空格

使用静态资源

使用 static_url() 模板方法,可以将传入的需要引用的文件路径添加上创建 app 时设置的 static_url_prefix 的路径,从而方便更改和引用静态资源

<link rel="stylesheet" href="{{ static_url('stytle.css') }}">

块和继承(母版)

定义块和继承母版与 flask 等 jinja2 的模板语法大致相同,不同处还是在于使用 {% end %} 而不是 {% endblock %}。

UI 模块

tornado 的 UI 模块类似于 flask 的宏,区别在于宏的定义是在模板中,而 UI 模块则是使用的 python 中的处理器类。

UI 模块使用时必须在 app 设置中进行注册,设置添加 ui_modules 字段,值是一个字典,字典键为模块名,字典值为类

# settings.py

settings = {
	'template_path': os.path.join(os.path.dirname(__file__), 'templates'),
	'static_path': os.path.join(os.path.dirname(__file__), "static"),
	'debug': True,
	# 注册 UI 模块
	'ui_modules' :{
		'Hello': HelloModule
	}
}
<!-- 引入 UI 模块 -->
{% extends 'base.html' %}
{% block content %}
	<!-- 引入和调用 UI 模块 -->
	{% module Hello() %}
{% end %}
from tonado.web import UIModule
# UI 模块类
class HelloModule(tornado.web.UIModule):
	def render(self):
		retrun '<h1>Hello, World!</h1>'

UI 模块深入

UI 模块的目的并不是直接渲染字符串,而是期望将一个动态渲染的模板作为 UI 渲染到另一个模板中。例如

<!-- 需要访问的模板页面 -->
{% extends "main.html" %}

{% block body %}
<h2>推荐读的书有</h2>
    {% for book in books %}
        {% module Book(book) %}
    {% end %}
{% end %}

这个模板页面调用了名为 Book 的 UI 处理模块,并传入参数 book 对象

from tonado.web import UIModule
# UI 处理模块
class BookModule(tornado.web.UIModule):
    def render(self, book):
    	# render_string() 方法可以显示的渲染模板文件为字符串,然后就可以返回给调用者了
        return self.render_string('modules/book.html', book=book)

UI 处理模块其实是渲染了另一个模板 book.html,并将渲染后的数据返回给调用者

<!-- UI 处理模块渲染的模板 book.html -->
<div class="book">
    <h3 class="book_title">{{ book["title"] }}</h3>
    <img src="{{ book["image"] }}" class="book_image"/>
</div>

嵌入 JS 和 CSS

在使用 UI 模块时,可以嵌入 JS 和 CSS 到调用的模块中。重写方法 embedded_javascript()embedded_css() 可以将相应的代码嵌入,例如

class BookModule(tornado.web.UIModule):
    def render(self, book):
        return self.render_string(
            "modules/book.html",
            book=book,
        )

    def embedded_javascript(self):
        return "document.write(\"hi!\")"

当模块被调用时,“document.write(“hi!”)” 会被 <script> 标签包围,并插入到 <body> 标签中。同样可以将 CSS 规则包裹在 <style> 中添加到 <head> 标签内。

def embedded_css(self):
    return ".book {background-color:#F5F5F5}"

更灵活的是,可以重写 html_body() 方法,在 <body> 内部添加完整的 HTML 标记

def html_body(self):
    return "<script>document.write(\"Hello!\")</script>"

虽然直接内嵌很有用,但是为了更严谨和简洁,推荐添加样式表文件或脚本文件:重写 css_files() 方法和 javascript_files() 方法

def css_files(self):
    return "/static/css/newreleases.css"

def javascript_files(self):
    return "https://ajax.googleapis.com/ajax/libs/jqueryui/1.8.14/jquery-ui.min.js"

需要注意的是,不要包含一个需要其他地方东西的方法,比如依赖其他文件的 JS 函数,因为可能不会按照期望顺序进行渲染。

ORM 数据模型

tornado 可以使用 SQLAlchemy 库来建立 ORM 数据模型。

SQLAlchemy 官方站(英文)
pip install sqlalchemy

SQLAlchemy 支持 PostgreSQL、MySQL / MariaDB、SQLite、Oracle、MSSQLServer 等常用数据库。具体的和需要的连接字符串可以看这里

支持的数据库及连接方式

创建和配置数据模型

首先使用 create_engine() 方法来创建数据库引擎,需要注意的是数据库连接字符串的语法中,前两项分别是数据库类型和驱动,之后跟的是用户名和密码,然后是数据库主机和端口,最后是数据库名称和字符集。

具体支持的数据库和需要的连接字符串可以查看文档。

然后生成数据库连接类、创建会话对象以及生成所有数据模型类的父类

from sqlalchemy import create_engine
from sqlalchemy.ext.declarative import declarative_base
from sqlalchemy.orm import sessionmaker

# 创建数据库引擎
engine = create_engine('mysql+mysqldb://user:[email protected]:3306/learn_tornado?charset=utf8')
# 生成数据库连接类
DbSession = sessionmaker(bind=engine)
# 创建会话对象
session = DbSession()
# 生成所有模型类的父类
Base = declarative_base(bind=engine)

创建数据模型类

根据基类来创建数据模型类,需要注意的是所有的模型类必须声明表名。

from sqlalchemy import Column, Integer, String, Text, ForeignKey
from sqlalchemy.orm import relationship
from sqlalchemy.sql import null

# 数据模型类
class Menu(Base):
    # 定义数据表名称
    __tablename__ = 'menu'
    # 定义各字段
    id = Column(Integer, primary_key=True, autoincrement=True)  # 整型,主键,自增
    title = Column(String(20), unique=True, nullable=False)  # 字符型,长度20,唯一,可以为空
    url = Column(String(50), unique=True)  # 字符型,长度50,唯一
    note = Column(Text)  # 文本型
    # 整型,外键绑定 menu 表的 id 字段,名称为 parent_id_fk,默认值 0
    # 注:default 是使用 sqlalchemy 插入数据时传递给数据库的默认值,server_default 是数据库定义的默认值,仅支持字符串
    # 注:默认空时可以省略
    parent_id = Column(Integer, ForeignKey('menu.id', name='parent_id_fk'), default=null(), server_default=null())
    # 定义关系,关联到 Menu 类中,可以根据 parent 属性进行反查
    # 此项只是类的一个属性,不会生成数据库字段
    childs = relationship('Menu')

数据模型的关系

可以使用 relationship() 函数来声明关系,进行便利的调用。例如对于两个模型

class User(Base):
    __tablename__ = 'user'
    id = Column(Integer, primary_key=True)
    name = Column(String)

    addresses = relationship("Address", backref="user")


class Address(Base):
    __tablename__ = 'address'
    id = Column(Integer, primary_key=True)
    email = Column(String)
    user_id = Column(Integer, ForeignKey('user.id'))

如果没有 relationship,只能这样调用关系数据

# 通过给定的 User.name 来获取该 user 的 address 数据
def get_addresses_from_user(user_name):
    user = session.query(User).filter_by(name=user_name).first()
    addresses = session.query(Address).filter_by(user_id=user.id).all()
    return addresses

如果像例子中定义了 relationship 的话,则可以这样调用

# 定义了 address = relationship('Address')
# 注意:参数指向的是需要关联的类名
def get_addresses_from_user(user_name):
    user = session.query(User).filter_by(name=user_name).first()
    return user.addresses

需要注意的是,这样只能正向调用,不能反向调用,即通过 user.address 来获取 Address 类的数据,不能反过来。如需反向调用,则要使用到 backref 参数了。

# 定义了 addresses = relationship('Address', backref='user')
# backref 参数值为反向调用使用的属性名称
def get_user_from_email(e_mail):
    address = session.query(Address).filter_by(email=e_mail).first()
    return addresses.user

创建与删除表

定义好数据模型类后,即确定了数据库表结构后,可以创建和删除表了。

# 注意使用创建的基类 Base 来创建和删除表
# 创建表
Base.metadata.create_all()
# 删除表
Base.metadata.drop_all()

CURD操作

CURD 是 create、update、read、delete,即增删改查。

增加数据

增加数据是以模型类创建对象,每个对象就是一条数据。然后将对象添加到会话中并提交即可。

# 创建数据对象
m1 = Menu()
# 给数据对象添加数据
m1.title = '用户管理'
m1.note = '管理使用者账户'
# 将数据对象添加到会话
session.add(m1)
# 提交会话
session.commit()

也可以批量添加数据

 # 直接添加数据对象列表到会话,并在创建对象的同时添加数据
 session.add_all([
 	Menu(title='订单管理'),
 	Menu(title='会员管理', url='/user1', parent_id=1)
 	Menu(title='派件员管理', url='user2', parent_id=1)
 	Menu(title='合作商管理', url='user3', parent_id=1)
 	Menu(title='订单统计', url='user4', parent_id=2)
 ])
 # 提交会话
 session.commit()

查询数据

使用 session.query() 方法来进行查询,此方法的参数是需要返回的数据类(表)或字段,会返会回一个查询器对象,所以可以通过链式调用方式添加筛选、排序等条件。查询器对象也可以当作数据对象列表使用,或使用 get()first() 等方法来获取具体的数据对象(或使用 all() 方法将查询器对象转成真正的数据对象列表)。

# 查询所有数据
session.query(Menu).all()
# 按照主键查询
session.query(Menu).get(2)

# 使用 filter 方法,此方法可以对参数进行比对筛选( where 方法与此方法相同)
session.query(Menu).filter(Menu.id == 1).first()
session.query(Menu).filter(Menu.parent_id == null()).all()
# 在 filter 方法的参数中使用方法
session.query(Menu).filter(Menu.url.startswith('/')).all()
session.query(Menu).filter(Menu.url.endswith('3')).all()
session.query(Menu).filter(Menu.url.contains('user')).all()
session.query(Menu).filter(Menu.url.like('/%s%3')).all()	# 条件中 % 作为通配符使用
session.query(Menu).filter(Menu.url.notlike('%user%')).all()
# 组合查询
session.query(Menu).filter(Menu.url.startswith('/'), Menu.url.endswith('2')).all()
# 条件与或非
from sqlalchemy import and_, or_, not_
session.query(Menu).filter(and_(Menu.url.startswith('/'), Menu.parent_id == 1)).all()
session.query(Menu).filter(or_(Menu.url.endswith('3'), Menu.parent_id == 2)).all()
session.query(Menu).filter(not_(Menu.url.endswith('3'))).all()
# 条件在集合内
session.query(Menu).filter(Menu.id.in_([1, 3, 4])).all()
# 条件在某区间内(闭合区间)
session.query(Menu).filter(Menu.id.between(2, 5)).all()

# 使用 filter_by 方法,效果同 filter 方法,此方法参数为 **kwargs,是按照参数值进行筛选
session.query(Menu).filter_by(parent_id=1, id=5).first()

另外查询器对象还可以进行排序、分页等操作

# 排序,默认使用 asc() 升序,也可以 desc() 降序
session.query(Menu.id, Menu.title).order_by(Menu.id.desc()).all()
# 有优先级的排序
session.query(Menu.id, Menu.title, Menu.parent_id).order_by(Menu.parent_id.asc(),Menu.id.desc()).all()
# 对查询结果去重
session.query(Menu.parent_id).distinct().all()
# 分页,忽略前2条,显示3条
session.query(Menu).offset(2).limit(3).all()
聚合查询

更新数据

获取了数据对象后,直接更改其成员(字段)的值,然后更新会话,就能够完成修改更新数据

menu = session.query(Menu).get(5)
menu.title = '合作伙伴'
session.commit()

删除数据

同样,获取数据对象后,可以删除数据

menu = session.query(Menu).get(5)
session.delete(menu)
session.commit()

其他

如果需要插入或更新数据时使用空值(注意数据库的空值和 python 的 None 不一样),可以使用 sqlalchemy.sql.null() 方法获取一个数据库空值。

同步、异步服务

tornado 封装提供了同步、异步的请求服务,当然也可以使用其他的请求库。

同步异步请求都可以直接发送 Request 对象,如果是 get 方法,则可以简单的只发送 url。

发起同步请求

tornado 提供了一个类用于发送同步请求

client = tornado.httpclient.HTTPClient()	# 创建客户端对象
response = client.fetch(url)		# 发送请求,并获取 response

如果是 https 需要验证证书,可以添加参数 validate_cert=False 不进行校验。

发起异步请求

不同于 HTTPClient ,tornado 提供了另一个类用于发送异步请求

client = tornado.httpclient.AsyncHTTPClient()		# 创建异步客户端
client.fetch(url, callback)		# 发送请求,并将 response 传入 callback 回调函数处理

需要注意的是

  • callback 必须接收 response 对象
  • 发起异步请求的方法需要使用 @tornado.web.asynchronous 修饰,表示不会关闭连接
  • 回调函数需要调用 self.finish() 手动关系连接

协程方式

也可以不使用回调函数,而使用 await 来异步等待并继续执行(协程方式)

client = tornado.httpclient.AsyncHTTPClient()		# 创建异步客户端
response = await client.fetch(url)			# 异步发送请求,并等待响应

此方法需要注意的是

  • @tornado.web.asynchronous 还是需要的
  • 发起异步请求的方法使用 async 修饰,然后异步请求使用 await 关键字
  • 返回的是一个 response 对象

异步生成器

有些类似于协程方式

client = tornado.httpclient.AsyncHTTPClient()		# 创建异步客户端
response = yield client.fetch(url)			# 异步发送请求,并等待响应
  • 除了 @tornado.web.asynchronous 外,还需要使用 @tornado.web.gen.coroutine 修饰
  • 返回的是一个 response 对象

WebSockets

WebSockets是HTML5规范中新提出的客户-服务器通讯协议。它提供了在客户端和服务器间持久连接的双向通信。协议本身使用新的ws://URL格式,但它是在标准HTTP上实现的。通过使用HTTP和HTTPS端口,它避免了从Web代理后的网络连接站点时引入的各种问题。HTML5规范不只描述了协议本身,还描述了使用WebSockets编写客户端代码所需要的浏览器API。

Tornado的WebSocket模块

Tornado在websocket模块中提供了一个WebSocketHandler类。这个类提供了和已连接的客户端通信的WebSocket事件和方法的钩子。当一个新的WebSocket连接打开时,open方法被调用,而on_message和on_close方法分别在连接接收到新的消息和客户端关闭时被调用。

在 tornado 中使用 WebSocket 时,编写一个继承自 WebSocketHandler 的类,并通过 open 、on_message 、on_connection_close 和 on_close 方法进行相应的处理。

from tornado.web import RequestHandler
from tornado.websocket import WebSocketHandler

class LoginChatroomHandler(RequestHandler):
    def get(self):
        self.write("""
        <form method='post'>
            <input name='name'>
            <button>登录</button>
        </form>
        """)

    def post(self):
        name = self.get_body_argument('name', '匿名')
        self.set_secure_cookie('username', name)
        self.render('chatRoom.html')

class MessageHandler(WebSocketHandler):
    # 所有的处理器实例(即获取所有与客户端通信的长连接)列表
    online_clients = []

    def open(self):
        # 建立连接时被调用,表示客户端请求连接
        # 相当于 socket 里的 server.accept()
        # 向客户端发送成功连接的消息
        data = {
            'host': self.request.remote_ip,
            'name': self.get_secure_cookie('username').decode(),
            'status': 'connect',
            'msg': ''
        }
        # 将当前实例对象添加到处理器实例列表中
        self.online_clients.append(self)
        self.send_message(data)

    def send_message(self, data):
        # 发送消息给每个连接的客户端
        for client in self.online_clients:
            client.write_message(data)

    def on_message(self, message):
        # 连接建立,等待接收信息
        # 接收到消息,进行处理,并向客户端返回需要的数据
        data = {
            'host': self.request.remote_ip,
            'name': self.get_secure_cookie('username').decode(),
            'status': 'OK',
            'msg': message
        }
        self.send_message(data)

    def on_connection_close(self):
        # 客户端断开连接
        # 从客户端连接列表中删除此连接
        self.online_clients.remove(self)
        data = {
            'host': self.request.remote_ip,
            'name': self.get_secure_cookie('username').decode(),
            'status': 'connection_close',
            'msg': ''
        }
        self.send_message(data)

    def on_close(self):
        # 服务端断开连接
        # 一般用于释放资源
        pass
// 根据 id 获取 dom
function $(id) {
    return document.getElementById(id)
}

window.onload = function (event) {
    // 创建 socket 对象
    var socket = new WebSocket('ws://127.0.0.1:5000/charRoom/message');
    // 发送请求建立连接
    socket.onopen = function (ev) {
        console.log('---------onopen---------')
        console.log(ev)
    };

    // 连接建立完成,等待接收服务端发送的消息
    socket.onmessage = function (ev) {
        console.log('---------onmessage-------------')
        console.log(ev)
        let data = JSON.parse(ev.data)
        if (data.status === 'connect') {
            $('message_body').innerHTML += data.host + '连接成功,' + data.name + '进入聊天室<br>'
        } else if (data.status === 'OK') {
            $('message_body').innerHTML += data.name + '(' + data.host +') 说 : ' + data.msg + '<br>'
        } else if (data.status === 'connection_close'){
            $('message_body').innerHTML += data.host + '断开连接,' + data.name + '退出聊天室<br>'
        }
    };

    // 当接收到错误信息
    socket.onerror = function (ev) {
        console.log('---------onerror-------------')
        console.log(ev)
    };

    // 绑定事件
    $('submit').onclick = function () {
        submit_msg(socket);
    };
    $('quit').onclick = function (){
        quit(socket);
    };
}

// 提交消息事件
function submit_msg(socket) {
    let msg = $('msg').value;
    // 向服务器发送消息
    socket.send(msg)
    $('msg').value = ''
    $('msg').focus()
}
// 退出聊天室事件
function quit(socket){
    // 断开连接
    socket.close();
    // 跳转页面
    window.location="/chatRoom";
}

参考文章

Tornado之源码解析

;