tornado源碼分析系列一

先來看一個簡單的示例:

#!/usr/bin/env  python
#coding:utf8

import socket


def run():
    sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
    sock.bind(('127.0.0.1',8008))
    sock.listen(5)
    
    while True:
        connection,address = sock.accept()
        handle_request(connection)
        connection.close()
    
def handle_request(client):
    but = client.recv(1024)
    client.send("HTTP/1.1 200 OK \r\n\r\n")
    client.send("Hello everyone!")
    
if __name__ == "__main__":
    run()

上述分析:

  1、瀏覽器其實就是一個socket客戶端,而web應用其實就是一個socket服務端,並且web應用在服務器上一直在監聽某個端口。

  2、當瀏覽器請求某個web應用時,需要指定服務器的IP(DNS解析)和端口建立一個socket連接。

  3、建立鏈接後,web應用根據請求的不同,給用戶返回相應的數據。

  4、斷開socket連接。(之所以說http是短鏈接,其實就是因爲每次請求完成後,服務器就會斷開socket鏈接)

  對於Web框架來說,一般分爲兩類,其中一類則是包含上述 4部分 內容的框架,另外一類就是隻包含 第3部分 功能的框架。tornado就是一中屬於前者的框架。tornado 是一個基於 Python 開發的web框架,較其他 Web 框架的區別是:採用了非阻塞的方式和對epoll的應用。這意味着對於實時 Web 服務來說,Tornado 是一個理想的 Web 框架。


tornado經典官網DEMO

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()

整個過程其實就是在創建一個socket服務端並監聽8888端口,當請求到來時,根據請求中的url和請求方式(post、get或put等)來指定相應的類中的方法來處理本次請求,在上述demo中只爲url爲http://127.0.0.1:8888/index的請求指定了處理類MainHandler。所以,在瀏覽器上訪問:http://127.0.0.1:8888/index,則服務器給瀏覽器就會返回 Hello,world ,否則返回 404: Not Found(tornado內部定義的值), 即完成一次http請求和響應。


tornado處理請求的步驟大體可以分爲兩個部分:啓動程序階段和處理請求階段

1、在啓動程序階段,第一步,獲取配置文件然後生成url映射(即:一個url對應一個XXRequestHandler,從而讓XXRequestHandler來處理指定url發送的請求);第二步,創建服務器socket對象並添加到epoll中;第三步,創建無線循環去監聽epoll。

2、在接收並處理請求階段,第一步,接收客戶端socket發送的請求(socket.accept);第二步,從請求中獲取請求頭信息,再然後根據請求頭中的請求url去匹配某個XXRequestHandler;第三步,匹配成功的XXRequestHandler處理請求;第四步,將處理後的請求發送給客戶端;第五步,關閉客戶端socket。

啓動程序階段:

    1、執行Application類的構造函數,並傳入一個列表類型的參數,這個列表裏保存的是url規則和對應的處理類,即:當客戶端的請求url可以配置這個規則時,那麼該請求就交由對應的Handler(繼承自RequestHandler的所有類)去執行。


Application.__init__:

class Application(object):    
    def __init__(self, handlers=None, default_host="", transforms=None,wsgi=False, **settings):        
    #設置響應的編碼和返回方式,對應的http相應頭:Content-Encoding和Transfer-Encoding
        #Content-Encoding:gzip 表示對數據進行壓縮,然後再返回給用戶,從而減少流量的傳輸。
        #Transfer-Encoding:chunck 表示數據的傳送方式通過一塊一塊的傳輸。
        if transforms is None:
            self.transforms = []            
            if settings.get("gzip"):
                self.transforms.append(GZipContentEncoding)
            self.transforms.append(ChunkedTransferEncoding)        
        else:
            self.transforms = transforms        #將參數賦值爲類的變量
        self.handlers = []
        self.named_handlers = {}
        self.default_host = default_host
        self.settings = settings        
        #ui_modules和ui_methods用於在模版語言中擴展自定義輸出
        #這裏將tornado內置的ui_modules和ui_methods添加到類的成員變量self.ui_modules和self.ui_methods中
        self.ui_modules = {'linkify': _linkify,                           
                           'xsrf_form_html': _xsrf_form_html,                           
                           'Template': TemplateModule,
                           }
        self.ui_methods = {}
        self._wsgi = wsgi        
        #獲取獲取用戶自定義的ui_modules和ui_methods,並將他們添加到之前創建的成員變量self.ui_modules和self.ui_methods中
        self._load_ui_modules(settings.get("ui_modules", {}))
        self._load_ui_methods(settings.get("ui_methods", {}))        
        
        #設置靜態文件路徑,設置方式則是通過正則表達式匹配url,讓StaticFileHandler來處理匹配的url
        if self.settings.get("static_path"):            
            #從settings中讀取key爲static_path的值,用於設置靜態文件路徑
            path = self.settings["static_path"]            
            #獲取參數中傳入的handlers,如果空則設置爲空列表
            handlers = list(handlers or [])            
            #靜態文件前綴,默認是/static/
            static_url_prefix = settings.get("static_url_prefix","/static/")            
            #在參數中傳入的handlers前再添加三個映射:
            #【/static/.*】            -->  StaticFileHandler
            #【/(favicon\.ico)】    -->  StaticFileHandler
            #【/(robots\.txt)】        -->  StaticFileHandler
            handlers = [
                (re.escape(static_url_prefix) + r"(.*)", StaticFileHandler,dict(path=path)),
                (r"/(favicon\.ico)", StaticFileHandler, dict(path=path)),
                (r"/(robots\.txt)", StaticFileHandler, dict(path=path)),
            ] + handlers        
        #執行本類的Application的add_handlers方法
        #此時,handlers是一個列表,其中的每個元素都是一個對應關係,即:url正則表達式和處理匹配該正則的url的Handler
        if handlers: self.add_handlers(".*$", handlers)        
        # Automatically reload modified modules
        #如果settings中設置了 debug 模式,那麼就使用自動加載重啓
        if self.settings.get("debug") and not wsgi:            
            import autoreload
            autoreload.start()

    2、Application.add_Handlers:

class Application(object):    
    def add_handlers(self, host_pattern, host_handlers):        
        #如果主機模型最後沒有結尾符,那麼就爲他添加一個結尾符。
        if not host_pattern.endswith("$"):
            host_pattern += "$"
        handlers = []        
        #對主機名先做一層路由映射,例如:http://www.5ihouse.com 和 http://safe.5ihouse.com
        #即:safe對應一組url映射,www對應一組url映射,那麼當請求到來時,先根據它做第一層匹配,之後再繼續進入內部匹配。

        #對於第一層url映射來說,由於.*會匹配所有的url,所將 .* 的永遠放在handlers列表的最後,不然 .* 就會先匹配了...
        #re.complie是編譯正則表達式,以後請求來的時候只需要執行編譯結果的match方法就可以去匹配了
        if self.handlers and self.handlers[-1][0].pattern == '.*$':
            self.handlers.insert(-1, (re.compile(host_pattern), handlers))        
        else:
            self.handlers.append((re.compile(host_pattern), handlers))        
        #遍歷我們設置的和構造函數中添加的【url->Handler】映射,將url和對應的Handler封裝到URLSpec類中(構造函數中會對url進行編譯)
        #並將所有的URLSpec對象添加到handlers列表中,而handlers列表和主機名模型組成一個元祖,添加到self.Handlers列表中。
        for spec in host_handlers:            
            if type(spec) is type(()):                
                assert len(spec) in (2, 3)
                pattern = spec[0]
                handler = spec[1]                
                if len(spec) == 3:
                    kwargs = spec[2]                
                else:
                    kwargs = {}
                spec = URLSpec(pattern, handler, kwargs)
            handlers.append(spec)            
            if spec.name:                
            #未使用該功能,默認spec.name = None
                if spec.name in self.named_handlers:
                    logging.warning("Multiple handlers named %s; replacing previous value",spec.name)
                self.named_handlers[spec.name] = spec

    3、URLspec:

