python wsgi+Odoo 的启动

参考:
WSGI初探
Odoo web 机制浅析
python的 WSGI 简介
python wsgi 简介

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对服务器的规定:

  • 准备environstart_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)。

另外的一些规定:environstart_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

可见,类ServerCommand的子类,而Command的元类是CommandType
在初始化该元类的实例(也就是类ServerCommand)时,会设置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()

runstarthttp_spawnhttp_thread,看到self.httpd被设置成ThreadedWSGIServerReloadable(来自Werkzeug)的实例,而self.app也和服务器关联了起来。

接下来看下应用这边。
openerp.service.wsgi_server中,applicationapplication_unproxiedmodule_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_unproxiedapplication等函数,可看做是middleware。

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 202,980评论 5 476
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,178评论 2 380
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 149,868评论 0 336
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,498评论 1 273
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,492评论 5 364
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,521评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,910评论 3 395
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,569评论 0 256
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,793评论 1 296
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,559评论 2 319
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,639评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,342评论 4 318
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,931评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,904评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,144评论 1 259
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 42,833评论 2 349
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,350评论 2 342

推荐阅读更多精彩内容