编写一个最简单的-wsgi-server
原文链接 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