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_HOSTHTTP_USER_AGENTSERVER_PROTOCOL 等。而 start_response()这个可调用对象必须在应用执行,生成最终会发送回客户端的响应。响应必须含有 HTTP 返回码(200300 等),以及 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
' % status)
              for header in headers:
                        body.write('%s: %s
' % header)
                return body.write
                
        iterable = app(environ, start_response)
        try:
                if not body.getvalue():
                        raise RuntimeError("start_response() not called by app!")
                body.write('
%s
' % '
'.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.inputwsig.errorswsgi.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 开发新手的选择余地很多。除了著名的 DjangoPyramid 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
"%(i,environ[i]))
        f.write("*"*50+"
")

    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()
View Code

   注意:网络传输使用的是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'>
View Code

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
View Code

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'>
View Code

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
View Code

  response对象中含有__iter__方法,因此response对象是可迭代对象。

原文地址:https://www.cnblogs.com/bad-robot/p/9737382.html