理解CGI,FastCGI,SCGI和WSGI

-- TOC --

本文简要介绍CGI,FastCGI,SCGI,WSGI,以求达到理解清楚其概念的程度。

CGI

CGI,Common Gateway Interface,是一种Web Server与本机上的外部程序通信的机制,用来产生动态网页内容。它是最早的动态网页解决方案。

每当客户端访问一个指向CGI程序的URL,Web Server收到后,就会创建一个新的子进程,以执行此CGI程序。CGI程序执行完毕后,向Web Server输出它的执行结果,子进程退出,然后Server将这些结果返回给客户端浏览器显示。

可能是因为很多CGI程序都是用脚本语言写的,所以很多人会说CGI脚本,但CGI可以是用C写的程序。

CGI模式的开销相对较大,因为它是经典的Fork-and-Execution模式,Web Server会为每一个CGI脚本的请求,以创建子进程的方式执行。所以,当请求非常多的时候,服务器上运行CGI脚本的子进程数量也会很多,而且子进程不停地在创建和退出。现在一般生产环境不会使用CGI模式,但是测试环境用的很多,因为很方便。Web Server对CGI的支持是最广泛的,而且配置相对简单。

cgi

上图中红色的XX,表示CGI子进程处理完毕后退出。

CGI还有一个优点,即你可以使用任何编程语言来编写CGI脚本或程序,没有限制。CGI程序的stdout被重定向,写入stdout,都是返回给Web Server的内容。

配置CGI: 如何在Httpd服务器上配置CGI

FastCGI(FCGI)

FastCGI,就是快速的CGI。

CGI的慢,在于收到请求后再创建和执行子进程。而FastCGI不用等待请求到来后再创建子进程,而是直接先启动一大堆子进程,然后等待请求的到来。这些子进程处理完请求后,并不会退出,而是继续等待下一个请求。因此,这样的机制就省掉了不断地重复创建子进程的消耗,整体效率比CGI要高,但是Web Server也会一直占用更多的内存。

fastcgi

FastCGI的实现,有一个Server,用来接收Web Server转发的请求。FCGI is a lot like proxying. 在CGI中,Web Server复杂根据请求启动CGI进程。在FCGI中,Web Server只是转发请求给FCGI Server Process。

上图中的socket,应该是指UNIX socket,用于同一主机内进程间通信。

SCGI

Simple CGI,简单的CGI,它是FastCGI在精简数据协议和响应过程后的产物。

SCGI设计目的是为了适应越来越多基于AJAX或REST的HTTP请求,而做出更快更简洁的应答。并且SCGI约定,当服务器返回对一个HTTP协议请求响应后,立刻关闭该HTTP的TCP连接。所以不难看出,SCGI更加适合于普遍意义上SOA所提倡的“请求-忘记”这种通信模式。

似乎网络上关于SCGI的内容不多,上面这段话好多地方都能看到。更多关于SCGI的内容:SCGI - http://en.wikipedia.org/wiki/SCGI

从原理上来看,SCGI和FastCGI类似,二者的性能并无多大差别。但比起后者复杂的协议内容来说,SCGI移除了许多非必要的功能,看起来十分简洁,且实现复杂度更低。

与FastCGI类似,一个SCGI服务器可以动态创建服务器子进程用于处理更多请求(处理完毕将转入睡眠),直至达到配置的子进程上限。当获得Web服务器转发的请求时,SCGI服务器进程会将其转发至子进程,并由子进程运行CGI程序处理该请求。此外,SCGI还能自动销毁退出和崩溃的子进程,具有良好的稳定性。

WSGI

WSGI是Python官方定义的一个Web Server与Python App之间的接口。在Web Server与Python程序之间,一般都还有一个Web Framework,比如Django,Flask。这些框架都是基于WSGI协议,Web Server要支持WSGI,现在基本都需要安装额外的模块。

Python官方之所以这样做,是为了子系统之间的解耦!Python的Web框架不必将自己限制在某一种特定的接口上,Python App在编写的时候,也不需要过度考虑使用什么样的Web Server。各自通过标准接口通信,各自发展壮大。

WSGI是一个函数调用接口规范,因此它不涉及在进程还是在线程中调用的问题,都可以。使用CGI方式可以调用Flask框架下的App,Httpd使用mod_wsgi模块,就是在线程中调用Python。

