About APR

我是特别赞成在小项目中使用前人轮子的, 一是可以使有限的资源投入到业务流上,不用为底层小细节伤脑精;另外一个是优秀的库会使我们的代码更易用,易移植,也更强壮;我们从使用优秀的库中吸取程序设计的精髓譬如设计模型、程序技巧等等。
 
APR是一个历史悠久的优秀的库,下面是摘自《Apache模块开发》;
 
Apache可移植运行时库

The Apache Portable Runtime

Apache可移植运行时库(Apache Portable Runtime,APR)和APR的实用库(APR Utilities、APR-UTILS或者APU)是Apache软件基金会旗下两个自主开发和维护的库,为Apache的httpd程序所使用。尽管 很多的内核开发者要同时涉及开发httpd(即Web服务器)和APR,然而这两个项目却是相互独立的。APR和APU提供了内核函数,这些函数不仅仅只 与Web服务相关,对于更加普遍的应用开发也很有用。

除了Web服务器之外,最有名的APR应用就是Subversion,它是一个版本管理和变更管理系统。另外一个就是Site Valet,一个在网上进行质量保证(QA)和易用性审计的软件套件。Site Valet是由笔者开发的。

本章介绍APR,并探讨如何把APR应用到Apache模块中。本章并没有深入介绍类似于应用程序初始化之类的主题,虽然这些也是必要的,不过它们 由Apache内核代码在内部进行处理。对于那些Web服务器内核之外的应用程序开发者,想更深入了解APR可以参考其自身文档,它介绍得很翔实,可以在http://dev.ariel-networks.com/apr/apr-tutorial/ html/ apr-tutorial.html找到APR的教程。

3.1  APR

APR的主要目的是为应用提供一个可移植的、平台无关的层。它使用底层的、交叉平台的库来提供文件系统访问、网络编程、进程和线程管理以及共享内存 等功能。那些使用Apache专有APR、而不是使用本地系统功能的模块在平台之间只可以移植的,并且能够在所有被Apache所支持的平台上被干净地 (最坏的情况也是需要很小程度修改)编译。

每一个APR模块由一个所有平台共享的应用编程接口(Application Programming Interface,API)和该API所定义的所有函数实现组成。对该API的函数实现基本上(至少一部分)是与平台相关的,尽管这些对于应用程序没有任何影响。

APR的核心就是Apache的资源管理(池),我们将在本章的后面部分进行更加详细的介绍。下表列出了APR中的所有模块。

APR模块

名称

目的

apr_allocator

内存分配,内部使用

apr_atomic

原子操作

apr_dso

