欢迎您访问程序员文章站本站旨在为大家提供分享程序员计算机编程知识!
您现在的位置是: 首页

python wsgi+Odoo 的启动

程序员文章站 2022-03-23 17:57:32
...

参考:
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里的图:

python wsgi+Odoo 的启动

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。

相关标签: python wsgi