tornado高效開發必備之源碼詳解

前言:本博文重在tornado源碼剖析,相信讀者讀完此文可以更加深刻的瞭解tornado的運行機制,從而更加高效的使用tornado框架。javascript

本文參考武sir博客地址:http://www.cnblogs.com/wupeiqi/tag/Tornado/css

初識tornado

首先咱們從經典的hello world 案例入手:html

import tornado.ioloop
import tornado.web
 
class MainHandler(tornado.web.RequestHandler):
    def get(self):
        self.write("Hello, world")
 
application = tornado.web.Application([
    (r"/index", MainHandler),
])
 
if __name__ == "__main__":
    application.listen(8888)
    tornado.ioloop.IOLoop.instance().start()

運行該腳本,依次執行:前端

  • 建立一個Application對象,並把一個正則表達式'/'和類名MainHandler傳入構造函數:tornado.web.Application(...)  
  • 執行Application對象的listen(...)方法,即:application.listen(8888)
  • 執行IOLoop類的類的 start() 方法,即:tornado.ioloop.IOLoop.instance().start()

1.首先咱們來分析以下代碼:java

application = tornado.web.Application([
    (r"/index", MainHandler),
])

  源碼截圖:python

  由上圖Application類的源碼咱們能夠看出,須要傳入的第一個參數是handlers,即上述代碼能夠進行以下表示:ios

application = tornado.web.Application(handlers=[
    (r"/index", MainHandler),
])

  這裏的參數handlers很是重要,值得咱們更加深刻的研究。它應該是一個元組組成的列表,其中每一個元組的第一個元素是一個用於匹配的正則表達式,第二個元素是一個RequestHanlder類。在hello.py中,咱們只指定了一個正則表達式-RequestHanlder對,但你能夠按你的須要指定任意多個。web

2.Application類的深刻:
源碼截圖:

這是源碼中關於靜態文件路徑的描述,從上到下的意思依次爲:正則表達式

  • 若是用戶在settings有配置名爲「static_path」的文件路徑(這就要求咱們若是須要配置靜態文件路徑,則key值必須是「static_path」)
  • 從源碼可看出,這是根據字典的方式進行取值,所以可判定settings是字典格式
  • ,這是配置靜態文件路徑前綴,便於tronado在前端的靜態文件路徑中找到靜態文件,即告訴tronado,我是靜態文件,按照靜態文件路徑查詢便可,這裏,靜態文件前綴也能夠理解爲靜態文件標識。

     

3.application.listen(8888):json

 application.listen(8888)

 即Application類的listen方法:

源碼截圖:

從上述源碼可看出listen方法接收port端口,address即ip地址,默認爲空。而後實例化HTTPServer對象,執行listen方法:

HTTPServer類listen方法源碼截圖:

上述listen方法綁定了ip和端口,並開啓socket。

bind方法源碼截圖:

 

由上述源碼可看出,bind方法內部建立socket對象,調用socket對象綁定ip和端口,並進行監聽。

4.tornado.ioloop.IOLoop.instance().start():

tornado.ioloop.IOLoop.instance().start()

  這是tronado的ioloop.py文件中的IOLoop類:

instance方法源碼截圖:

這裏使用了類方法,便可以經過類名直接訪問。咱們須要關注的是最後面的代碼:

if not hasattr(cls, "_instance"):
            cls._instance = cls()
        return cls._instance

 注:這裏使用了單例模式,由於咱們不須要爲每一次鏈接IO都建立一個對象,換句話說,每次鏈接IO只須要是同一個對象便可

start方法:

class IOLoop(object):
    def add_handler(self, fd, handler, events):
        #HttpServer的Start方法中會調用該方法
        self._handlers[fd] = stack_context.wrap(handler)
        self._impl.register(fd, events | self.ERROR)
         
    def start(self):
        while True:
            poll_timeout = 0.2
            try:
                #epoll中輪詢
                event_pairs = self._impl.poll(poll_timeout)
            except Exception, e:
                #省略其餘
            #若是有讀可用信息,則把該socket對象句柄和Event Code序列添加到self._events中
            self._events.update(event_pairs)
            #遍歷self._events,處理每一個請求
            while self._events:
                fd, events = self._events.popitem()
                try:
                    #以socket爲句柄爲key,取出self._handlers中的stack_context.wrap(handler),並執行
                    #stack_context.wrap(handler)包裝了HTTPServer類的_handle_events函數的一個函數
                    #是在上一步中執行add_handler方法時候,添加到self._handlers中的數據。
                    self._handlers[fd](fd, events)
                except:
                    #省略其餘

  由上述源碼中while Ture:能夠看出當執行了start方法後程序會進入死循環,不斷檢測是否有用戶發送請求過來,若是有請求到達,則執行封裝了HttpServer類的_handle_events方法和相關上下文的stack_context.wrap(handler)(其實就是執行HttpServer類的_handle_events方法),詳細見下篇博文,簡要代碼以下:

