tornado 系列讲解之二(web 框架之RequestHandler和Application)

tornado.web

tornado.web提供了一个具有异步特征的web框架,能支持很多连接,支持长轮询。

import tornado.ioloop
import tornado.web

class MainHandler(tornado.web.RequestHandler):
    def get(self):
        self.write("Hello, world")

if __name__ == "__main__":
    application = tornado.web.Application([
        (r"/", MainHandler),
    ])
    application.listen(8888)
    tornado.ioloop.IOLoop.current().start()

线程安全说明

一般来讲,RequestHandler提供的方法和tornado其他地方的方法都不是线程安全的,特别对于write,finish,和flush来讲,必须在主线程调用。如果使用多线程,请求完成前最好使用 IOLoop.add_callback将控制权交给主线程,或者限制其他线程在IOLoop.run_in_executor,保证回调运行在executor,且不会引用到tornado对象。

Request handlers

tornado.web.RequestHandler(...)
是所有http请求的基类,子类必须实现至少一个http方法,子类不要覆盖__init__方法,可以使用 initialize方法代替。

Entry points

  • RequestHandler.initialize()
    子类初始化方法,每个request都会被调用,URLSpec的第三个参数,是dict的话,会传到initialize() 方法里
class ProfileHandler(RequestHandler):
    def initialize(self, database):
        self.database = database

    def get(self, username):
        ...

app = Application([
    (r'/user/(.*)', ProfileHandler, dict(database=database)),
    ])
  • RequestHandler.prepare()
    在调用HTTP方法之前调用
  • RequestHandler.on_finish()
    request结束后调用,重载这个方法以便进行一些清理操作

Input

获取参数方法支持HTML表单形式,如果希望使用其他格式的参数,比如json,可以:

def prepare(self):
    if self.request.headers['Content-Type'] == 'application/x-json':
        self.args = json_decode(self.request.body)
    # Access self.args directly instead of using self.get_argument.
  • RequestHandler.get_argumentt(name: str, default: Union[None, str, RAISE] = RAISE, strip: bool = True)
    获取指定name的query和body的参数。
  • RequestHandler.get_arguments(name: str, strip: bool = True) → List[str]
    返回指定name的参数列表,包括query和body
  • RequestHandler.get_query_argument(name: str, default: Union[None, str, RAISE] = RAISE, strip: bool = True) → Optional[str]
    获取指定name的query参数
  • RequestHandler.get_query_arguments(name: str, strip: bool = True) → List[str]
    获取指定name的query参数列表
  • RequestHandler.get_body_argument(name: str, default: Union[None, str, RAISE] = RAISE, strip: bool = True) → Optional[str]
    获取指定name的body参数
  • RequestHandler.get_body_arguments(name: str, strip: bool = True) → List[str]
    获取指定name的body参数列表
  • RequestHandler.decode_argument(value: bytes, name: str = None) → str
    对参数解码
  • RequestHandler.request
    tornado.httputil.HTTPServerRequest对象包含了请求数据和header

Output

  • RequestHandler.set_status(status_code: int, reason: str = None) → None
    为response设置状态码
  • RequestHandler.set_header(name: str, value: Union[bytes, str, int, numbers.Integral, datetime.datetime]) → None
    为response设置header
  • RequestHandler.add_header
    为response添加header
  • RequestHandler.clear_header
  • RequestHandler.set_default_headers() → None
    request到来前设置header
  • RequestHandler.write(chunk: Union[str, bytes, dict]) → None
    如果chunk是dict,会被转成json,并且设置 Content-Type为application/json
  • RequestHandler.flush(include_footers: bool = False) → Future[None]
    把当前输出缓存到网络中
  • RequestHandler.finish(chunk: Union[str, bytes, dict] = None) → Future[None]
    结束掉http请求,接受的参数和write一样。
  • RequestHandler.render(template_name: str, **kwargs) → Future[None]
    以给定参数呈现给模板,这个方法会调用finish,因此这个方法调用后,后面再调用其他方法也不会有output了
  • RequestHandler.render_string(template_name: str, **kwargs) → bytes
    用给定的参数生成模板。
  • RequestHandler.redirect(url: str, permanent: bool = False, status: int = None) → None
    重定向到给定的url
  • RequestHandler.send_error(status_code: int = 500, **kwargs) → None
    发送指定错误码给浏览器
  • RequestHandler.clear() → None
    为这个response重置所有的headers和content

