编写一个最简单的-wsgi-server

2016-04-24 Mithrilwoodrat 更多博文 » 博客 » GitHub »

原文链接 http://woodrat.xyz/2016/04/24/%E7%BC%96%E5%86%99%E4%B8%80%E4%B8%AA%E6%9C%80%E7%AE%80%E5%8D%95%E7%9A%84-wsgi-server/
注:以下为加速网络访问所做的原文缓存,经过重新格式化,可能存在格式方面的问题,或偶有遗漏信息,请以原文为准。


之前没事看了一点 flask 和 werkzeug 的源码, 就想着试一下做一个简单的 WSGI Server。

说到 WSGI, 可以先从 CGI 说起,所谓 CGI(Common Gateway Interface),可以理解为 Web Server 调用本地的可执行文件来参生动态内容的方式。 参考 CGI-维基百科

比如一个 GET 请求,URI 为 /cgi-bin/adder?a=1&b=2。 此时,服务器匹配到特殊的路径 /cgi-bin/ 于是判定这是一个 cgi 请求,请求的文件为 adder。 这时服务器将去寻找指定路径下名为 adder 的文件,检查是否存在是否有权限执行等。检查成功后将通过特殊的方式执行这个文件。

在这个列子中, 服务器需要将请求的 Method, 这里为GET, 还有请求的参数,即 a:1, b:2 传递给 adder。并且需要得到 adder 执行后的结果。

一般的做法是,将 method、 http header 等信息通过环境变量的方式传递给 CGI 程序。 如果是 POST 等带 request body 的方法,则将 request body 传递给 stdin。 然后 CGI 程序将 结果写回 stdout, 服务器从 stdout 读到结果后返回给客户端。

                  +-------------+
                  |             |
                  |             |
                  |  Web Server |
+------------+    |             |
|method      |    +-+---+-----^->
|http headers|      |   |     |
|etc..       +------+   |Fork |result via stdout
+via ENV     |      |   |&    |
+------------+      |   |EXEC |
                  +-v---v-----+---+
                  |               |
                  |               |
                  |   CGI Program |
                  |               |
                  |               |
                  +---------------+

示例程序(修改自 csapp 11-34)

void exec_cgi(int client_fd, char *filename, char*cgiargs) {
    char *emptylist[] = {NULL};
    write(client_fd, "HTTP/1.1 200 OK\r\n");

    if (fork() == 0) {
        setenv("QUERY_STRING", cgiargs, 1);
        dup2(client_fd, STDOUT_FILENO);
        execve(filename, emptylist, envrion);
    }
    wait();
}

类似 CGI, WSGI 是起到连接 Web Server 和 Python Web 框架的作用。 不过WSGI 的目标并不只是像 CGI 一样提供动态内容,还有其他的目标,但是和这里主题不相关,详见 PEP 333 -- Python Web Server Gateway Interface v1.0

WSGI 中,应用/框架这一端,作为被调用者,按照要求需要提供一个接受 environ, start_response 这两个参数的函数/类。

例如

def app(environ, start_response):
    """Simplest possible application object"""
    status = '200 OK'
    response_headers = [('Content-type', 'text/plain')]
    start_response(status, response_headers)
    return ['Hello world!\n']

这个函数的第一个参数为环境变量,第二个参数为一个函数,wsgi 程序需要将状态码和 附加的 header 项 传入这个函数。

WSGI 中,服务器这一端,作为被调用者,按特定的方式去调用 wsgi 程序

import os, sys

def run_with_cgi(application):

    environ = {} # 设定的环境变量,这里略过

    # 传递给 wsgi app 的函数, 获取 status code 和 headers 后构建 response 
    # 返回给客户端, 这里省略具体内容
    def start_response(status, response_headers, exc_info=None):
        pass

    # 调用 wsgi 程序, 获得其返回值, 作为 response body 返回
    result = application(environ, start_response)

Let’s Build A Web Server. Part 2.这篇博客讲述了如何用 Python 构建一个 WSGI Server。各位可以先读一下做参考。

使用 Python 编写的时候调用目标时 import 后执行即可,相对容易一点。这里我们试着使用 Python/C API 来编写。因为之前的博客中提到了如何写一个简单的 HTTP Server,这里就不写如何将结果返回给客户端,只调用相应的 WSGI 程序,观察其运行结果。

代码部分参考 bjoern A screamingly fast Python WSGI server written in C.

程序入口

/* 用于存放返回的结果 */
typedef struct
{
    PyObject* status; /* string */
    PyObject* headers; /* list */
    PyObject* body; /* string */
} Response;