动态加载代码(.so/.dll

apr_env

读取/设定环境变量

apr_errno

定义错误条件和宏

apr_file_info

文件系统对象和路径的属性

apr_file_io

文件系统输入/输出

apr_fnmatch

文件系统模式匹配

apr_general

初始化/终结,有用的宏

名称

目的

apr_getopt

命令参数

apr_global_mutex

全局锁

apr_hash

哈希表

apr_inherit

文件句柄继承助手

apr_lib

奇数和末端

apr_mmap

内存映射

apr_network_io

网络输入/输出(套接字)

apr_poll

投票

apr_pools

资源管理

apr_portable

APR到本地映射转换

apr_proc_mutex

进程锁

apr_random

随机数

apr_ring

环数据结构和宏

apr_shm

共享内存

apr_signal

信号处理

apr_strings

字符串操作

apr_support

内部支持函数

apr_tables

表格和数组函数

apr_thread_cond

线程条件

apr_thread_mutex

线程锁

apr_thread_proc

线程和进程函数

apr_thread_rwlock

读写锁

apr_time

时间/日期函数

apr_user

用户和组ID服务

apr_version

APR版本

apr_want

标准头文件支持



APR实用库

APR实用库(APR-UTIL,或者APU)是APR项目的第二个库。它在APR基础上,使用统一标准的编程接口,提供了一部分功能函数集。APU并不是每在一个平台上都有一个单独的模块,但是它为某些其他常用的资源例如数据库提供了一个类似的方法。

表3-2列出了APU的所有模块。

表3-2  APU模块

名称

目的

apr_anylock

透明的、任何锁的封装

apr_base64

Base-64编码

apr_buckets

Buckets/Bucket brigade

apr_date

时间字符串解析

apr_dbd

针对SQL数据库的常用API

apr_dbm

针对DBM数据库的常用API

apr_hooks

钩子实现宏

apr_ldap

LDAP授权API

apr_ldap_init

LDAP初始化API,主要应用在和LDAP服务器的初始安全连接

apr_ldap_option

设置LDAP选项的API

apr_ldap_url

解析和处理LDAP URLAPI

apr_md4

MD4编码

apr_md5

MD5编码

apr_optional

可选函数

apr_optional_hooks

可选钩子

apr_queue

线程安全的FIFO队列

apr_reslist

资源池

apr_rmm

可再定位地址的内存

名称

目的

apr_sdbm

SDBM

apr_sha1

SHA1编码

apr_strmatch

字符串模式匹配

apr_uri

URI解析/构造

apr_uuid

用户标识

apr_xlate

字符集转换(I18N

apr_xml

XML解析



 基本的约定

3.3  基本的约定

APR和APR-UTIL采用了一些约定,使得它们的API具有同质性,并且易于使用。

3.3.1  参考手册:API文档和Doxygen

APR和APU在代码层都有非常好的文档。每一个公开函数和数据类型都在定义它们的头文件中进行了注释,使用了doxygen 友好的格式。那些头文件,或者doxygen生成的文档,为程序员提供了完整的API参考手册。如果你安装了doxygen,那么就可以通过make dox命令从源代码中生成你自己版本的APR参考手册。

3.3.2  命名空间

所有的APR和APU的公开接口都使用了字符串前缀“apr_”(数据类型和函数)和“APR_”(宏),这就为APR定义了一个“保留”的命名空间。

在APR命名空间中,绝大部分的APR和APU模块使用了二级命名空间。这个约定通常基于正在讨论的那个模块的名字。例如,模块apr_dbd中的 所有函数使用字符串“apr_dbd_”前缀。有时候使用一个明显的描述性的二级命名空间。例如,在模块apr_network_io中套接字操作使用 “apr_socket_”前缀。

3.3.3  声明的宏

APR和APU的公开函数使用类似于APR_DECLARE、APU_DECLARE和APR_ DECLARE_NONSTD的宏进行声明。例如:

APR_DECLARE(apr_status_t) apr_initialize(void);

在很多的平台上,这是一个空声明,并且扩展为

apr_status_t apr_initialize(void);

例如在Windows的Visual C++平台上,需要使用它们自己的、非标准的关键字,例如“_dllexport”来允许其他的模块使用一个函数,这些宏就需要扩展以适应这些需要的关键字。

3.3.4  apr_status_t和返回值

在APR和APU中广泛采用的一个约定是:函数返回一个状态值,用来为调用者指示成功或者是返回一个错误代码。这个类型是apr_status_t,在apr_errno.h中定义,并赋予整数值。因此一个APR函数的常见原型就是:

APR_DECLARE(apr_status_t) apr_do_something(…function args…);

返回值应当在逻辑上进行判断,并且实现一个错误处理函数(进行回复或者对错误进行进一步的描述)。返回值APR_SUCCESS意味着成功,我们通常可以用如下的方式进行错误处理结构:

apr_status_t rv;
...
rv = apr_do_something(... args ...);
if (rv != APR_SUCCESS) {
/* 记录一个错误 */
return rv;
}

有时候我们可能需要做得更多。例如,如果do_something是一个非闭塞的I/O操作并且返回APR_EAGAIN,我们可能需要重试这个操作。
有些函数返回一个字符串(char *或者const char *)、一个void *或者void。这些函数就被认为在没有失败条件或者在错误发生时返回一个空指针。

3.3.5  条件编译

本质上说,APR的一些特色可能并不是每个平台都支持的。例如,FreeBSD在5.x版本之前并没有适合Apache的本地线程实现,因此线程在APR中就不被支持(除非编译时手动设置相应的操作)。

为了在这种情况下应用程序依然能够工作,APR为这些情况提供了APR_HAS_*宏。如果一个应用处于这种情况,它应当使用这些宏进行条件编译。例如,一个模块执行了一个操作,这个操作可能导致在多线程环境下的竞争条件,那么它就可能使用以下的方式。

#if APR_HAS_THREADS
rv = apr_thread_mutex_lock(mutex);
if (rv != APR_SUCCESS) {
/* 记录一个错误 */
/* 放弃关键的操作*/
}
#endif

    /* ... 在这里执行关键代码... */

#if APR_HAS_THREAD
apr_thread_mutex_unlock(mutex);
#endif

 

3.4.1 资源管理的问题

3.4  资源管理:APR池

APR池是基础的构建块,是APR和Apache的核心,它们作为所有资源管理的基础。池进行内存分配,既有直接的方式(类似于malloc方 式),也有间接的方式(例如,在字符串操作过程中),最主要的是,它保证分配的内存在最恰当的时候被释放掉。由于池扩展得很大,用来保证其他的资源例如文 件或者互斥信号量能够被分配,并且总是能够被正确地清理释放。它们甚至可以管理被第三方库透明管理的资源。

注意:在Apache中通常认为池的内存分配永远不会失败。这个假设成立的原因在于如果内存分配失败,那么系统是不可恢复的,任何错误处理也将会失败。

3.4.1  资源管理的问题

每一个程序员都了解,当你分配一个资源后,你必须保证在你结束使用该资源时将其释放掉。例如:

char* buf = malloc(n) ;
... 检查它是否为空 ...
... 使用该缓冲区 ...
free(buf) ;

或者

FILE* f = fopen(path, "r") ;
... 检查是否为空 ...
... 从f中读取....
fclose(f) ;

很明显,如果没有释放buf或者关闭f,那就是一个Bug。在长时间运行的程序中,例如Apache,这个Bug将导致严重的后果,甚至导致整个服务器宕机。显然,正确地管理分配的资源非常重要。

在比较简单的环境下,资源管理比较容易。但是如果在一个拥有多个错误路径的更加复杂的情况下,资源被分配时甚至资源本身的范围都不确定,在这种情况下保证每个执行路径上的资源都能够被释放是非常困难的。在这种环境下,我们需要管理资源有更好的办法。

Constructor/Destructor模型(构造/析构模型)

在C++中,每一个对象都有一个构造函数(constructor)和一个析构函数(destructor),这是资源管理的一种方法。很多的 C++程序员让析构函数负责清理所有为这个对象所分配的资源。这种让对象负责动态管理资源的方法运行得很好。不过,和简单的C语言方法相比,我们需要用大 量精力去关注细节——例如,当资源有条件地进行分配时,或者在多个不同的对象之间共享时,这种方法很容易带来编程的Bug。

Garbage Collection模型(垃圾回收模型)

垃圾回收模型是一个高层次的资源管理方法,以Lisp语言和Java语言为代表。这种方法的优势是将资源管理的问题交由语言本身负责,而不是由程序 员负责,这样就完全避免了由于编程的缺点导致错误发生的危险。它的缺点是,垃圾回收在它不需要时带来了额外的负担,并且将程序员的某些对资源有用的控制权 剥夺了,例如控制一个资源生命周期的能力。它还需要所有的编程组件——包括第三方的库——建立在同一个系统上,那么这在使用C语言开发的开放系统中很明显 是不可满足的。

3.4.2 APR池

3.4.2  APR池

APR池为资源管理提供了一个可选的模型。和垃圾回收类似,APR池将程序员从各种可能的情况下进行清理操作的复杂性中解放出来。除此之外,APR池具有一些其他的优势,包括对资源的生命周期内的完全控制和管理不同种类资源的能力。

APR池的基本理念和此类似:无论何时你分配了一个需要清理的资源,你必须使用一个池来对其进行注册。然后这个池就负责对这个资源进行清理,清理操 作将会在这个池本身被清理的时候发生。使用这种方式,资源管理的问题被简化成为对一个单一资源的分配和清除:池本身。如果Apache的池是被服务器来管 理,那么就从应用编程的角度不去涉及资源管理的复杂性。程序要做的事情就是为一个资源的生命周期选择一个合适的池。

基本的内存管理

池最基本的应用就是内存管理。我们不使用如下的方式:

mytype* myvar = malloc(sizeof(mytype)) ;
/* 在任一个可能的执行路径上,该资源需要被释放 */

而是采用如下的方式:

mytype* myvar = apr_palloc(pool, sizeof(mytype)) ;

不管在此期间发生了什么,池将自动地负责释放这个资源。第二个好处就是池的分配在很多平台上都比molloc快得多。

基本的内存管理在APR和Apache中已经成形,内存使用其他的函数进行分配。下面是字符串操作和审计的例子,我们可以从中马上获益,使用APR版本的sprintf()不需要事先知道一个字符串的大小:

char* result = apr_psprintf(pool, fmt, ...) ;

APR也提供了池内存的高层次抽象——例如,用来向过滤链传送数据的Bucket。

普遍适用的内存管理

APR提供了用来进行内存管理内建的函数,以及一些其他的基本资源,例如文件、套接字和互斥信号量。但是,程序员不需要使用这些函数和资源。一个可选方案就是使用本地的分配函数并通过池显式地注册一个清理操作。

mytype* myvar = malloc(sizeof(mytype)) ;
apr_pool_cleanup_register(pool, myvar, free,
apr_pool_cleanup_null);

或者

FILE* f = fopen(filename, "r") ;
apr_pool_cleanup_register(pool, f, fclose, apr_pool_cleanup_null);

这个代码将清理的责任委托给池,因此对于程序员来说则不需要额外的步骤。不过,相对于APR的池,本地的、功能和APR的apr_pools和apr_file_io类似的函数,其移植性要差一些,而且molloc在绝大部分平台上比使用池要慢一些。

这种对任何资源普遍适用的内存管理方法对于Apache和APR是透明的。例如,如果想打开一个MySQL数据库连接并在使用之后关闭,你可以写下面的代码。

MYSQL* sql = NULL ;
sql = mysql_init(sql) ;
if ( sql == NULL ) { log error and return failure ; }
apr_pool_cleanup_register(pool, sql, mysql_close,
apr_pool_cleanup_null) ;

sql = mysql_real_connect(sql, host, user, pass,
dbname, port, sock, 0) ;
if ( sql == NULL ) { log error and return failure ; }

注意,apr_dbd(我们将在第11章进行讨论)为管理数据库连接提供了一个总的来说更好的方法。

作为第二个例子,考虑XML的处理。

xmlDocPtr doc = xmlReadFile(filename);
apr_pool_cleanup_register(pool, doc, xmlFreeDoc,
apr_pool_cleanup_null) ;

/* 我们目前在操作doc,将需要分配内存,
* 该内存由XML库管理,不过将通过xmlFreeDoc 释放。
*/

如果集成C++的析构函数做资源释放,我们提供了下面这个例子,假设我们定义了一个类:

class myclass {
public:
virtual ~myclass() { do cleanup ; }
// ....
} ;

我们定义了一个C的封装:

void myclassCleanup(void* ptr) { delete (myclass*)ptr ; }

然后我们在分配myclass时通过池注册这个封装:

myclass* myobj = new myclass(...) ;
apr_pool_cleanup_register(pool, (void*)myobj, myclassCleanup,
apr_pool_cleanup_null) ;

//我们已经为C++的资源管理在Apache中挂了钩子
//我们不再需要删除myobj
//池将为我们做这个清除操作

隐式和显式地清除

假设我们想要在请求结束之前显式地释放资源——例如,因为我们正在做一些内存消耗比较大的操作而有些对象我们可以释放。我们可能会想根据普通的代码 域规则做一些事情,或者仅仅使用基于池的清理作为后退(fallback)来处理错误路径。不过,由于我们已经注册了清理,因此清理操作将不顾我们的意愿 而运行。在最坏的情形下,它可能会导致一个两次释放(double-free)并产生段错误。

另外一个池函数,apr_pool_cleanup_kill,就是用来处理这种情形的。当我们运行了显式的清除,我们从池中注销清理操作。当然, 我们对于如何进行这项任务有更加聪明的选择。下面是一段代码摘要,一个C++类基于一个池管理自己,不管是否显式删除了这个池。

class poolclass {
private:
apr_pool_t* pool ;
public:
poolclass(apr_pool_t* p) : pool(p) {
apr_pool_cleanup_register(pool, (void*)this,
myclassCleanup, apr_pool_cleanup_null) ;
}
virtual ~poolclass() {
apr_pool_cleanup_kill(pool, (void*)this, myclassCleanup) ;
}
} ;

如果你使用C++开发Apache(或者APR),你可以从poolclass类继承。大部分的APR函数做的和这个一样,随时在资源被分配或者清除时进行注册和清理操作。

在C语言中,我们可以使用以下的通用格式。

/* 分配一些资源 */
my_type* my_res = my_res_alloc(args) ;
/* 处理错误 */
if (my_res == NULL) {
/* 记录错误并跳出 */
}
/* 通过注册清理保证资源不会泄漏 */
apr_pool_cleanup_register(pool, my_res,
my_res_free, apr_pool_cleanup_null) ;

/* ... 现在就按照需求使用这个资源 ... */

/* OK,我们已经完成任务,现在要尽快释放资源了 */
rv = my_res_free(my_res) ;
/* 既然我们已经释放了,我们必须kill掉清除操作 */
apr_pool_cleanup_kill(pool, my_res, my_res_free) ;
/* 现在进行错误处理并继续 */
if (rv != APR_SUCCESS) { /* or whatever test may be appropriate */
/* ... 记录错误并跳出,或者尝试进行恢复 ... */
}

我们甚至可以将这个格式进行流水线作业,通过使用一个函数在池中进行清理和注销。

apr_pool_cleanup_run(pool, my_res, my_res_free) ;

3.4.3 资源的生命周期

3.4.3  资源的生命周期

当我们使用池进行资源分配时,我们就会确信这些资源将在某个时间点被清理释放。但具体是什么时间呢?我们需要确保清理发生在正确的时间——即,既不是资源正在使用的时候,也不是在资源不再需要的很长时间之后。

Apache的池

庆幸的是,Apache通过采取为不同类型的资源分配不同类型的池的方法,将这个过程变得非常简单。这些池和httpd的相关结构结合起来,和相应的结构具有同样的生命周期。在Apache中,有四个通用意义的池可以使用。

·请求池(request pool),生命周期为HTTP请求的生命周期

·进程池(process pool),生命周期为一个服务进程的生命周期

·连接池(connection pool),生命周期为一个TCP连接的生命周期

·配置池(configuration pool)

前三个池结合Apache的相关数据结构,分别通过request->pool,connection->pool和 process->pool进行访问。第四个池也是和进程结合,通过process->pconf进行访问,不过和进程池不同的是它将在 Apache再次读取配置文件时进行清理。

进程池适合于长时间运行的资源,例如那些在服务器启动时被初始化的资源。请求池适合于处理一个单一请求所使用的临时资源。

连接通常由一个或者多个请求组成,连接池的生命周期和连接相同。连接池对于不能和一个请求相结合的临时资源来说非常有用,最值得一提的是在一个连接层过滤器中,request_rec结构还没有定义,或者在一个非HTTP协议的处理函数中。

除了以上这些标准的池,还有一些为了某些原因特别创建的专用池,例如配置和审计,或者模块自己创建的、供自己使用的池。

在Apache中使用池:处理请求

所有的请求处理的钩子函数采用如下的形式:

int my_func(request_rec* r) {
/* 在这里实现请求处理的钩子函数 */
}

这个钩子将请求池r->pool放在我们的处理函数中。我们之前曾经介绍过,这个请求池对涉及处理请求的绝大部分操作都适合。我们将请求池传递给Apache和APR函数,这些函数需要一个池作为参数,也需要我们自己的参数。

对于那些需要分配长时间运行资源的操作来说,进程池可以通过r->server->process->pool的方式进行访问 ——例如,缓存一个资源,该资源需要被计算一次,并随后被其他的请求重用。不过,这个过程有一些复杂,通常我们更愿意从这个进程池派生一个子池,我们将在 第4章和第10章进行介绍。

连接池通过r->connection->pool进行访问。

在Apache中使用池:初始化和配置

Apache初始化的内部流程是比较复杂的。不过,就所涉及的模块而言,初始化通常被当作是一个简单的流程:仅仅建立一个配置,然后所有的东西都是持久的。Apache将这个过程变得很简单,因为大部分的相关钩子都具有将相关的池作为第一个参数进行传递的原型。

配置处理函数

static const char* my_cfg(cmd_parms* cmd, void* cfg, /* 参数 */ )

使用配置池cmd->pool将给这个配置赋予指令的生命周期。

前向配置和后向配置钩子

这些钩子通常不传递几个池。

static int my_pre_config(apr_pool_t* pool,
apr_pool_t* plog, apr_pool_t* ptemp)

对于大多数情况来说,仅仅使用第一个pool参数。ptemp适用于在配置阶段使用的资源,不过这些资源将在Apache进入运行阶段之前释放。plog在服务器的整个生命周期内保持激活,在每次读取配置文件时被释放。

子池初始化(Child init)

static void my_child_init(apr_pool_t* pool, server_rec* s)

子池是第一个参数。

监控器

static int my_monitor(apr_pool_t* pool)

监控器是一个特殊的情况:它在父进程中运行,而且没有绑定到任何时间受限的结构。因此,在一个监控器函数中分配的资源应当被显式地释放。如果需要,监控器将创建、管理和释放自己的子池,这将在第4章进行讨论。很少的应用程序需要使用监控器钩子。

在Apache中使用池:其他的情形

我们已经介绍了大部分在初始化和请求处理中涉及的模块。不过,还有两种其他的情形:连接函数和过滤器函数。

连接函数

pre_connection和process_connection这两个连接层的钩子将conn_rec作为第一个参数进行传递;就所涉及的池 资源而言,它们与请求函数非常类似。连接初始化的钩子create_connection也将池作为它的第一个参数进行传递。任何实现这个钩子的模块需要 负责建立连接。

过滤器函数

过滤器函数接收一个ap_filter_t作为它们的第一个参数。这个对象模糊地包含一个request_rec和一个conn_rec成员,不管 它究竟是一个请求层的过滤器,还是一个连接层的过滤器。内容过滤器通常应当使用请求池。连接层的过滤器将在f->r中获取一个垃圾指针(请求不存在 于协议层之外,参考第8章)并且必须使用连接池。一定要小心,这可能是一个不经意的陷阱。

3.4.4 池的局限性

3.4.4  池的局限性

到目前为止,我们已经看到了在资源管理中使用池的好处。不过池也有一些不足。

·如果要管理的资源的生命周期和Apache中主要对象的生命周期不能对应,往往需要做更多的工作。我们将会在第4章对这个问题进行深入地讨论。

·从池中分配资源不是线程安全(thread safe)的。当Apache运行在一个多线程基础之上时,它使用一个对象(HTTP请求或者TCP连接)所拥有的池,这个池在使用时是线程私有的,大部 分的池分配都是通过模块进行的,因此池中分配资源不是线程安全的情况很少。第4章将讨论需要线程安全时的一些情况。

·APR池在它们被释放之前(当然,它们确实是复用内存,因此基于池的应用程序不会无限增长)是不会向操作系统返回内存的。因此在分配非常大的内存 块时,有时需要使用malloc函数而不是使用池。相反地,在代码中使用malloc可能会影响二进制兼容性。在Windows系统中,考虑到和运行时库 的不兼容性,系统将使用其他不同版本的Visual C++编译的二进制代码和你的代码进行链接。

3.5.1 字符串和格式

3.5  精选的APR主题

系统中有些函数对我们来说非常熟悉,而且在不需要安装APR的情况下也可以在我们的系统上使用。APR为这些函数提供了APR版本的实现,作为普通系统函数的替代选择。不过,使用这些函数的APR版本具有以下的特点:

 APR函数是平台独立的,可以提供更好的移植性。

 APR函数可以自由地从APR的基于池的资源管理中获取优势。

我们将不再进行详细地讨论。如果你想获取更多的信息,请参考头文件中那些非常不错的文档。

3.5.1  字符串和格式

apr_string模块提供了以下的APR实现。

·常用的字符串函数:字符串比较、字符串匹配、字符串拷贝和字符串连接。

·类似stdio的函数:sprintf和printf家族函数,包括变参的格式化(vformatter)。

·字符串解析,包括线程安全的strtok。

·字符串变换为其他的数据类型,和从其他的数据类型变换为字符串(例如:atoi)。

APR的字符串处理是基于池的。由于我们很少会去关注缓冲区的大小,因此基于池的机制带来了实质性的简化。例如,如果要连接一个随意长度的字符串,我们可以使用:

result = apr_pstrcat(pool, str1, str2, str3, ..., NULL);

而不再需要计算result的长度,并预先分配一个缓冲区。类似地:

result = apr_psprintf(pool, fmt, ...) ;

也不需要去关注单调乏味的字符串空间的大小,之前我们必须使用如下的方式:

length = [compute length here] ;
buf = malloc(length) ;
sprintf(buf, fmt, ...) ;

APR中不支持正则表达式(尽管在Apache中支持),不过apr_strmatch模块提供了更快的字符串匹配机制,它使用不分大小写(以及分大小写)的查询和非空终结(non-null-terminated)的方式进行处理。

3.5.2 国际化

3.5.2  国际化

apr_xlate模块提供了在不同字符集之间转换的功能。

在本书写作之际,由于Windows缺乏本地国际化的支持,Windows平台的apr_xlate还需要一个第三方的APR库apr_iconv,这个依赖将会在以后的版本中消除掉。

3.5.3 时间和日期

3.5.3  时间和日期

apr_time模块提供了一个微秒计时器和时钟。由于APR工作在微秒环境下,它的基本数据类型apr_time_t是一个64比特的整型,与time_t是不能互换的。以下的宏用来进行转换:

/** @return apr_time_t,它的单位为秒 */
#define apr_time_sec(time) ((time) / APR_USEC_PER_SEC)

/** @将秒作为 an apr_time_t 返回*/
#define apr_time_from_sec(sec) ((apr_time_t)(sec) * APR_USEC_PER_SEC)

其他的数据类型,包括时间间隔和一个与“struct tm”类似的类型apr_time_exp_t。APR的时间函数包括:

·当前时间

·格林威治时间(Greenwich Mean Time,GMT),本地时间和任何时区

·时间算法

·休眠

·时间格式化为ctime或者RFC822字符串

apr_date模块提供了一些辅助的函数进行常用的时间和日期格式的解析。

3.5.4 数据结构

3.5.4  数据结构

Apache提供了4个数据结构模块。

·apr_table:提供表和数组

·apr_hash:提供了哈希表

·apr_queue:提供了先进先出队列(FIFO)

·apr_ring:提供了一个环结构,该结构也是APR的Bucket brigade的基础

3.5.4.1  数组

APR数组类型由apr_array_header_t类型提供,可以通过对象和指针两种方式进行访问。数组数据类型也可以作为堆栈。一个数组拥有 一个默认的大小,并在数组创建时被设置。尽管数组在其默认大小范围内工作效率最高,但它还是可以根据需要增长。数组最常用的操作就是增加(压栈)和迭代。

/* 分配一个my_type 类型的数组 */
apr_array_header_t* arr = apr_array_make(pool, sz, sizeof(my_type)

/* 在数组中分配一个未初始化的元素 */
my_type* newelt = apr_array_push(arr) ;

/* 为elt进行赋值 */
newelt->foo = abc ;
newelt->bar = "foo" ;

/* 弹出最后面的元素 */
my_type* oldelt = apr_array_pop(arr) ;

/* 迭代所有的元素 */
for (i = 0; i < arr->nelts; i++) {
/* 一个 C++引用最能清晰地体现这个操作 */
my_type& elt = arr->elts[i] ;
}

其他的数组操作,包括pop出栈操作、复制(shallow copy,浅拷贝)、懒复制(lazy copy)、连接(concatenation)、增加(append)和转换为一个字符串变量(这个只有在数组的内容都是字符串变量时才比较有意义)。

3.5.4.2  表

apr_table_t是一个建立在数组之上的、直观的高层数据结构,用来存贮键/值(key/value)对。它支持增加元素(几个变种)、删除元素(效率不高)、查找、迭代以及清除整个表的操作。它也支持融合和覆盖操作,对数据进行融合或者清除冗余的元素。
表的键一般都是不分大小写的(这和APR哈希表的键不同)。

/* 新建一个表 */
apr_table_t* table = apr_table_make(pool, sz) ;

/* 设置键/值对 */
apr_table_setn(table, key, val) ;

apr_table_set的几个变种包括:apr_table_setn、apr_table_add、apr_table_addn、apr_table_merge、apr_table_mergen。

·apr_table_setn:设置一个值,为这个键覆盖任何已经存在的值。

·apr_table_addn:增加一个值,如果该键已经有值则留下复制的键。

·apr_table_mergen:增加一个新的值,将这个值和该键的任何一个已经有的值进行融合。

·apr_table_set:拷贝进入表中的数据。

·apr_table_setn:不拷贝进入表中的数据(因此如果表中的值是持久的,或者分配在同一个池上,这样做效率更高)。同样应用在其他函数上。

/* 取出一个数据项 */
val = apr_table_get(table, key) ;

/* 对表进行迭代(参见第5章)*/
apr_table_do(func, rec, table, NULL) ;

/* 清除这个表 */
apr_table_clear(table) ;

/* 融合两个表 */
newtable = apr_table_overlay(pool, table1, table2) ;

/* 删除冗余项 */
apr_table_compress(table, flags) ;

高层的API以及一些可用的函数如apr_table_merge和apr_table_overlap为处理HTTP头和Apache的环境变量打下了坚实的基础。

3.5.4.3  哈希表

apr_hash_t也是用来存贮键/值的,不过相对于apr_table_t它只是一个低层的数据类型。它有两个优点。

1. 键和值可以是任何的数据类型(并且,和表不同的是,键和值是大小写敏感的)。

2. 哈希表在元素数目增长时可以更加有效地扩大规模。

和数组、表不同的是,哈希表没有初始的大小。最常用的操作是插入和查找。哈希表支持的其他操作包括迭代、复制、覆盖和融合。

apr_hash_t* hash = apr_hash_make(pool) ;

/* 键和值是任何数据类型的指针 */
apr_hash_set(hash, key, sizeof(*key), value) ;
value = apr_hash_get(hash, key, sizeof(*key)) ;

我们通常会遇到的一个特殊情况是:键是一个字符串。为了保证使用正确的语义上的字符串比较,我们在size参数的位置使用APR_HASH_KEY_STRING宏。

3.5.4.4  队列

apr_queue_t是一个线程安全的、先进先出绑定的队列。它只能在使用线程的APR构建版本上使用。它用来保证多个线程在处理任务时的协作。 队列具有固定大小的容量,该容量使用apr_queue_create设定。队列的主要操作是阻塞和非阻塞(blocking and nonblocking)的压入与弹出操作。

3.5.4.5  环

APR_RING实际上并不是一个数据类型,而有些类似于C++模板的一些宏的集合,这些宏实现了环状的双向链表。Apache中最主要的环状示例 为Bucket brigade,我们将在3.5.5节进行介绍,并在第8章进行详细讨论。Bucket是环中的一个元素,而Brigade是这个环结构本身。下面是实现 环结构的声明。

struct apr_Bucket {
/** 和Brigade的其他部分链接 */
APR_RING_ENTRY(apr_Bucket) link;
/** Bucket的数据区域 */
};

/** Bucket数据列表 */
struct apr_Bucket_brigade {
/** 和Brigade相关的池,数据没有在池之外进行分配,
*  但是在池中注册了一个清除操作。
*  如果这个Brigade 使用其他的机制进行清理,而不是使用池的方式,
*  那么这个清理函数需要负责注销这个清理操作。
*/
apr_pool_t *p;
/** 在Brigade中的Bucket都在这个链表上 */
/*
* apr_Bucket_list结构实际上不需要一个命名标签。
* 因为它不是脱离apr_Bucket_brigade结构而存在的。
* 设计了环的宏,在这种情况下你可以将命名标签置为空。
* 不过显然Windows的编译器不喜欢这么做。
*/
APR_RING_HEAD(apr_Bucket_list, apr_Bucket) list;
/** 从Bucket分配得到的freelist */
apr_Bucket_alloc_t *Bucket_alloc;
};

 

3.5.5  Bucket和Brigade

这里有一句充满专业术语的概述:Bucket brigade代表了一个复杂的数据流,它可以在没有不必要拷贝的情况下经过一个分层的输入/输出系统(Bucket brigades represent a complex data stream that can be passed through a layered I/O system without unnecessary copying)。

Bucket和Brigade构成了Apache数据处理、输入/输出和过滤链(基本上是对同一个事物的三种不同的说法)的基础。对于过滤模块来说,使用并操纵Bucket和Brigade是基础,我们将在第8章进行详细介绍。

Bucket brigade是Bucket的一个双向的链表(环),因此我们没有被限制在队首插入数据并在队尾删除数据。尽管单独的一个Bucket可以在短时间内发生,但是Bucket仅仅作为Brigade的成员进行环绕传递。

Bucket用来存储各种类型的数据。它们可以是内存中的数据的指针,文件的一部分或者mmap区域的一部分,或者是一个进程的输出,以及其他的种种情况。Bucket也有一些类型相关的访问器函数。

read函数返回在Bucket中数据的地址和大小。如果这个数据不在内存,那么数据将会被读入内存,并且Bucket会改变类型,它将成为数据新 的内存地址的指针。如果所有的数据不能在Bucket中装配好,那么一个新的Bucket将会被插入到这个Bucket中用来装载剩余的数据。

split函数将Bucket中的数据分成两个区域。分隔之后,原始的Bucket指向数据的前一部分,而新的Bucket被插入到Brigade中,位于原始的Bucket之后,并指向数据的后一部分。引用计数在需要的时候被保存并维护。

setaside函数保证Bucket中的数据拥有足够的生命周期。例如,有时候很方便去创建一个Bucket,用它指向堆栈的数据,期望它将在堆 栈松散之前被消耗掉(例如,向网络输出)。如果这个期望被证实为不可能实现,那么setaside函数就被调用,将数据移送到更安全的区域。

copy函数在一个单一数据拷贝需要多个引用时创建一个复制的Bucket结构。并不是所有的Bucket类型都能够被拷贝。

destroy函数维护Bucket所使用的资源的引用计数,并在需要的时候将资源释放。

注意:所有的这些函数都有wrapper宏。如apr_bucket_read()、apr_bucket_destroy()等。这些wrapper宏可以直接使用,而不是通过使用函数指针的方式。

如果要写一个Bucket brigade,我们需要首先将数据转变为iovec,因此我们一次不能写太少的数据。如果我们需要性能出色,我们得在把这些数据转换为iovec之前,或者当正要转换为iovec时,尽量紧凑Bucket。

在APR中原生支持下列Bucket类型。

·File:Bucket的内容是一个文件。通常在处理一个静态文件时使用。

·Pipe:Bucket的内容是一个管道(文件系统FIFO)。

·Socket:Bucket的内容是一个套接字,通常为网络过滤器使用。

·Mmap:Bucket的内容是一个内存映像文件。

·Immortal:Bucket的内容是内存,该内存保证在Bucket生命周期内是正确的。

·Pool:Bucket的内容在池上进行分配。

·Transient:Bucket内容可能在超出范围并消失。

·Flush(元数据):Brigade的内容应该在继续之前被清空。在Apache中,这就意味着向过滤链的下一个过滤器传递有用的数据。

·EOS(元数据):数据的结束。

其他类型也可能被实现——事实上,Apache内部使用一些额外的元数据类型。笔者为SQL查询(使用apr_dbd)和脚本片断实现了 Bucket类型,这两种类型的Bucket在读取时执行并将数据转换为其他的Bucket类型。serf是一个第三方的库,它实现了很多种Bucket 类型 。

3.5.6  文件系统

APR中和文件系统相关的模块包括以下几个。

 apr_file_io:提供了标准的文件操作,打开/关闭,stdio类型的读/写操作,文件锁定,以及创建/删除/拷贝/重命名/更改模式等。这个模块可以操作的对象包括普通文件、临时文件、目录和管道。

·apr_file_info:提供了文件系统信息(状态),目录处理函数(例如,打开,关闭,读),文件路径处理和相对路径解析。

·apr_fnmatch:为文件系统提供了模式匹配,支持通配符操作。

·apr_mmap:内存映射文件。

我们将在后续的章节中看到这些模块的一些示例。

apvfs 是一个第三方扩展的库,为一系列不同(虚拟)的文件系统,例如标准文件、APR Bucket、归档IPC和数据库等实现了通用的、基于APR的前端。

3.5.7  网络

APR提供了两个和网络相关的模块。

·apr_network_io:它是一个套接字层,支持IPv4、IPv6以及TCP、UDP和SCTP协议。它支持一些和低层操作系统相关的特性,并且在没有这些特性时模拟这些特性。功能包括发送文件、接收滤波以及多播等。

·apr_poll:提供轮流检测一个套接字的函数(或者其他的描述符)。
3.5.8 编码和密码

3.5.8  编码和密码

APR没有提供一个密码函数库,而且Apache的mod_ssl也依赖于外部的OpenSSL包来实现传输层安全。APR在它的apr_base64、apr_md4、apr_md5和apr_sha1模块

中支持一些数据编码和哈希技术。

3.5.9 URI处理

3.5.9  URI处理

apr_uri模块为URI/URL定义了一个结构体,并提供了解析和组装函数。

/**
* 一个包括URI中所有域的结构
*/
struct apr_uri_t {
/** 模式 (“http”/“ftp”/...) */
char *scheme;
/** 组合 [user[:password]\@]host[:port] */
char *hostinfo;
/** 用户名,在
http://user:passwd\@host:port/ */
char *user;
/** 密码,在 
http://user:passwd\@host:port/ */
char *password;
/** 从URI获取的主机名(或者来自Host: header) */
char *hostname;
/** 端口字符串 (“port”的整数表示) */
char *port_str;
/** 请求路径 (或者“/”如果只是给出scheme://host)*/
char *path;
/** 在路径中‘?’后面的任何字符 */
char *query;
/** 拖尾的“#fragment”字符串 */
char *fragment;

/** 由gethostbyname() 返回的数据结构 */
struct hostent *hostent;

/** 数字形式的端口号,仅在port_str != NULL 是有效 */
apr_port_t port;

/** 结构被初始化了么? */
unsigned is_initialized:1;

/** DNS已经被查找了么? */
unsigned dns_looked_up:1;
/** DNS已经被解析了么? */
unsigned dns_resolved:1;
};

该模块提供的主要的函数是apr_uri_parse和apr_uri_unparse,这两个函数在字符串和apr_uri结构体之间进行转换。

3.5.10 进程和线程

·apr_thread_proc:提供了进程和线程管理函数——创建,父子关联函数包括环境传播、管道、集合点以及等待。

·apr_signal:提供了基本的信号处理函数。

·apr_global_mutex:提供了全局锁,用来保护从其他线程和进程中调用的线程。

进程

·apr_proc_mutex:提供了被调用进程的锁定,防止其他进程调用。

·apr_shm:提供了共享内存段。

线程

·apr_thread_mutex和apr_thread_rwlock:提供了线程锁和线程信号量。

·apr_thread_cond:提供了一个进程中不同线程同步的线程条件。

模块应当能够在多进程和/或者多线程的环境下运行。尽管它们很少需要创建一个新的线程,它们可能会使用信号量、共享内存和其他的技巧来共享资源并避开竞争条件。我们将在第4章介绍在Apache中使用进程和线程。

3.5.11  资源池

apr_reslist模块管理着持久资源的池。

数据库是很多Web应用的核心组件。不幸的是,如何连接到数据库成为影响传统应用架构——例如CGI和常用的LAMP(Linux、Apache、 MySQL、[Perl|PHP|Python])环境的主要问题。在Apache 2的线程化的MPM中使用apr_reslist(APR资源池模块),我们可以在很大程度上改善应用程序的性能和可扩展性,这些应用程序使用“昂贵的” 资源,例如数据库,或者代理一个应用服务器的后端连接。

第11章介绍了DBD框架,它是连接池的主要应用之一。

3.5.12  API扩展

以下的模块用来扩展新的API。

·apr_hooks:提供Apache的钩子,这是一种在扩展(模块)可以插入自己处理功能而导出API的机制。

·apr_optional_hooks:提供可选的钩子,允许不同的模块在同时被呈现并没有产生依赖时互相使用API。

·apr_optional:提供可选的函数,这样一个模块可以使用其他模块所导出的函数而不会产生依赖。

3.6  APR/Apache中的数据库

某些年龄段的读者可能回想起在20世纪80年代时,每一个计算机的应用程序都打包伴随着一大堆软盘,这些软盘上有数以百计的不同的打印机驱动。后 来,操作系统实现了一个非常聪明的解决方案:一个统一标准的API,因此每一个打印机具有一个单独的驱动,每一个应用程序使用一个单一的、兼容任何驱动的 打印函数。

在Apache中支持数据库的发展历程也如出一辙。最初,Apache不支持数据库,因此每一个需要数据库支持的模块不得不自己去实现。 Apache 1.3为NDBM和Berkeley DB提供了单独的、事实上是同样的认证模块,并为MySQL等流行SQL数据库提供了不同的(第三方)认证模块。类似地,每一个脚本语言——例如 Perl、PHP和Python——都有自己的数据库管理模块。

到Apache 2.0发布时,人们开发了apr_dbm模块用来为DBM(简单的键/值查询)类数据库提供一个统一的接口。最近,Apache中引入了apr_dbd模 块,用来为SQL数据库提供类似的API。和打印驱动类似,APR的数据库类不再需要多种驱动,而且已经成为Apache和APR中为新应用提供的首选数 据库支持方案。

3.6.1  DMB和apr_dbm模块

从计算机的早期时代意识到快速查找关键字的巨大需求之后,DBM就一直伴随着我们。最初的DMB是一个基于Unix的库和文件格式,提供快速、高扩 展性、使用键访问数据的解决方案。它包括(按照顺序)NDBM(New DBM)、GDBM(GNU DBM)和Berkeley DB。Berkeley DB是目前为止最先进的,也是唯一在今天还处于活跃开发状态的DBM。不过,从NDBM开始所有的DBM都提供了同样的核心功能,这些功能被大部分程序使 用,包括Apache在内。和APR绑定SDBM是DBM的一个最小实现,适用于使用其他DBM的应用程序。

尽管NDBM对于目前来说已经太古老了——和公元前600年左右希腊人命名New Town(Neapolis),目前叫做Naples的城市一样古老,它保留了DBM的基线。NDBM在早期的Apache模块中使用,例如Apache 1.x版本中的mod_auth_dbm和mod_rewrite。GDBM和Berkeley DB都提供了NDBM的功能,Linux的发布版本将这两个中的一个移植为“真正的”NDBM,并由于版权原因最终排除在外。不幸的是,不同DBM的文件 格式都是完全不兼容的,而且在有关数据库锁的行为上也有些差异。这些问题导致在Apache 1.x中使用DBM的Linux用户总是不停地报告一些问题。

Apache 2使用了一个统一的封装层apr_dbm取代对DBM的直接访问。在下面可能有一个或者多个数据库,可以通过一个配置选项或者通过构建脚本自动检测(默认 的行为)的方式在构建时决定具体和哪个数据库连接。只要一个DBM是运行的,那么被应用程序所使用的数据库就可以通过参数的方式传递,这样程序员就可以直 接控制(如果数据库是可配置的,那么管理员也可以控制),并且在需要的时候进行转换。作为可选项,对于类似于认证这些已经能够和任何DBM配合很好的情 况,可以使用系统默认的。由于Apache只支持唯一的DBM接口,因此可以使用唯一的DBM认证模块,而不管后面具体使用哪种DBM。

和DBM API类似的apr_dbm层的文档在apr_dbm.h中。尽管更新操作无论在GDBM中还是原始的NDBM中都是安全的,需要编程使用apr_dbm时,不要设想有任何的锁。

在任何情况下,对关键的更新要使用信号量,这使得更新变得安全。

在APR中支持的DBM函数和所有的DBM中常用的功能是相同的,即,APR的API和NDBM、GDBM以及Berkeley DB的早期版本本质上是等价的。Berkeley DB早期版本的一些高级功能,例如事务,就不再支持了,因此应用程序如果需要这些功能就只能够直接访问Berkeley DB了。

示例

在mod_authn_dbm中的fetch_dbm_value函数在一个DBM数据库中查找一个值。

static apr_status_t fetch_dbm_value(const char *dbmtype,
const char *dbmfile,
const char *user, char **value,
apr_pool_t *pool)
{
apr_dbm_t *f;
apr_datum_t key, val;
apr_status_t rv;

rv = apr_dbm_open_ex(&f, dbmtype, dbmfile, APR_DBM_READONLY,
APR_OS_DEFAULT, pool);

if (rv != APR_SUCCESS) {
return rv;
}

key.dptr = (char*)user;
#ifndef NETSCAPE_DBM_COMPAT
key.dsize = strlen(key.dptr);
#else
key.dsize = strlen(key.dptr) + 1;
#endif

    *value = NULL;

if (apr_dbm_fetch(f, key, &val) == APR_SUCCESS && val.dptr) {
*value = apr_pstrmemdup(pool, val.dptr, val.dsize);
}

apr_dbm_close(f);

return rv;
}

3.6.2  SQL数据库和apr_dbd

注意:apr_dbd模块目前在APR 0.x版本中还不能使用,因此在Apache 2.0也不能使用。它需要APR 1.2或者更高版本,或者CVS中的当前版本。

SQL数据库是大型数据库应用的标准,而且很多数据库在Web应用中普遍结合Apache使用。其中最流行的是轻量级的开放源代码的MySQL,不过它也仅仅是很多可供选择中的一种而已。

SQL数据库比DBM要更加庞大和复杂,而且通常不能互换,除非应用显式地设计为可移植的(或者在一个有限范围内的简单任务)。不过,为SQL应用程序设计的一个通用API可以获得和打印机驱动类似的效果。

apr_dbd模块是为Apache以及其他APR应用使用SQL数据库而提供的一个统一API。这个想法和Perl的DBI/DBD框架以及C语 言的libdbi的想法类似,只是apr_dbd和它们不同之处在于它使用了APR池进行资源管理。因此,在APR应用使用apr_dbd更加方便。

在APR内部,apr_dbd模块在它的方法方面也异乎寻常。由于apr_dbd API被编译为libaprutil,个别数据库的驱动可能在运行时被动态加载。因此,如果你安装了一个新的数据库包,你可以只安装该数据库的APR驱 动,而不需要重新编译整个APR或者APR-UTIL。

本书编写之际,apr_dbd支持MySQL、PostgreSQL、SQLite和Oracle数据库,其他数据库的驱动将很可能根据不同情况进行发布。

MySQL驱动

Apache将MySQL当作一个特殊的情况。由于MySQL使用GNU通用公共许可证(General Public License,GPL)发布,因此针对MySQL的驱动也必须在GPL下发布(或者根本就不发布)。因为可能对Apache用户带来额外的限制,这个要 求和Apache的许可证策略不兼容。

本书作者采用如下的方式处理这个问题:单独制作一个MySQL驱动 并在GPL之下许可。用户如果需要这个驱动,就将这个驱动下载到apr_dbd目录下或者构建apr_dbd的文件夹中。如果MySQL安装在标准目录 下,那么这个驱动就能够自动检测并使用标准的APR-UTIL配置流程进行构建。

用法

Apache模块通常通过提供者模块mod_dbd使用apr_dbd。

3.7  小结

本章对APR和APR-UTIL(APU)进行了概述,主要关注Apache应用开发者最可能感兴趣的那些模块。本章介绍的很多主题将在后续的章节进行深入的介绍。在后续的章节中,这些主题将与真正实质的内容更加相关。

本章特别指出了APR的如下主要角色:

·一个与平台无关的操作系统层

·一个资源管理的解决方案

·一个功能库和类库

我们详细介绍了以下的主题:

·APR惯例和风格

·APR池和Apache中的资源管理

·APR数据库类

·APR的主要类型

我们也对其他的APR模块进行了简略的介绍。

APR给人的感觉就是它在Apache中是全C编程的基础,而且本书的后续章节将广泛地使用它。如果想更加深入了解APR,你可以参考从头文件中自 动生成的非常不错的API文档(可以在apr.apache.org在线浏览),或者参考INOUE Seiichiro的教程 。

  • 4
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值