class URLSpec(object):    
    def __init__(self, pattern, handler_class, kwargs={}, name=None):        
        if not pattern.endswith('$'):
            pattern += '$'
        self.regex = re.compile(pattern)
        self.handler_class = handler_class
        self.kwargs = kwargs
        self.name = name
        self._path, self._group_count = self._find_groups()

上述三個步驟主要完成了:

    1、靜態文件路徑設置

    2、ui_modules和ui_methods(模板語言中使用)

    3、是否debug模式運行

    4、生成URL映射 

    5、封裝數據,將配置信息和url映射關係封裝到Application對象中

     6、保存編碼和返回方式信息(self.transforms)

    7、self.settings 保存配置信息

      8、self.Handler 保存着所有的主機名對應的Handlers,每個handlers則是url正則對應的Handler


以上爲application = tornado.web.Application([(r"/index",MainHandler),])完成的工作


   4、application.listen(8888)

    1-3步的操作將配置和url映射等信息封裝到了application對象中,而這第二步執行application對象的listen方法,該方法內部又把之前包含各種信息的application對象封裝到了一個HttpServer對象中,然後繼續調用HttpServer對象的liseten方法。

class Application(object):    
    #創建服務端socket,並綁定IP和端口並添加相應設置,注:未開始通過while監聽accept,等待客戶端連接    
    def listen(self, port, address="", **kwargs):
        from tornado.httpserver import HTTPServer
        server = HTTPServer(self, **kwargs)
        server.listen(port, address)

    5、HTTPServer類代碼

class HTTPServer(object):    
    def __init__(self, request_callback, no_keep_alive=False, io_loop=None,xheaders=False, ssl_options=None):        
        #Application對象
        self.request_callback = request_callback        
        #是否長連接
        self.no_keep_alive = no_keep_alive        
        #IO循環
        self.io_loop = io_loop
        self.xheaders = xheaders        
        #Http和Https
        self.ssl_options = ssl_options
        self._socket = None
        self._started = False    
    def listen(self, port, address=""):
        self.bind(port, address)
        self.start(1)    
    def bind(self, port, address=None, family=socket.AF_UNSPEC):        
        assert not self._socket        
        #創建服務端socket對象,IPV4和TCP連接
        self._socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM, 0)
        flags = fcntl.fcntl(self._socket.fileno(), fcntl.F_GETFD)
        flags |= fcntl.FD_CLOEXEC
        fcntl.fcntl(self._socket.fileno(), fcntl.F_SETFD, flags)        
        #配置socket對象
        self._socket.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
        self._socket.setblocking(0)        
        #綁定IP和端口        
        self._socket.bind((address, port))        
        #最大阻塞數量
        self._socket.listen(128)    
    def start(self, num_processes=1):        
        assert not self._started
        self._started = True        
        if num_processes is None or num_processes <= 0:
            num_processes = _cpu_count()        
        if num_processes > 1 and ioloop.IOLoop.initialized():
            logging.error("Cannot run in multiple processes: IOLoop instance "
                          "has already been initialized. You cannot call "
                          "IOLoop.instance() before calling start()")
            num_processes = 1        
        #如果進程數大於1
        if num_processes > 1:
            logging.info("Pre-forking %d server processes", num_processes)            
            for i in range(num_processes):                
                if os.fork() == 0:                    
                    import random                    
                    from binascii import hexlify                    
                    try:                        
                    # If available, use the same method as
                        # random.py
                        seed = long(hexlify(os.urandom(16)), 16)                    
                    except NotImplementedError:                        
                        # Include the pid to avoid initializing two
                        # processes to the same value
                        seed(int(time.time() * 1000) ^ os.getpid())
                    random.seed(seed)
                    self.io_loop = ioloop.IOLoop.instance()
                    self.io_loop.add_handler(
                        self._socket.fileno(), self._handle_events,
                        ioloop.IOLoop.READ)                    
                    return
            os.waitpid(-1, 0)        
        #進程數等於1,默認
        else:            
            if not self.io_loop:                
                #設置成員變量self.io_loop爲IOLoop的實例,注:IOLoop使用methodclass完成了一個單例模式
                self.io_loop = ioloop.IOLoop.instance()            
                #執行IOLoop的add_handler方法,將socket句柄、self._handle_events方法和IOLoop.READ當參數傳入            
                self.io_loop.add_handler(self._socket.fileno(),
                                     self._handle_events,
                                     ioloop.IOLoop.READ)    
     def _handle_events(self, fd, events):        
         while True:            
             try:                
                 #====important=====#
                 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:                    
                    #====important=====#
                    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)                
                    #====important=====#                
                    HTTPConnection(stream, address, self.request_callback,self.no_keep_alive, self.xheaders) 
            except:
                logging.error("Error in connection callback", exc_info=True)

    6、IOloop類代碼:

class IOLoop(object):    
    # Constants from the epoll module
    _EPOLLIN = 0x001
    _EPOLLPRI = 0x002
    _EPOLLOUT = 0x004
    _EPOLLERR = 0x008
    _EPOLLHUP = 0x010
    _EPOLLRDHUP = 0x2000
    _EPOLLONESHOT = (1 << 30)
    _EPOLLET = (1 << 31)    
    # Our events map exactly to the epoll events
    NONE = 0
    READ = _EPOLLIN
    WRITE = _EPOLLOUT
    ERROR = _EPOLLERR | _EPOLLHUP | _EPOLLRDHUP    
    def __init__(self, impl=None):
        self._impl = impl or _poll()        
        if hasattr(self._impl, 'fileno'):
            self._set_close_exec(self._impl.fileno())
        self._handlers = {}
        self._events = {}
        self._callbacks = []
        self._timeouts = []
        self._running = False
        self._stopped = False
        self._blocking_signal_threshold = None        
        # Create a pipe that we send bogus data to when we want to wake
        # the I/O loop when it is idle
        if os.name != 'nt':  
            r, w = os.pipe()
            self._set_nonblocking(r)
            self._set_nonblocking(w)
            self._set_close_exec(r)
            self._set_close_exec(w)
            self._waker_reader = os.fdopen(r, "rb", 0)
            self._waker_writer = os.fdopen(w, "wb", 0)        
        else:
            self._waker_reader = self._waker_writer = win32_support.Pipe()
            r = self._waker_writer.reader_fd
        self.add_handler(r, self._read_waker, self.READ)

    @classmethod    
    def instance(cls):        
        if not hasattr(cls, "_instance"):   #單例模式
            cls._instance = cls()        
        return cls._instance        
    def add_handler(self, fd, handler, events):        
    """Registers the given handler to receive the given events for fd."""
        self._handlers[fd] = stack_context.wrap(handler)
        self._impl.register(fd, events | self.ERROR)

上述代碼本質上就幹了以下這麼四件事:

  1. 把包含了各種配置信息的application對象封裝到了HttpServer對象的request_callback字段中

  2. 創建了服務端socket對象

  3. 單例模式創建IOLoop對象,然後將socket對象句柄作爲key,被封裝了的函數_handle_events作爲value,添加到IOLoop對象的_handlers字段中

  4. 向epoll中註冊監聽服務端socket對象的讀可用事件

目前,我們只是看到上述代碼大致幹了這四件事,而其目的有什麼?他們之間的聯繫又是什麼呢?

答:現在不妨先來做一個猜想,待之後再在源碼中確認驗證是否正確!猜想:通過epoll監聽服務端socket事件,一旦請求到達時,則執行3中被封裝了的_handle_events函數,該函數又利用application中封裝了的各種配置信息對客戶端url來指定判定,然後指定對應的Handler處理該請求。


發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章