cookie

  • RequestHandler.cookies
    self.request.cookies的别名
  • RequestHandler.get_cookie(name: str, default: str = None) → Optional[str]
    返回指定那么的cookie
  • RequestHandler.set_cookie(name: str, value: Union[str, bytes], domain: str = None, expires: Union[float, Tuple, datetime.datetime] = None, path: str = '/', expires_days: int = None, **kwargs) → None
    设置cookie
  • RequestHandler.clear_cookie(name: str, path: str = '/', domain: str = None) → None
    清除指定name的cookie
  • RequestHandler.clear_all_cookies(path: str = '/', domain: str = None) → None
    清除这个用户的所有cookie
  • RequestHandler.get_secure_cookie
    如果有效,返回签名cookie
  • RequestHandler.set_secure_cookie
    为cookie签名,防止篡改
    ...

Other

  • RequestHandler.application
    Application对象。
  • RequestHandler.current_user
    当前已认证用户
    这个可以被两种方式改写:
  1. 重新get_current_user方法
def get_current_user(self):
    user_cookie = self.get_secure_cookie("user")
    if user_cookie:
        return json.loads(user_cookie)
    return None
  1. 重载prepare():
@gen.coroutine
def prepare(self):
    user_id_cookie = self.get_secure_cookie("user_id")
    if user_id_cookie:
        self.current_user = yield load_user(user_id_cookie)
  • RequestHandler.get_login_url() → str
    重载这个方法可以自定义login UR,默认情况下,使用的是application中settings的login_url。
  • RequestHandler.on_connection_close() → None
    如果客户端关闭连接,会在异步handler调用。重载这个方法用于长连接的清理工作。
  • RequestHandler.reverse_url(name: str, *args) → str
    Application.reverse_url的别名
  • RequestHandler.settings
    self.application.settings的别名
  • RequestHandler.static_url(path: str, include_host: bool = None, **kwargs) → str
    返回指定的相对路径的静态文件的url,使用这个方法需要先设置static_path,

Application configuration

  • 类tornado.web.Application(handlers: List[Union[Rule, Tuple]] = None, default_host: str = None, transforms: List[Type[OutputTransform]] = None, **settings)
    这个类的实例能直接通过httpserver去启动:
application = web.Application([
    (r"/", MainPageHandler),
])
http_server = httpserver.HTTPServer(application)
http_server.listen(8080)
ioloop.IOLoop.current().start()

Application的参数是list,我们按顺序遍历列表,并实例化第一个匹配到的requestHandler。也可在这配置静态文件位置:

application = web.Application([
    (r"/static/(.*)", web.StaticFileHandler, {"path": "/var/www"}),
])

使用add_handlers也可以添加虚拟主机:

application.add_handlers(r"www\.myhost\.com", [
    (r"/article/([0-9]+)", ArticleHandler),
])

可以使用add_handlers绑定多个域名。

  • Application.reverse_url(name: str, *args) → str
    返回指定handler名的url。

装饰器(Decorators)

  • tornado.web.authenticated(method: Callable[[...], Optional[Awaitable[None]]]) → Callable[[...], Optional[Awaitable[None]]]
    一个用于控制登录的装饰器,没有登录的话,会跳转到配置的login_url
  • tornado.web.addslash(method: Callable[[...], Optional[Awaitable[None]]]) → Callable[[...], Optional[Awaitable[None]]]
    使用此装饰器向请求路径添加缺少的尾随斜杠。比如一个/foo的请求将会跳转到/foo/,路由规则中需要与这样配置r'/foo/?'配合使用
  • tornado.web.removeslash(method: Callable[[...], Optional[Awaitable[None]]]) → Callable[[...], Optional[Awaitable[None]]]
    使用此装饰器从请求路径中删除尾随斜杠.比如:
    使用此装饰器向请求路径添加缺少的尾随斜杠。比如一个/foo/的请求将会跳转到/foo,路由规则中需要与这样配置r'/foo/*'配合使用
  • tornado.web.stream_request_body
    支持streaming body,和文件上传有关,具体参考tornado.web.stream_request_body
    文件上传
#!/usr/bin/env python

"""Usage: python file_uploader.py [--put] file1.txt file2.png ...
Demonstrates uploading files to a server, without concurrency. It can either
POST a multipart-form-encoded request containing one or more files, or PUT a
single file without encoding.
See also file_receiver.py in this directory, a server that receives uploads.
"""

import mimetypes
import os
import sys
from functools import partial
from uuid import uuid4

try:
    from urllib.parse import quote
except ImportError:
    # Python 2.
    from urllib import quote

from tornado import gen, httpclient, ioloop
from tornado.options import define, options


