Web开发:CGI和WSGI
1、CGI:通用网关接口
CGI 是Web 服务器运行时外部程序的规范,按CGI 编写的程序可以扩展服务器功能。CGI 应用程序能与浏览器进行交互,还可通过数据库API 与数据库服务器等外部数据源进行通信,从数据库服务器中获取数据。格式化为HTML文档后,发送给浏览器,也可以将从浏览器获得的数据放到数据库中。几乎所有服务器都支持CGI,可用任何语言编写CGI,包括流行的C、C ++、VB 和Delphi 等。CGI 分为标准CGI 和间接CGI两种。标准CGI 使用命令行参数或环境变量表示服务器的详细请求,服务器与浏览器通信采用标准输入输出方式。间接CGI 又称缓冲CGI,在CGI 程序和CGI 接口之间插入一个缓冲程序,缓冲程序与CGI 接口间用标准输入输出进行通信。
2、WSGI
2.1、动机(替代CGI)
现在读者已经对 CGI 有深入的了解,并知道为什么需要 CGI。因为服务器无法创建动态内容,它们不知道用户特定的应用信息和数据,如验证信息、银行账户、在线支付等。 Web服务器必须与外部的进程通信才能处理这些自定义工作。本章前 2/3 部分讨论了 CGI 如何解决这个问题,同时也介绍了其工作原理。但是这种方式无法扩展, CGI 进程(类似 Python 解释器)针对每个请求进行创建,用完就抛弃。如果应用程序接收数千个请求,创建大量的语言解释器进程很快就会导致服务器停机。有两种方法可以解决这个问题,一是服务器集成, 二是外部进程。下面分别介绍这两种方法。
2.2、服务器集成
服务器集成,也称为服务器 API。这其中包括如 Netscape 服务器应用编程接口(NSAPI)和微软的因特网服务器编程接口(ISAPI)这样的商业解决方案。当前(从 20 世纪 90 年代中期开始)应用最广泛的服务器解决方案是 Apache HTTP Web 服务器,这是一个开源的解决方案,通常称为 Apache,拥有一个服务器 API。另外,使用术语模块来描述服务器上插入的编译后的组件,这些组件可以扩展服务器的功能和用途。
所有这三个针对 CGI 性能的解决方案都将网关集成进服务器。换句话说,不是将服务器切分成多个语言解释器来分别处理请求,而是生成函数调用,运行应用程序代码,在运行过程中进行响应。服务器根据对应的 API 通过一组预先创建的进程或线程来处理工作。大部分可以根据所支持应用的需求进行相应的调整。例如,服务器一般还会提供压缩数据、安全、代理、虚拟主机等功能。
当然,任何方案都有缺点,对于服务器 API,这种方式会带来许多问题,如含有 bug 的代码会影响到服务器实现执行效率,不同语言的实现无法完全兼容,需要 API 开发者使用与Web 服务器实现相同的编程语言,应用程序需要整合到商业解决方案中(如果没有使用开源服务器 API),应用程序必须是线程安全的,等等。
2.3、外部进程
另一个解决方案是外部进程。这是让 CGI 应用在服务器外部运行。当有请求进入时,服务器将这个请求传递到外部进程中。这种方式的可扩展性比纯 CGI 要好,因为外部进程存在的时间很长,而不是处理完单个请求后就终止。使用外部进程最广为人知的解决方案是FastCGI。有了外部进程,就可以利用服务器 API 的好处,同时避免了其缺点。比如,在服务器外部运行就可以自由选择实现语言,应用程序的缺陷不会影响到 Web 服务器,不需要必须与闭源的商业软件结合起来。
很自然, FastCGI 有 Python 实现,除此之外还有 Apache 的其他 Python 模块(如 PyApache、mod_snkae、 mod_python 等),其中有些已经不再维护了。所有这些模块加上纯 CGI 解决方案,组成了各种 Web 服务器 API 网关解决方案,以调用 Python Web 应用程序。
由于使用了不同的调用机制,所以开发者有了新的负担。不仅要开发应用本身,还要决定与 Web 服务器的集成。实际上,在编写应用时,就需要完全知道最后会使用哪个机制,并以相应的方式执行。
对于 Web 框架开发者,问题就更加突出了,由于需要给予用户最大的灵活性。如果不想强迫他们开发多版本的应用,就必须为所有服务器解决方案提供接口,以此来让更多的用户采用你的框架。这个困境看起来绝不是 Python 的风格, 就导致了 Web 服务器网类接口(WebServer Gateway Interface, WSGI)标准的建立。
2.4、WSGI简介
WSGI 不是服务器,也不是用于与程序交互的 API,更不是真实的代码,而只是定义的一个接口。 WSGI 规范作为 PEP 333 于 2003 年创建,用于处理日益增多的不同 Web 框架、Web 服务器,及其他调用方式(如纯 CGI、服务器 API、外部进程)。
其目标是在 Web 服务器和 Web 框架层之间提供一个通用的 API 标准,减少之间的互操作性并形成统一的调用方式。 WSGI 刚出现就得到了广泛应用。基本上所有基于 Python 的Web 服务器都兼容 WSGI。将 WSGI 作为标准对应用开发者、框架作者和社区都有帮助。
根据 WSGI 定义,其应用是可调用的对象,其参数固定为以下两个:一个是含有服务器环境变量的字典,另一个是可调用对象,该对象使用 HTTP 状态码和会返回给客户端的 HTTP头来初始化响应。这个可调用对象必须返回一个可迭代对象用于组成响应负载
在下面这个 WSGI 应用的“Hello World”示例中,这些内容分别命名为 environ 变量和start_response()。
def simple_wsgi_app(environ, start_response): status = '200 OK' headers = [('Content-type', 'text/plain')] start_response(status, headers) return ['Hello world!']
environ 变量包含一些熟悉的环境变量,如 HTTP_HOST、 HTTP_USER_AGENT、SERVER_PROTOCOL 等。而 start_response()这个可调用对象必须在应用执行,生成最终会发送回客户端的响应。响应必须含有 HTTP 返回码(200、 300 等),以及 HTTP 响应头。
建议使用 WSGI 时只返回可迭代对象,让 Web 服务器负责管理数据并返回给客户端(而不是让应用程序处理这些不精通的事情)。由于这些原因,大多数应用并不使用或保存 start_response()的返回值,只是简单将其抛弃。
关于 start_response()最后一件事是其第三个参数,这是个可选参数,这个参数含有异常信息,通常大家知道其缩写 exc_info。如果应用将 HTTP 头设置为“200 OK”(但还没有发送),并且在执行过程中遇到问题,则可以将 HTTP 头改成其他内容,如“ 403Forbidden”或“500 Internal Server Error”。
为了做到这一点,可以假设应用使用一对正常的参数开始执行。当发生错误时,会再次调用 start_response(),但会将新的状态码与 HTTP 头和 exc_info 一起传入,替换原有的内容。
如果第二次调用时 start_response()没有提供 exc_info,则会发生错误。而且必须在发送HTTP 头之前第二次调用 start_response()。如果发送完 HTTP 头才调用,则必须抛出一个异常,抛出类似 exc_info[0]、 exc_info[1]或 exc_info[2]等内容。
2.5、WSGI服务器
在服务器端,必须调用应用(前面已经介绍了),传入环境变量和 start_response()这个可调用对象,接着等待应用执行完毕。在执行完成后,必须获得返回的可迭代对象,将这些数据返回给客户端。在下面这段代码中,给出了一个具有简单功能的例子,这个例子演示了WSGI 服务器看起来会是什么样子的。
import StringIO import sys def run_wsgi_app(app, environ): body = StringIO.StringIO() def start_response(status, headers): body.write('Status: %s\r\n' % status) for header in headers: body.write('%s: %s\r\n' % header) return body.write iterable = app(environ, start_response) try: if not body.getvalue(): raise RuntimeError("start_response() not called by app!") body.write('\r\n%s\r\n' % '\r\n'.join(line for line in iterable)) finally: if hasattr(iterable, 'close') and callable(iterable.close): iterable.close() sys.stdout.write(body.getvalue()) sys.stdout.flush()
底层的服务器/网关会获得开发者提供的应用程序,将其与 envrion 字典放在一起,envrion 字典含有 os.environ()中的内容,以及 WSGI 相关的 wsig.*环境变量(参见 PEP,但不包括 wsgi.input、 wsig.errors、 wsgi.version 等元素),以及任何框架或中间件环境变量(下面会引入更多中间件)。使用这些内容来调用 run_wsgi_app(),该函数将响应传送给客户端 。
事实上,应用开发者不会在意这些细节。如创建一个提供 WSGI 规范的服务器,并为应用程序提供一致的执行框架。从前面的例子中可以看到, WSGI 在应用端和服务器端有明显的界线。任何应用都可以传递到上面描述的服务器(或任何其他 WSGI 服务器)中。同样,在任何应用中,无须关心哪种服务器会调用这个应用。只须在意当前的环境,以及将数据返回给客户端之前需要执行的 start_response()可调用对象。
2.6、WSGI应用示例
前面已经提到, WSGI 现在已经是标准了,几乎所有 Python Web 框架都支持它,虽然有些从表面上看不出来。例如, Google App Engine 处理程序类,在正常导入后,可能看到如下代码。
class MainHandler(webapp.RequestHandler): def get(self): self.response.out.write('Hello world!') application = webapp.WSGIApplication([('/', MainHandler)], debug=True) run_wsgi_app(application)
不是所有框架都是这种模式,但可以清楚地看到 WSGI 的引用。为了进一步比较,可以深入底层,在 App Engine 的 Python SDK 中,以及在 webapp 子包的 util.py 模块中,可以看到 run_bare_wsgi_app()函数。其中的代码与 simple_wsgi_app()非常像。
2.7、中间件及封装 WSGI 应用
在某些情况下,除了运行应用本身之外,还想在应用执行之前(处理请求)或之后(发送响应)添加一些处理程序。这就是熟知的中间件,它用于在 Web 服务器和 Web 应用之间添加额外的功能。中间件要么对来自用户的数据进行预处理,然后发送给应用;要么在应用将响应负载返回给用户之前,对结果数据进行一些最终的调整。这种方式类似洋葱结构,应用程序在内部,而额外的处理层在周围
。
预处理可以包括动作,如拦截、修改、添加、移除请求参数,修改环境变量(包括用户提交的表单(CGI)变量),使用 URL 路径分派应用的功能,转发或重定向请求,通过入站客户端 IP 地址对网络流量进行负载平衡,委托其功能(如使用 User-Agent 头向移动用户发送简化过的 UI/应用),以及其他功能。
而后期处理主要包括调整应用程序的输出。下面这个示例类似第 2 章创建的时间戳服务器,其中对于应用返回的每一行结果,都会添加一个时间戳。在实际中,这会更加复杂,但大致方式都相同,如添加将应用的输出转为大写或小写的功能。这里使用 ts_simple_wsgi_app()封装了 simple_wsgi_app(),将前者作为应用注册到服务器中。
#!/usr/bin/env python from time import ctime from wsgiref.simple_server import make_server def ts_simple_wsgi_app(environ, start_response): return ('[%s] %s' % (ctime(), x) for x in \ simple_wsgi_app(environ, start_response)) httpd = make_server('', 8000, ts_simple_wsgi_app) print "Started app serving on port 8000..." httpd.serve_forever()
如果需要进行更多的处理工作,可以使用类封装,而不是前面的函数封装。此外,由于添加了封装的类和方法,因此还可以将 environ 和 start_response()整合到一个元组变量中,使用这个元组作为参数来减少代码量(见下面示例中的 stuff)。
class Ts_ci_wrapp(object): def __init__(self, app): self.orig_app = app def __call__(self, *stuff): return ('[%s] %s' % (ctime(), x) for x in self.orig_app(*stuff)) httpd = make_server('', 8000, Ts_ci_wrapp(simple_wsgi_app)) print "Started app serving on port 8000..." httpd.serve_forever()
这个类命名为 Ts_ci_wrapp,这是“timestamp callable instance wrapped application”的简称,该类会在创建服务器时实例化。其初始化函数将原先的应用作为输入,并缓存它以备后用。当服务器执行应用程序时,和之前一样,它依然传入 environ 字典和 start_response()可调用对象。由于做了一些改动,程序会调用示例本身(因为定义了__call__()方法)。 environ 和start_response()都会通过 stuff 传递给原先的应用。
尽管在这里使用了可调用实例,而前面使用的是函数,但也可以使用其他可调用对象。还要记住,后面几个例子都没有以任何方式修改 simple_wsgi_app()。 WSGI 的主旨是在 Web 应用和 Web 服务器之间做了明显的分割。这样有利于分块开发,让团队更方便地划分任务,让 Web应用能以一致且灵活的方式在任何兼容 WSGI 的后端中运行。同时无论用户使用(Web)服务器软件运行什么应用程序, Web 服务器开发者都无须处理任何自定义或特定的 hook。
2.8、现实世界中的 Web 开发
CGI 是过去用来开发 Web 的方式,其引入的概念至今仍应用于 Web 开发当中。因此,这就是为什么在这里花时间学习 CGI。而对 WSGI 的介绍让读者更接近真实的开发流程。
今天, Python Web 开发新手的选择余地很多。除了著名的 Django、 Pyramid 和 Google AppEngine 这些 Web 框架之外,用户还可以在其他众多的框架中选择。实际上,框架甚至都不是必需的,直接使用 Python,不用任何其他额外的工具或框架特性,就能开发出兼容 WSGI 的Web 服务器。但最好继续使用框架,这样可以方便地用到框架提供的其他 Web 功能。
现代的 Web 执行环境一般由多线程或多进程模型、认证/安全 cookie、基本的用户验证、会话管理组成。普通应用程序的开发者都会了解这其中大部分内容。验证表示的是用户通过用户名和密码进行登录, cookie 用来维护用户信息,会话管理有时候也是如此。为了使应用具有可扩展性, Web 服务器应当能够处理多个用户的请求。因此,需要用到多线程或多进程。但会话在这里还没有完全涉及。
如果读者阅读本章中运行在服务器上的所有应用程序代码,那么就需要说明一下,脚本从头到尾执行一次,服务器循环永远执行, Web 应用(Java 中称为 servlet)针对每个请求执行。代码中不会保存状态信息,前面已经提到过, HTTP 是无状态的。换句话说,数据是不会保存到局部或全局变量中,或以其他方式保存起来。这就相当于把一个请求当成一个事务。 每次来了一个事务,就进行处理,处理完就结束,在代码库中不保存任何信息。
此时就需要用到会话管理,会话管理一段时间内在一个或多个请求之间保存用户的状态。一般来说,这是通过某种形式的持久存储完成的,如缓存、平面文件甚至是数据库。开发者需要自己处理这些内容,特别是编写底层代码时,本章中已经见到过这些代码。毫无疑问,已经做了很多无用功,因此许多著名的大型Web框架,包括Django,都有自己的会话管理软件(下一章将介绍Django的相关内容。)
3、environ与start_response
3.1、自定义框架
自定义框架源码:
from wsgiref.simple_server import make_server from jinja2 import Template def index(): ret = "index ok" return [bytes(ret, encoding="utf8"), ] def home(): with open("yimi.html", "rb") as f: data = f.read() return [data, ] # 定义一个url和函数的对应关系 URL_LIST = [ ("/index/", index), ("/home/", home), ] def run_server(environ, start_response): with open("environ.txt","a+") as f: for i in environ: f.write("%s:%s\n"%(i,environ[i])) f.write("*"*50+"\n") print("type start_response", type(start_response)) start_response('200 OK', [('Content-Type', 'text/html;charset=utf8'), ]) # 设置HTTP响应的状态码和头信息 url = environ['PATH_INFO'] # 取到用户输入的url func = None # 将要执行的函数 for i in URL_LIST: if i[0] == url: func = i[1] # 去之前定义好的url列表里找url应该执行的函数 break if func: # 如果能找到要执行的函数 return func() # 返回函数的执行结果 else: return [bytes("404没有该页面", encoding="utf8"), ] if __name__ == '__main__': httpd = make_server('', 8008, run_server) print("Serving HTTP on port 8008...") httpd.serve_forever()
注意:网络传输使用的是bytes类型,因此要把数据转成bytes类型。
1、如果只含有ASCII表中字符,直接使用:b"ok"
2、如果含有非ASCII表中字符,则必须编码成utf-8:
bytes("你好", encoding="utf-8") 或 "你好".encode(encoding="utf-8") 或 "你好".encode("utf-8") ;
例如:阿尔法(希腊字母)也要转成uft-8格式,"α".encode("utf-8")。
3.1.1、自定义框架中的environ
# # ALLUSERSPROFILE:C:\ProgramData REMOTE_ADDR:127.0.0.1 CONTENT_TYPE:text/plain HTTP_HOST:127.0.0.1:8008 HTTP_CONNECTION:keep-alive HTTP_USER_AGENT:Mozilla/5.0 (Windows NT 6.1; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/69.0.3497.100 Safari/537.36 HTTP_ACCEPT:image/webp,image/apng,image/*,*/*;q=0.8 HTTP_REFERER:http://127.0.0.1:8008/index/ HTTP_ACCEPT_ENCODING:gzip, deflate, br HTTP_ACCEPT_LANGUAGE:zh-CN,zh;q=0.9,en;q=0.8 wsgi.input:<_io.BufferedReader name=632> wsgi.errors:<_io.TextIOWrapper name='<stderr>' mode='w' encoding='utf-8'> wsgi.version:(1, 0) wsgi.run_once:False wsgi.url_scheme:http wsgi.multithread:True wsgi.multiprocess:False wsgi.file_wrapper:<class 'wsgiref.util.FileWrapper'>
3.1.2、自定义框架中的start_response
start_response在python模块wsgiref的BaseHandler类中,定义如下:
def start_response(self, status, headers,exc_info=None): """'start_response()' callable as specified by PEP 3333""" if exc_info: try: if self.headers_sent: # Re-raise original exception if headers sent raise exc_info[0](exc_info[1]).with_traceback(exc_info[2]) finally: exc_info = None # avoid dangling circular ref elif self.headers is not None: raise AssertionError("Headers already set!") self.status = status self.headers = self.headers_class(headers) status = self._convert_string_type(status, "Status") assert len(status)>=4,"Status must be at least 4 characters" assert status[:3].isdigit(), "Status message must begin w/3-digit code" assert status[3]==" ", "Status message must have a space after code" if __debug__: for name, val in headers: name = self._convert_string_type(name, "Header name") val = self._convert_string_type(val, "Header value") assert not is_hop_by_hop(name),"Hop-by-hop headers not allowed" return self.write
3.2、Django框架
3.2.1、Django中request.environ
# # ALLUSERSPROFILE:C:\ProgramData GATEWAY_INTERFACE:CGI/1.1 SERVER_PORT:8000 REMOTE_HOST: CONTENT_LENGTH: SCRIPT_NAME: SERVER_PROTOCOL:HTTP/1.1 SERVER_SOFTWARE:WSGIServer/0.2 REQUEST_METHOD:GET PATH_INFO:/index/ QUERY_STRING: REMOTE_ADDR:127.0.0.1 CONTENT_TYPE:text/plain HTTP_HOST:127.0.0.1:8000 HTTP_CONNECTION:keep-alive HTTP_CACHE_CONTROL:max-age=0 HTTP_UPGRADE_INSECURE_REQUESTS:1 HTTP_USER_AGENT:Mozilla/5.0 (Windows NT 6.1; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/69.0.3497.100 Safari/537.36 HTTP_ACCEPT:text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,image/apng,*/*;q=0.8 HTTP_ACCEPT_ENCODING:gzip, deflate, br HTTP_ACCEPT_LANGUAGE:zh-CN,zh;q=0.9,en;q=0.8 wsgi.input:<_io.BufferedReader name=728> wsgi.errors:<_io.TextIOWrapper name='<stderr>' mode='w' encoding='UTF-8'> wsgi.version:(1, 0) wsgi.run_once:False wsgi.url_scheme:http wsgi.multithread:True wsgi.multiprocess:False wsgi.file_wrapper:<class 'wsgiref.util.FileWrapper'>
3.2.2、Django中的HttpResponse()对象
response = HttpResponse("ok") print("response:",response)
输出结果:
response: <HttpResponse status_code=200, "text/html; charset=utf-8">
使用dir()获取response对象的属性、方法列表,输出如下:
__bytes__ __class__ __contains__ __delattr__ __delitem__ __dict__ __dir__ __doc__ __eq__ __format__ __ge__ __getattribute__ __getitem__ __gt__ __hash__ __init__ __init_subclass__ __iter__ __le__ __lt__ __module__ __ne__ __new__ __reduce__ __reduce_ex__ __repr__ __setattr__ __setitem__ __sizeof__ __str__ __subclasshook__ __weakref__ _charset _closable_objects _container _content_type_for_repr _convert_to_charset _handler_class _headers _reason_phrase charset close closed content cookies delete_cookie flush get getvalue has_header items make_bytes readable reason_phrase seekable serialize serialize_headers set_cookie set_signed_cookie setdefault status_code streaming tell writable write writelines
response对象中含有__iter__方法,因此response对象是可迭代对象。