WSGI为什么需要WSGIWSGI原理Server 调用 Application
示例environ参数start_resposne参数application对象的返回值 WSGI的实现和部署 WSGI
WSGI(Web Server Gateway Interface) ,作为一个规范,定义了Web服务器如何与Python应用程序进行交互,使得使用Python写的Web应用程序可以和Web服务器对接起来。
为什么需要WSGI在Web部署的方案上,有一个方案是目前应用最广泛的:
首先,部署一个Web服务器专门用来处理HTTP协议层面相关的事情,比如如何在一个物理机上提供多个不同的Web服务(单IP多域名,单IP多端口等)这种事情。然后,部署一个用各种语言编写(Java, PHP, Python, Ruby等)的应用程序,这个应用程序会从Web服务器上接收客户端的请求,处理完成后,再返回响应给Web服务器,最后由Web服务器返回给客户端。
那么 Web Server 和 Application 之间就要知道如何进行交互。为了定义Web服务器和应用程序之间的交互过程,就形成了很多不同的规范。比如改进CGI性能的FasgCGI,Java专用的Servlet规范,还有Python专用的WSGI规范等。提出这些规范的目的就是为了定义统一的标准,提升程序的可移植性。
WSGI原理WSGI 相当于 Web Server 和 Python Application 之间的桥梁,隐藏了很多HTTP相关的细节。其存在的目的有两个:
让 Web Server 知道如何调用 Python Application,并且将 Client Request 传递给 Application。让 Python Application 能理解 Client Request 并执行对应操作,以及将执行结果返回给 Web Server,最终响应到Client。 Server 调用 Application从上图可知,Server端调用Application端必须以WSGI为桥梁,因此WSGI定义了application可调用对象以提供Server端和Application端通信,这个可调用对象既可以是函数也可以是类。
# 函数形式def application(environ, start_response): ''' doing something ''' return [response_body]# 类形式class Application:def __init__(self, environ, start_response):self.environ = environ self.start = start_response def __iter__(self):status = ‘200 OK’ response_headers = [(‘Content-type’, ‘text/plain’)] self.start(status, response_headers) yield HELLO_WORLD# 上面的类形式是将“Application”类作为服务端调用的application,# 调用这个类会返回它的实例,其结果会返回规范中要求的可迭代响应值。# 如果要使用这个类的实例作为application对象,就需要实现__call__方法,服务端会调用这个实例去执行应用。# 下面是pecan的实现方法。class Pecanbase(object): def __init__(self, root, default_renderer='mako', template_path='templates', hooks=lambda: [], custom_renderers={}, extra_template_vars={}, force_canonical=True, guess_content_type_from_ext=True, context_local_factory=None, request_cls=Request, response_cls=Response, **kw): ''' 省略 ''' def __call__(self, environ, start_response): ''' Implements the WSGI specification for Pecan applications, utilizing ``WebOb``. ''' # create the request and response object req = self.request_cls(environ) resp = self.response_cls() state = RoutingState(req, resp, self) environ['pecan.locals'] = { 'request': req, 'response': resp } controller = None # track internal redirects internal_redirect = False # handle the request try: # add context and environment to the request req.context = environ.get('pecan.recursive.context', {}) req.pecan = dict(content_type=None) controller, args, kwargs = self.find_controller(state) self.invoke_controller(controller, args, kwargs, state) except Exception as e: # if this is an HTTP Exception, set it as the response if isinstance(e, exc.HTTPException): # if the client asked for JSON, do our best to provide it accept_header = acceptparse.create_accept_header( getattr(req.accept, 'header_value', '**') offers = accept_header.acceptable_offers( ('text/plain', 'text/html', 'application/json')) best_match = offers[0][0] if offers else None state.response = e if best_match == 'application/json': json_body = dumps({ 'code': e.status_int, 'title': e.title, 'description': e.detail }) if isinstance(json_body, six.text_type): e.text = json_body else: e.body = json_body state.response.content_type = best_match environ['pecan.original_exception'] = e # note if this is an internal redirect internal_redirect = isinstance(e, ForwardRequestException) # if this is not an internal redirect, run error hooks on_error_result = None if not internal_redirect: on_error_result = self.handle_hooks( self.determine_hooks(state.controller), 'on_error', state, e ) # if the on_error handler returned a Response, use it. if isinstance(on_error_result, WebObResponse): state.response = on_error_result else: if not isinstance(e, exc.HTTPException): raise # if this is an HTTP 405, attempt to specify an Allow header if isinstance(e, exc.HTTPMethodNotAllowed) and controller: allowed_methods = _cfg(controller).get('allowed_methods', []) if allowed_methods: state.response.allow = sorted(allowed_methods) finally: # if this is not an internal redirect, run "after" hooks if not internal_redirect: self.handle_hooks( self.determine_hooks(state.controller), 'after', state ) self._handle_empty_response_body(state) # get the response return state.response(environ, start_response)
这个可调用对象需要满足两个条件:
两个参数
一个dict对象,Web Server会将HTTP请求相关的信息添加到这个字典中,供Web application使用一个callback函数,Web application通过这个函数将HTTP status code和headers发送给Web Server 以字符串的形式返回response,并且包含在可迭代的list中
Server端将http请求相关信息、wsgi变量以及一些服务端环境变量添加到environ传给Application端,Application端处理完所需信息后将http状态码和header通过start_response回调函数传给Server端,而http响应body则以返回值的形式传给服务端。
可以看出,仅仅一个application(environ, start_response)仍然显得太底层,在web应用开发过程中效率不高,因此衍生出各种 Web 框架来帮助开发人员快速的开发Web应用,开发人员只需要关注业务层逻辑,不需要过多的处理http相关信息。
示例在Python中就有一个WSGI server,提供给开发人员测试使用。
# WSGI server in Pythonfrom wsgiref.simple_server import make_serverdef application(environ, start_response): response_body = ['%s: %s' % (key, value) for key, value in sorted(environ.items())] response_body = 'n'.join(response_body) status = '200 OK' response_headers = [('Content-Type', 'text/plain'), ('Content-Length', str(len(response_body)))] start_response(status, response_headers) return [response_body.encode('utf8')]httpd = make_server( 'localhost', 8080, application )# 请求处理完退出httpd.handle_request()
访问http://localhost:8080返回结果:
COLORTERM: truecolorCONTENT_LENGTH: CONTENT_TYPE: text/plainGATEWAY_INTERFACE: CGI/1.1GIT_ASKPASS: /home/lem/.vscode-server/bin/899d46d82c4c95423fb7e10e68eba52050e30ba3/extensions/git/dist/askpass.shGOPATH: /home/lem/WorkSpace/GOPATHGOROOT: /usr/local/goHOME: /home/lemHOSTTYPE: x86_64HTTP_ACCEPT: text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.9HTTP_ACCEPT_ENCODING: gzip, deflate, brHTTP_ACCEPT_LANGUAGE: zh-CN,zh;q=0.9HTTP_CACHE_CONTROL: max-age=0HTTP_CONNECTION: keep-aliveHTTP_HOST: localhost:8080HTTP_SEC_CH_UA: " Not;A Brand";v="99", "Google Chrome";v="97", "Chromium";v="97"HTTP_SEC_CH_UA_MOBILE: ?0HTTP_SEC_CH_UA_PLATFORM: "Windows"HTTP_SEC_FETCH_DEST: documentHTTP_SEC_FETCH_MODE: navigateHTTP_SEC_FETCH_SITE: noneHTTP_SEC_FETCH_USER: ?1HTTP_UPGRADE_INSECURE_REQUESTS: 1HTTP_USER_AGENT: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/97.0.4692.8 Safari/537.36JAVA_HOME: /usr/local/java8LANG: C.UTF-8LESSCLOSE: /usr/bin/lesspipe %s %sLESSOPEN: | /usr/bin/lesspipe %sLOGNAME: lemMOTD_SHOWN: update-motdNAME: LAPTOP-CAL2DKNCOLDPWD: /home/lemPATH: /home/lem/WorkSpace/github.com/openstack/neutron/.venv/bin:/home/lem/.vscode-server/bin/899d46d82c4c95423fb7e10e68eba52050e30ba3/bin:/home/lem/.local/bin:/home/lem/.local/bin:/usr/local/go/bin:/home/lem/WorkSpace/GOPATH/bin:/usr/local/python3.6/bin:/usr/local/java8/bin:/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin:/usr/games:/usr/local/games:/usr/lib/wsl/lib:/mnt/c/Windows/system32:/mnt/c/Windows:/mnt/c/Windows/System32/Wbem:/mnt/c/Windows/System32/WindowsPowerShell/v1.0/:/mnt/c/Windows/System32/OpenSSH/:/mnt/c/Program Files (x86)/NVIDIA Corporation/PhysX/Common:/mnt/c/Program Files/NVIDIA Corporation/NVIDIA NvDLISR:/mnt/c/Users/lem/AppData/Local/Microsoft/WindowsApps:/mnt/d/WorkProgram/Microsoft VS Code/bin:/mnt/c/Users/lem/AppData/Local/GitHubDesktop/bin:/snap/binPATH_INFO: /PS1: (.venv) [e]0;u@h: wa]${debian_chroot:+($debian_chroot)}[33[01;32m]u@h[33[00m]:[33[01;34m]w[33[00m]$ PWD: /home/lem/WorkSpace/github.com/openstack/neutronPYTHON36_HOME: /usr/local/python3.6QUERY_STRING: REMOTE_ADDR: 127.0.0.1REMOTE_HOST: REQUEST_METHOD: GETscript_NAME: SERVER_NAME: localhostSERVER_PORT: 8080SERVER_PROTOCOL: HTTP/1.1SERVER_SOFTWARE: WSGIServer/0.2SHELL: /bin/bashSHLVL: 1TERM: xterm-256colorTERM_PROGRAM: vscodeTERM_PROGRAM_VERSION: 1.63.2USER: lemVIRTUAL_ENV: /home/lem/WorkSpace/github.com/openstack/neutron/.venvVSCODE_GIT_ASKPASS_EXTRA_ARGS: VSCODE_GIT_ASKPASS_MAIN: /home/lem/.vscode-server/bin/899d46d82c4c95423fb7e10e68eba52050e30ba3/extensions/git/dist/askpass-main.jsVSCODE_GIT_ASKPASS_NODE: /home/lem/.vscode-server/bin/899d46d82c4c95423fb7e10e68eba52050e30ba3/nodeVSCODE_GIT_IPC_HANDLE: /tmp/vscode-git-4b78d7aee1.sockVSCODE_IPC_HOOK_CLI: /tmp/vscode-ipc-8d0c519f-65dd-4046-9d4f-7e4c1a4a79ce.sockWSLENV: VSCODE_WSL_EXT_LOCATION/upWSL_DISTRO_NAME: Ubuntu-20.04WSL_INTEROP: /run/WSL/11_interopXDG_DATA_DIRS: /usr/local/share:/usr/share:/var/lib/snapd/desktop_: /home/lem/WorkSpace/github.com/openstack/neutron/.venv/bin/pythonwsgi.errors: <_io.TextIOWrapper name='
environ字典包含了一些CGI规范要求的数据,以及WSGI规范新增的数据,还可能包含一些操作系统的环境变量以及Web服务器相关的环境变量。
CGI规范中要求的变量:
REQUEST_METHOD: 请求方法,是个字符串,‘GET’, 'POST’等script_NAME: HTTP请求的path中的用于查找到application对象的部分,比如Web服务器可以根据path的一部分来决定请求由哪个virtual host处理PATH_INFO: HTTP请求的path中剩余的部分,也就是application要处理的部分QUERY_STRING: HTTP请求中的查询字符串,URL中?后面的内容CONTENT_TYPE: HTTP headers中的content-type内容CONTENT_LENGTH: HTTP headers中的content-length内容SERVER_NAME和SERVER_PORT: 服务器名和端口,这两个值和前面的script_NAME, PATH_INFO拼起来可以得到完整的URL路径SERVER_PROTOCOL: HTTP协议版本,HTTP/1.0或者HTTP/1.1HTTP_: 和HTTP请求中的headers对应。
WSGI规范中相关变量:
wsgi.version:表示WSGI版本,一个元组(1, 0),表示版本1.0wsgi.url_scheme:http或者httpswsgi.input:一个类文件的输入流,application可以通过这个获取HTTP request bodywsgi.errors:一个输出流,当应用程序出错时,可以将错误信息写入这里wsgi.multithread:当application对象可能被多个线程同时调用时,这个值需要为Truewsgi.multiprocess:当application对象可能被多个进程同时调用时,这个值需要为Truewsgi.run_once:当server期望application对象在进程的生命周期内只被调用一次时,该值为True start_resposne参数
start_response是一个可调用对象,接收两个必选参数和一个可选参数:
status: 一个字符串,表示HTTP响应状态字符串response_headers: 一个列表,包含有如下形式的元组:(header_name, header_value),用来表示HTTP响应的headersexc_info(可选): 用于出错时,server需要返回给浏览器的信息 application对象的返回值
application对象的返回值用于为HTTP响应提供body,如果没有body,那么可以返回None。如果有body,那么需要返回一个可迭代的对象。server端通过遍历这个可迭代对象可以获得body的全部内容。
WSGI的实现和部署要使用WSGI,需要分别实现server端和application端。
Application端的实现一般是由Python的各种框架来实现的,比如Django, web.py等,一般开发者不需要关心WSGI的实现,框架会会提供接口让开发者获取HTTP请求的内容以及发送HTTP响应。
Server端的实现会比较复杂一点,这个主要是因为软件架构的原因。一般常用的Web服务器,如Apache和nginx,都不会内置WSGI的支持,而是通过扩展来完成。比如Apache服务器,会通过扩展模块mod_wsgi来支持WSGI。Apache和mod_wsgi之间通过程序内部接口传递信息,mod_wsgi会实现WSGI的server端、进程管理以及对application的调用。Nginx上一般是用proxy的方式,用nginx的协议将请求封装好,发送给应用服务器,比如uWSGI,应用服务器会实现WSGI的服务端、进程管理以及对application的调用。
参考资料:
WSGI简介