# Using HTTP POST, upload one or more files in a single multipart-form-encoded
# request.
@gen.coroutine
def multipart_producer(boundary, filenames, write):
    boundary_bytes = boundary.encode()

    for filename in filenames:
        filename_bytes = filename.encode()
        mtype = mimetypes.guess_type(filename)[0] or "application/octet-stream"
        buf = (
            (b"--%s\r\n" % boundary_bytes)
            + (
                b'Content-Disposition: form-data; name="%s"; filename="%s"\r\n'
                % (filename_bytes, filename_bytes)
            )
            + (b"Content-Type: %s\r\n" % mtype.encode())
            + b"\r\n"
        )
        yield write(buf)
        with open(filename, "rb") as f:
            while True:
                # 16k at a time.
                chunk = f.read(16 * 1024)
                if not chunk:
                    break
                yield write(chunk)

        yield write(b"\r\n")

    yield write(b"--%s--\r\n" % (boundary_bytes,))


# Using HTTP PUT, upload one raw file. This is preferred for large files since
# the server can stream the data instead of buffering it entirely in memory.
@gen.coroutine
def post(filenames):
    client = httpclient.AsyncHTTPClient()
    boundary = uuid4().hex
    headers = {"Content-Type": "multipart/form-data; boundary=%s" % boundary}
    producer = partial(multipart_producer, boundary, filenames)
    response = yield client.fetch(
        "http://localhost:8888/post",
        method="POST",
        headers=headers,
        body_producer=producer,
    )

    print(response)


@gen.coroutine
def raw_producer(filename, write):
    with open(filename, "rb") as f:
        while True:
            # 16K at a time.
            chunk = f.read(16 * 1024)
            if not chunk:
                # Complete.
                break

            yield write(chunk)


@gen.coroutine
def put(filenames):
    client = httpclient.AsyncHTTPClient()
    for filename in filenames:
        mtype = mimetypes.guess_type(filename)[0] or "application/octet-stream"
        headers = {"Content-Type": mtype}
        producer = partial(raw_producer, filename)
        url_path = quote(os.path.basename(filename))
        response = yield client.fetch(
            "http://localhost:8888/%s" % url_path,
            method="PUT",
            headers=headers,
            body_producer=producer,
        )
        print(response)


if __name__ == "__main__":
    define("put", type=bool, help="Use PUT instead of POST", group="file uploader")

    # Tornado configures logging from command line opts and returns remaining args.
    filenames = options.parse_command_line()
    if not filenames:
        print("Provide a list of filenames to upload.", file=sys.stderr)
        sys.exit(1)

    method = put if options.put else post
    ioloop.IOLoop.current().run_sync(lambda: method(filenames))

文件接收

#!/usr/bin/env python

"""Usage: python file_receiver.py
Demonstrates a server that receives a multipart-form-encoded set of files in an
HTTP POST, or streams in the raw data of a single file in an HTTP PUT.
See file_uploader.py in this directory for code that uploads files in this format.
"""

import logging

try:
    from urllib.parse import unquote
except ImportError:
    # Python 2.
    from urllib import unquote

import tornado.ioloop
import tornado.web
from tornado import options


class POSTHandler(tornado.web.RequestHandler):
    def post(self):
        for field_name, files in self.request.files.items():
            for info in files:
                filename, content_type = info["filename"], info["content_type"]
                body = info["body"]
                logging.info(
                    'POST "%s" "%s" %d bytes', filename, content_type, len(body)
                )

        self.write("OK")


@tornado.web.stream_request_body
class PUTHandler(tornado.web.RequestHandler):
    def initialize(self):
        self.bytes_read = 0

    def data_received(self, chunk):
        self.bytes_read += len(chunk)

    def put(self, filename):
        filename = unquote(filename)
        mtype = self.request.headers.get("Content-Type")
        logging.info('PUT "%s" "%s" %d bytes', filename, mtype, self.bytes_read)
        self.write("OK")


def make_app():
    return tornado.web.Application([(r"/post", POSTHandler), (r"/(.*)", PUTHandler)])


if __name__ == "__main__":
    # Tornado configures logging.
    options.parse_command_line()
    app = make_app()
    app.listen(8888)
    tornado.ioloop.IOLoop.current().start()
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 212,029评论 6 492
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 90,395评论 3 385
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 157,570评论 0 348
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 56,535评论 1 284
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 65,650评论 6 386
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 49,850评论 1 290
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,006评论 3 408
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 37,747评论 0 268
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,207评论 1 303
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,536评论 2 327
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,683评论 1 341
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,342评论 4 330
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,964评论 3 315
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,772评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,004评论 1 266
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 46,401评论 2 360
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 43,566评论 2 349

推荐阅读更多精彩内容