天天看點

詳解wsgi的http請求過程

一、概述

wsgi服務啟動并監聽http請求的流程:

  • 1.利用paste.deploy子產品的loadapp函數加載指定服務(如proxy)的配置檔案,擷取到使用者的application,即業務程式
  • 2.調用wsgi.server,其中wsgi.server會綁定IP和端口,監聽來自用戶端的消息。并由process_request函數負責處理消息。

下面主要說下處理http請求的過程(其他的在另外的文章中已有講解)

我們都知道wsgi application都需要實作__call__()方法,并且參數必須為environ, start_response;

那麼這兩個參數是從哪裡傳遞進來的呢?下面就來解密下這個。

二、http請求處理

子產品:../site-packages/eventlet/wsgi.py

1、入口函數server

wsgi.py檔案中定義了Server類,用于開啟一個服務端socket,處理socket通信;

檔案中還定義了一個入口函數:server,源碼如下:

def server():
   serv = Server(sock, sock.getsockname(),
			  site, log,
			  environ=environ,
			  max_http_version=max_http_version,
			  protocol=protocol,
			  minimum_chunk_size=minimum_chunk_size,
			  log_x_forwarded_for=log_x_forwarded_for,
			  keepalive=keepalive,
			  log_output=log_output,
			  log_format=log_format,
			  url_length_limit=url_length_limit,
			  debug=debug,
			  socket_timeout=socket_timeout,
			  capitalize_response_headers=capitalize_response_headers,
			  )
    if server_event is not None:
        server_event.send(serv)
    if max_size is None:
        max_size = DEFAULT_MAX_SIMULTANEOUS_REQUESTS
    if custom_pool is not None:
        pool = custom_pool
    else:
        pool = greenpool.GreenPool(max_size)
    try:
        serv.log.info("(%s) wsgi starting up on %s" % (
            serv.pid, socket_repr(sock)))
        while is_accepting:
            try:
                client_socket = sock.accept()
                client_socket[0].settimeout(serv.socket_timeout)
                serv.log.debug("(%s) accepted %r" % (
                    serv.pid, client_socket[1]))
                try:
                    pool.spawn_n(serv.process_request, client_socket)
                except AttributeError:
                    warnings.warn("wsgi's pool should be an instance of "
                                  "eventlet.greenpool.GreenPool, is %s. Please convert your"
                                  " call site to use GreenPool instead" % type(pool),
                                  DeprecationWarning, stacklevel=2)
                    pool.execute_async(serv.process_request, client_socket)
            except ACCEPT_EXCEPTIONS as e:
                if support.get_errno(e) not in ACCEPT_ERRNO:
                    raise
            except (KeyboardInterrupt, SystemExit):
                serv.log.info("wsgi exiting")
                break      

該函數首先建立一個伺服器對象(可以指定協定);

通過外部指定的協程池大小初始化協程池;

最後循環監聽來自用戶端的連接配接:每次收到一個請求,就新開一個協程去處理該請求。

2、請求處理函數process_request

在入口函數server中,調用了process_request方法來處理請求,源碼如下:

def process_request(self, sock_params):
	# The actual request handling takes place in __init__, so we need to
	# set minimum_chunk_size before __init__ executes and we don't want to modify
	# class variable
	sock, address = sock_params
	proto = new(self.protocol)
	if self.minimum_chunk_size is not None:
		proto.minimum_chunk_size = self.minimum_chunk_size
	proto.capitalize_response_headers = self.capitalize_response_headers
	try:
		proto.__init__(sock, address, self)
	except socket.timeout:
		# Expected exceptions are not exceptional
		sock.close()
		# similar to logging "accepted" in server()
		self.log.debug('(%s) timed out %r' % (self.pid, address))      

該方法執行個體化了協定類(由外部指定協定),調用了該協定類的__init__方法。

