Tornado核心框架

Yeolar   2013-02-09 13:51  

Tornado源码剖析 Tornado异步网络访问

Tornado是一个基于 epoll 的异步WEB框架,采用Python语言实现。有关它的介绍可以参见 Tornado概览深入理解Tornado 两篇文章。本文和接下来的三篇文章基于 tornado-2.4.1 深入分析了它的框架和实现。

Tornado的核心框架借鉴了 web.py 的实现,非常简洁优雅。此外,它还实现了一个同样简洁的模板框架。Tornado在自己的异步网络访问框架之上完整地实现了HTTP服务器,这也是它的核心框架的重要部分。

Tornado设计模型概览

在深入到模块进行分析之前,首先来看看Tornado的设计模型。

/media/note/2013/02/09/tornado-core/fig1.png

Tornado框架设计模型

Tornado不仅仅是一个WEB框架,它还完整地实现了HTTP服务器和客户端,在此基础上提供WEB服务。它可以分为四层:最底层的EVENT层处理IO事件;TCP层实现了TCP服务器,负责数据传输;HTTP/HTTPS层基于HTTP协议实现了HTTP服务器和客户端;最上层为WEB框架,包含了处理器、模板、数据库连接、认证、本地化等等WEB框架需要具备的功能。

tornado.web - RequestHandlerApplication

关键字

threading.Lock, httplib.responses, calendar.timegm, datetime.utctimetuple, email.utils.formatdate, email.utils.parsedate, re, RegexObject.pattern, RegexObject.groupindex, MatchObject.groupdict, Cookie.SimpleCookie, Cookie.Morsel.OutputString, datetime.utcnow, urlparse.urljoin, urlparse.urlsplit, urllib.urlencode, sys.exc_info, sys._getframe, frame.f_code.co_filename, frame.f_back, inspect, traceback.format_exception, uuid.uuid4, binascii.b2a_hex, functools.partitial, functools.wraps, hashlib.md5, hashlib.sha1, hashlib.update, hashlib.hexdigest, itertools.chain, types.ModuleType, os.path, os.stat, mimetypes.guess_type, StringIO, BytesIO, gzip, base64, hmac, stat

Tornado使用 web 模块的 Application 做URI转发,然后通过 RequestHandler 处理请求。 Application 提供了一个 listen 方法作为 HTTPServer 中的 listen 的封装。

下面是最简单的实例化并启动 Application 的方式:

1 import ioloop
2 import web
3 
4 application = web.Application([
5     (r'/', MainHandler),
6 ])
7 application.listen(8888)
8 ioloop.IOLoop.instance().start()

初始化 Application 时,一般将处理器直接传入,它会调用 add_handlers 添加这些处理器,初始化还包括 transforms (分块、压缩等)、UI模块、静态文件处理器的初始化。 add_handlers 方法负责添加URI和处理器的映射。 Application 实现URI转发时使用了一个技巧,它实现了 __call__ 方法,并将 Application 的实例传递给 HTTPServer ,当监听到请求时,它通过调用 Application 实例触发 __call____call__ 方法中完成具体的URI转发工作,并调用已注册的处理器的 _execute 方法,处理请求。

 1 def __call__(self, request):
 2     transforms = [t(request) for t in self.transforms]
 3     handler = None
 4     args = []
 5     kwargs = {}
 6     handlers = self._get_host_handlers(request) # 取得请求的host的一组处理器
 7     if not handlers:
 8         handler = RedirectHandler(
 9             self, request, url="http://" + self.default_host + "/")
10     else:
11         for spec in handlers:
12             match = spec.regex.match(request.path)  # 匹配请求的URI
13             if match:
14                 handler = spec.handler_class(self, request, **spec.kwargs) # 实例化
15                 if spec.regex.groups:   # 取得参数
16                     ...
17                     if spec.regex.groupindex:
18                         kwargs = dict(
19                             (str(k), unquote(v))
20                             for (k, v) in match.groupdict().iteritems())
21                     else:
22                         args = [unquote(s) for s in match.groups()]
23                 break
24         if not handler:     # 无匹配
25             handler = ErrorHandler(self, request, status_code=404)
26     ...
27     handler._execute(transforms, *args, **kwargs)   # 处理请求
28     return handler

