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。

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。

推薦閱讀更多精彩內容