Tornado

引言

回想Django的部署方式

以Django爲表明的python web應用部署時採用wsgi協議與服務器對接(被服務器託管),而這類服務器一般都是基於多線程的,也就是說每個網絡請求服務器都會有一個對應的線程來用web應用(如Django)進行處理php

考慮兩類應用場景

  1. 用戶量大,高併發html

    如秒殺搶購、雙十一某寶購物、春節搶火車票java

  2. 大量的HTTP持久鏈接python

    使用同一個TCP鏈接來發送和接收多個HTTP請求/應答,而不是爲每個新的請求/應答打開新的鏈接的方法。ios

    對於HTTP 1.0,能夠在請求的包頭(Header)中添加Connection: Keep-Alive。nginx

    對於HTTP 1.1,全部的鏈接默認都是持久鏈接。c++

對於這兩種場景,一般基於多線程的服務器很難應對程序員

C10K問題

對於前文提出的這種高併發問題,咱們一般用C10K這一律念來描述。C10K—— Concurrently handling ten thousand connections,即併發10000個鏈接。對於單臺服務器而言,根本沒法承擔,而採用多臺服務器分佈式又意味着高昂的成本。如何解決C10K問題?web

Tornado

Tornado在設計之初就考慮到了性能因素,旨在解決C10K問題,這樣的設計使得其成爲一個擁有很是高性能的解決方案(服務器與框架的集合體)。正則表達式

1 關於Tornado

知識點

  • 瞭解什麼是Tornado框架
  • 瞭解Tornado與Django的區別

1.1 Tornado是爲什麼物

Tornado全稱Tornado Web Server,是一個用Python語言寫成的Web服務器兼Web應用框架,由FriendFeed公司在本身的網站FriendFeed中使用,被Facebook收購之後框架在2009年9月以開源軟件形式開放給大衆。

特色:

  • 做爲Web框架,是一個輕量級的Web框架,相似於另外一個Python web框架Web.py,其擁有異步非阻塞IO的處理方式
  • 做爲Web服務器,Tornado有較爲出色的抗負載能力,官方用nginx反向代理的方式部署Tornado和其它Python web應用框架進行對比,結果最大瀏覽量超過第二名近40%。

性能: Tornado有着優異的性能。它試圖解決C10k問題,即處理大於或等於一萬的併發,下表是和一些其餘Web框架與服務器的對比:

Tornado框架和服務器一塊兒組成一個WSGI的全棧替代品。單獨在WSGI容器中使用tornado網絡框架或者tornaod http服務器,有必定的侷限性,爲了最大化的利用tornado的性能,推薦同時使用tornaod的網絡框架和HTTP服務器

1.2 Tornado與Django

Django

Django是走大而全的方向,注重的是高效開發,它最出名的是其全自動化的管理後臺:只須要使用起ORM,作簡單的對象定義,它就能自動生成數據庫結構、以及全功能的管理後臺

Django提供的方便,也意味着Django內置的ORM跟框架內的其餘模塊耦合程度高,應用程序必須使用Django內置的ORM,不然就不能享受到框架內提供的種種基於其ORM的便利。

  • session功能
  • 後臺管理
  • ORM

Tornado

Tornado走的是少而精的方向,注重的是性能優越,它最出名的是異步非阻塞的設計方式。

  • HTTP服務器
  • 異步編程
  • WebSockets

2 初識Tornado

知識點

  • Tornado的安裝
  • 瞭解Tornado的原理
  • 掌握Tornado的基本寫法
  • 掌握Tornado的基本模塊
    • tornado.web
    • tornado.ioloop
    • tornado.httpserver
    • tornado.options

 

2.1 安裝

自動安裝

  1.查看本身當前的環境是否已安裝

$ pip list

 

  2.安裝

$ pip install tornado

 

