4.2.3. process_header 回调函数 现在轮到process_header了,就像create_request把链表指针挂到请求结构体上去一样,process_header把响应指针移到客户端可以接收到的部分。同时它还会从upstream 读入头信息,并且相应的设置发往客户端的响应头。 这里有个小例子,读进两个字符的响应。我们假设第一个字符代表“状态”字符。如果它是问号,我们将返回一个404错误并丢弃剩下的那个字符。如果它是空格,我们将以 200 OK的响应把另一个字符返回给客户端。好吧,这不是什么多有用的协议,不过可以作为一个不错的例子。那么我们如何来实现这个process_header 函数呢? static ngx_int_t ngx_http_character_server_process_header(ngx_http_request_t *r) { ngx_http_upstream_t *u; u = r->upstream; /* read the first character */ switch(u->buffer.pos[0]) { case '?': r->header_only; /* suppress this buffer from the client */ u->headers_in.status_n = 404; break; case ' ': u->buffer.pos++; /* move the buffer to point to the next character */ u->headers_in.status_n = 200; break; } return NGX_OK; } 就是这样。操作头部,改变指针,搞定!注意headers_in实际上就是我们之前提到过的头部结构体( http/ngx_http_request.h),但是它位于来自upstream的头中。一个真正的代理模块会在头信息的处理上做很多文章,不光是错误处理,做什么完全取决于你的想法。 但是……如果一个buffer没有能够装下全部的从upstream来的头信息,该怎么办呢? 4.2.4. 状态保持 好了,还记得我说过abort_request, reinit_request和finalize_request 可以用来重置内部状态吗?这是因为许多upstream模块都有其内部状态。模块需要定义一个 自定义上下文结构 ,来标记目前为止从upstream读到了什么。这跟之前说的“模块上下文”不是一个概念。“模块上下文”是预定义类型,而自定义上下文结构可以包含任何你需要的数据和字段(这可是你自己定义的结构体)。这个结构体在create_request函数中被实例化,大概像这样: ngx_http_character_server_ctx_t *p; /* my custom context struct */ p = ngx_pcalloc(r->pool, sizeof(ngx_http_character_server_ctx_t)); if (p == NULL) { return NGX_HTTP_INTERNAL_SERVER_ERROR; } ngx_http_set_ctx(r, p, ngx_http_character_server_module); 最后一行实际上将自定义上下文结构体注册到了特定的请求和模块名上,以便在稍后取用。当你需要这个结构体时(可能所有的回调函数中都需要它),只需要: ngx_http_proxy_ctx_t *p; p = ngx_http_get_module_ctx(r, ngx_http_proxy_module); 指针 p 可以得到当前的状态. 设置、重置、增加、减少、往里填数据……你可以随心所欲的操作它。当upstream服务器返回一块一块的响应时,读取这些响应的过程中使用持久状态机是个很nx的办法,它不用阻塞主事件循环。很好很强大! 4.3. Handler的装载 Handler的装载通过往模块启用了的指令的回调函数中添加代码来完成。比如,我的circle gif 中ngx_command_t是这样的: { ngx_string("circle_gif"), NGX_HTTP_LOC_CONF|NGX_CONF_NOARGS, ngx_http_circle_gif, 0, 0, NULL } 回调函数是里面的第三个元素,在这个例子中就是那个ngx_http_circle_gif。回调函数的参数是由指令结构体(ngx_conf_t, 包含用户配置的参数),相应的ngx_command_t结构体以及一个指向模块自定义配置结构体的指针组成的。我的circle gif模块中,这些函数是这样子的: static char * ngx_http_circle_gif(ngx_conf_t *cf, ngx_command_t *cmd, void *conf) { ngx_http_core_loc_conf_t *clcf; clcf = ngx_http_conf_get_module_loc_conf(cf, ngx_http_core_module); clcf->handler = ngx_http_circle_gif_handler; return NGX_CONF_OK; } 这里可以分为两步:首先,得到当前location的“core”结构体,再分配给它一个 handler。很简单,不是吗? 我已经把我知道的关于hanler模块的东西全招了,现在可以来说说输出过滤链上的filter模块了。 5. Filters Filter操作handler生成的响应。头部filter操作HTTP头,body filter操作响应的内容。 5.1. 剖析Header Filter 头部Filter由三个步骤组成: 1. 决定何时操作响应 2. 操作响应 3. 调用下一个filter 举个例子,比如有一个简化版本的“不改变”头部filter:如果客户请求头中的If- Modified-Since和响应头中的Last-Modified相符,它把响应状态设置成304。注意这个头部filter只读入一个参数:ngx_http_request_t结构体,而我们可以通过它操作到客户请求头和一会将被发送的响应消息头。 static ngx_int_t ngx_http_not_modified_header_filter(ngx_http_request_t *r) { time_t if_modified_since; if_modified_since = ngx_http_parse_time(r->headers_in.if_modified_since->value.data, r->headers_in.if_modified_since->value.len); /* step 1: decide whether to operate */ if (if_modified_since != NGX_ERROR && if_modified_since == r->headers_out.last_modified_time) { /* step 2: operate on the header */ r->headers_out.status = NGX_HTTP_NOT_MODIFIED; r->headers_out.content_type.len = 0; ngx_http_clear_content_length(r); ngx_http_clear_accept_ranges(r); } /* step 3: call the next filter */ return ngx_http_next_header_filter(r); } 结构headers_out和我们在hander那一节中看到的是一样的(参考http/ngx_http_request.h),也可以随意处置。 5.2. 剖析Body Filter 因为body filter一次只能操作一个buffer(链表),这使得编写body filter需要一定的技巧。模块需要知道什么时候可以覆盖输入buffer,用新申请的buffer_替换已有的,或者在现有的某个buffer前或后插入一个新buffer。有时候模块会收到许多buffer使得它不得不操作一个_不完整的链表,这使得事情变得更加复杂了。而更加不幸的是,Nginx没有为我们提供上层的API来操作buffer链表,所以body filter是比较难懂(当然也比较难写)。但是,有些操作你还是可以看出来的。 一个body filter原型大概是这个样子(例子代码从Nginx源代码的“chunked” filter中取得): static ngx_int_t ngx_http_chunked_body_filter(ngx_http_request_t *r, ngx_chain_t *in); 第一个参数是我们的老朋友——请求结构体,第二个参数则是指向当前部分链表(可能包含0,1,或更多的buffer)头的指针。 再来举个例子好了。假设我们想要做的是在每个请求之后插入文本"<l!-- Served by Nginx -->"。首先,我们需要判断给我们的buffer链表中是否已经包含响应的最终buffer。就像之前我说的,这里没有简便好用的API,所以我们只能自己来写个循环: ngx_chain_t *chain_link; int chain_contains_last_buffer = 0; for ( chain_link = in; chain_link != NULL; chain_link = chain_link->next ) { if (chain_link->buf->last_buf) chain_contains_last_buffer = 1; } 如果我们没有最后的缓冲区,就返回: if (!chain_contains_last_buffer) return ngx_http_next_body_filter(r, in); 很好,现在最后一个缓冲区已经存在链表中了。接下来我们分配一个新缓冲区: ngx_buf_t *b; b = ngx_calloc_buf(r->pool); if (b == NULL) { return NGX_ERROR; } 把数据放进去: b->pos = (u_char *) ""; b->last = b->pos + sizeof("") - 1; 把这个缓冲区挂在新的链表上: ngx_chain_t added_link; added_link.buf = b; added_link.next = NULL; 最后,把这个新链表挂在先前链表的末尾: chain_link->next = added_link; 并根据变化重置变量"last_buf"的值: chain_link->buf->last_buf = 0; added_link->buf->last_buf = 1; 再将修改过的链表传递给下一个输出过滤函数: return ngx_http_next_body_filter(r, in); 现有的函数做了比我们更多的工作,比如mod_perl($response->body =~ s/$/<!-- Served by mod_perl -->/),但是缓冲区链确实是一个强大的构想,它可以让程序员渐进地处理数据,这使得客户端可以尽可能早地得到响应。但是依我来看,缓冲区链表实在需要一个更为干净的接口,这样程序员也可以避免操作不一致状态的链表。但是目前为止,所有的操作风险都得自己控制。 5.3. Filter的装载 Filter在在回调函数post-configuration中被装载。header filter和body filter都是在这里被装载的。 我们以chunked filter模块为例来具体看看: static ngx_http_module_t ngx_http_chunked_filter_module_ctx = { NULL, /* preconfiguration */ ngx_http_chunked_filter_init, /* postconfiguration */ ... }; ngx_http_chunked_filter_init中的具体实现如下: static ngx_int_t ngx_http_chunked_filter_init(ngx_conf_t *cf) { ngx_http_next_header_filter = ngx_http_top_header_filter; ngx_http_top_header_filter = ngx_http_chunked_header_filter; ngx_http_next_body_filter = ngx_http_top_body_filter; ngx_http_top_body_filter = ngx_http_chunked_body_filter; return NGX_OK; } 发生了什么呢?好吧,如果你还记得,过滤模块组成了一条”接力链表“。当handler生成一个响应后,调用2个函数:ngx_http_output_filter它调用全局函数ngx_http_top_body_filter;以及ngx_http_send_header 它调用全局函数ngx_top_header_filter。 ngx_http_top_body_filter 和 ngx_http_top_header_filter是body和header各自的头部filter链的”链表头“。链表上的每一个”连接“都保存着链表中下一个连接的函数引用(分别是 ngx_http_next_body_filter 和 ngx_http_next_header_filter)。当一个filter完成工作之后,它只需要调用下一个filter,直到一个特殊的被定义成”写“的filter被调用,这个”写“filter的作用是包装最终的HTTP响应。你在这个filter_init函数中看到的就是,模块把自己添加到filter链表中;它先把旧的”头部“filter当做是自己的”下一个“,然后再声明”它自己“是”头部“filter。(因此,最后一个被添加的filter会第一个被执行。) 边注: 这到底是怎么工作的? 每个filter要么返回一个错误码,要么用`return ngx_http_next_body_filter();`来作为返回语句 因此,如果filter顺利链执行到了链尾(那个特别定义的的”写“filter),将返回一个"OK"响应,但如果执行过程中遇到了错误,链将被砍断,同时Nginx将给出一个错误的信息。这是一个单向的,错误快速返回的,只使用函数引用实现的链表。帅啊! 6. Load-balancers Load-balancer用来决定哪一个后端将会收到请求;具体的实现是round-robin方式或者把请求进行hash。本节将介绍load-balancer模块的装载及其调用。我们将用upstream_hash_module(full source)作例子。upstream_hash将对nginx.conf里配置的变量进行 hash,来选择后端服务器。 一个load-balancer分为六个部分: 1. 启用配置指令 (e.g, hash;) 将会调用注册函数 2. 注册函数将定义一些合法的server 参数 (e.g., weight=) 并注册一个 upstream初始化函数 3. upstream初始化函数将在配置经过验证后被调用,并且: * 解析 server 名称为特定的IP地址 * 为每个sokcet连接分配空间 * 设置对端初始化函数的回调入口 4. 对端初始化函数将在每次请求时被调用一次,它主要负责设置一些负载均衡函数将会使用的数据结构。 5. 负载均衡函数决定把请求分发到哪里;每个请求将至少调用一次这个函数(如果后端服务器失败了,那就是多次了),有意思的事情就是在这里做的。 6. 最后,对端释放函数 可以在与对应的后端服务器结束通信之后更新统计信息 (成功或失败) 好像很多嘛,我来逐一讲讲。 6.1. 启用指令 指令声明,既确定了他们在哪里生效又确定了一旦流程遇到指令将要调用什么函数。load-balancer的指令需要置NGX_HTTP_UPS_CONF标志位,一遍让Nginx知道这个指令只会在upstream块中有效。同时它需要提供一个指向注册函数的指针。下面列出的是upstream_hash模块的指令声明: { ngx_string("hash"), NGX_HTTP_UPS_CONF|NGX_CONF_NOARGS, ngx_http_upstream_hash, 0, 0, NULL }, 都是些很眼熟的东西。 6.2. 注册函数 上面的回调函数ngx_http_upstream_hash就是所谓的注册函数。之所以这样叫(我起得名字)是因为它注册了把upstream初始化函数和周边的upstream配置注册到了一块。另外,注册函数还定义了特定upstream块中的server指令的一些选项(如weight=, fail_timeout=),下面是upstream_hash模块的注册函数: ngx_http_upstream_hash(ngx_conf_t *cf, ngx_command_t *cmd, void *conf) { ngx_http_upstream_srv_conf_t *uscf; ngx_http_script_compile_t sc; ngx_str_t *value; ngx_array_t *vars_lengths, *vars_values; value = cf->args->elts; /* the following is necessary to evaluate the argument to "hash" as a $variable */ ngx_memzero(&sc, sizeof(ngx_http_script_compile_t)); vars_lengths = NULL; vars_values = NULL; sc.cf = cf; sc.source = &value[1]; sc.lengths = &vars_lengths; sc.values = &vars_values; sc.complete_lengths = 1; sc.complete_values = 1; if (ngx_http_script_compile(&sc) != NGX_OK) { return NGX_CONF_ERROR; } /* end of $variable stuff */ uscf = ngx_http_conf_get_module_srv_conf(cf, ngx_http_upstream_module); /* the upstream initialization function */ uscf->peer.init_upstream = ngx_http_upstream_init_hash; uscf->flags = NGX_HTTP_UPSTREAM_CREATE; /* OK, more $variable stuff */ uscf->values = vars_values->elts; uscf->lengths = vars_lengths->elts; /* set a default value for "hash_method" */ if (uscf->hash_function == NULL) { uscf->hash_function = ngx_hash_key; } return NGX_CONF_OK; } 除了依葫芦画瓢的用来计算$variable的代码,剩下的都很简单,就是分配一个回调函数,设置一些标志位。哪些标志位是有效的呢? * NGX_HTTP_UPSTREAM_CREATE: 让upstream块中有 server 指令。我实在想不出那种情形会用不到它。 * NGX_HTTP_UPSTREAM_WEIGHT: 让server指令获取选项 weight= * NGX_HTTP_UPSTREAM_MAX_FAILS: 允许选项max_fails= * NGX_HTTP_UPSTREAM_FAIL_TIMEOUT: 允许选项fail_timeout= * NGX_HTTP_UPSTREAM_DOWN: 允许选项 down * NGX_HTTP_UPSTREAM_BACKUP: 允许选项backup 每一个模块都可以访问这些配置值。 一切都取决于模块自己的决定 。也就是说,max_fails不会被自动强制执行;所有的失败逻辑都是由模块作者决定的。过会我们再说这个。目前,我们还没有完成对回调函数的追踪呢。接下来,我们来看upstream初始化函数 (上面的函数中的回调函数init_upstream )。 6.3. upstream 初始化函数 upstream 初始化函数的目的是,解析主机名,为socket分配空间,分配(另一个)回调函数。下面是upstream_hash: ngx_int_t ngx_http_upstream_init_hash(ngx_conf_t *cf, ngx_http_upstream_srv_conf_t *us) { ngx_uint_t i, j, n; ngx_http_upstream_server_t *server; ngx_http_upstream_hash_peers_t *peers; /* set the callback */ us->peer.init = ngx_http_upstream_init_upstream_hash_peer; if (!us->servers) { return NGX_ERROR; } server = us->servers->elts; /* figure out how many IP addresses are in this upstream block. */ /* remember a domain name can resolve to multiple IP addresses. */ for (n = 0, i = 0; i < us->servers->nelts; i++) { n += server[i].naddrs; } /* allocate space for sockets, etc */ peers = ngx_pcalloc(cf->pool, sizeof(ngx_http_upstream_hash_peers_t) + sizeof(ngx_peer_addr_t) * (n - 1)); if (peers == NULL) { return NGX_ERROR; } peers->number = n; /* one port/IP address per peer */ for (n = 0, i = 0; i < us->servers->nelts; i++) { for (j = 0; j < server[i].naddrs; j++, n++) { peers->peer[n].sockaddr = server[i].addrs[j].sockaddr; peers->peer[n].socklen = server[i].addrs[j].socklen; peers->peer[n].name = server[i].addrs[j].name; } } /* save a pointer to our peers for later */ us->peer.data = peers; return NGX_OK; } 这个函数包含的东西ms比我们期望的多些。大部分的工作ms都该被抽象出来,但事实却不是,我们只能忍受这一点。倒是有一种简化的策略:调用另一个模块的upstream初始化函数,把这些脏活累活(对端的分配等等)都让它干了,然后再覆盖其us->peer.init这个回调函数。例子可以参见http/modules/ngx_http_upstream_ip_hash_module.c。 在我们这个观点中的关键点是设置对端初始化函数的指向,在我们这个例子里是ngx_http_upstream_init_upstream_hash_peer。 6.4. 对端初始化函数 对端初始化函数每个请求调用一次。它会构造一个数据结构,模块会用这个数据结构来选择合适的后端服务器;这个数据结构保存着和后端交互的重试次数,通过它可以很容易的跟踪链接失败次数或者是计算好的哈希值。这个结构体习惯性地被命名为ngx_http_upstream_<module name>_peer_data_t。 另外,对端初始化函数还会构建两个回调函数: * get: load-balancing 函数 * free: 对端释放函数 (通常只是在连接完成后更新一些统计信息) 似乎还不止这些,它同时还初始化了一个叫做tries的变量。只要tries是正数,Nginx将继续重试当前的load-banlancer。当tries变为0时,Nginx将放弃重试。一切都取决于get 和 free 如何设置合适的tries。 下面是upstream_hash中对端初始化函数的例子: static ngx_int_t ngx_http_upstream_init_hash_peer(ngx_http_request_t *r, ngx_http_upstream_srv_conf_t *us) { ngx_http_upstream_hash_peer_data_t *uhpd; ngx_str_t val; /* evaluate the argument to "hash" */ if (ngx_http_script_run(r, &val, us->lengths, 0, us->values) == NULL) { return NGX_ERROR; } /* data persistent through the request */ uhpd = ngx_pcalloc(r->pool, sizeof(ngx_http_upstream_hash_peer_data_t) + sizeof(uintptr_t) * ((ngx_http_upstream_hash_peers_t *)us->peer.data)->number / (8 * sizeof(uintptr_t))); if (uhpd == NULL) { return NGX_ERROR; } /* save our struct for later */ r->upstream->peer.data = uhpd; uhpd->peers = us->peer.data; /* set the callbacks and initialize "tries" to "hash_again" + 1*/ r->upstream->peer.free = ngx_http_upstream_free_hash_peer; r->upstream->peer.get = ngx_http_upstream_get_hash_peer; r->upstream->peer.tries = us->retries + 1; /* do the hash and save the result */ uhpd->hash = us->hash_function(val.data, val.len); return NGX_OK; } 看上去不错,我们现在可以来选择一台upstream服务器了。 6.5. 负载均衡函数 主要部分现在才开始。货真价实的哦。模块就是在这里选择upstream服务器的。负载均衡函数的原型看上去是这样的: static ngx_int_t ngx_http_upstream_get__peer(ngx_peer_connection_t *pc, void *data); data是我们存放所关注的客户端连接中有用信息的结构体。pc则是要存放我们将要去连接的server的相关信息。负载均衡函数做的事情就是填写pc->sockaddr, pc->socklen, 和 pc->name。如果你懂一点网络编程的话,这些东西应该都比较熟悉了;但实际上他们跟我们手头上的任务来比并不算很重要。我们不关心他们代表什么;我们只想知道从哪里找到合适的值来填写他们。 这个函数必须找到一个可用server的列表,挑一个分配给pc。我们来看看upstream_hash是怎么做的吧: 之前upstream_hash模块已经通过调用ngx_http_upstream_init_hash,把server列表存放在了ngx_http_upstream_hash_peer_data_t 这一结构中。这个结构就是现在的data: ngx_http_upstream_hash_peer_data_t *uhpd = data; 对端列表现在在uhpd->peers->peer中了。我们通过对哈希值与 server总数取模来从这个数组中取得最终的对端服务器: ngx_peer_addr_t *peer = &uhpd->peers->peer[uhpd->hash % uhpd->peers->number]; 终于大功告成了: pc->sockaddr = peers->sockaddr; pc->socklen = peers->socklen; pc->name = &peers->name; return NGX_OK; 就是这样!如果load-balancer模块返回 NGX_OK,则意味着”来吧,上这个 server吧!“。如果返回的是NGX_BUSY,说明所有的后端服务器目前都不可用,此时Nginx应该重试。 但是……我们怎么记录哪些个服务器不可用了?我们如果不想重试了怎么办? 6.6. 对端释放函数 对端释放函数在upstream连接就绪之后开始运行,它的目的是跟踪失败。函数原型如下: void ngx_http_upstream_free__peer(ngx_peer_connection_t *pc, void *data, ngx_uint_t state); 头两个参数和我们在load-balancer函数中看到的一样。第三个参数是一个state变量,它表明了当前连接是成功还是失败。它可能是NGX_PEER_FAILED (连接失败) 和 NGX_PEER_NEXT (连接失败或者连接成功但程序返回了错误)按位或的结果。如果它是0则代表连接成功。 这些失败如何处理则由模块的开发者自己定。如果根本不再用,那结果则应存放到data中,这是一个指向每个请求自定义的结构体。 但是对端释放函数的关键作用是可以设置pc->tries为 0来阻止Nginx在load-balancer模块中重试。最简单的对端释放函数应该是这样的: pc->tries = 0; 这样就保证了如果发往后端服务器的请求遇到了错误,客户端将得到一个502 Bad Proxy的错误。 这儿还有一个更为复杂的例子,是从upstream_hash模块中拿来的。如果后端连接失败,它会在位向量 (叫做 tried,一个 uintptr_t类型的数组)中标示失败,然后继续选择一个新的后端服务器直至成功。 #define ngx_bitvector_index(index) index / (8 * sizeof(uintptr_t)) #define ngx_bitvector_bit(index) (uintptr_t) 1 << index % (8 * sizeof(uintptr_t)) static void ngx_http_upstream_free_hash_peer(ngx_peer_connection_t *pc, void *data, ngx_uint_t state) { ngx_http_upstream_hash_peer_data_t *uhpd = data; ngx_uint_t current; if (state & NGX_PEER_FAILED && --pc->tries) { /* the backend that failed */ current = uhpd->hash % uhpd->peers->number; /* mark it in the bit-vector */ uhpd->tried[ngx_bitvector_index(current)] |= ngx_bitvector_bit(current); do { /* rehash until we're out of retries or we find one that hasn't been tried */ uhpd->hash = ngx_hash_key((u_char *)&uhpd->hash, sizeof(ngx_uint_t)); current = uhpd->hash % uhpd->peers->number; } while ((uhpd->tried[ngx_bitvector_index(current)] & ngx_bitvector_bit(current)) && --pc->tries); } } 因为load-balancer函数只会看新的uhpd->hash的值,所以这样是行之有效的。 许多应用程序不提供重试功能,或者在更高层的逻辑中进行了控制。但其实你也看到了,只需这么几行代码这个功能就可以实现了。 7. 编写并编译一个新的Nginx模块 至此,你应该可以来找一个现成的Nginx模块来看看,尝试着理解其工作原理。可以看看src/http/modules/,这里一些现成可用的模块。从里面找一个跟你想要的大概相似的模块深入地看看。看上去很熟悉?没错,应该很熟悉。对照着代码和这篇文章,慢慢理解吧。 (译者注:介于这节已不再是晦涩的概念介绍,就不做翻译了) But Emiller didn't write a Balls-In Guide to Reading Nginx Modules. Hell no. This is a Balls-Out Guide. We're not reading. We're writing. Creating. Sharing with the world. First thing, you're going to need a place to work on your module. Make a folder for your module anywhere on your hard drive, but separate from the Nginx source (and make sure you have the latest copy from nginx.net). Your new folder should contain two files to start with: * "config" * "ngx_http<your module>module.c" The "config" file will be included by ./configure, and its contents will depend on the type of module. "config" for filter modules: ngx_addon_name=ngx_http__module HTTP_AUX_FILTER_MODULES="$HTTP_AUX_FILTER_MODULES ngx_http__module" NGX_ADDON_SRCS="$NGX_ADDON_SRCS $ngx_addon_dir/ngx_http__module.c" "config" for other modules: ngx_addon_name=ngx_http__module HTTP_MODULES="$HTTP_MODULES ngx_http__module" NGX_ADDON_SRCS="$NGX_ADDON_SRCS $ngx_addon_dir/ngx_http__module.c" Now for your C file. I recommend copying an existing module that does something similar to what you want, but rename it "ngx_http<your module>module.c". Let this be your model as you change the behavior to suit your needs, and refer to this guide as you understand and refashion the different pieces. When you're ready to compile, just go into the Nginx directory and type ./configure --add-module=path/to/your/new/module/directory and then make and make install like you normally would. If all goes well, your module will be compiled right in. Nice, huh? No need to muck with the Nginx source, and adding your module to new versions of Nginx is a snap, just use that same ./configure command. By the way, if your module needs any dynamically linked libraries, you can add this to your "config" file: CORE_LIBS="$CORE_LIBS -lfoo" Where foo is the library you need. If you make a cool or useful module, be sure to send a note to the Nginx mailing list and share your work. 8. 高级话题 本指南只涵盖了Nginx模块开发的基础。想开发更为精巧的模块,一定要去 Emiller's Advanced Topics In Nginx Module Development看看。 (责任编辑:IT) |