RequestHandler 完成具体的请求,开发者需要继承它,并根据需要,覆盖 headgetpostdeletepatchputoptions 等方法,定义处理对应请求的业务逻辑。 RequestHandler 提供了很多钩子,包括 initializeprepareon_finishon_connection_closeset_default_headers 等等。

下面是 _execute 的处理流程:

RequestHandler 中涉及到很多HTTP相关的技术,包括Header、Status、Cookie、Etag、Content-Type、链接参数、重定向、长连接等等,还有和用户身份相关的XSRF和CSRF等等。这方面的知识可以参考《HTTP权威指南》。

Tornado默认实现了几个常用的处理器:

  • ErrorHandler :生成指定状态码的错误响应。
  • RedirectHandler :重定向请求。
  • StaticFileHandler :处理静态文件请求。
  • FallbackHandler :使可以在Tornado中混合使用其他HTTP服务器。

上面提到了 transform ,Tornado使用这种机制来对输出做分块和压缩的转换,默认给出了 GZipContentEncodingChunkedTransferEncoding 。也可以实现自定义的转换,只要实现 transform_first_chunktransform_chunk 接口即可,它们由 RequestHandler 中的 flush 调用。

tornado.httpserver - 非阻塞HTTP服务器

Tornado自己实现了一个非阻塞的HTTP服务器。 httpserver 模块的 HTTPServer 通过继承 netutil.TCPServer 实现了一个非阻塞的单线程HTTP服务器。

关于 HTTPServer 的用法,下面是一个简单的例子:

 1 import httpserver
 2 import ioloop
 3 
 4 def handle_request(request):
 5    message = "You requested %s\n" % request.uri
 6    request.write("HTTP/1.1 200 OK\r\nContent-Length: %d\r\n\r\n%s" % (
 7                  len(message), message))
 8    request.finish()
 9 
10 http_server = httpserver.HTTPServer(handle_request)
11 http_server.listen(8888)
12 ioloop.IOLoop.instance().start()

HTTPServer 需要用一个请求回调函数来实例化,该函数接受一个请求作为参数,可以通过 write 来写入一个有效的HTTP响应,用 finish 完成请求(HTTP/1.1 长连接请求不关闭连接)。 HTTPServer 只负责解析请求,除了HTTP/1.1 keep-alive语义之外,它不负责处理任何东西。它不处理分块编码,所以在请求回调中需要提供 Content-Length 头或者实现分块编码来正确响应HTTP/1.1客户端,也可以干脆将 no_keep_alive 设为 True 来关闭连接。 xheadersTrue 时,支持 X-Real-IpX-Scheme 头,覆盖所有请求的远程IP地址和HTTP规范,这在前端使用了反向代理或负载均衡的时候有用。

因为 HTTPServer 是继承了 TCPServer ,所以它也支持SSL传输。和 TCPServer 对应, HTTPServer 也有三种初始化方式:

  1. listen 单进程:

    1 server = HTTPServer(app)
    2 server.listen(8888)
    3 IOLoop.instance().start()
    
  2. bind/start 多进程:

    1 server = HTTPServer(app)    # 不能传递IOLoop
    2 server.bind(8888)
    3 server.start(0)  # 创建多进程
    4 IOLoop.instance().start()
    
  3. add_sockets 更高级的多进程模式:

    1 sockets = tornado.netutil.bind_sockets(8888)
    2 tornado.process.fork_processes(0)   # 创建多进程
    3 server = HTTPServer(app)    # 可以传递IOLoop
    4 server.add_sockets(sockets)
    5 IOLoop.instance().start()
    

    add_sockets 的方法也可以用在单进程模式下。

TCPServer_handle_connection 作为 add_accept_handler 的回调函数,它会对接收到的连接请求用 IOStream 建立连接,并调用 handle_stream 处理数据流。 handle_stream 的实现留给继承 TCPServer 的应用层实现来完成。 TCPServer 也支持ssl连接。

tornado.template - 可扩展的输出生成

关键字

compile, slice, enumerate, linecache.clearcache, StringIO, threading.RLock, os.path, posixpath

template 模块实现了一个精巧的PSP语言模板框架。熟悉Django或者Jinja的同学应该对这种语法不陌生。类似这种语法解析的工作大都采用状态机的思路实现,对这方面感兴趣的话,可以研究一下docutils工具,后者还可以实现自己的扩展语法。