/* 返回到 stdout 的内容在这里直接 print */
void call_app(Response* response);

int main()
{
    /* 需要 import 的 Python 文件的目录,这里设置为和编译后的可执行文件同目录 */
    setenv("PYTHONPATH",".",1); 
    /* 初始化 Python 解释器 */
    Py_Initialize();
    Response response;
    call_app(&response);

    if (response.status != NULL) {
        printf("response->status is %s\n", PyString_AsString(response.status));
    } else {
        printf("response->status is NULL");
    }

    Py_Finalize();
    return 0;
}

初始化运行环境后直接调用了 call_app 函数,传入的是一个名为 Response 的自定义的结构。

call_app

/* 为了方便,这里直接写死了调用的 wsgi app 的文件名和函数名 */
static char *py_app = "app";
static char *py_module = "app";


void call_app(Response* response)
{
    PyObject* moduleString = PyString_FromString(py_app);
    PyObject* app_module = PyImport_Import(moduleString);


    /* get function reference */
    PyObject* app_func = PyObject_GetAttrString(app_module, py_app);
    printf("get app\n");

    /* StartResponse 为自定义的 Python type object */
    StartResponse* start_response = PyObject_NEW(StartResponse, &StartResponse_Type);

    start_response->response = response;

    /* 为了方便,直接传入了固定的环境变量 */
    PyObject* env = PyDict_New();
    PyDict_SetItem(env,
                   PyString_FromString("REQUEST_METHOD"),
                   PyString_FromString("GET"));
    PyDict_SetItem(env,
                   PyString_FromString("PATH_INFO"),
                   PyString_FromString("/"));

    PyObject* args = PyTuple_Pack(2, env, start_response);
    printf("calling app\n");
    PyObject* output = PyObject_CallObject(app_func, args);

    Py_DECREF(start_response);

    if(!PyString_Check(output)) {
        printf("error : app should return string");
        return ;
    }
    /* 查看返回的结果 */
    printf("Application returns:%s\n", PyString_AsString(output));
}

可以看到,在上面的代码中 start_response 既做了被调用的函数, 又做了一个包含 response 结构的结构体。下面可以看到这是如何实现的。

typedef struct 
{
    PyObject_HEAD
    Response* response;
} StartResponse;

PyTypeObject StartResponse_Type = {
  PyVarObject_HEAD_INIT(NULL, 0)
  "start_response",           /* tp_name (__name__)                         */
  sizeof(StartResponse),      /* tp_basicsize                               */
  0,                          /* tp_itemsize                                */
  (destructor)PyObject_FREE,  /* tp_dealloc                                 */
  0, 0, 0, 0, 0, 0, 0, 0, 0,  /* tp_{print,getattr,setattr,compare,...}     */
  start_response              /* tp_call (__call__)                          */
};

可以看到 StartRespnse 是一个自定义的 Python 类, 包含 Response* 的数据。 并且在类型定义中定义了 call 的方法,所以能作为函数被调用,这里也可以给它添加一个名为 start_response 的成员函数。 关于 Python/C API 中自定义数据结构参考 define a new type(class) in c

下面看一下这个 start_response 函数究竟如何实现的。

start_response

static PyObject*
start_response(PyObject* self, PyObject *args, PyObject *kwargs)
{
    /* 从类中获取 response 的指针  */
    Response* response = ((StartResponse*) self)->response;

    PyObject* status = NULL;
    PyObject* headers = NULL;

    if(!PyArg_UnpackTuple(args, "start_response", 2, 2, &status, &headers))
        return NULL;

    if(!PyString_Check(status)) {
        printf("start_response argument 1 should be a 'status reason' string\n");
        return NULL;
    }
    /* 将传入的 status 变量赋给 response 结构的 status 变量 */
    response->status = status;
    /* 这里暂时忽略 headers */
    if(!PyList_Check(headers)) {
        printf("start response argument 2 should be a list of 2-tuples\n");
        return NULL;
    }
    Py_INCREF(response->status);
    //Py_INCREF(self_headers);
    Py_RETURN_NONE;
}

简单的 wsgi 测试程序已经基本编写完成,加入一个 app.py 便可以看一下运行结果了。

app.py

def app(env, start_response):
    status = '200 OK'
    response_headers = []
    start_response(status, response_headers)
    return "Hello World"

运行结果

$ ./wsgi                                     [23:54:43]
get app
calling app
Application returns: Hello World
response->status is 200 OK

完整代码在我的GitHub