注:HttpProtocol對象自身沒有init函數,是以會調用父類BaseRequestHandler(PythonXX/Lib/SocketServer.py)的init 函數 ,源碼如下:

def __init__(self, request, client_address, server):
	self.request = request
	self.client_address = client_address
	self.server = server
	self.setup()
	try:
		self.handle()
	finally:
		self.finish()      

此函數中,執行了setup, handle, finish 三個操作,下面依次進行分析。

I、setup函數

HttpProtocal 類重寫了setup函數,根據client socket生成rfile和wfile。

II、handle函數

BaseHTTPRequestHandler(../site-packages/eventlet/green/http/server.py) 重寫了handle函數,此函數主要調用 handle_one_request (HttpProtocl類重寫)函數。

handle_one_request函數,調用parse_request(BaseHTTPRequestHandler類)函數,通過mimetools.Message類生成headers,以及command,path,version等參數;

調用get_environ (HttpProtocol類)函數生成environ字典;

将self.server.app指派self.application,其中self.server.app就是外部初始化wsgi.Server()時傳遞進來的,具體後面講解;

調用handle_one_response函數。

handle_one_response函數,調用self.application函數,傳遞environ參數和start_response函數,執行使用者業務流程并傳回響應資料;

調用write函數,重新構造響應資料,調用wfile.writelines函數傳回給用戶端。

在self.application(即__call__函數)執行過程中,根據envrion 會生成request對象。

注:這就回答了文章開頭提出的那個問題。

III、finish函數

HttpProtocol類重寫,核心功能由StreamRequestHandler類完成。重新整理緩存,關閉wfile,rfile,釋放協程和socket資源。

start_response函數解析

在多個中間件項目中,start_response函數會被作為參數繼續傳遞,直到最後一個app時,利用app過程中執行的status和headers參數,重寫handle_one_response内部的header_set變量,作為執行結果傳回給用戶端。

3、Application對象

在上一節中我們知道在handle_one_response方法中會執行application()方法,而這個application是怎麼來的呢?

它是通過../site-packages/oslo_service/wsgi.py中load_app來的,源碼如下:

def load_app(self, name):
	"""Return the paste URLMap wrapped WSGI application.

	:param name: Name of the application to load.
	:returns: Paste URLMap object wrapping the requested application.
	:raises: PasteAppNotFound

	"""
	try:
		LOG.debug("Loading app %(name)s from %(path)s",
				  {'name': name, 'path': self.config_path})
		return deploy.loadapp("config:%s" % self.config_path, name=name)
	except LookupError:
		LOG.exception("Couldn't lookup app: %s", name)
		raise PasteAppNotFound(name=name, path=self.config_path)      

這裡的name就是在api-paste.ini檔案中定義的入口section,例如下面的app_name

[composite:app_name]
use = egg:Paste#urlmap
/: apiversions
/v1: xxxx      
def __call__(self, environ, start_response):
	host = environ.get('HTTP_HOST', environ.get('SERVER_NAME')).lower()
	if ':' in host:
		host, port = host.split(':', 1)
	else:
		if environ['wsgi.url_scheme'] == 'http':
			port = '80'
		else:
			port = '443'
	path_info = environ.get('PATH_INFO')
	path_info = self.normalize_url(path_info, False)[1]
	for (domain, app_url), app in self.applications:
		if domain and domain != host and domain != host+':'+port:
			continue
		if (path_info == app_url
			or path_info.startswith(app_url + '/')):
			environ['SCRIPT_NAME'] += app_url
			environ['PATH_INFO'] = path_info[len(app_url):]
			return app(environ, start_response)
	environ['paste.urlmap_object'] = self
	return self.not_found_application(environ, start_response)      

4、業務route

  • 一個就是我們業務中定義的route mapper,即每個url對應的具體處理的controller類。
  • 一個就是持有controller引用的資源分發處理類,該類必須也是一個可調用對象:在__call__中,解析出url對應的處理方法、參數,以及response封裝等等。