模板解析的实现一般是对不同的元素标记实现对应的组件类,然后实现一个解析器,完成对整个源文件的处理,在遇到特定标记时,创建对应的组件实例。组件类通常有一个通用的接口,这里是 generate 方法,通过它来生成组件的输出。

tornado.escape - 转义和一些字符串操作

关键字

bytes, cgi.parse_qs, urlparse.parse_qs, json, simplejson, re, re.sub, urllib.quote_plus, urllib.unquote_plus, sys.version_info, unichr, htmlentitydefs.name2codepoint

escape 模块主要处理了一些和转义、字符串相关的操作。

  • xhtml_escapexhtml_unescape :前者将 &<>" 替换为 & 开头的XML转义形式,后者将匹配 &(#?)(\w+?); 的字符串做反转义。它们和Python标准库中的 cgi.escapexml.sax.saxutils 中的 quoteattr(un)escape 类似,但更宽泛、方便。
  • json_encodejson_decode :和标准库的类似,区别是增加了Unicode处理和 / 的转义( \/ ),转义的理由可以参考: http://stackoverflow.com/questions/1580647/json-why-are-forward-slashes-escaped
  • url_escapeurl_unescape 近似等价于 urllibquote_plusunquote_plus ,增加了Unicode处理。
  • squeeze 用来删除连续的空白字符,替换为单个空格。
  • 还有一些和Unicode、utf-8相关的函数,包括 utf8to_unicodeto_basestring ,还有 recursive_unicode 可以将列表、元组和字典对象中的字符串转换为Unicode形式。

最后还有一个 linkify 函数,它可以将文本中的链接替换为用 a 标签包含的形式,如 linkify("Hello http://tornadoweb.org!") 会转换为 Hello <a href="http://tornadoweb.org">http://tornadoweb.org</a>! ,有一些定制的参数。这里使用了一个用来匹配URL的正则表达式:

1 ur"""\b((?:([\w-]+):(/{1,3})|www[.])(?:(?:(?:[^\s&()]|&amp;|&quot;)*(?:[^!"#$%&'()*+,.:;<=>?@\[\]^`{|}~\s]))|(?:\((?:[^\s&()]|&amp;|&quot;)*\)))+)"""

tornado.locale - 本地化支持

关键字

frozenset, os, re, csv.reader, gettext.translation, datetime.datetime.utcfromtimestamp, datetime.datetime.utcnow, datetime.timedelta, gettext.translation.ungettext

locale 模块提供了本地化的相关功能。它分成两部分来实现: load_translationsload_gettext_translations 加载本地化文件到 _translations 字典中, Localeget 用来取得 Locale 实例,完成本地化翻译工作。

它支持CSV和gettext两种本地化文件形式。前者定义本地化条目如:

"I love you","Te amo"
"%(name)s liked this","A %(name)s les gustó esto","plural"
"%(name)s liked this","A %(name)s le gustó esto","singular"

第一条单复数不分,不加标识;第二条为复数形式,用 plural 标识;第三条为单数形式,用 singular 标识。

后者的用法在Linux中比较常见,Django中也用的这种方式:

1 $ xgettext --language=Python --keyword=_:1,2 -d cyclone file1.py file2.html etc # 生成POT文件
2 $ msgmerge old.po cyclone.po > new.po   # 合并
3 $ msgfmt cyclone.po -o {directory}/pt_BR/LC_MESSAGES/cyclone.mo # 编译

使用加载函数 load_* 会把本地化文件加载到 _translations

使用 get 会调用 Locale.get_closest 获取近似匹配的本地化文件,如果没有,取 _default_locale 。在代码中,使用 Locale.translate 方法来将字符串转换为翻译文本,它支持单复数,具体用法可以参考文档。实际使用时,为了方便可以定义如 _ = user_locale.translate 这样的方法别名。

Locale 类还有几个和本地化有点相关的方法: format_dateformat_day 用来生成更适合显示的时间字符串,如:"2 minutes ago", "Monday, January 22"; list 用来生成适合显示的列表; friendly_number 用来给整数加千分位的逗号分隔。

http://www.yeolar.com/note/2013/02/09/tornado-core/

http://www.yeolar.com/note/2013/02/09/tornado-core/