wsgi只是一个接口规范:

The WSGI application interface is implemented as a callable object: a function, a method, a class or an instance with an object.__call__() method. That callable must:

(1). accept two positional parameters:

接口有两个参数,第一个是dict对象,保存着各种环境变量;第二个是一个callback,用来传递status code and msg和http headers。

(2). return the response body to the server as byte-strings wrapped in an iterable.

将body转换成byte-string,并放在一个可迭代对象中返回。

The application skeleton:

# The application interface is a callable object
def application ( # It accepts two arguments:
    # environ points to a dictionary containing CGI like environment
    # variables which is populated by the server for each
    # received request from the client
    environ,
    # start_response is a callback function supplied by the server
    # which takes the HTTP status and headers as arguments
    start_response
):

    # Build the response body possibly
    # using the supplied environ dictionary
    response_body = 'Request method: %s' % environ['REQUEST_METHOD']

    # HTTP response code and message
    status = '200 OK'

    # HTTP headers expected by the client
    # They must be wrapped as a list of tupled pairs:
    # [(Header name, Header value)].
    response_headers = [
        ('Content-Type', 'text/plain'),
        ('Content-Length', str(len(response_body)))
    ]

    # Send them to the server using the supplied function
    start_response(status, response_headers)

    # Return the response body. Notice it is wrapped
    # in a list although it could be any iterable.
    return [response_body.encode()]

环境变量中,REQUEST_METHOD指定了GET或者POST,PATH_INFO指定了URL,QUERY_STRING?后面的那个字符串,用来传递参数的,可以用cgi.parse_qs()和cgi.escape()来安全获取,POST数据放在wsgi.input字段中,但有可能CONTENT_LENGTH不一定存在!

实现一个简易WSGI APP

WSGI是Python生态圈内特有的名字,它指一个接口规范,Web Server通过此接口规范与Web App交互。

下面是一个简易的WSGI APP的实现,将请求的部分内容返回输出:

$ cat my_wsgi_app.py 
#!/usr/bin/env python3
import urllib.parse as urlparse
from wsgiref.simple_server import make_server


def wsgi_app(environ, start_response):

    # USER AGENT
    user_agent = environ['HTTP_USER_AGENT']
    # GET or POST 
    request_method = environ['REQUEST_METHOD']
    # PATH_INFO
    path_info = environ['PATH_INFO']
    # QUERY_STRING
    query_string = environ['QUERY_STRING']
    qs = urlparse.parse_qs(query_string)
    # CONTENT_LENGTH
    try:
        content_length = int(environ.get('CONTENT_LENGTH', 0))
    except (ValueError):
        content_length = 0
    # CONTENT_TYPE
    content_type = environ['CONTENT_TYPE']

    # When the method is POST the variable will be sent
    # in the HTTP request body which is passed by the WSGI server
    # in the file like wsgi.input environment variable.
    if request_method == 'POST':
        request_body = environ['wsgi.input'].read(content_length)
    else:
        request_body = b''

    #
    response = []
    response.append('USER_AGENT:'     + user_agent)
    response.append('REQUEST_METHOD:' + request_method)
    response.append('PATH_INFO:'      + path_info)
    response.append('QUERY_STRING:'   + query_string)
    response.append('qs:'             + str(qs))
    response.append('CONTENT_LENGTH:' + str(content_length))
    response.append('CONTENT_TYPE:'   + content_type)
    response.append('REQUEST_BODY:'   + request_body.decode())
    response = '\n'.join(response) + '\n'

    status = '200 OK'
    response_headers = [
        ('Content-Type', 'text/plain'),
        ('Content-Length', str(len(response)))
    ]
    start_response(status, response_headers)

    return [response.encode()]


# Instantiate the server
httpd = make_server (
    'localhost', # The host name
    8051,        # A port number where to wait for the request
    wsgi_app     # The application object name, in this case a function
)


httpd.serve_forever()

小结

CGI最简单,开发人员测试必备!WSGI是Python独有的,Python程序员如果做Web开发,必备。

本文链接:https://cs.pynote.net/net/202110015/

-- EOF --

-- MORE --