Apache httpd 自 2.0 之后,针对 1.0 做了大量的改进,包括对自身内核的改造,扩展机制的改进,APR(Apache Portable Runtime) 的剥离 ( 使得 Apache 成为一个真正意义的跨平台服务器 )。Apache 2.0 成为一个很容易扩展的开发平台。
如上图中所示,Apache 中包含了大量的扩展,也就是本文中要详细讨论的模块 (module), 如 mod_cgi 用以处理 cgi 脚本,mod_perl 用以处理 perl 脚本,将 perl 的能力与 Apache httpd 结合起来等等。用户可以通过一定的开发标准来开发符合自己业务场景的模块,并动态的加载到 Apache 中,Apache 会根据配置文件中的规则来定位,调用模块,完成客户端请求。
Apache httpd 由一个内核和大量的模块组成,包括用以加载其他模块的功能单元自身也是一个模块。一般而言,一个 HTTP 服务器的工作序列是这样的:接受客户端请求 ( 可能是请求一个
部署在 HTTP 服务器程序可访问的文件 ),读取该文件作为响应返回。
我们在浏览器的地址栏中输入类似这样的 URL:http://host/index.html,浏览器将会尝试与 host 指定的 HTTP 服务器的 80 端口建立连接,如果成功,则发送 HTTP 请求,获取 index.html 页面。如果成功,则在浏览器中解析该 HTML 文件。
这种工作方式在静态页面的场景下没有任何问题。但是实际应用往往会与数据库交互,动态生成页面内容。如服务端较为流行的 cgi/php 脚本等。这就需要更高级,更灵活的内容生成器做支持。
在请求处理流程中,包括预处理,内容生成及其他善后操作等。在预处理阶段,可以进行权限校验,HTTP 头信息识别等;内容生成阶段则通过与操作系统其他资源交互 ( 如文件读写,数据库访问等 ) 来完成动态内容的生成;最后在善后操作中,可能会进行日志记录,资源释放等操作。
下面我们就来开始我们的 Apache 模块开发之旅,首先定义一个最简单的模块原型,然后逐步扩展,使其可以完成更多的功能。在最后,这个模块可以读取客户端 POST 的数据,并将该数据加工,最终回显给客户端 ( 可以是浏览器或其他应用 )。
事实上,Apache 为开发人员提供了一系列的开发工具,使用它们,开发人员可以很方便的定义符合业务场景的 Apache 模块。这些工具中,较为常用的是 apxs。用 apxs 可以生成模块的模板文件,Makefile,目录结构等等。同时也可以用以编译,链接而生成最终的共享库文件 ( 在 Linux 系统下为 so 文件,类似于 windows 平台的动态链接库 dll),Apache 模块的最终形式即为共享库文件。
这一小节中,我们先使用 apxs 工具来生成一个模块模板,命名为 sample,命令如下:
$ /usr/sbin/apxs -g -n sample |
选项 -g 表示生成 (generate), 选项 -n sample 指定模块名称为 sample, apxs 会生成以下内容:
$ /usr/sbin/apxs -g -n sample Creating [DIR] sample Creating [FILE] sample/Makefile Creating [FILE] sample/modules.mk Creating [FILE] sample/mod_sample.c Creating [FILE] sample/.deps |
在当前目录创建了一个名为 sample( 模块名 ) 的目录,然后在该目录下生成了 Makefile 及 mod_sample.c 文件,这个文件中已经包含了完整的模块代码。可以看到,Apache 模块代码与其他 C 工程中的代码并无二致。在不做任何修改的情况下,编译该文件就可以生成我们的第一个 Apache 模块 mod_sample.so。这个模块的功能非常简单——仅以 text/html 形式向发起请求的客户端返回一个字符串:“The sample page from mod_sample.c”。
通用模板
所有的 Apache 模块都需要遵从一定的规范来编写,这样 Apache 动态加载模块才能识别我们编写的模块并正常的工作。上例中通过 apxs 生成的 mod_sample.c 即为一个典型的模块模板,该模板包括头文件的引入,模块存根的生成,handler 的示例等,开发人员可以通过 handler 的示例来完成模块的开发。
/* The sample content handler */ static int sample_handler(request_rec *r) { if (strcmp(r->handler, "sample")) { return DECLINED; } r->content_type = "text/html"; if (!r->header_only) ap_rputs("The sample page from mod_sample.c\n", r); return OK; } static void sample_register_hooks(apr_pool_t *p) { ap_hook_handler(sample_handler, NULL, NULL, APR_HOOK_MIDDLE); } /* Dispatch list for API hooks */ module AP_MODULE_DECLARE_DATA sample_module = { STANDARD20_MODULE_STUFF, NULL, /* create per-dir config structures */ NULL, /* merge per-dir config structures */ NULL, /* create per-server config structures */ NULL, /* merge per-server config structures */ NULL, /* table of config file commands */ sample_register_hooks /* register hooks */ }; |
首先需要一个实际处理客户端请求的函数 (handler),命名方式一般为”模块名 _handler”,接收一个 request_rec 类型的指针,并返回一个 int 类型的状态值。如:
static int sample_handler(request_rec *r); |
request_rec 指针中包括所有的客户端连接信息及 Apache 内部的指针,如连接信息表,内存池等,这个结构类似于 J2EE 开发中 servlet 的 HttpRequest 对象及 HttpResponse 对象。通过 request_rec,我们可以读取客户端请求数据 / 写入响应数据,获取请求中的信息 ( 如客户端浏览器类型,编码方式等 )。
紧接着是一个注册函数,一般命名为”模块名 _register_hooks”,传入参数为 Apache 的内存池指针。这个函数用于通知 Apache 在何时,以何种方式注册响应函数 (handler)。
最后,是模块的定义,Apache 模块加载器通过这个结构体中的定义来在适当的时刻调用适当的函数以处理响应。应该注意的是,第一个成员默认填写为 STANDARD20_MODULE_STUFF,其他成员我们将在后续的小节中详细讨论。最后一个成员为上边讨论过的注册函数。
运行模块
编写好模块之后,通过 apxs 的编译功能将模块编译为共享库。生成的文件位于模块目录下的 .libs( 以点号开始的文件及文件夹在 Linux 系统中为隐藏文件,如果不指定 ls 命令的参数,ls 不会将此目录列出来,需要指定 -a 选项 ) 目录下。将此文件拷贝至 apache 安装目录下的 modules 目录 ( 为了便于描述,下文中以 apache_home 表示 apache 的安装目录 )。然后修改 apache_home/conf/httpd.conf,在该配置文件的最后加入:
LoadModule sample_module modules/mod_sample.so <Location /sample> SetHandler sample </Location> |
LoadModule 指令意义为加载模块,setHandler 设置 handler,此处为”sample”,这个字符串与代码中的 r->handler 相同。这样,Apache 在处理对 /sample 的请求时,会调用我们编写的模块。
$ apache_home/bin/httpd – d apache_home – k stop $ apache_home/bin/httpd – d apache_home – k start |
然后,调用 curl 测试 sample 的模块:
$ curl http://10.111.43.145:9527/sample -v * About to connect() to 10.111.43.145 port 9527 (#0) * Trying 10.111.43.145... connected * Connected to 10.111.43.145 (10.111.43.145) port 9527 (#0) > GET /sample HTTP/1.1 > User-Agent: curl/7.21.6 (i686-pc-linux-gnu) \ libcurl/7.21.6 OpenSSL/0.9.7a zlib/1.2.1.2 libidn/0.5.6 > Host: 10.111.43.145:9527 > Accept: */* > < HTTP/1.1 200 OK < Date: Tue, 26 Jul 2011 07:12:19 GMT < Server: Apache/2.0.63 (Unix) < Content-Length: 34 < Connection: close < Content-Type: text/html < The sample page from mod_sample.c * Closing connection #0 |
我们在使用 curl 命令的时候,指定 -v 选项,则会打印出详细的客户端和服务端的交互信息,”>”开头的行表示 curl 发送到服务器的请求信息,而”<”开始的则为服务器端的响应信息。可以看到,模块已经如我们所预期的那样工作了。下面,我们将逐步扩展这个模块,使其功能更加强大。
如果 Apache 模块只能产生内容,那么使用普通的 HTML 文件 ( 即使用 httpd 默认的内容生成器 ) 也可以完成。模块存在的意义在于,它可以轻松地处理客户端传递的数据,并将这些数据加工,然后响应客户端请求。我们在这一小节将开发一个可以接收客户端发送的 POST 请求,并将请求原封不动的回显给客户端的模块。
在这个模块中,我们自定义一个函数,用于读取 POST 请求数据。
读取客户端数据
/** * @brief read_post_data 从 request 中获取 POST 数据到缓冲区 * * @param req apache request_rec 对象 * @param post 接收缓冲区 * @param post_size 接收缓冲区长度 * * @return */ static int read_post_data(request_rec *req, char **post, size_t *post_size){ char buffer[DFT_BUF_SIZE] = {0}; size_t bytes, count, offset; bytes = count = offset = 0; if(ap_setup_client_block(req, REQUEST_CHUNKED_DECHUNK) != OK){ return HTTP_BAD_REQUEST; } if(ap_should_client_block(req)){ for(bytes = ap_get_client_block(req, buffer, DFT_BUF_SIZE); bytes > 0; bytes = ap_get_client_block(req, buffer, DFT_BUF_SIZE)){ count += bytes; if(count > *post_size){ *post = (char *)realloc(*post, count); if(*post == NULL){ return HTTP_INTERNAL_SERVER_ERROR; } } *post_size = count; offset = count - bytes; memcpy((char *)*post+offset, buffer, bytes); } }else{ *post_size = 0; return OK; } return OK; } |