class HTTPServer(object):
    def _handle_events(self, fd, events):
        while True:
            try:
                connection, address = self._socket.accept()
            except socket.error, e:
                if e.args[0] in (errno.EWOULDBLOCK, errno.EAGAIN):
                    return
                raise
            if self.ssl_options is not None:
                assert ssl, "Python 2.6+ and OpenSSL required for SSL"
                try:
                    connection = ssl.wrap_socket(connection,
                                                 server_side=True,
                                                 do_handshake_on_connect=False,
                                                 **self.ssl_options)
                except ssl.SSLError, err:
                    if err.args[0] == ssl.SSL_ERROR_EOF:
                        return connection.close()
                    else:
                        raise
                except socket.error, err:
                    if err.args[0] == errno.ECONNABORTED:
                        return connection.close()
                    else:
                        raise
            try:
                if self.ssl_options is not None:
                    stream = iostream.SSLIOStream(connection, io_loop=self.io_loop)
                else:
                    stream = iostream.IOStream(connection, io_loop=self.io_loop)
                HTTPConnection(stream, address, self.request_callback,
                               self.no_keep_alive, self.xheaders)
            except:
                logging.error("Error in connection callback", exc_info=True)

5.tornado.web.RequestHandler

這是全部業務處理handler須要繼承的父類,接下來,介紹一些RequestHandler類中經常使用的一些方法:

 

  •  initialize:

從源碼中能夠看出initialize函數會在RequestHandler類初始化的時候執行,可是源碼中initialize函數並無作任何事情,這實際上是tornado爲咱們預留的修改源碼的地方,這就容許程序在執行全部的handler前首先執行咱們在initialize中定義的方法。

  • write

源碼截圖:

write方法是後端向前端頁面直接展現的方法。從上述源碼中能夠看出,write方法接收字典和字符串類型的參數,若是用戶傳來的數據是字典類型,源碼中會自動用json對字典進行序列化,最終序列化成字符串。

self._write_buffer是源碼中定義的一個臨時存放須要輸出的字符串的地方,是列表格式。

  • 須要write的內容添加到 self._write_buffer後,系統會執行flush方法:

flush方法源碼截圖:

 

 由上述源碼能夠看出:flush方法會self._write_buffer列表中的全部元素拼接成字符串,並賦值給chunk,而後清空self._write_buffer列表,而後設置請求頭,最終調用request.write方法在前端頁面顯示。

  • render方法:

源碼截圖:

由上述源碼可看出render方法是根據參數渲染模板,下面咱們來介紹具體源碼中是如何渲染的:

js和css部分的源碼截圖:

由上述源碼可看出,靜態文件(以JavaScript爲例,css是相似的)的渲染流程是:

首先經過module.embedded_javascript() 獲取須要插入JavaScript字符串,添加到js_embed 列表中;

進而經過module.javascript_files()獲取已有的列表格式的JavaScript files,最終將它加入js_files.

下面對js_embed和js_files作進一步介紹:

js_embed源碼截圖:

上圖源碼即生成script標籤,這是一些咱們本身定義的一些JavaScript代碼;最終是經過字符串拼接方式插入到整個html中。

js_files源碼截圖:

 

 

上圖源碼即生成script標籤,這是一些須要引入的JavaScript代碼塊;最終是經過字符串拼接方式插入到整個html中。須要注意的是:其中靜態路徑是調用self.static_url(path)實現的。

static_url方法源碼截圖:

由上述代碼可看出:源碼首先會判斷用戶有沒有設置靜態路徑的前綴,而後將靜態路徑與相對路徑進行拼接成絕對路徑,接下來按照絕對路徑打開文件,並對文件內容(f.read())作md5加密,最終將根目錄+靜態路徑前綴+相對路徑拼接在前端html中展現。

 

render_string方法:

這是render方法中最重要的一個子方法,它負責去處理Html模板並返回最終結果:

詳細流程:

 

    1. 建立Loader對象,並執行load方法
          -- 經過open函數打開html文件並讀取內容,並將內容做爲參數又建立一個 Template 對象
          -- 當執行Template的 __init__ 方法時,根據模板語言的標籤 {{}}、{%%}等分割並html文件,最後生成一個字符串表示的函數
    2. 獲取全部要嵌入到html模板中的變量,包括:用戶返回和框架默認
    3. 執行Template對象的generate方法
          -- 編譯字符串表示的函數,並將用戶定義的值和框架默認的值做爲全局變量
          -- 執行被編譯的函數獲取被嵌套了數據的內容,而後將內容返回(用於響應給請求客戶端)

 

源碼註釋:

class RequestHandler(object):
 
    def render_string(self, template_name, **kwargs):
         
        #獲取配置文件中指定的模板文件夾路徑,即:template_path = 'views'
        template_path = self.get_template_path()
 
        #若是沒有配置模板文件的路徑,則默認去啓動程序所在的目錄去找
        if not template_path:
            frame = sys._getframe(0)
            web_file = frame.f_code.co_filename
            while frame.f_code.co_filename == web_file:
                frame = frame.f_back
            template_path = os.path.dirname(frame.f_code.co_filename)
        if not getattr(RequestHandler, "_templates", None):
            RequestHandler._templates = {}
         
        #建立Loader對象,第一次建立後,會將該值保存在RequestHandler的靜態字段_template_loaders中
        if template_path not in RequestHandler._templates:
            loader = self.application.settings.get("template_loader") or\
              template.Loader(template_path)
            RequestHandler._templates[template_path] = loader
 
        #執行Loader對象的load方法,該方法內部執行執行Loader的_create_template方法
        #在_create_template方法內部使用open方法會打開html文件並讀取html的內容,而後將其做爲參數來建立一個Template對象
        #Template的構造方法被執行時,內部解析html文件的內容,並根據內部的 {{}} {%%}標籤對內容進行分割,最後生成一個字符串類表示的函數並保存在self.code字段中
        t = RequestHandler._templates[template_path].load(template_name)
         
        #獲取全部要嵌入到html中的值和框架默認提供的值
        args = dict(
            handler=self,
            request=self.request,
            current_user=self.current_user,
            locale=self.locale,
            _=self.locale.translate,
            static_url=self.static_url,
            xsrf_form_html=self.xsrf_form_html,
            reverse_url=self.application.reverse_url
        )
        args.update(self.ui)
        args.update(kwargs)
 
        #執行Template的generate方法,編譯字符串表示的函數並將namespace中的全部key,value設置成全局變量,而後執行該函數。從而將值嵌套進html並返回。
        return t.generate(**args) 
class Loader(object):
    """A template loader that loads from a single root directory.

    You must use a template loader to use template constructs like
    {% extends %} and {% include %}. Loader caches all templates after
    they are loaded the first time.
    """
    def __init__(self, root_directory):
        self.root = os.path.abspath(root_directory)
        self.templates = {}

Loader.__init__
Loader.__init__
class Loader(object):
    def load(self, name, parent_path=None):
        name = self.resolve_path(name, parent_path=parent_path)
        if name not in self.templates:
            path = os.path.join(self.root, name)
            f = open(path, "r")
            #讀取html文件的內容
            #建立Template對象
            #name是文件名
            self.templates[name] = Template(f.read(), name=name, loader=self)
            f.close()
        return self.templates[name]

Loader.load
Loader.load
class Template(object):

    def __init__(self, template_string, name="<string>", loader=None,compress_whitespace=None):
        # template_string是Html文件的內容

        self.name = name
        if compress_whitespace is None:
            compress_whitespace = name.endswith(".html") or name.endswith(".js")
        
        #將內容封裝到_TemplateReader對象中,用於以後根據模板語言的標籤分割html文件
        reader = _TemplateReader(name, template_string)
        
        #分割html文件成爲一個一個的對象
        #執行_parse方法,將html文件分割成_ChunkList對象
        self.file = _File(_parse(reader))
        
        #將html內容格式化成字符串表示的函數
        self.code = self._generate_python(loader, compress_whitespace)
        
        try:
            #將字符串表示的函數編譯成函數
            self.compiled = compile(self.code, self.name, "exec")
            
        except:
            formatted_code = _format_code(self.code).rstrip()
            logging.error("%s code:\n%s", self.name, formatted_code)
            raise

Template.__init__
Template.__init__
class Template(object):
    def generate(self, **kwargs):
        """Generate this template with the given arguments."""
        namespace = {
            "escape": escape.xhtml_escape,
            "xhtml_escape": escape.xhtml_escape,
            "url_escape": escape.url_escape,
            "json_encode": escape.json_encode,
            "squeeze": escape.squeeze,
            "linkify": escape.linkify,
            "datetime": datetime,
        }

        #建立變量環境並執行函數,詳細Demo見上一篇博文
        namespace.update(kwargs)
        exec self.compiled in namespace
        execute = namespace["_execute"]

        try:
            #執行編譯好的字符串格式的函數,獲取嵌套了值的html文件
            return execute()
        except:
            formatted_code = _format_code(self.code).rstrip()
            logging.error("%s code:\n%s", self.name, formatted_code)
            raise

Template.generate
Template.generate

示例html:

源碼模板語言處理部分的截圖:

結束語

  本博文從tornado  url正則匹配、路由與映射、底層socket實現、端口監聽、多請求併發、Handler類方法的源碼剖析,便於tronado web開發人員可以更深刻的理解tronado的運行機制,從而更高效的從事tronado web開發。

若是您以爲本文對您有參考價值,歡迎幫博主點擊文章下方的推薦,謝謝!

相關文章
相關標籤/搜索