tornado
阅读tornado源码期待达成的目标:
- 掌握tornado的整体流程
- 掌握tornado的异步实现原理
- 吸收tornado的编码细节, 从类的定义、函数的定义等多个角度去吸收
大体有以下几个方面细节可以入手:
- cookie和session
- xsrf攻击
- 日志
- 数据库操作
- 应答response的压缩或其他处理
- 中间件
- 重定向, 多级路由
- 数据的流式取和流式返回
- 配置文件采用的格式以及解析方式
- 信号
- websocket
- wsgi, asgi
- 部署
import tornado.web
import tornado.ioloop
class MainHandler(tornado.web.RequestHandler):
def get(self):
print(self.request.headers)
print(self.application.settings)
self.write({"name": "jack"})
class IndexHandler(tornado.web.RequestHandler):
def get(self, num, nid):
self.write(str(num + nid))
settings = {
'debug': True,
'gzip': True,
'autoescape': None,
'xsrf_cookies': False,
'cookie_secret': 'xxxx'
}
application = tornado.web.Application([
(r"/", MainHandler),
(r"/index/(?P<num>\d+)/(?P<nid>\d+)", IndexHandler)
], **settings)
if __name__ == '__main__':
application.listen(8888)
tornado.ioloop.IOLoop.instance().start()
看了上面最简单的使用案例来说,我们可能有如下疑问:
- tornado.web.RequestHandler 是个什么鬼,凭什么我们写一个类继承自它里面写一些get, post方法就行了
- 为什么在我们写的MainHandler里面可以获取请求self.request, 还可以获取self.application, tornado.web.RequestHandler到底帮我们做了些啥
- tornado.web.Application类传了两个参数进去, 第一个参数是列表套元组的形式,第二个参数是配置字典, 然后就能路由匹配到我们写的MainHandler啦?
- application.listen: 啥? application去listen, 我们的socket的创建和监听在哪里做的
- tornado.ioloop.IOLoop.instance().start(): 开启ioloop, ioloop 又是怎么个玩意, 开启它干嘛使呢?我们之间创建socket并等待连接不就行了吗,引入这么个看似复杂的玩意干嘛呢?
- self.write({"name": "jack"}), 这里既然可以返回字符串、字典,那么能不能返回列表呢?我们在这里write的数据, 框架究竟帮我们做了哪些事?
以上问题是一个人第一眼看到这段代码可能会提出的几个问题, 从这些问题中我们可以总结出日后我们自己开源框架应该怎么去做:尽可能地简化用户的操作, 将细节进行封装, 另外io多路复用是实现并发的一项技术, 这一点在打着高并发的tornado框架中得到了很好的体现.另外, 马上就要开启tornado的源码之旅, 在第一遍看源码的过程中不要陷进去, 头脑一定要保持清醒: 我们这一次看源码是为了解决什么样的疑惑, 没必要在很多细节的问题上究竟。等我们理清了这些疑惑之后, 看第二遍第三遍的时候再去逐步深入各种细节问题。
在正式看源码之前,关上电脑,如果让我们去实现这么一段代码功能,那我们应该怎么去做?但凡是有一点编程基础的,可能会有如下想法:作为服务端,创建一个用于处理连接的socket, 然后把这个socket交给linux操作系统的epoll去帮我们监听,一下子有10个用户过来连接了(三次握手建立连接,此时还没有发送请求数据), accept一次就会生成一个相应的conn, 这时把这个coon也交给epoll去帮我们监听,然后接着accept 9次, "生产"9个conn交给epoll去帮我们监听。这10个连接其中如果有3个连接发送了请求数据到服务端,那么假设服务端采用单进程单线程模型,这3个请求来了,服务端使用循环拿到请求数据并一个个去做相应的处理。那么这里问题来了,如果在处理一个请求的过程中发生阻塞(比如第一个请求的处理函数中对数据库做查询操作且查询很慢或者在处理函数中往别地发了一个网络请求), 势必会影响到另外的2个请求的处理。这时候就需要我们在处理函数中编写这类阻塞代码使用正确的姿势了, tornado 1.2版本中使用的是回调(其他版本暂时没研究过), 可以说在tornado早期版本中回调随处可见。
处理单个请求的过程应该是什么样的呢?拿到http请求报文, 并根据http协议进行解析封装成一个HTTPRequest对象, 然后我们根据请求去匹配路由,找到对应的handler, 根据请求的method去调用handler不同的方法做数据相应处理, 并通过conn发送数据。有了epoll之后我们可以把数据的发送和conn进行绑定,等conn可以发送的时候再去发送数据。我们发现,有了epoll之后我们就可以把socket对象和要进行的处理回调全部一股脑交给epoll去监听,然后我们在一个事件循环中去获取就绪的描述符和相应的回调函数。
web_server
- 服务器端bind到一个端口,然后开始listen。
- 客户端connect上来以后,将请求发送给服务端。
- 服务端处理完成后返回给客户端。
这样,一个请求就处理结束了。不过,当需要处理成千上万的连接的时候,我们就会在这个基础上考虑更多的情况,这也就是大家熟悉的The C10K problem。Tornado采用的解决办法是:多进程 + 非阻塞 + epoll模型。
这里留一个问题:为什么io多路复用常常和非阻塞io联系在一起?事件循环有内容的时候,此时socket是可以读写的,那么socket设置成非阻塞的意义何在呢?
在网上看到一张图,感觉画的不错,等tornado源码看完之后再回过头看这个图就很简单了
tornado的class
- HTTPServer:处理套接字的 listen/bind/accept。
- IOStream: 处理套接字的 read/write。
- HTTPConnection: 处理与 HTTP client 建立的连接,解析 HTTP Request 的 header 和 body, 调用用户自定义的处理方法,以及把响应数据写给客户端socket。
- HTTPRequest: http 请求对象, 包含了请求头、请求体等数据
- IOLoop: I/O loop,循环取出可用的 fd,并调用对应的事件处理函数。
- RequestHandler: 处理请求,支持 GET/POST 等操作。
大体流程
现在看这图可能不能完全get到里面所有的点, but没关系, 随着源码看的越来越多, 也就可以理解上面的图了。
tornado.web.Application
从上面的例子可以看到application封装了路由和自定义handler对应关系,还放了settings, 然后调用了一下applicatio.listen(8888).
先不看源码,我们来猜一猜就这样2步:初始化和listen(8888)分别应该干了些什么事?
初始化: 绑定了url和handler的对应关系.
listen: 创建了一个socket, 监听端口, 并把这个socket交给epoll去监听。
这里提出一个问题:对于"/index/(?P
Application对外方法:
- add_handlers(self, host_pattern, host_handlers)
- add_transform(self, transform_class)
- listen(self, port, address="", **kwargs)
- log_request(self, handler)
- reverse_url(self, name, *args)
内部方法:
__init__(self, handlers=None, default_host="", transforms=None, wsgi=False, **settings)
__call__(self, request)
构造方法
看一个方法,关注输入输出。输入:有2个参数值得注意,列表的handlers和字典的settings. 构造方法没有输出,那就关注在构造方法里给application对象绑定了什么属性:一般在构造方法会把这个对象所有应该有的属性都设置上,哪怕初始化时的时候属性是空,在后续的方法也会把这个属性的值给填充上。我们需要关注的是application绑定的属性有:transforms(用户传值以用户传的为准,这里用户传的必须是一个列表,用户没传根据settings去设置,默认会添加一个ChunkedTransferEncoding), settings, handlers. 对于这几个属性的数据结构是什么,里面放置的是什么我们也应该有所认识。其中transforms是list,里面存放ChunkedTransferEncoding、GZipContentEncoding等类,handlers是list,里面存放什么需要看self.add_handlers方法,当然我们传给构造方法的handlers里列表套元组的形式。
def __init__(self, handlers=None, default_host="", transforms=None,
wsgi=False, **settings):
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.settings = settings
...
self._wsgi = wsgi
...
if self.settings.get("static_path"):
path = self.settings["static_path"]
handlers = list(handlers or [])
static_url_prefix = settings.get("static_url_prefix",
"/static/")
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
if handlers: self.add_handlers(".*$", handlers)
# Automatically reload modified modules
...
add_handlers
add_handlers接收2个参数, 一个是字符串的host_pattern, 另一个是列表套元组的host_handlers。含义是主机名:主机一系列url 对应关系。
参数host_handlers是一个list,list里每个object是一个URLSpec的对象或tuple。tuple可以是二到四个element,分别是URI的正则、handler类、用于初始化URLSpec的kwargs、handler的name。从下面代码可以看出最终self.handlers的数据结构是[("host1", [URLSpec(), URLSpec()]),("host2", [URLSpec(), URLSpec()])]
.
这里有一个问题需要思考, 为啥要把pattern, handler, kwargs组成一个URLSpec对象呢?从我的角度去理解的话,把这3个玩意封装成一个对象,对象除了包含这3个属性之外,还会有一些针对这3个属性的一些方法。所以在以后我们可能需要用到这个对象里的一些方法,那么这个对象有哪些方法呢?
URLSpec.
def add_handlers(self, host_pattern, host_handlers):
# add_handlers 做的事更像是二级路由,先匹配主机名,然后在去匹配路由规则
if not host_pattern.endswith("$"):
host_pattern += "$"
handlers = []
# 对主机名先做一层路由映射,例如:http://www.wupeiqi.com 和 http://safe.wupeiqi.com
# 即:safe对应一组url映射,www对应一组url映射,那么当请求到来时,先根据它做第一层匹配,之后再继续进入内部匹配。
# 对于第一层url映射来说,由于.*会匹配所有的url,所将 .* 的永远放在handlers列表的最后,不然 .* 就会截和了
# add_handlers 可能会被调用多次,所以才有如下判断, 并且第一个条件的and是我们可以吸收的一个写代码的思路
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))
# host_handlers 是用户传的list套tuple的数据
for spec in host_handlers:
if isinstance(spec, type(())):
assert len(spec) in (2, 3)
pattern = spec[0]
handler = spec[1]
# kwargs = spec[2] if len(spec) == 3 else {}, 保证可以传kwargs参数
if len(spec) == 3:
kwargs = spec[2]
else:
kwargs = {}
# 这里把pattern, 自定义的handler, 需要给handler的参数kwargs封装成一个对象
spec = URLSpec(pattern, handler, kwargs)
handlers.append(spec)
if spec.name:
if spec.name in self.named_handlers:
logging.warning(
"Multiple handlers named %s; replacing previous value",
spec.name)
self.named_handlers[spec.name] = spec
listen
application的listen其实没必要存在,就是一个简单的包装,这种包装很容易让不看源码的人产生疑惑。listen所做的是就是创建一个HTTPServer,将application绑定上去。HTTPServer
def listen(self, port, address="", **kwargs):
# import is here rather than top level because HTTPServer
# is not importable on appengine
from tornado.httpserver import HTTPServer
server = HTTPServer(self, **kwargs)
server.listen(port, address)
__call__
HTTPConnection解析完header和body之后调用self.request_callback(self._request)
执行Application的__call__方法
。首先,我们还是可以猜测一下这个方法里应该完成什么样的事:因为参数是request, 所以可以拿到request.path, 然后进行匹配,选择对应的Handler类并实例化,再根据request.method去调用Handler对应的方法。和Handler的结合点的两行代码:handler = spec.handler_class(self, request, **spec.kwargs)
以及handler._execute(transforms, *args, **kwargs)
.
新知识点:groupdict和groups
def __call__(self, request):
"""Called by HTTPServer to execute the request."""
transforms = [t(request) for t in self.transforms]
handler = None
# args和kwargs就是我们路由捕捉的参数, 也就是我们常常在我们自定义Handler的get方法里看到的get(self, *args, **kwargs).
args = []
kwargs = {}
handlers = self._get_host_handlers(request)
if not handlers:
handler = RedirectHandler(
self, request, url="http://" + self.default_host + "/")
else:
# [URLSpec(), URLSpec()]
for spec in handlers:
match = spec.regex.match(request.path)
if match:
# url反编译
def unquote(s):
if s is None: return s
return urllib.unquote(s)
# 实例化handler
handler = spec.handler_class(self, request, **spec.kwargs)
kwargs = dict((k, unquote(v))
for (k, v) in match.groupdict().iteritems())
if kwargs:
args = []
else:
args = [unquote(s) for s in match.groups()]
# 从前到后匹配,匹配上就不往后匹配了
break
# 没有一个匹配上,使用ErrorHandler去处理
if not handler:
handler = ErrorHandler(self, request, status_code=404)
...
handler._execute(transforms, *args, **kwargs)
return handler
In [1]: import re
In [2]: spec = re.compile(r"/index/(?P<name>\w+)")
In [3]: match = spec.match("/index/jack")
In [4]: match.group()
Out[4]: '/index/jack'
In [5]: match.groups()
Out[5]: ('jack',)
In [6]: match.groupdict()
Out[6]: {'name': 'jack'}
_get_host_handlers
根据host得到该host的handlers列表。
def _get_host_handlers(self, request):
host = request.host.lower().split(':')[0]
for pattern, handlers in self.handlers:
if pattern.match(host):
return handlers
...
return None
URLSpec
URLSpec这个类比较简单, 就是一个简单的工具类. 对于其构造方法,我们关注的是单个参数,字符串的pattern,类名handler_class以及字典kwargs。
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()
def _find_groups(self):
"""Returns a tuple (reverse string, group count) for a url.
For example: Given the url pattern /([0-9]{4})/([a-z-]+)/, this method
would return ('/%s/%s/', 2).
"""
# 大致看了下_find_groups的输出,那段for fragment in pattern.split('(')有点垃圾,re.sub(r"\(.+?\)", "%s", pattern)就能完成相同功能
# 所以下面的代码没必要看了,唯一需要关注的是这个方法的输出('/%s/%s/', 2),以及/([0-9]{4})/([a-z-]+)/ 这样被编译后的regex的两个属性:pattern和groups
pattern = self.regex.pattern
if pattern.startswith('^'):
pattern = pattern[1:]
if pattern.endswith('$'):
pattern = pattern[:-1]
if self.regex.groups != pattern.count('('):
# The pattern is too complicated for our simplistic matching,
# so we can't support reversing it.
return (None, None)
pieces = []
for fragment in pattern.split('('):
if ')' in fragment:
paren_loc = fragment.index(')')
if paren_loc >= 0:
pieces.append('%s' + fragment[paren_loc + 1:])
else:
pieces.append(fragment)
return (''.join(pieces), self.regex.groups)
def reverse(self, *args):
# reverse 方法就是就是一个简单的字符串格式化"%s%s"%("name", "age"), 封装成一个方法也是为了做一些简单地校验
assert self._path is not None, \
"Cannot reverse url regex " + self.regex.pattern
assert len(args) == self._group_count, "required number of arguments "\
"not found"
if not len(args):
return self._path
return self._path % tuple([str(a) for a in args])
HTTPServer
HTTPServer类的外部方法:
- bind(self, port, address="")
- listen(self, port, address="")
- start(self, num_processes=1)
- stop(self):
HTTPServer类的内部方法:
__init__(self, request_callback, no_keep_alive=False, io_loop=None, xheaders=False, ssl_options=None)
_handle_events
(self, fd, events)
还是和之前一样,不看源码的情况下我们来猜测一些这个类。HTTPServer?处理http请求的服务器?既然是服务器, 那么肯定涉及到socket.所以这个类肯定有创建socket并监听socket等操作。另外, 它是http类型的服务器, 那么这个服务器有对http协议做解析吗?再结合io多路复用, 进一步猜测创建的这个socket绑定read事件,当有客户端来连接时,那么就accept得到一个conn, 然后我们就能拿着这个conn去服务客户端了。框架比我们自己写的那种简单的httpserver复制的地方就是在对于conn的处理上,框架用了一系列类做了很多的事情。
构造方法
我们发现在构造方法里,有如下4个属性值得关注:request_callback(application对象)、no_keep_alive、io_loop、_started。
在httpserver中封装了application对象, io_loop和socket. ioloop和socket是必不可少的, 因为要做io多路复用嘛,至于_started可以看做是一个辅助参数了。
这里有一个问题。长连接是怎么做到的
def __init__(self, request_callback, no_keep_alive=False, io_loop=None,
xheaders=False, ssl_options=None):
self.request_callback = request_callback
# 默认是保持长连接
self.no_keep_alive = no_keep_alive
self.io_loop = io_loop
...
self._socket = None
self._started = False
bind
bind:创建socket, 绑定ip和端口,listen。bind函数其实说白了就是socket server初始的三步战略。
def bind(self, port, address=""):
assert not self._socket
...
self._socket.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
self._socket.setblocking(0)
self._socket.bind((address, port))
self._socket.listen(128)
start
在IOLoop中去开启服务器,说白了就是让epoll去帮我们看着socket. 接收的参数num_processes表示创建的进程数,num_processes<0会内部纠正为服务器的cpu个数。torndao中的多进程体现在这:因为多进程的是写时复制,所以这里开了多个进程,在每个进程中用不同的IOLoop对象去监视同一个socket描述符。这里给socket绑定监听事件,绑定的函数是_handle_events, 这个函数很重要,可以看做是一切的起始地.
def start(self, num_processes=1):
"""Starts this server in the IOLoop.
"""
# 保证start方法只能被调用一次
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
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, 否则会死循环的
return
os.waitpid(-1, 0)
else:
if not self.io_loop:
self.io_loop = ioloop.IOLoop.instance()
self.io_loop.add_handler(self._socket.fileno(),
self._handle_events,
ioloop.IOLoop.READ)
listen
listen就是提供给外部调用者的一个方法,外部调用者直接调用这个方法,它不需要知道创建socket和给IOLoop去监听的细节。
def listen(self, port, address=""):
"""Binds to the given port and starts the server in a single process.
This method is a shortcut for:
server.bind(port, address)
server.start(1)
"""
self.bind(port, address)
self.start(1)
stop
移除IOLoop中监听的描述符,关闭socket对象
def stop(self):
self.io_loop.remove_handler(self._socket.fileno())
self._socket.close()
_handle_events
在_handle_events
里面对socket进行accept操作,并创建了一个IOStream对象,以及一个用来处理连接的HTTPConnection对象。_handle_events
是一个死循环,因为一次可能有多个客户端过来连接,此时就可以accept多次。当accept报错时,循环退出。可以看到,我们每次accept得到一个connection, 最终的处理方式就是仅仅实例化一个HTTPConnection对象,实例化这个对象也就算了,竟然没有调用这个对象的任何方法就能把一个客户端连接给处理了,这和一般的惯性思维不太一样啊。那么是不是在HTTPConnection的构造方法里就直接调用了某些方法呢?感觉也不是,如果HTTPConnection是顺序去处理连接的内容的话,一旦某个连接出现了阻塞,那么势必会影响下一轮的accept循环,这样tornado就不是之前吹嘘的高性能框架了。那到底是怎么做的呢?
def _handle_events(self, fd, events):
while True:
try:
connection, address = self._socket.accept()
except socket.error, e:
# 多进程情况下,当socket可read时,多个进程的ioloop都能感知到,如果第一个进程进行了accept,那么别的进程就会走到下面的错误直接return
if e.args[0] in (errno.EWOULDBLOCK, errno.EAGAIN):
return
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)
HTTPConnection
通过HTTPServer中的 _handle_events(self, fd, events)
方法, 里面仅仅实例化一下HTTPConnection, 主循环就不管这次accept得到的connection了, 接着进行下一轮的accept了。那么仅仅实例化一次HTTPConnection对象,是怎么做到处理一个客户端连接的呢。通过看构造方法的参数:传了一个stream、address(ip+port)、request_callback(Application对象)等就能处理一个客户端连接了?同样,在看源码之前我们想象一下这是怎么做的?首先要处理客户端连接肯定需要connection这个socket, stream对象封装了这个socket和针对这个socket的一些读写操作。因为要和io多路复用结合起来一起分析,所以我们可以把这个connection的这个socket也交给epoll去监听,监听有客户端发送http数据的时候进行解析,然后根据请求的method去执行request_callback()。
外部方法:
内部方法:
__init__(self, stream, address, request_callback, no_keep_alive=False, xheaders=False)
_finish_request(self)
_on_headers(self, data)
_on_request_body(self, data)
_on_write_complete(self)
_parse_mime_body(self, boundary, data)
__init__
HTTPConnection因为涉及到解析http报文,并执行request_callback(request)方法,所以每一个HTTPConnection对象需要有一个对应的HTTPRequest对象,所以HTTPConnection内部封装了一个_request
属性, 然后最重要的一步到了,self.stream.read_until("\r\n\r\n", self._header_callback)
. HTTPConnection因为需要connection这个socket对象,而这个对象是被封装在IOStream里的,所以HTTPConnection对connection的操作就拜托给IOStream对象了。跳转到IOStream查看这个类的相关内容之后,HTTPConnection的构造方法把connection这个socket也交给epoll去监听,等有动静就调用IOStream的_handle_events
.
class HTTPConnection(object):
"""Handles a connection to an HTTP client, executing HTTP requests.
We parse HTTP headers and bodies, and execute the request callback
until the HTTP conection is closed.
"""
def __init__(self, stream, address, request_callback, no_keep_alive=False,
xheaders=False):
self.stream = stream
self.address = address
self.request_callback = request_callback
self.no_keep_alive = no_keep_alive
self.xheaders = xheaders
self._request = None
self._request_finished = False
# Save stack context here, outside of any request. This keeps
# contexts from one request from leaking into the next.
# stack_context.wrap 这个以后再介绍
self._header_callback = stack_context.wrap(self._on_headers)
# 向epoll注册read事件
self.stream.read_until("\r\n\r\n", self._header_callback)
_on_headers
HTTPConnection的主要功能是读取客户端发来的数据,并以http协议解析,生成HTTPRequest对象。用python代码来实现http解析也不是很难,大概就这么几步:解析消息头,一般的get,head,delete,option类型的请求只有消息头,无消息体。如果是post或者put请求,会有消息体,主要是提交页面