Tornado源码分析之起步
Tornado是什么
一个网络框架,同时也是一个异步网络库。其网络库适合长轮训和网络库。
源码下载
从这里 将源码进行下载,下载后切换到分支1.2,因为最小的版本,则意味着源码分析的难度降低。我们看看如何切换。
切换分支到1.2
可以看到现在我么那种本地分支master分支上。通过git branch -a 可以查看所有的分支信息,下面是切换版本 使用 git checkout 来切换。
目录结构
其中,tornado目录为该文件的核心目录,我们将demos里面中的helloworld.py代码,拿出来,进行debug。
hello world.py
import tornado.httpserver
import tornado.ioloop
import tornado.options
import tornado.web
from tornado.options import define, options
define("port", default=8888, help="run on the given port", type=int)
class MainHandler(tornado.web.RequestHandler):
def get(self):
self.write("Hello, world")
def main():
tornado.options.parse_command_line()
application = tornado.web.Application([
(r"/", MainHandler),
])
http_server = tornado.httpserver.HTTPServer(application)
http_server.listen(options.port)
tornado.ioloop.IOLoop.instance().start()
if __name__ == "__main__":
main()
看上去很简单,就是当输入localhost:8888的时候,返回Hello, world。首先看一下,解析命令行参数。
parse_command_line函数
这里首先是解析命令行参数,设计得很精妙。
def parse_command_line(args=None):
"""Parses all options given on the command line.
We return all command line arguments that are not options as a list.
"""
# 默认情况下使用sys.argv列表,那么args至少有一个值,就是你脚本的名称。
if args is None: args = sys.argv
remaining = []
# 显然len(args)至少为1,如果为1,表示没有参数,xrange(1, len(args))
# 生成一个空对象。
for i in xrange(1, len(args)):
# All things after the last option are command line arguments
# 开始的参数如果没有以-开始,那么整个参数都有问题。
if not args[i].startswith("-"):
remaining = args[i:]
break
# 如果你的参数仅仅是--,后面没有跟参数,那么后面整个参数都有问题。
if args[i] == "--":
remaining = args[i+1:]
break
# 获取参数名称
arg = args[i].lstrip("-")
name, equals, value = arg.partition("=")
name = name.replace('-', '_')
# 如果name不在tornado支持的options中,表示不识别该选项或命令行参数
if not name in options:
print_help()
raise Error('Unrecognized command line option: %r' % name)
# 获取已经定义的选项值。
option = options[name]
# 等号为空字符串,那么表示该命令行参数可能是一个选项
if not equals:
#本身是一个选项,打开即可
if option.type == bool:
value = "true"
else:
# 否则,应该有值
raise Error('Option %r requires a value' % name)
option.parse(value)
# 如果是help命令,打印出help信息,退出即可
if options.help:
print_help()
sys.exit(0)
# Set up log level and pretty console logging by default
# 如果设置的是logging,那么设置logging level
if options.logging != 'none':
logging.getLogger().setLevel(getattr(logging, options.logging.upper()))
enable_pretty_logging()
return remaining
我们用如下代码进行测试:
#!/usr/bin/env/python
import tornado.options
from tornado.options import parse_command_line
def main():
"""
"""
args = ["file_name", "--help"]
parse_command_line(args)
if __name__== "__main__":
main()
options源码查看
在tornado中有一个已经定义的实例options,我们在parse_command_line中已经看到了,其已经定义了很多tornado支持的选项。我们看看其实例化和_Options类实现:
# options实例化,用的是_Options类的类方法。
options = _Options.instance()
class _Options(dict):
"""Our global program options, an dictionary with object-like access."""
@classmethod
def instance(cls):
if not hasattr(cls, "_instance"):
cls._instance = cls()
return cls._instance
def __getattr__(self, name):
# 如果值对象类型为_Option,那么就返回其值。
if isinstance(self.get(name), _Option):
return self[name].value()
raise AttributeError("Unrecognized option %r" % name)options = _Options.instance()
我们看看,_Options实际上是一个字典,其定义如上,使用了一个单例模式,注意到hasattr的使用,以及__getattr__魔法方法的使用。我们看看值类型_Option
定义:
class _Option(object):
def __init__(self, name, default=None, type=str, help=None, metavar=None,
multiple=False, file_name=None):
if default is None and multiple:
default = []
self.name = name
self.type = type
self.help = help
self.metavar = metavar
self.multiple = multiple
self.file_name = file_name
self.default = default
# 默认值为None
self._value = None
_Option的parse方法
在parse_command_line中有一句option.parse(value),其根据将参数传入的值,设置option为该值。
def parse(self, value):
# 注意这种字典方式
# 其作用是根据参数的类型(比如:是一个简单的选项,或者其值应该为字符串)
# 选择相应的解析函数。
_parse = {
#
datetime.datetime: self._parse_datetime,
datetime.timedelta: self._parse_timedelta,
bool: self._parse_bool,
str: self._parse_string,
}.get(self.type, self.type)
# 值有多个
if self.multiple:
if self._value is None:
self._value = []
# 获取每个
for part in value.split(","):
if self.type in (int, long):
# allow ranges of the form X:Y (inclusive at both ends)
lo, _, hi = part.partition(":")
# 对于int, long type,_parse返回的是int()和long()方法,
# 很巧妙
lo = _parse(lo)
hi = _parse(hi) if hi else lo
# 设置添加多个系列值
self._value.extend(range(lo, hi+1))
else:
self._value.append(_parse(part))
else:
self._value = _parse(value)
return self.value()
_parse = { xxxx}.get(self.type, self.type)
有点继承的味道,根据传入值的类型,来选择相应的解析方式。实现的比较优雅,值得学习。
define函数
我们知道tornado实际上在项目中,在options
添加了几个默认的_Option
对象。
define("help", type=bool, help="show this help information")
define("logging", default="info",
help=("Set the Python log level. If 'none', tornado won't touch the "
"logging configuration."),
metavar="info|warning|error|none")
define("log_to_stderr", type=bool, default=None,
help=("Send log output to stderr (colorized if possible). "
"By default use stderr if --log_file_prefix is not set and "
"no other logging is configured."))
define("log_file_prefix", type=str, default=None, metavar="PATH",
help=("Path prefix for log files. "
"Note that if you are running multiple tornado processes, "
"log_file_prefix must be different for each of them (e.g. "
"include the port number)"))
define("log_file_max_size", type=int, default=100 * 1000 * 1000,
help="max size of log files before rollover")
define("log_file_num_backups", type=int, default=10,
help="number of log files to keep")
具体实现:
if name in options:
# 重复定义不允许
raise Error("Option %r already defined in %s", name,
options[name].file_name)
frame = sys._getframe(0)
# 用来获取当前文件名
options_file = frame.f_code.co_filename
file_name = frame.f_back.f_code.co_filename
# 如果是在options.py使用了define这个函数,
# 那么file_name就默认为空
if file_name == options_file: file_name = ""
options[name] = _Option(name, file_name=file_name, default=default,
type=type, help=help, metavar=metavar,
multiple=multiple)
sys._getframe(0)表示调用栈帧顶部,也就是define这个函数。下面的frame.f_code.co_filename表示定义define这个函数脚本名称(包含路径), 后面的frame.f_back.f_code.co_filename表示调用define函数的脚本名称(包含路径)。比较冷门的使用。下面是,调试结果,我们在helloworld.py中使用了define函数,结果如下: