开发者社区> 科技小能手> 正文

tornado源码分析系列一

简介:
+关注继续查看

先来看一个简单的示例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
#!/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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
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__:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
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:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
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 (23)
                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:

1
2
3
4
5
6
7
8
9
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方法。

1
2
3
4
5
6
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类代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
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 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(-10)        
        #进程数等于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[0in (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类代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
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()
            = 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处理该请求。


本文转自 AltBoy 51CTO博客,原文链接:http://blog.51cto.com/altboy/1958775


版权声明:本文内容由阿里云实名注册用户自发贡献,版权归原作者所有,阿里云开发者社区不拥有其著作权,亦不承担相应法律责任。具体规则请查看《阿里云开发者社区用户服务协议》和《阿里云开发者社区知识产权保护指引》。如果您发现本社区中有涉嫌抄袭的内容,填写侵权投诉表单进行举报,一经查实,本社区将立刻删除涉嫌侵权内容。

相关文章
阿里云服务器如何登录?阿里云服务器的三种登录方法
购买阿里云ECS云服务器后如何登录?场景不同,大概有三种登录方式:
9614 0
使用SSH远程登录阿里云ECS服务器
远程连接服务器以及配置环境
13343 0
如何设置阿里云服务器安全组?阿里云安全组规则详细解说
阿里云安全组设置详细图文教程(收藏起来) 阿里云服务器安全组设置规则分享,阿里云服务器安全组如何放行端口设置教程。阿里云会要求客户设置安全组,如果不设置,阿里云会指定默认的安全组。那么,这个安全组是什么呢?顾名思义,就是为了服务器安全设置的。安全组其实就是一个虚拟的防火墙,可以让用户从端口、IP的维度来筛选对应服务器的访问者,从而形成一个云上的安全域。
17110 0
阿里云服务器如何登录?阿里云服务器的三种登录方法
购买阿里云ECS云服务器后如何登录?场景不同,阿里云优惠总结大概有三种登录方式: 登录到ECS云服务器控制台 在ECS云服务器控制台用户可以更改密码、更换系.
25025 0
阿里云服务器如何登录?阿里云服务器的三种登录方法
购买阿里云ECS云服务器后如何登录?场景不同,云吞铺子总结大概有三种登录方式: 登录到ECS云服务器控制台 在ECS云服务器控制台用户可以更改密码、更换系统盘、创建快照、配置安全组等操作如何登录ECS云服务器控制台? 1、先登录到阿里云ECS服务器控制台 2、点击顶部的“控制台” 3、通过左侧栏,切换到“云服务器ECS”即可,如下图所示 通过ECS控制台的远程连接来登录到云服务器 阿里云ECS云服务器自带远程连接功能,使用该功能可以登录到云服务器,简单且方便,如下图:点击“远程连接”,第一次连接会自动生成6位数字密码,输入密码即可登录到云服务器上。
33250 0
阿里云服务器ECS登录用户名是什么?系统不同默认账号也不同
阿里云服务器Windows系统默认用户名administrator,Linux镜像服务器用户名root
13876 0
阿里云服务器端口号设置
阿里云服务器初级使用者可能面临的问题之一. 使用tomcat或者其他服务器软件设置端口号后,比如 一些不是默认的, mysql的 3306, mssql的1433,有时候打不开网页, 原因是没有在ecs安全组去设置这个端口号. 解决: 点击ecs下网络和安全下的安全组 在弹出的安全组中,如果没有就新建安全组,然后点击配置规则 最后如上图点击添加...或快速创建.   have fun!  将编程看作是一门艺术,而不单单是个技术。
18562 0
腾讯云服务器 设置ngxin + fastdfs +tomcat 开机自启动
在tomcat中新建一个可以启动的 .sh 脚本文件 /usr/local/tomcat7/bin/ export JAVA_HOME=/usr/local/java/jdk7 export PATH=$JAVA_HOME/bin/:$PATH export CLASSPATH=.
13847 0
23705
文章
0
问答
文章排行榜
最热
最新
相关电子书
更多
JS零基础入门教程(上册)
立即下载
性能优化方法论
立即下载
手把手学习日志服务SLS,云启实验室实战指南
立即下载