python wsgi+Odoo 的启动
wsgi的定义
一个请求从客户端发到服务端,具体需要怎么样才能无缝对接呢?
在python中,服务端负责实际处理逻辑的有好几种,常见的也就是被大家所熟知的各个框架,如Django、Flask等。那请求经过怎样的处理进入到这些负责具体逻辑的框架呢?
那肯定不会是一个框架一个处理方式,那肯定是有规定好的一套逻辑,让各个框架来适配!
那就是WSGI协议:The Web Server Gateway Interface。
从名字就可以看出来,这东西是一个Gateway,也就是网关。网关的作用就是在协议之间进行转换。
按照上面的描述,WSGI应该是单独实现的,和Django、Flask这些框架是隔离的。
实际上,的确有单独的,比如python下生产环境中经常用的WSGI容器Gunicorn。
但是呢,这些框架(包括以下提到的Odoo)都自己实现了WSGI协议——是自带Web服务器的。实现这些的目的是用于开发,生成环境还得用上面的。
也就是说,Django等框架分为WSGI容器和负责具体处理逻辑的部分,前者是不必要的。这点要认识清楚。
WSGI标准在PEP333中定义,后来在PEP3333中更新。它定义了在网络和python之间的沟通接口,一边连着Web服务器,一边连着具体的处理逻辑(后文统称应用或app)。对应用而言,它就是服务器程序,对服务器而言,它就是应用程序。
一张引用自参考4里的图:
wsgi示例图
wsgi规定的标准
WSGI对应用的规定:
- 应用(Application)必须是一个可调用(callable)对象。
- 这个可调用对象接受两个参数:environ(WSGI的环境信息,是个字典)和start_response(开始响应请求的函数)。
- 应用在返回前调用start_response。
- start_response也是可调用对象,接受两个参数:status(HTTP状态)和response_headers(响应头)。
- 应用要返回一个可迭代(iterable)对象。
例子:
def application(environ, start_response):
HELLO = 'hello world!'
status = '200 OK'
response_headers = [('Content-Type', 'text/plain'), ('Content-Length', len(HELLO))]
start_response(status, response_headers)
return [HELLO]
WSGI对服务器的规定:
- 准备environ和start_response。
- 调用应用。
- 迭代应用的返回结果,并将其通过网络传送至客户端。
例子:
import os, sys
def run_with_cgi(application):
environ = dict(os.environ.items())
headers = []
def write(data):
sys.stdout.write(data)
sys.stdout.flush()
def start_response(status, response_headers):
headers = [status, response_headers]
return write
result = application(environ, start_response)
try:
for data in result:
write(data)
finally:
if hasattr(result, 'close'):
result.close()
WSGI对中间层middleware的规定:
- 被服务器调用,返回结果。
- 调用应用,把参数传过去。
其实,对于服务器,它就是应用,对于应用,它就是服务器(是不是和上文对WSGI的描述很像?)。
middleware 对服务器程序和应用是透明的,它像一个代理/管道一样,把接收到的请求进行一些处理,然后往后传递,一直传递到客户端程序,最后把程序的客户端处理的结果再返回。
一般中间件这里都会举一个url route的例子,具体见参考4:
class Route(object):
def __init__(self):
self.path_info = {}
def route(self, environ, start_response):
application = self.path_info[environ['PATH_INFO']]
return application(environ, start_response)
def __call__(self, path):
def wrapper(application):
self.path_info[path] = application
return wrapper
route = Route()
服务器、中间件和应用都在服务端,它们一起合作,处理请求,返回应答。
其实无论是服务器程序,middleware 还是应用程序,都在服务端,为客户端提供服务,之所以把他们抽象成不同层,就是为了控制复杂度,使得每一次都不太复杂,各司其职。
更详细的wsgi
详情可查看PEP3333。
谈一下environ。这个参数是一个dict,首先需要包括CGI(Common Gateway Interface)的环境变量,然后需要包括WSGI相关的变量。
下面是Werkzeug库中的Map类的bind_to_environ方法,具体作用见本人的《flask/odoo/werkzeug的url mapping》。我把这个方法中感兴趣的一些变量做了注释。
也可以看出CGI变量一般大写,而WSGI变量一般是wsgi.*。
def bind_to_environ(self, environ, server_name=None, subdomain=None):
environ = _get_environ(environ)
if 'HTTP_HOST' in environ:
wsgi_server_name = environ['HTTP_HOST']
if environ['wsgi.url_scheme'] == 'http' \ # 表示 url 的模式,例如 "https" 还是 "http"
and wsgi_server_name.endswith(':80'):
wsgi_server_name = wsgi_server_name[:-3]
elif environ['wsgi.url_scheme'] == 'https' \
and wsgi_server_name.endswith(':443'):
wsgi_server_name = wsgi_server_name[:-4]
else:
wsgi_server_name = environ['SERVER_NAME']
if (environ['wsgi.url_scheme'], environ['SERVER_PORT']) not \
in (('https', '443'), ('http', '80')):
wsgi_server_name += ':' + environ['SERVER_PORT']
wsgi_server_name = wsgi_server_name.lower()
if server_name is None:
server_name = wsgi_server_name
else:
server_name = server_name.lower()
if subdomain is None and not self.host_matching:
cur_server_name = wsgi_server_name.split('.')
real_server_name = server_name.split('.')
offset = -len(real_server_name)
if cur_server_name[offset:] != real_server_name:
subdomain = '<invalid>'
else:
subdomain = '.'.join(filter(None, cur_server_name[:offset]))
def _get_wsgi_string(name):
val = environ.get(name)
if val is not None:
return wsgi_decoding_dance(val, self.charset)
script_name = _get_wsgi_string('SCRIPT_NAME')
path_info = _get_wsgi_string('PATH_INFO') # URL 路径除了起始部分后的剩余部分,用于找到相应的应用程序对象,如果请求的路径就是根路径,这个值为空字符串
query_args = _get_wsgi_string('QUERY_STRING') # URL路径中?后面的部分
return Map.bind(self, server_name, script_name,
subdomain, environ['wsgi.url_scheme'],
environ['REQUEST_METHOD'], path_info, # HTTP 请求方法,例如 "GET", "POST"
query_args=query_args)
还有就是start_response。参数status是状态码,而response_headers参数是一个列表,列表项的形式为(header_name, header_value)。
另外的一些规定:environ和start_response是位置参数,不是关键字参数。应用必须在第一次返回前调用start_response,这是因为返回的可迭代对象是返回数据的body部分,在它返回前,需要先返回response_headers数据。
Odoo的启动
分为python导入和命令启动两部分。Odoo自己实现了几个Web Server,将Application与对应的服务器相连,期间大量依赖Werkzeug。
在python导入时,会在commands中注册一个Server类。
# openerp.cli.server中
class Server(Command):
"""Start the odoo server (default command)"""
def run(self, args):
main(args)
# openerp.cli.__init__中
commands = {}
class CommandType(type):
def __init__(cls, name, bases, attrs):
super(CommandType, cls).__init__(name, bases, attrs)
name = getattr(cls, name, cls.__name__.lower())
cls.name = name
if name != 'command':
commands[name] = cls
class Command(object):
"""Subclass this class to define new openerp subcommands """
__metaclass__ = CommandType
def run(self, args):
pass
可见,类Server是Command的子类,而Command的元类是CommandType。
在初始化该元类的实例(也就是类Server或Command)时,会设置commands[name] = cls。
依据逻辑,在字典commands中,键server对应的值就是类Server。
Odoo通过openerp.cli.main()启动。
def main():
args = sys.argv[1:]
# Default legacy command
command = "server"
# Subcommand discovery
if len(args) and not args[0].startswith("-"):
command = args[0]
args = args[1:]
if command in commands:
o = commands[command]()
o.run(args)
可见,就是通过键server找到类Server,而o是类Server的实例。
args是一个列表,大致是:['-c', './configs/my-openerp-server.conf', '-d', 'my_database']
o.run由上面的代码可知,和类Server同处于openerp.cli.server中:
def main(args):
config = openerp.tools.config
# This needs to be done now to ensure the use of the multiprocessing
# signaling mecanism for registries loaded with -d
if config['workers']:
openerp.multi_process = True
preload = []
if config['db_name']:
preload = config['db_name'].split(',')
stop = config["stop_after_init"]
setup_pid_file()
rc = openerp.service.server.start(preload=preload, stop=stop)
sys.exit(rc)
我去除了一些和该框架强相关的东西。其实关键只有倒数第二句,preload是数据库列表,为['my_database']。
接下来是位于openerp.service.server中的start函数:
def start(preload=None, stop=False):
""" Start the openerp http server and cron processor.
"""
global server
load_server_wide_modules()
if openerp.evented:
server = GeventServer(openerp.service.wsgi_server.application)
elif config['workers']:
server = PreforkServer(openerp.service.wsgi_server.application)
else:
server = ThreadedServer(openerp.service.wsgi_server.application)
rc = server.run(preload, stop)
return rc if rc else 0
server是个全局变量,根据选项,有三种类型的Web Server可选,它们的父类叫做CommonServer,一般启动时是创建ThreadedServer的实例。也就是说服务器是ThreadedServer的实例。
而应用则是openerp.service.wsgi_server中的application函数(也可以认为是一个middleware)。ThreadedServer的实例初始化时,会设置self.app为该应用。
花开两朵,各表一枝。先说服务器这边,上面代码中关键的逻辑是倒数第二句。
def run(self, preload=None, stop=False):
""" Start the http server and the cron thread then wait for a signal.
The first SIGINT or SIGTERM signal will initiate a graceful shutdown while
a second one if any will force an immediate exit.
"""
self.start(stop=stop)
if stop:
self.stop()
return rc
try:
while self.quit_signals_received == 0:
time.sleep(60)
except KeyboardInterrupt:
pass
self.stop()
def start(self, stop=False):
if os.name == 'posix':
signal.signal(signal.SIGINT, self.signal_handler)
signal.signal(signal.SIGTERM, self.signal_handler)
signal.signal(signal.SIGCHLD, self.signal_handler)
signal.signal(signal.SIGHUP, self.signal_handler)
signal.signal(signal.SIGQUIT, dumpstacks)
signal.signal(signal.SIGUSR1, log_ormcache_stats)
elif os.name == 'nt':
import win32api
win32api.SetConsoleCtrlHandler(lambda sig: self.signal_handler(sig, None), 1)
self.http_spawn()
def http_spawn(self):
t = threading.Thread(target=self.http_thread, name="openerp.service.httpd")
t.setDaemon(True)
t.start()
def http_thread(self):
def app(e, s):
return self.app(e, s)
self.httpd = ThreadedWSGIServerReloadable(self.interface, self.port, app)
self.httpd.serve_forever()
run→start→http_spawn→http_thread,看到self.httpd被设置成ThreadedWSGIServerReloadable(来自Werkzeug)的实例,而self.app也和服务器关联了起来。
接下来看下应用这边。
在openerp.service.wsgi_server中,application→application_unproxied→module_handlers:
# WSGI handlers registered through the register_wsgi_handler() function below.
module_handlers = []
def register_wsgi_handler(handler):
""" Register a WSGI handler.
Handlers are tried in the order they are added. We might provide a way to
register a handler for specific routes later.
"""
module_handlers.append(handler)
def application_unproxied(environ, start_response):
""" WSGI entry point."""
with openerp.api.Environment.manage():
# Try all handlers until one returns some result (i.e. not None).
wsgi_handlers = [wsgi_xmlrpc]
wsgi_handlers += module_handlers
for handler in wsgi_handlers:
result = handler(environ, start_response)
if result is None:
continue
return result
# We never returned from the loop.
response = 'No handler found.\n'
start_response('404 Not Found', [('Content-Type', 'text/plain'), ('Content-Length', str(len(response)))])
return [response]
def application(environ, start_response):
if config['proxy_mode'] and 'HTTP_X_FORWARDED_HOST' in environ:
return werkzeug.contrib.fixers.ProxyFix(application_unproxied)(environ, start_response)
else:
return application_unproxied(environ, start_response)
找到在openerp.http中调用register_wsgi_handler,实际上这部分应该归于python导入那部分:
# register main wsgi handler
root = Root()
openerp.service.wsgi_server.register_wsgi_handler(root)
而Root:
class Root(object):
"""Root WSGI application for the OpenERP Web Client.
"""
def __call__(self, environ, start_response):
""" Handle a WSGI request
"""
return self.dispatch(environ, start_response)
def dispatch(self, environ, start_response):
"""
Performs the actual WSGI dispatching for the application.
"""
可见,root实例是应用,是一个可调用对象(符合WSGI规定),实际上是调用dispatch方法。而之前的application_unproxied或application等函数,可看做是middleware。