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對應用的規定:
- 應用(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。