手動安裝

  1. 下載安裝包tornado-4.3.tar.gz(https://pypi.python.org/packages/source/t/tornado/tornado-4.3.tar.gz
    $ tar xvzf tornado-4.3.tar.gz $ cd tornado-4.3 $ python setup.py build $ sudo python setup.py install 

關於使用平臺的說明

Tornado should run on any Unix-like platform, although for the best performance and scalability only Linux (with epoll) and BSD (with kqueue) are recommended for production deployment (even though Mac OS X is derived from BSD and supports kqueue, its networking performance is generally poor so it is recommended only for development use). Tornado will also run on Windows, although this configuration is not officially supported and is recommended only for development use.

Tornado應該運行在類Unix平臺,在線上部署時爲了最佳的性能和擴展性,僅推薦Linux和BSD(由於充分利用Linux的epoll工具和BSD的kqueue工具,是Tornado不依靠多進程/多線程而達到高性能的緣由)。

對於Mac OS X,雖然也是衍生自BSD而且支持kqueue,可是其網絡性能一般不太給力,所以僅推薦用於開發。

對於Windows,Tornado官方沒有提供配置支持,可是也能夠運行起來,不過僅推薦在開發中使用。

2.2 Hello Itcast

上代碼

新建文件hello.py,代碼以下:

複製代碼
# coding:utf-8

import tornado.web
import tornado.ioloop

class IndexHandler(tornado.web.RequestHandler):
    """主路由處理類"""
    def get(self):
        """對應http的get請求方式"""
        self.write("Hello Itcast!")

if __name__ == "__main__":
    app = tornado.web.Application([
        (r"/", IndexHandler),
    ])
    app.listen(8000)
    tornado.ioloop.IOLoop.current().start()
複製代碼

 

執行以下命令,開啓tornado:

$ python hello.py

打開瀏覽器,輸入網址127.0.0.1:8000(或localhost:8000),查看效果: 

代碼講解

1. tornado.web

tornado的基礎web框架模塊

  • RequestHandler

    封裝了對應一個請求的全部信息和方法,write(響應信息)就是寫響應信息的一個方法;對應每一種http請求方式(get、post等),把對應的處理邏輯寫進同名的成員方法中(如對應get請求方式,就將對應的處理邏輯寫在get()方法中),當沒有對應請求方式的成員方法時,會返回「405: Method Not Allowed」錯誤。

    咱們將代碼中定義的get()方法更改成post()後,再用瀏覽器從新訪問(瀏覽器地址欄中輸入網址訪問的方式爲get請求方式),演示以下:

複製代碼
 # coding:utf-8

  import tornado.web
  import tornado.ioloop

  class IndexHandler(tornado.web.RequestHandler):
      """主路由處理類"""
      def post(self):  # 咱們修改了這裏
          """對應http的post請求方式"""
          self.write("Hello Itcast!")

  if __name__ == "__main__":
      app = tornado.web.Application([
          (r"/", IndexHandler),
      ])
      app.listen(8000)
      tornado.ioloop.IOLoop.current().start()
複製代碼

  • Application

    Tornado Web框架的核心應用類,是與服務器對接的接口裏面保存了路由信息表,其初始化接收的第一個參數就是一個路由信息映射元組的列表其listen(端口)方法用來建立一個http服務器實例,並綁定到給定端口(注意:此時服務器並未開啓監聽)。

2. tornado.ioloop

tornado的核心io循環模塊,封裝了Linux的epoll和BSD的kqueue,tornado高性能的基石。 以Linux的epoll爲例,其原理以下圖:

  • IOLoop.current()

    返回當前線程的IOLoop實例。

  • IOLoop.start()

    啓動IOLoop實例的I/O循環,同時服務器監聽被打開

總結Tornado Web程序編寫思路

  1. 建立web應用實例對象,第一個初始化參數爲路由映射列表。
  2. 定義實現路由映射列表中的handler類。
  3. 建立服務器實例,綁定服務器端口。
  4. 啓動當前線程的IOLoop。

2.3 httpserver

上一節咱們說在tornado.web.Application.listen()(示例代碼中的app.listen(8000))的方法中,建立了一個http服務器示例並綁定到給定端口,咱們能不能本身動手來實現這一部分功能呢?

如今咱們修改上一示例代碼以下:

複製代碼
# coding:utf-8

import tornado.web
import tornado.ioloop
import tornado.httpserver # 新引入httpserver模塊

class IndexHandler(tornado.web.RequestHandler):
    """主路由處理類"""
    def get(self):
        """對應http的get請求方式"""
        self.write("Hello Itcast!")

if __name__ == "__main__":
    app = tornado.web.Application([
        (r"/", IndexHandler),
    ])
    # ------------------------------
    # 咱們修改這個部分
    # app.listen(8000)
    http_server = tornado.httpserver.HTTPServer(app) http_server.listen(8000) # ------------------------------
    tornado.ioloop.IOLoop.current().start()
複製代碼

 

在這一修改版本中,咱們引入了tornado.httpserver模塊,顧名思義,它就是tornado的HTTP服務器實現

咱們建立了一個HTTP服務器實例http_server,由於服務器要服務於咱們剛剛創建的web應用,將接收到的客戶端請求經過web應用中的路由映射表引導到對應的handler中,因此在構建http_server對象的時候須要傳出web應用對象app。http_server.listen(8000)將服務器綁定到8000端口。

實際上一版代碼中app.listen(8000)正是對這一過程的簡寫。

單進程與多進程

咱們剛剛實現的都是單進程,能夠經過命令來查看:

$ ps -ef | grep hello.py

咱們也能夠一次啓動多個進程,修改上面的代碼以下:

複製代碼
# coding:utf-8

import tornado.web
import tornado.ioloop
import tornado.httpserver 

class IndexHandler(tornado.web.RequestHandler):
    """主路由處理類"""
    def get(self):
        """對應http的get請求方式"""
        self.write("Hello Itcast!")

if __name__ == "__main__":
    app = tornado.web.Application([
        (r"/", IndexHandler),
    ])
    http_server = tornado.httpserver.HTTPServer(app) 
    # -----------修改----------------
    http_server.bind(8000)
    http_server.start(0)
    # ------------------------------
    tornado.ioloop.IOLoop.current().start()
複製代碼

 

http_server.bind(port)方法是將服務器綁定到指定端口。

http_server.start(num_processes=1)方法指定開啓幾個進程,參數num_processes默認值爲1,即默認僅開啓一個進程;若是num_processes爲None或者<=0,則自動根據機器硬件的cpu核芯數建立同等數目的子進程;若是num_processes>0,則建立num_processes個子進程。

本例中,咱們使用http_server.start(0),而個人虛擬機設定cpu核數爲2,演示結果: 

咱們在前面寫的http_server.listen(8000)實際上就等同於:

http_server.bind(8000)
http_server.start(1)

說明

1.關於app.listen()

app.listen()這個方法只能在單進程模式中使用

對於app.listen()與手動建立HTTPServer實例

http_server = tornado.httpserver.HTTPServer(app) 
http_server.listen(8000)

 

這兩種方式,建議你們先使用後者即建立HTTPServer實例的方式,由於其對於理解tornado web應用工做流程的完整性有幫助,便於你們記憶tornado開發的模塊組成和程序結構;在熟練使用後,能夠改成簡寫

2.關於多進程

雖然tornado給咱們提供了一次開啓多個進程的方法,可是因爲:

  • 每一個子進程都會從父進程中複製一份IOLoop實例,如過在建立子進程前咱們的代碼動了IOLoop實例,那麼會影響到每個子進程,勢必會干擾到子進程IOLoop的工做;
  • 全部進程是由一個命令一次開啓的,也就沒法作到在不停服務的狀況下更新代碼;
  • 全部進程共享同一個端口,想要分別單獨監控每個進程就很困難。

不建議使用這種多進程的方式,而是手動開啓多個進程,而且綁定不一樣的端口。

2.4 options

在前面的示例中咱們都是將服務端口的參數寫死在程序中,很不靈活。

tornado爲咱們提供了一個便捷的工具,tornado.options模塊——全局參數定義、存儲、轉換

tornado.options.define()

用來定義options選項變量的方法,定義的變量能夠在全局的tornado.options.options中獲取使用,傳入參數:

  • name 選項變量名,須保證全局惟一性,不然會報「Option 'xxx' already defined in ...」的錯誤;
  • default 選項變量的默認值,如不傳默認爲None
  • type 選項變量的類型,從命令行或配置文件導入參數的時候tornado會根據這個類型轉換輸入的值,轉換不成功時會報錯,能夠是str、float、int、datetime、timedelta中的某個,若未設置則根據default的值自動推斷,若default也未設置,那麼再也不進行轉換。能夠經過利用設置type類型字段來過濾不正確的輸入。
  • multiple 選項變量的值是否能夠爲多個,布爾類型,默認值爲False,若是multiple爲True,那麼設置選項變量時值與值之間用英文逗號分隔,而選項變量則是一個list列表(若默認值和輸入均未設置,則爲空列表[])。
  • help 選項變量的幫助提示信息,在命令行啓動tornado時,經過加入命令行參數 --help 能夠查看全部選項變量的信息(注意,代碼中須要加入tornado.options.parse_command_line())。

tornado.options.options

全局的options對象,全部定義的選項變量都會做爲該對象的屬性。

tornado.options.parse_command_line()

轉換命令行參數,並將轉換後的值對應的設置到全局options對象相關屬性上。追加命令行參數的方式是--myoption=myvalue

新建opt.py,咱們用代碼來看一下如何使用:

複製代碼
# coding:utf-8

import tornado.web
import tornado.ioloop
import tornado.httpserver
import tornado.options # 新導入的options模塊

tornado.options.define("port", default=8000, type=int, help="run server on the given port.") # 定義服務器監聽端口選項
tornado.options.define("itcast", default=[], type=str, multiple=True, help="itcast subjects.") # 無心義,演示多值狀況

class IndexHandler(tornado.web.RequestHandler):
    """主路由處理類"""
    def get(self):
        """對應http的get請求方式"""
        self.write("Hello Itcast!")

if __name__ == "__main__":
    tornado.options.parse_command_line()
    print tornado.options.options.itcast # 輸出多值選項
    app = tornado.web.Application([
        (r"/", IndexHandler),
    ])
    http_server = tornado.httpserver.HTTPServer(app)
    http_server.listen(tornado.options.options.port)
    tornado.ioloop.IOLoop.current().start()
複製代碼

執行以下命令開啓程序:

$ python opt.py --port=9000 --itcast=python,c++,java,php,ios

效果以下: 

tornado.options.parse_config_file(path)

從配置文件導入option,配置文件中的選項格式以下:

myoption = "myvalue"
myotheroption = "myothervalue" 

咱們用代碼來看一下如何使用,新建配置文件config,注意字符串和列表按照python的語法格式:

port = 8000
itcast = ["python","c++","java","php","ios"]

修改opt.py文件:

複製代碼
# coding:utf-8

import tornado.web
import tornado.ioloop
import tornado.httpserver
import tornado.options # 新導入的options模塊

tornado.options.define("port", default=8000, type=int, help="run server on the given port.") # 定義服務器監聽端口選項
tornado.options.define("itcast", default=[], type=str, multiple=True, help="itcast subjects.") # 無心義,演示多值狀況

class IndexHandler(tornado.web.RequestHandler):
    """主路由處理類"""
    def get(self):
        """對應http的get請求方式"""
        self.write("Hello Itcast!")

if __name__ == "__main__":
    tornado.options.parse_config_file("./config") # 僅僅修改了此處
    print tornado.options.options.itcast # 輸出多值選項
    app = tornado.web.Application([
        (r"/", IndexHandler),
    ])
    http_server = tornado.httpserver.HTTPServer(app)
    http_server.listen(tornado.options.options.port)
    tornado.ioloop.IOLoop.current().start()
複製代碼

說明

1. 日誌

當咱們在代碼中調用parse_command_line()或者parse_config_file()的方法時,tornado會默認爲咱們配置標準logging模塊,即默認開啓了日誌功能,並向標準輸出(屏幕)打印日誌信息。

若是想關閉tornado默認的日誌功能,能夠在命令行中添加--logging=none 或者在代碼中執行以下操做:

from tornado.options import options, parse_command_line
options.logging = None
parse_command_line()

 

2. 配置文件

咱們看到在使用prase_config_file()的時候,配置文件的書寫格式仍須要按照python的語法要求,其優點是能夠直接將配置文件的參數轉換設置到全局對象tornado.options.options中;然而,其不方便的地方在於須要在代碼中調用tornado.options.define()來定義選項,並且不支持字典類型,故而在實際應用中大都不使用這種方法。

在使用配置文件的時候,一般會新建一個python文件(如config.py),而後在裏面直接定義python類型的變量(能夠是字典類型);在須要配置文件參數的地方,將config.py做爲模塊導入,並使用其中的變量參數。

如config.py文件:

複製代碼
# conding:utf-8

# Redis配置
redis_options = {
    'redis_host':'127.0.0.1',
    'redis_port':6379,
    'redis_pass':'',
}

# Tornado app配置
settings = {
    'template_path': os.path.join(os.path.dirname(__file__), 'templates'),
    'static_path': os.path.join(os.path.dirname(__file__), 'statics'),
    'cookie_secret':'0Q1AKOKTQHqaa+N80XhYW7KCGskOUE2snCW06UIxXgI=',
    'xsrf_cookies':False,
    'login_url':'/login',
    'debug':True,
}

# 日誌
log_path = os.path.join(os.path.dirname(__file__), 'logs/log')
複製代碼

使用config.py的模塊中導入config,以下:

複製代碼
# conding:utf-8

import tornado.web
import config

if __name__ = "__main__":
    app = tornado.web.Application([], **config.settings)
...
複製代碼

2.5 練習

  1. 嘗試解釋清Tornado利用epoll機制實現支持高併發的緣由。

  2. 可以不參考課件熟練默寫出Tornado的基本代碼案例。

3 深刻Tornado

知識點

  • Application設置
  • debug模式
  • 路由設置擴展
  • RequestHandler的使用
    • 輸入方法
    • 輸出方法
    • 可重寫接口

3.1 Application

settings

前面的學習中,咱們在建立tornado.web.Application的對象時,傳入了第一個參數——路由映射列表。實際上Application類的構造函數還接收不少關於tornado web應用的配置參數,在後面的學習中咱們用到的地方會爲你們介紹。

咱們先來學習一個參數:

debug,設置tornado是否工做在調試模式,默認爲False即工做在生產模式。當設置debug=True 後,tornado會工做在調試/開發模式,在此種模式下,tornado爲方便咱們開發而提供了幾種特性:

  • 自動重啓,tornado應用會監控咱們的源代碼文件,當有改動保存後便會重啓程序,這能夠減小咱們手動重啓程序的次數。須要注意的是,一旦咱們保存的更改有錯誤,自動重啓會致使程序報錯而退出,從而須要咱們保存修正錯誤後手動啓動程序。這一特性也可單獨經過autoreload=True設置;
  • 取消緩存編譯的模板,能夠單獨經過compiled_template_cache=False來設置;
  • 取消緩存靜態文件hash值,能夠單獨經過static_hash_cache=False來設置;
  • 提供追蹤信息,當RequestHandler或者其子類拋出一個異常而未被捕獲後,會生成一個包含追蹤信息的頁面,能夠單獨經過serve_traceback=True來設置。

使用debug參數的方法:

import tornado.web
app = tornado.web.Application([], debug=True)

路由映射

先前咱們在構建路由映射列表的時候,使用的是二元元組,如:

[(r"/", IndexHandler),]

對於這個映射列表中的路由,實際上還能夠傳入多個信息,如:

[
    (r"/", Indexhandler),
    (r"/cpp", ItcastHandler, {"subject":"c++"}),
    url(r"/python", ItcastHandler, {"subject":"python"}, name="python_url")
]

對於路由中的字典,會傳入到對應的RequestHandler的initialize()方法中:

複製代碼
from tornado.web import RequestHandler
class ItcastHandler(RequestHandler):
    def initialize(self, subject):
        self.subject = subject

    def get(self):
        self.write(self.subject)
複製代碼

對於路由中的name字段,注意此時不能再使用元組,而應使用tornado.web.url來構建。name是給該路由起一個名字,能夠經過調用RequestHandler.reverse_url(name)來獲取該名子對應的url

複製代碼
# coding:utf-8

import tornado.web
import tornado.ioloop
import tornado.httpserver
import tornado.options
from tornado.options import options, define
from tornado.web import url, RequestHandler

define("port", default=8000, type=int, help="run server on the given port.")

class IndexHandler(RequestHandler):
    def get(self):
        python_url = self.reverse_url("python_url")
        self.write('<a href="%s">itcast</a>' %
                   python_url)

class ItcastHandler(RequestHandler):
    def initialize(self, subject):
        self.subject = subject

    def get(self):
        self.write(self.subject)

if __name__ == "__main__":
    tornado.options.parse_command_line()
    app = tornado.web.Application([
            (r"/", Indexhandler),
            (r"/cpp", ItcastHandler, {"subject":"c++"}),
            url(r"/python", ItcastHandler, {"subject":"python"}, name="python_url")
        ],
        debug = True)
    http_server = tornado.httpserver.HTTPServer(app)
    http_server.listen(options.port)
    tornado.ioloop.IOLoop.current().start()
複製代碼

3.2 輸入

下面幾節主要講解tornado.web.RequestHandler。

回想一下,利用HTTP協議向服務器傳參有幾種途徑?

  • 查詢字符串(query string),形如key1=value1&key2=value2;
  • 請求體(body)中發送的數據,好比表單數據、json、xml
  • 提取uri的特定部分,如/blogs/2016/09/0001,能夠在服務器端的路由中用正則表達式截取;
  • 在http報文的頭(header)中增長自定義字段,如X-XSRFToken=itcast。

咱們如今來看下tornado中爲咱們提供了哪些方法來獲取請求的信息。

1. 獲取查詢字符串參數

從請求的查詢字符串中返回指定參數name的值,若是出現多個同名參數,則返回最後一個的值。

default爲設值未傳name參數時返回的默認值,如若default也未設置,則會拋出tornado.web.MissingArgumentError異常。

strip表示是否過濾掉左右兩邊的空白字符,默認爲過濾。

get_query_arguments(name, strip=True)

從請求的查詢字符串中返回指定參數name的值,注意返回的是list列表(即便對應name參數只有一個值)。若未找到name參數,則返回空列表[]。

strip同前,再也不贅述。

2. 獲取請求體參數

get_body_argument(name, default=_ARG_DEFAULT, strip=True)

從請求體中返回指定參數name的值,若是出現多個同名參數,則返回最後一個的值。

default與strip同前,再也不贅述。

get_body_arguments(name, strip=True)

從請求體中返回指定參數name的值,注意返回的是list列表(即便對應name參數只有一個值)。若未找到name參數,則返回空列表[]。

strip同前,再也不贅述。

說明

對於請求體中的數據要求爲字符串,且格式爲表單編碼格式(與url中的請求字符串格式相同),即key1=value1&key2=value2,HTTP報文頭Header中的"Content-Type"爲application/x-www-form-urlencoded 或 multipart/form-data。對於請求體數據爲json或xml的,沒法經過這兩個方法獲取。

3. 前兩類方法的整合

get_argument(name, default=_ARG_DEFAULT, strip=True)

從請求體和查詢字符串中返回指定參數name的值,若是出現多個同名參數,則返回最後一個的值。

default與strip同前,再也不贅述。

get_arguments(name, strip=True)

從請求體和查詢字符串中返回指定參數name的值,注意返回的是list列表(即便對應name參數只有一個值)。若未找到name參數,則返回空列表[]。

strip同前,再也不贅述。

說明

對於請求體中數據的要求同前。 這兩個方法最經常使用。

用代碼來看上述六中方法的使用:

複製代碼
# coding:utf-8

import tornado.web
import tornado.ioloop
import tornado.httpserver
import tornado.options
from tornado.options import options, define
from tornado.web import RequestHandler, MissingArgumentError

define("port", default=8000, type=int, help="run server on the given port.")

class IndexHandler(RequestHandler):
    def post(self):
        query_arg = self.get_query_argument("a")
        query_args = self.get_query_arguments("a")
        body_arg = self.get_body_argument("a")
        body_args = self.get_body_arguments("a", strip=False)
        arg = self.get_argument("a")
        args = self.get_arguments("a")

        default_arg = self.get_argument("b", "itcast")
        default_args = self.get_arguments("b")

        try:
            missing_arg = self.get_argument("c")
        except MissingArgumentError as e:
            missing_arg = "We catched the MissingArgumentError!"
            print e
        missing_args = self.get_arguments("c")

        rep = "query_arg:%s<br/>" % query_arg
        rep += "query_args:%s<br/>" % query_args 
        rep += "body_arg:%s<br/>"  % body_arg
        rep += "body_args:%s<br/>" % body_args
        rep += "arg:%s<br/>"  % arg
        rep += "args:%s<br/>" % args 
        rep += "default_arg:%s<br/>" % default_arg 
        rep += "default_args:%s<br/>" % default_args 
        rep += "missing_arg:%s<br/>" % missing_arg
        rep += "missing_args:%s<br/>" % missing_args

        self.write(rep)

if __name__ == "__main__":
    tornado.options.parse_command_line()
    app = tornado.web.Application([
        (r"/", IndexHandler),
    ])
    http_server = tornado.httpserver.HTTPServer(app)
    http_server.listen(options.port)
    tornado.ioloop.IOLoop.current().start()
複製代碼

注意:以上方法返回的都是unicode字符串

思考

  1. 何時設置default,何時不設置default?
  2. default的默認值_ARG_DEFAULT是什麼?
  3. 何時使用strip,亦即何時要截斷空白字符,何時不須要?

4. 關於請求的其餘信息

RequestHandler.request 對象存儲了關於請求的相關信息,具體屬性有:

  • method HTTP的請求方式,如GET或POST;
  • host 被請求的主機名;
  • uri 請求的完整資源標示,包括路徑和查詢字符串;
  • path 請求的路徑部分;
  • query 請求的查詢字符串部分;
  • version 使用的HTTP版本;
  • headers 請求的協議頭,是類字典型的對象,支持關鍵字索引的方式獲取特定協議頭信息,例如:request.headers["Content-Type"]
  • body 請求體數據;
  • remote_ip 客戶端的IP地址;
  • files 用戶上傳的文件,爲字典類型,型如:
    {
      "form_filename1":[<tornado.httputil.HTTPFile>, <tornado.httputil.HTTPFile>],
      "form_filename2":[<tornado.httputil.HTTPFile>,],
      ... 
    }

     

    tornado.httputil.HTTPFile是接收到的文件對象,它有三個屬性
    • filename 文件的實際名字,與form_filename1不一樣,字典中的鍵名錶明的是表單對應項的名字;
    • body 文件的數據實體
    • content_type 文件的類型。 這三個對象屬性能夠像字典同樣支持關鍵字索引,如request.files["form_filename1"][0]["body"]。

咱們來實現一個上傳文件並保存在服務器本地的小程序upload.py:

複製代碼
# coding:utf-8
import tornado.web import tornado.ioloop import tornado.httpserver import tornado.options from tornado.options import options, define from tornado.web import RequestHandler define("port", default=8000, type=int, help="run server on the given port.") class IndexHandler(RequestHandler): def get(self): self.write("hello itcast.") class UploadHandler(RequestHandler): def post(self): files = self.request.files img_files = files.get('img') if img_files: img_file = img_files[0]["body"] file = open("./itcast", 'w+') file.write(img_file) file.close() self.write("OK") if __name__ == "__main__": tornado.options.parse_command_line() app = tornado.web.Application([ (r"/", IndexHandler), (r"/upload", UploadHandler), ]) http_server = tornado.httpserver.HTTPServer(app) http_server.listen(options.port) tornado.ioloop.IOLoop.current().start()
複製代碼

 

5. 正則提取uri

tornado中對於路由映射也支持正則提取uri,提取出來的參數會做爲RequestHandler中對應請求方式的成員方法參數。若在正則表達式中定義了名字,則參數按名傳遞;若未定義名字,則參數按順序傳遞。提取出來的參數會做爲對應請求方式的成員方法的參數。

複製代碼
# coding:utf-8

import tornado.web
import tornado.ioloop
import tornado.httpserver
import tornado.options
from tornado.options import options, define
from tornado.web import RequestHandler

define("port", default=8000, type=int, help="run server on the given port.")

class IndexHandler(RequestHandler):
    def get(self):
        self.write("hello itcast.")

class SubjectCityHandler(RequestHandler):
    def get(self, subject, city):
        self.write(("Subject: %s<br/>City: %s" % (subject, city)))

class SubjectDateHandler(RequestHandler):
    def get(self, date, subject):
        self.write(("Date: %s<br/>Subject: %s" % (date, subject)))

if __name__ == "__main__":
    tornado.options.parse_command_line()
    app = tornado.web.Application([
        (r"/", IndexHandler),
        (r"/sub-city/(.+)/([a-z]+)", SubjectCityHandler), # 無名方式
        (r"/sub-date/(?P<subject>.+)/(?P<date>\d+)", SubjectDateHandler), # 命名方式
    ])
    http_server = tornado.httpserver.HTTPServer(app)
    http_server.listen(options.port)
    tornado.ioloop.IOLoop.current().start()
複製代碼

建議:提取多個值時最好用命名方式。

3.3 輸出

1. write(chunk)

將chunk數據寫到輸出緩衝區。如咱們在以前的示例代碼中寫的:

class IndexHandler(RequestHandler):
    def get(self):
        self.write("hello itcast!")

 

想想,可不能夠在同一個處理方法中屢次使用write方法?

下面的代碼會出現什麼效果?

class IndexHandler(RequestHandler):
    def get(self):
        self.write("hello itcast 1!")
        self.write("hello itcast 2!")
        self.write("hello itcast 3!")

write方法是寫到緩衝區的,咱們能夠像寫文件同樣屢次使用write方法不斷追加響應內容,最終全部寫到緩衝區的內容一塊兒做爲本次請求的響應輸出

想想,如何利用write方法寫json數據?

複製代碼
import json

class IndexHandler(RequestHandler):
    def get(self):
        stu = {
            "name":"zhangsan",
            "age":24,
            "gender":1,
        }
        stu_json = json.dumps(stu)
        self.write(stu_json)
複製代碼

實際上,咱們能夠不用本身手動去作json序列化,當write方法檢測到咱們傳入的chunk參數是字典類型後,會自動幫咱們轉換爲json字符串。

複製代碼
class IndexHandler(RequestHandler):
    def get(self):
        stu = {
            "name":"zhangsan",
            "age":24,
            "gender":1,
        }
        self.write(stu)
複製代碼

兩種方式有什麼差別

對比一下兩種方式的響應頭header中Content-Type字段,本身手動序列化時爲Content-Type:text/html; charset=UTF-8,而採用write方法時爲Content-Type:application/json; charset=UTF-8

write方法除了幫咱們將字典轉換爲json字符串以外,還幫咱們將Content-Type設置爲application/json; charset=UTF-8

2. set_header(name, value)

利用set_header(name, value)方法,能夠手動設置一個名爲name、值爲value的響應頭header字段

用set_header方法來完成上面write所作的工做。

複製代碼
import json

class IndexHandler(RequestHandler):
    def get(self):
        stu = {
            "name":"zhangsan",
            "age":24,
            "gender":1,
        }
        stu_json = json.dumps(stu)
        self.write(stu_json)
        self.set_header("Content-Type", "application/json; charset=UTF-8")
複製代碼

3. set_default_headers()

該方法會在進入HTTP處理方法前先被調用,能夠重寫此方法來預先設置默認的headers。注意:在HTTP處理方法中使用set_header()方法會覆蓋掉在set_default_headers()方法中設置的同名header。

複製代碼
class IndexHandler(RequestHandler):
    def set_default_headers(self):
        print "執行了set_default_headers()"
        # 設置get與post方式的默認響應體格式爲json
        self.set_header("Content-Type", "application/json; charset=UTF-8")
        # 設置一個名爲itcast、值爲python的header
        self.set_header("itcast", "python")

    def get(self):
        print "執行了get()"
        stu = {
            "name":"zhangsan",
            "age":24,
            "gender":1,
        }
        stu_json = json.dumps(stu)
        self.write(stu_json)
        self.set_header("itcast", "i love python") # 注意此處重寫了header中的itcast字段

    def post(self):
        print "執行了post()"
        stu = {
            "name":"zhangsan",
            "age":24,
            "gender":1,
        }
        stu_json = json.dumps(stu)
        self.write(stu_json)
複製代碼

 

終端中打印出的執行順序:

get請求方式的響應header:

post請求方式的響應header:

4. set_status(status_code, reason=None)

爲響應設置狀態碼

參數說明:

  • status_code int類型,狀態碼,若reason爲None,則狀態碼必須爲下表中的。
  • reason string類型,描述狀態碼的詞組,若爲None,則會被自動填充爲下表中的內容。
Code Enum Name Details
100 CONTINUE HTTP/1.1 RFC 7231, Section 6.2.1
101 SWITCHING_PROTOCOLS HTTP/1.1 RFC 7231, Section 6.2.2
102 PROCESSING WebDAV RFC 2518, Section 10.1
200 OK HTTP/1.1 RFC 7231, Section 6.3.1
201 CREATED HTTP/1.1 RFC 7231, Section 6.3.2
202 ACCEPTED HTTP/1.1 RFC 7231, Section 6.3.3
203 NON_AUTHORITATIVE_INFORMATION HTTP/1.1 RFC 7231, Section 6.3.4
204 NO_CONTENT HTTP/1.1 RFC 7231, Section 6.3.5
205 RESET_CONTENT HTTP/1.1 RFC 7231, Section 6.3.6
206 PARTIAL_CONTENT HTTP/1.1 RFC 7233, Section 4.1
207 MULTI_STATUS WebDAV RFC 4918, Section 11.1
208 ALREADY_REPORTED WebDAV Binding Extensions RFC 5842, Section 7.1 (Experimental)
226 IM_USED Delta Encoding in HTTP RFC 3229, Section 10.4.1
300 MULTIPLE_CHOICES HTTP/1.1 RFC 7231, Section 6.4.1
301 MOVED_PERMANENTLY HTTP/1.1 RFC 7231, Section 6.4.2
302 FOUND HTTP/1.1 RFC 7231, Section 6.4.3
303 SEE_OTHER HTTP/1.1 RFC 7231, Section 6.4.4
304 NOT_MODIFIED HTTP/1.1 RFC 7232, Section 4.1
305 USE_PROXY HTTP/1.1 RFC 7231, Section 6.4.5
307 TEMPORARY_REDIRECT HTTP/1.1 RFC 7231, Section 6.4.7
308 PERMANENT_REDIRECT Permanent Redirect RFC 7238, Section 3 (Experimental)
400 BAD_REQUEST HTTP/1.1 RFC 7231, Section 6.5.1
401 UNAUTHORIZED HTTP/1.1 Authentication RFC 7235, Section 3.1
402 PAYMENT_REQUIRED HTTP/1.1 RFC 7231, Section 6.5.2
403 FORBIDDEN HTTP/1.1 RFC 7231, Section 6.5.3
404 NOT_FOUND HTTP/1.1 RFC 7231, Section 6.5.4
405 METHOD_NOT_ALLOWED HTTP/1.1 RFC 7231, Section 6.5.5
406 NOT_ACCEPTABLE HTTP/1.1 RFC 7231, Section 6.5.6
407 PROXY_AUTHENTICATION_REQUIRED HTTP/1.1 Authentication RFC 7235, Section 3.2
408 REQUEST_TIMEOUT HTTP/1.1 RFC 7231, Section 6.5.7
409 CONFLICT HTTP/1.1 RFC 7231, Section 6.5.8
410 GONE HTTP/1.1 RFC 7231, Section 6.5.9
411 LENGTH_REQUIRED HTTP/1.1 RFC 7231, Section 6.5.10
412 PRECONDITION_FAILED HTTP/1.1 RFC 7232, Section 4.2
413 REQUEST_ENTITY_TOO_LARGE HTTP/1.1 RFC 7231, Section 6.5.11
414 REQUEST_URI_TOO_LONG HTTP/1.1 RFC 7231, Section 6.5.12
415 UNSUPPORTED_MEDIA_TYPE HTTP/1.1 RFC 7231, Section 6.5.13
416 REQUEST_RANGE_NOT_SATISFIABLE HTTP/1.1 Range Requests RFC 7233, Section 4.4
417 EXPECTATION_FAILED HTTP/1.1 RFC 7231, Section 6.5.14
422 UNPROCESSABLE_ENTITY WebDAV RFC 4918, Section 11.2
423 LOCKED WebDAV RFC 4918, Section 11.3
424 FAILED_DEPENDENCY WebDAV RFC 4918, Section 11.4
426 UPGRADE_REQUIRED HTTP/1.1 RFC 7231, Section 6.5.15
428 PRECONDITION_REQUIRED Additional HTTP Status Codes RFC 6585
429 TOO_MANY_REQUESTS Additional HTTP Status Codes RFC 6585
431 REQUEST_HEADER_FIELDS_TOO_LARGE Additional HTTP Status Codes RFC 6585
500 INTERNAL_SERVER_ERROR HTTP/1.1 RFC 7231, Section 6.6.1
501 NOT_IMPLEMENTED HTTP/1.1 RFC 7231, Section 6.6.2
502 BAD_GATEWAY HTTP/1.1 RFC 7231, Section 6.6.3
503 SERVICE_UNAVAILABLE HTTP/1.1 RFC 7231, Section 6.6.4
504 GATEWAY_TIMEOUT HTTP/1.1 RFC 7231, Section 6.6.5
505 HTTP_VERSION_NOT_SUPPORTED HTTP/1.1 RFC 7231, Section 6.6.6
506 VARIANT_ALSO_NEGOTIATES Transparent Content Negotiation in HTTP RFC 2295, Section 8.1 (Experimental)
507 INSUFFICIENT_STORAGE WebDAV RFC 4918, Section 11.5
508 LOOP_DETECTED WebDAV Binding Extensions RFC 5842, Section 7.2 (Experimental)
510 NOT_EXTENDED An HTTP Extension Framework RFC 2774, Section 7 (Experimental)
511 NETWORK_AUTHENTICATION_REQUIRED Additional HTTP Status Codes RFC 6585, Section 6
複製代碼
class Err404Handler(RequestHandler):
    """對應/err/404"""
    def get(self):
        self.write("hello itcast")
        self.set_status(404) # 標準狀態碼,不用設置reason

class Err210Handler(RequestHandler):
    """對應/err/210"""
    def get(self):
        self.write("hello itcast")
        self.set_status(210, "itcast error") # 非標準狀態碼,設置了reason

class Err211Handler(RequestHandler):
    """對應/err/211"""
    def get(self):
        self.write("hello itcast")
        self.set_status(211) # 非標準狀態碼,未設置reason,錯誤
複製代碼

 

5. redirect(url)

告知瀏覽器跳轉到url

複製代碼
class IndexHandler(RequestHandler):
    """對應/"""
    def get(self):
        self.write("主頁")

class LoginHandler(RequestHandler):
    """對應/login"""
    def get(self):
        self.write('<form method="post"><input type="submit" value="登錄"></form>')

    def post(self):
        self.redirect("/")
複製代碼

6. send_error(status_code=500, **kwargs)

拋出HTTP錯誤狀態碼status_code,默認爲500,kwargs爲可變命名參數。使用send_error拋出錯誤後tornado會調用write_error()方法進行處理,並返回給瀏覽器處理後的錯誤頁面。

class IndexHandler(RequestHandler):
    def get(self):
        self.write("主頁")
        self.send_error(404, content="出現404錯誤")

注意:默認的write\_error()方法不會處理send\_error拋出的kwargs參數,即上面的代碼中content="出現404錯誤"是沒有意義的。

嘗試下面的代碼會出現什麼問題?

class IndexHandler(RequestHandler):
    def get(self):
        self.write("主頁")
        self.send_error(404, content="出現404錯誤")
        self.write("結束") # 咱們在send_error再次向輸出緩衝區寫內容

注意:使用send_error()方法後就不要再向輸出緩衝區寫內容了!

7. write_error(status_code, **kwargs)

用來處理send_error拋出的錯誤信息並返回給瀏覽器錯誤信息頁面。能夠重寫此方法來定製本身的錯誤顯示頁面。

複製代碼
class IndexHandler(RequestHandler):
    def get(self):
        err_code = self.get_argument("code", None) # 注意返回的是unicode字符串,下同
        err_title = self.get_argument("title", "")
        err_content = self.get_argument("content", "")
        if err_code:
            self.send_error(err_code, title=err_title, content=err_content)
        else:
            self.write("主頁")

    def write_error(self, status_code, **kwargs):
        self.write(u"<h1>出錯了,程序員GG正在趕過來!</h1>")
        self.write(u"<p>錯誤名:%s</p>" % kwargs["title"])
        self.write(u"<p>錯誤詳情:%s</p>" % kwargs["content"])
複製代碼

3.4 接口與調用順序

下面的接口方法是由tornado框架進行調用的,咱們能夠選擇性的重寫這些方法。

1. initialize()

對應每一個請求的處理類Handler在構造一個實例後首先執行initialize()方法。在講輸入時提到,路由映射中的第三個字典型參數會做爲該方法的命名參數傳遞,如:

複製代碼
class ProfileHandler(RequestHandler):
    def initialize(self, database):
        self.database = database

    def get(self):
        ...

app = Application([
    (r'/user/(.*)', ProfileHandler, dict(database=database)),
    ])
複製代碼

此方法一般用來初始化參數(對象屬性),不多使用。

2. prepare()

預處理,即在執行對應請求方式的HTTP方法(如get、post等)前先執行,注意:不論以何種HTTP方式請求,都會執行prepare()方法。

以預處理請求體中的json數據爲例:

複製代碼
import json

class IndexHandler(RequestHandler):
    def prepare(self):
        if self.request.headers.get("Content-Type").startswith("application/json"):
            self.json_dict = json.loads(self.request.body)
        else:
            self.json_dict = None

    def post(self):
        if self.json_dict:
            for key, value in self.json_dict.items():
                self.write("<h3>%s</h3><p>%s</p>" % (key, value))

    def put(self):
        if self.json_dict:
            for key, value in self.json_dict.items():
                self.write("<h3>%s</h3><p>%s</p>" % (key, value))
複製代碼

用post方式發送json數據時:

用put方式發送json數據時:

3. HTTP方法

方法 描述
get 請求指定的頁面信息,並返回實體主體。
head 相似於get請求,只不過返回的響應中沒有具體的內容,用於獲取報頭
post 向指定資源提交數據進行處理請求(例如提交表單或者上傳文件)。數據被包含在請求體中。POST請求可能會致使新的資源的創建和/或已有資源的修改。
delete 請求服務器刪除指定的內容。
patch 請求修改局部數據。
put 從客戶端向服務器傳送的數據取代指定的文檔的內容。
options 返回給定URL支持的全部HTTP方法。

4. on_finish()

在請求處理結束後調用,即在調用HTTP方法後調用。一般該方法用來進行資源清理釋放或處理日誌等。注意:請儘可能不要在此方法中進行響應輸出。

5. set_default_headers()

6. write_error()

7. 調用順序

咱們經過一段程序來看上面這些接口的調用順序。

複製代碼
class IndexHandler(RequestHandler):

    def initialize(self):
        print "調用了initialize()"

    def prepare(self):
        print "調用了prepare()"

    def set_default_headers(self):
        print "調用了set_default_headers()"

    def write_error(self, status_code, **kwargs):
        print "調用了write_error()"

    def get(self):
        print "調用了get()"

    def post(self):
        print "調用了post()"
        self.send_error(200)  # 注意此出拋出了錯誤

    def on_finish(self):
        print "調用了on_finish()"
複製代碼

在正常狀況未拋出錯誤時,調用順序爲:

  1. set_defautl_headers()
  2. initialize()
  3. prepare()
  4. HTTP方法
  5. on_finish()

在有錯誤拋出時,調用順序爲:

  1. set_default_headers()
  2. initialize()
  3. prepare()
  4. HTTP方法
  5. set_default_headers()
  6. write_error()
  7. on_finish()
相關文章
相關標籤/搜索