Feb
25
花了两天的时间在libevent上,想总结下,就以写简单tutorial的方式吧,貌似没有一篇简单的说明,让人马上就能上手用的。
首先给出官方文档吧: http://libevent.org ,首页有个Programming with Libevent,里面是一节一节的介绍libevent,但是感觉信息量太大了,而且还是英文的-。-(当然,如果想好好用libevent,看看还是很有必要的),还有个Reference,大致就是对各个版本的libevent使用doxgen生成的文档,用来查函数原型和基本用法什么的。
下面假定已经学习过基本的socket编程(socket,bind,listen,accept,connect,recv,send,close),并且对异步/callback有基本认识。
基本的socket编程是阻塞/同步的,每个操作除非已经完成或者出错才会返回,这样对于每一个请求,要使用一个线程或者单独的进程去处理,系统资源没法支撑大量的请求(所谓c10k problem),例如内存(默认情况下每个线程需要占用2~8M的栈空间),以及进程切换带来的原因等。posix定义了可以使用异步的select系统调用,但是因为其采用了轮询的方式来判断某个fd是否变成active,效率不高[O(n)],连接数一多,也还是撑不住。于是各系统分别提出了基于异步/callback的系统调用,例如Linux的epoll,BSD的kqueue,Windows的IOCP。由于在内核层面做了支持,所以可以用O(1)的效率查找到active的fd。基本上,libevent就是对这些高效IO的封装,提供统一的API,简化开发。
libevent大概是这样的:
默认情况下是单线程的(可以配置成多线程,如果有需要的话),每个线程有且只有一个event_base,对应一个struct event_base结构体(以及附于其上的事件管理器),用来schedule托管给它的一系列event,可以和操作系统的进程管理类比,当然,要更简单一点。当一个事件发生后,event_base会在合适的时间(不一定是立即)去调用绑定在这个事件上的函数(传入一些预定义的参数,以及在绑定时指定的一个参数),直到这个函数执行完,再返回schedule其他事件。
event_base内部有一个循环,循环阻塞在epoll/kqueue等系统调用上,直到有一个/一些事件发生,然后去处理这些事件。当然,这些事件要被绑定在这个event_base上。每个事件对应一个struct event,可以是监听一个fd或者POSIX信号量之类(这里只讲fd了,其他的看manual吧)。struct event使用event_new来创建和绑定,使用event_add来启用:
注:libevent支持的事件及属性包括(使用bitfield实现,所以要用 | 来让它们合体)
(a) EV_TIMEOUT: 超时
(b) EV_READ: 只要网络缓冲中还有数据,回调函数就会被触发
(c) EV_WRITE: 只要塞给网络缓冲的数据被写完,回调函数就会被触发
(d) EV_SIGNAL: POSIX信号量,参考manual吧
(e) EV_PERSIST: 不指定这个属性的话,回调函数被触发后事件会被删除
(f) EV_ET: Edge-Trigger边缘触发,参考EPOLL_ET
然后需要启动event_base的循环,这样才能开始处理发生的事件。循环的启动使用event_base_dispatch,循环将一直持续,直到不再有需要关注的事件,或者是遇到event_loopbreak()/event_loopexit()函数。
接下来关注下绑定到event的回调函数callback_func:传递给它的是一个socket fd、一个event类型及属性bit_field、以及传递给event_new的最后一个参数(去上面几行回顾一下,把event_base给传进来了,实际上更多地是分配一个结构体,把相关的数据都撂进去,然后丢给event_new,在这里就能取得到了)。其原型是:
对于一个服务器而言,上面的流程大概是这样组合的:
1. listener = socket(),bind(),listen(),设置nonblocking(POSIX系统中可使用fcntl设置,windows不需要设置,实际上libevent提供了统一的包装evutil_make_socket_nonblocking)
2. 创建一个event_base
3. 创建一个event,将该socket托管给event_base,指定要监听的事件类型,并绑定上相应的回调函数(及需要给它的参数)。对于listener socket来说,只需要监听EV_READ|EV_PERSIST
4. 启用该事件
5. 进入事件循环
---------------
6. (异步) 当有client发起请求的时候,调用该回调函数,进行处理。
问题:为什么不在listen完马上调用accept,获得客户端连接以后再丢给event_base呢?这个问题先想想噢。
回调函数要做什么事情呢?当然是处理client的请求了。首先要accept,获得一个可以与client通信的sockfd,然后……调用recv/send吗?错!大错特错!如果直接调用recv/send的话,这个线程就阻塞在这个地方了,如果这个客户端非常的阴险(比如一直不发消息,或者网络不好,老是丢包),libevent就只能等它,没法处理其他的请求了——所以应该创建一个新的event来托管这个sockfd。
在老版本libevent上的实现,比较罗嗦[如果不想详细了解的话,看下一部分]。
对于服务器希望先从client获取数据的情况,大致流程是这样的:
1. 将这个sockfd设置为nonblocking
2. 创建2个event:
event_read,绑上sockfd的EV_READ|EV_PERSIST,设置回调函数和参数(后面提到的struct)
event_write,绑上sockfd的EV_WRITE|EV_PERSIST,设置回调函数和参数(后面提到的struct)
3. 启用event_read事件
------
4. (异步) 等待event_read事件的发生, 调用相应的回调函数。这里麻烦来了:回调函数用recv读入的数据,不能直接用send丢给sockfd了事——因为sockfd是nonblocking的,丢给它的话,不能保证正确(为什么呢?)。所以需要一个自己管理的缓存用来保存读入的数据中(在accept以后就创建一个struct,作为第2步回调函数的arg传进来),在合适的时间(比如遇到换行符)启用event_write事件【event_add(event_write, NULL)】,等待EV_WRITE事件的触发
------
5. (异步) 当event_write事件的回调函数被调用的时候,往sockfd写入数据,然后删除event_write事件【event_del(event_write)】,等待event_read事件的下一次执行。
以上步骤比较晦涩,具体代码可参考官方文档里面的【Example: A low-level ROT13 server with Libevent】
由于需要自己管理缓冲区,且过程晦涩难懂,并且不兼容于Windows的IOCP,所以libevent2开始,提供了bufferevent这个神器,用来提供更加优雅、易用的API。struct bufferevent内建了两个event(read/write)和对应的缓冲区【struct evbuffer *input, *output】,并提供相应的函数用来操作缓冲区(或者直接操作bufferevent)。每当有数据被读入input的时候,read_cb函数被调用;每当output被输出完的时候,write_cb被调用;在网络IO操作出现错误的情况(连接中断、超时、其他错误),error_cb被调用。于是上一部分的步骤被简化为:
1. 设置sockfd为nonblocking
2. 使用bufferevent_socket_new创建一个struct bufferevent *bev,关联该sockfd,托管给event_base
3. 使用bufferevent_setcb(bev, read_cb, write_cb, error_cb, (void *)arg)将EV_READ/EV_WRITE对应的函数
4. 使用bufferevent_enable(bev, EV_READ|EV_WRITE|EV_PERSIST)来启用read/write事件
------
5. (异步)
在read_cb里面从input读取数据,处理完毕后塞到output里(会被自动写入到sockfd)
在write_cb里面(需要做什么吗?对于一个echo server来说,read_cb就足够了)
在error_cb里面处理遇到的错误
*. 可以使用bufferevent_set_timeouts(bev, struct timeval *READ, struct timeval *WRITE)来设置读写超时, 在error_cb里面处理超时。
*. read_cb和write_cb的原型是
void read_or_write_callback(struct bufferevent *bev, void *arg)
error_cb的原型是
void error_cb(struct bufferevent *bev, short error, void *arg) //这个是event的标准回调函数原型
可以从bev中用libevent的API提取出event_base、sockfd、input/output等相关数据,详情RTFM~
于是代码简化到只需要几行的read_cb和error_cb函数即可:
于是一个支持大并发量的echo server就成型了!下面附上无注释的echo server源码,110行,多抄几遍,就能完全弄懂啦!更复杂的例子参见官方文档里面的【Example: A simpler ROT13 server with Libevent】
首先给出官方文档吧: http://libevent.org ,首页有个Programming with Libevent,里面是一节一节的介绍libevent,但是感觉信息量太大了,而且还是英文的-。-(当然,如果想好好用libevent,看看还是很有必要的),还有个Reference,大致就是对各个版本的libevent使用doxgen生成的文档,用来查函数原型和基本用法什么的。
下面假定已经学习过基本的socket编程(socket,bind,listen,accept,connect,recv,send,close),并且对异步/callback有基本认识。
基本的socket编程是阻塞/同步的,每个操作除非已经完成或者出错才会返回,这样对于每一个请求,要使用一个线程或者单独的进程去处理,系统资源没法支撑大量的请求(所谓c10k problem),例如内存(默认情况下每个线程需要占用2~8M的栈空间),以及进程切换带来的原因等。posix定义了可以使用异步的select系统调用,但是因为其采用了轮询的方式来判断某个fd是否变成active,效率不高[O(n)],连接数一多,也还是撑不住。于是各系统分别提出了基于异步/callback的系统调用,例如Linux的epoll,BSD的kqueue,Windows的IOCP。由于在内核层面做了支持,所以可以用O(1)的效率查找到active的fd。基本上,libevent就是对这些高效IO的封装,提供统一的API,简化开发。
libevent大概是这样的:
默认情况下是单线程的(可以配置成多线程,如果有需要的话),每个线程有且只有一个event_base,对应一个struct event_base结构体(以及附于其上的事件管理器),用来schedule托管给它的一系列event,可以和操作系统的进程管理类比,当然,要更简单一点。当一个事件发生后,event_base会在合适的时间(不一定是立即)去调用绑定在这个事件上的函数(传入一些预定义的参数,以及在绑定时指定的一个参数),直到这个函数执行完,再返回schedule其他事件。
//创建一个event_base
struct event_base *base = event_base_new();
assert(base != NULL);
struct event_base *base = event_base_new();
assert(base != NULL);
event_base内部有一个循环,循环阻塞在epoll/kqueue等系统调用上,直到有一个/一些事件发生,然后去处理这些事件。当然,这些事件要被绑定在这个event_base上。每个事件对应一个struct event,可以是监听一个fd或者POSIX信号量之类(这里只讲fd了,其他的看manual吧)。struct event使用event_new来创建和绑定,使用event_add来启用:
//创建并绑定一个event
struct event *listen_event;
//参数:event_base, 监听的fd,事件类型及属性,绑定的回调函数,给回调函数的参数
listen_event = event_new(base, listener, EV_READ|EV_PERSIST, callback_func, (void*)base);
//参数:event,超时时间(struct timeval *类型的,NULL表示无超时设置)
event_add(listen_event, NULL);
struct event *listen_event;
//参数:event_base, 监听的fd,事件类型及属性,绑定的回调函数,给回调函数的参数
listen_event = event_new(base, listener, EV_READ|EV_PERSIST, callback_func, (void*)base);
//参数:event,超时时间(struct timeval *类型的,NULL表示无超时设置)
event_add(listen_event, NULL);
注:libevent支持的事件及属性包括(使用bitfield实现,所以要用 | 来让它们合体)
(a) EV_TIMEOUT: 超时
(b) EV_READ: 只要网络缓冲中还有数据,回调函数就会被触发
(c) EV_WRITE: 只要塞给网络缓冲的数据被写完,回调函数就会被触发
(d) EV_SIGNAL: POSIX信号量,参考manual吧
(e) EV_PERSIST: 不指定这个属性的话,回调函数被触发后事件会被删除
(f) EV_ET: Edge-Trigger边缘触发,参考EPOLL_ET
然后需要启动event_base的循环,这样才能开始处理发生的事件。循环的启动使用event_base_dispatch,循环将一直持续,直到不再有需要关注的事件,或者是遇到event_loopbreak()/event_loopexit()函数。
//启动事件循环
event_base_dispatch(base);
event_base_dispatch(base);
接下来关注下绑定到event的回调函数callback_func:传递给它的是一个socket fd、一个event类型及属性bit_field、以及传递给event_new的最后一个参数(去上面几行回顾一下,把event_base给传进来了,实际上更多地是分配一个结构体,把相关的数据都撂进去,然后丢给event_new,在这里就能取得到了)。其原型是:
typedef void(* event_callback_fn)(evutil_socket_t sockfd, short event_type, void *arg)
对于一个服务器而言,上面的流程大概是这样组合的:
1. listener = socket(),bind(),listen(),设置nonblocking(POSIX系统中可使用fcntl设置,windows不需要设置,实际上libevent提供了统一的包装evutil_make_socket_nonblocking)
2. 创建一个event_base
3. 创建一个event,将该socket托管给event_base,指定要监听的事件类型,并绑定上相应的回调函数(及需要给它的参数)。对于listener socket来说,只需要监听EV_READ|EV_PERSIST
4. 启用该事件
5. 进入事件循环
---------------
6. (异步) 当有client发起请求的时候,调用该回调函数,进行处理。
问题:为什么不在listen完马上调用accept,获得客户端连接以后再丢给event_base呢?这个问题先想想噢。
回调函数要做什么事情呢?当然是处理client的请求了。首先要accept,获得一个可以与client通信的sockfd,然后……调用recv/send吗?错!大错特错!如果直接调用recv/send的话,这个线程就阻塞在这个地方了,如果这个客户端非常的阴险(比如一直不发消息,或者网络不好,老是丢包),libevent就只能等它,没法处理其他的请求了——所以应该创建一个新的event来托管这个sockfd。
在老版本libevent上的实现,比较罗嗦[如果不想详细了解的话,看下一部分]。
对于服务器希望先从client获取数据的情况,大致流程是这样的:
1. 将这个sockfd设置为nonblocking
2. 创建2个event:
event_read,绑上sockfd的EV_READ|EV_PERSIST,设置回调函数和参数(后面提到的struct)
event_write,绑上sockfd的EV_WRITE|EV_PERSIST,设置回调函数和参数(后面提到的struct)
3. 启用event_read事件
------
4. (异步) 等待event_read事件的发生, 调用相应的回调函数。这里麻烦来了:回调函数用recv读入的数据,不能直接用send丢给sockfd了事——因为sockfd是nonblocking的,丢给它的话,不能保证正确(为什么呢?)。所以需要一个自己管理的缓存用来保存读入的数据中(在accept以后就创建一个struct,作为第2步回调函数的arg传进来),在合适的时间(比如遇到换行符)启用event_write事件【event_add(event_write, NULL)】,等待EV_WRITE事件的触发
------
5. (异步) 当event_write事件的回调函数被调用的时候,往sockfd写入数据,然后删除event_write事件【event_del(event_write)】,等待event_read事件的下一次执行。
以上步骤比较晦涩,具体代码可参考官方文档里面的【Example: A low-level ROT13 server with Libevent】
由于需要自己管理缓冲区,且过程晦涩难懂,并且不兼容于Windows的IOCP,所以libevent2开始,提供了bufferevent这个神器,用来提供更加优雅、易用的API。struct bufferevent内建了两个event(read/write)和对应的缓冲区【struct evbuffer *input, *output】,并提供相应的函数用来操作缓冲区(或者直接操作bufferevent)。每当有数据被读入input的时候,read_cb函数被调用;每当output被输出完的时候,write_cb被调用;在网络IO操作出现错误的情况(连接中断、超时、其他错误),error_cb被调用。于是上一部分的步骤被简化为:
1. 设置sockfd为nonblocking
2. 使用bufferevent_socket_new创建一个struct bufferevent *bev,关联该sockfd,托管给event_base
3. 使用bufferevent_setcb(bev, read_cb, write_cb, error_cb, (void *)arg)将EV_READ/EV_WRITE对应的函数
4. 使用bufferevent_enable(bev, EV_READ|EV_WRITE|EV_PERSIST)来启用read/write事件
------
5. (异步)
在read_cb里面从input读取数据,处理完毕后塞到output里(会被自动写入到sockfd)
在write_cb里面(需要做什么吗?对于一个echo server来说,read_cb就足够了)
在error_cb里面处理遇到的错误
*. 可以使用bufferevent_set_timeouts(bev, struct timeval *READ, struct timeval *WRITE)来设置读写超时, 在error_cb里面处理超时。
*. read_cb和write_cb的原型是
void read_or_write_callback(struct bufferevent *bev, void *arg)
error_cb的原型是
void error_cb(struct bufferevent *bev, short error, void *arg) //这个是event的标准回调函数原型
可以从bev中用libevent的API提取出event_base、sockfd、input/output等相关数据,详情RTFM~
于是代码简化到只需要几行的read_cb和error_cb函数即可:
void read_cb(struct bufferevent *bev, void *arg) {
char line[256];
int n;
evutil_socket_t fd = bufferevent_getfd(bev);
while (n = bufferevent_read(bev, line, 256), n > 0)
bufferevent_write(bev, line, n);
}
void error_cb(struct bufferevent *bev, short event, void *arg) {
bufferevent_free(bev);
}
char line[256];
int n;
evutil_socket_t fd = bufferevent_getfd(bev);
while (n = bufferevent_read(bev, line, 256), n > 0)
bufferevent_write(bev, line, n);
}
void error_cb(struct bufferevent *bev, short event, void *arg) {
bufferevent_free(bev);
}
于是一个支持大并发量的echo server就成型了!下面附上无注释的echo server源码,110行,多抄几遍,就能完全弄懂啦!更复杂的例子参见官方文档里面的【Example: A simpler ROT13 server with Libevent】
#include <stdio.h>
#include <stdlib.h>
#include <errno.h>
#include <assert.h>
#include <event2/event.h>
#include <event2/bufferevent.h>
#define LISTEN_PORT 9999
#define LISTEN_BACKLOG 32
void do_accept(evutil_socket_t listener, short event, void *arg);
void read_cb(struct bufferevent *bev, void *arg);
void error_cb(struct bufferevent *bev, short event, void *arg);
void write_cb(struct bufferevent *bev, void *arg);
int main(int argc, char *argv[])
{
int ret;
evutil_socket_t listener;
listener = socket(AF_INET, SOCK_STREAM, 0);
assert(listener > 0);
evutil_make_listen_socket_reuseable(listener);
struct sockaddr_in sin;
sin.sin_family = AF_INET;
sin.sin_addr.s_addr = 0;
sin.sin_port = htons(LISTEN_PORT);
if (bind(listener, (struct sockaddr *)&sin, sizeof(sin)) < 0) {
perror("bind");
return 1;
}
if (listen(listener, LISTEN_BACKLOG) < 0) {
perror("listen");
return 1;
}
printf ("Listening...\n");
evutil_make_socket_nonblocking(listener);
struct event_base *base = event_base_new();
assert(base != NULL);
struct event *listen_event;
listen_event = event_new(base, listener, EV_READ|EV_PERSIST, do_accept, (void*)base);
event_add(listen_event, NULL);
event_base_dispatch(base);
printf("The End.");
return 0;
}
void do_accept(evutil_socket_t listener, short event, void *arg)
{
struct event_base *base = (struct event_base *)arg;
evutil_socket_t fd;
struct sockaddr_in sin;
socklen_t slen = sizeof(sin);
fd = accept(listener, (struct sockaddr *)&sin, &slen);
if (fd < 0) {
perror("accept");
return;
}
if (fd > FD_SETSIZE) { //这个if是参考了那个ROT13的例子,貌似是官方的疏漏,从select-based例子里抄过来忘了改
perror("fd > FD_SETSIZE\n");
return;
}
printf("ACCEPT: fd = %u\n", fd);
struct bufferevent *bev = bufferevent_socket_new(base, fd, BEV_OPT_CLOSE_ON_FREE);
bufferevent_setcb(bev, read_cb, NULL, error_cb, arg);
bufferevent_enable(bev, EV_READ|EV_WRITE|EV_PERSIST);
}
void read_cb(struct bufferevent *bev, void *arg)
{
#define MAX_LINE 256
char line[MAX_LINE+1];
int n;
evutil_socket_t fd = bufferevent_getfd(bev);
while (n = bufferevent_read(bev, line, MAX_LINE), n > 0) {
line[n] = '\0';
printf("fd=%u, read line: %s\n", fd, line);
bufferevent_write(bev, line, n);
}
}
void write_cb(struct bufferevent *bev, void *arg) {}
void error_cb(struct bufferevent *bev, short event, void *arg)
{
evutil_socket_t fd = bufferevent_getfd(bev);
printf("fd = %u, ", fd);
if (event & BEV_EVENT_TIMEOUT) {
printf("Timed out\n"); //if bufferevent_set_timeouts() called
}
else if (event & BEV_EVENT_EOF) {
printf("connection closed\n");
}
else if (event & BEV_EVENT_ERROR) {
printf("some other error\n");
}
bufferevent_free(bev);
}
#include <stdlib.h>
#include <errno.h>
#include <assert.h>
#include <event2/event.h>
#include <event2/bufferevent.h>
#define LISTEN_PORT 9999
#define LISTEN_BACKLOG 32
void do_accept(evutil_socket_t listener, short event, void *arg);
void read_cb(struct bufferevent *bev, void *arg);
void error_cb(struct bufferevent *bev, short event, void *arg);
void write_cb(struct bufferevent *bev, void *arg);
int main(int argc, char *argv[])
{
int ret;
evutil_socket_t listener;
listener = socket(AF_INET, SOCK_STREAM, 0);
assert(listener > 0);
evutil_make_listen_socket_reuseable(listener);
struct sockaddr_in sin;
sin.sin_family = AF_INET;
sin.sin_addr.s_addr = 0;
sin.sin_port = htons(LISTEN_PORT);
if (bind(listener, (struct sockaddr *)&sin, sizeof(sin)) < 0) {
perror("bind");
return 1;
}
if (listen(listener, LISTEN_BACKLOG) < 0) {
perror("listen");
return 1;
}
printf ("Listening...\n");
evutil_make_socket_nonblocking(listener);
struct event_base *base = event_base_new();
assert(base != NULL);
struct event *listen_event;
listen_event = event_new(base, listener, EV_READ|EV_PERSIST, do_accept, (void*)base);
event_add(listen_event, NULL);
event_base_dispatch(base);
printf("The End.");
return 0;
}
void do_accept(evutil_socket_t listener, short event, void *arg)
{
struct event_base *base = (struct event_base *)arg;
evutil_socket_t fd;
struct sockaddr_in sin;
socklen_t slen = sizeof(sin);
fd = accept(listener, (struct sockaddr *)&sin, &slen);
if (fd < 0) {
perror("accept");
return;
}
if (fd > FD_SETSIZE) { //这个if是参考了那个ROT13的例子,貌似是官方的疏漏,从select-based例子里抄过来忘了改
perror("fd > FD_SETSIZE\n");
return;
}
printf("ACCEPT: fd = %u\n", fd);
struct bufferevent *bev = bufferevent_socket_new(base, fd, BEV_OPT_CLOSE_ON_FREE);
bufferevent_setcb(bev, read_cb, NULL, error_cb, arg);
bufferevent_enable(bev, EV_READ|EV_WRITE|EV_PERSIST);
}
void read_cb(struct bufferevent *bev, void *arg)
{
#define MAX_LINE 256
char line[MAX_LINE+1];
int n;
evutil_socket_t fd = bufferevent_getfd(bev);
while (n = bufferevent_read(bev, line, MAX_LINE), n > 0) {
line[n] = '\0';
printf("fd=%u, read line: %s\n", fd, line);
bufferevent_write(bev, line, n);
}
}
void write_cb(struct bufferevent *bev, void *arg) {}
void error_cb(struct bufferevent *bev, short event, void *arg)
{
evutil_socket_t fd = bufferevent_getfd(bev);
printf("fd = %u, ", fd);
if (event & BEV_EVENT_TIMEOUT) {
printf("Timed out\n"); //if bufferevent_set_timeouts() called
}
else if (event & BEV_EVENT_EOF) {
printf("connection closed\n");
}
else if (event & BEV_EVENT_ERROR) {
printf("some other error\n");
}
bufferevent_free(bev);
}
Feb
24
上一篇TCP Port Multiplexing提到,proxy_connect模块貌似不能限制被代理的HOST。也就是说,通过这台服务器可以PROXY到任意机器这样是有风险的。可是mod_proxy模块又没有提供专门的配置来限制可代理的HOST,所以只好舍近求远写了个multiplexer。
实际上要解决这个问题也是很简单的,可以想到,知道找到相应的地方,直接修改源码,只需要几行就行。虽然以前没有搞过apache的module,但是跟php的也比较类似了。看了下代码发现,前文的“貌似”两个字可以去掉了,的确没有什么地方限制了可代理的HOST。
在Ubuntu下尤其简单:(假定已经配置好了mod_proxy/mod_proxy_connect,比如AllowConnect 22、ProxyRequests On什么的)
添加如下几行:
保存后,执行
然后再试试看?
实际上要解决这个问题也是很简单的,可以想到,知道找到相应的地方,直接修改源码,只需要几行就行。虽然以前没有搞过apache的module,但是跟php的也比较类似了。看了下代码发现,前文的“貌似”两个字可以去掉了,的确没有什么地方限制了可代理的HOST。
在Ubuntu下尤其简单:(假定已经配置好了mod_proxy/mod_proxy_connect,比如AllowConnect 22、ProxyRequests On什么的)
引用
$ apt-get source apache2
$ cd apache2-2.2.14/modules/proxy
$ vi mod_proxy_connect.c +123 #就是打开123行,这里刚从http请求里分离出HOST
$ cd apache2-2.2.14/modules/proxy
$ vi mod_proxy_connect.c +123 #就是打开123行,这里刚从http请求里分离出HOST
添加如下几行:
char *allowed_hosts[] = {
"SOME_HOST_NAME",
"SOME_IP",
"127.0.0.1",
"localhost"
};
int hosts_num = sizeof(allowed_hosts) / sizeof(allowed_hosts[0]);
int k;
for (k = 0; k < hosts_num; k++) {
if (strncmp(uri.hostname, allowed_hosts[k], strlen(allowed_hosts[k])) == 0) {
break;
}
}
if (k == hosts_num) {
return ap_proxyerror(r, HTTP_BAD_GATEWAY,
apr_pstrcat(p, "host not allowed for: ", uri.hostname, NULL));
}
"SOME_HOST_NAME",
"SOME_IP",
"127.0.0.1",
"localhost"
};
int hosts_num = sizeof(allowed_hosts) / sizeof(allowed_hosts[0]);
int k;
for (k = 0; k < hosts_num; k++) {
if (strncmp(uri.hostname, allowed_hosts[k], strlen(allowed_hosts[k])) == 0) {
break;
}
}
if (k == hosts_num) {
return ap_proxyerror(r, HTTP_BAD_GATEWAY,
apr_pstrcat(p, "host not allowed for: ", uri.hostname, NULL));
}
保存后,执行
引用
$ sudo apt-get install apache2-dev
$ apxs2 -c mod_proxy_connect.c proxy_util.c
$ cp .libs/mod_proxy_connect.so /usr/lib/apache2/modules #这里会覆盖现有的,最好备份一下
$ sudo /etc/init.d/apache2 restart
$ apxs2 -c mod_proxy_connect.c proxy_util.c
$ cp .libs/mod_proxy_connect.so /usr/lib/apache2/modules #这里会覆盖现有的,最好备份一下
$ sudo /etc/init.d/apache2 restart
然后再试试看?
Feb
24
很久很久以前就想过这个问题,是不是可以写这样一个frontend proxy,通过client的请求来判断需要连接的端口号,实现http/ssh共用80端口呢?
不久后我发现,通过apache的mod_proxy+mod_proxy_connect可以使用HTTP的CONNECT HOST:PORT命令来代理22端口,也就是tunneling ssh over http, google可以发现很多都用到了proxytunnel,当作openssh client的代理;securecrt内建支持,使用Option->Global里面的firewall,其实就是proxy。
于是这个想法被搁置了,好几年。为什么又想起了呢…………是因为,proxy_connect模块貌似不能限制被代理的HOST。也就是说,通过这台服务器可以PROXY到任意机器这样是有风险的。
于是昨天花了半个下午+半个晚上看libevent的文档。其实本来是打算找个速成教程的,但是好像没有,所以还是老老实实看文档。当天晚上写出了第一个版本的multiplexer,各种BUG。无奈,第二天干脆推倒重来,写了第二个版本,还是各种BUG。各种蛋疼之后,终于实现了一个基本可用的multiplexer,250行左右,挺短的代码。
遇到的主要障碍包括:
1. bufferevent *bev的read_cb函数被调用以后,应该用一个while循环把bev里面所有的数据都读出来(比如使用bufferevent_read);
2. set_nonblocking的操作应该在bind/listen/connect等操作之后完成;否则这些操作也是nonblocking了……
3. 最好加上 |EV_PERSIST ,免得每次callback的时候都要再enable或者add一次;
4. ssh协议对client/server哪个先说话貌似没要求,比如openssh是等server先说话,而securecrt是自己先说话,所以这个multiplexer的实现很蛋疼,需要设置一个timeout,如果client过了一会儿没说话,就forward给sshd。
5. 对于4,更蛋疼的是,(我用的libevent2.0.17-stable)bufferevent_set_timeouts(bev, NULL, NULL)貌似不能清除timeout(或许是个BUG?),所以使用了一个struct timeval tv = {86400*1024, 0} 来绕过timeout的问题。
大概就这些,代码如下
不久后我发现,通过apache的mod_proxy+mod_proxy_connect可以使用HTTP的CONNECT HOST:PORT命令来代理22端口,也就是tunneling ssh over http, google可以发现很多都用到了proxytunnel,当作openssh client的代理;securecrt内建支持,使用Option->Global里面的firewall,其实就是proxy。
于是这个想法被搁置了,好几年。为什么又想起了呢…………是因为,proxy_connect模块貌似不能限制被代理的HOST。也就是说,通过这台服务器可以PROXY到任意机器这样是有风险的。
于是昨天花了半个下午+半个晚上看libevent的文档。其实本来是打算找个速成教程的,但是好像没有,所以还是老老实实看文档。当天晚上写出了第一个版本的multiplexer,各种BUG。无奈,第二天干脆推倒重来,写了第二个版本,还是各种BUG。各种蛋疼之后,终于实现了一个基本可用的multiplexer,250行左右,挺短的代码。
遇到的主要障碍包括:
1. bufferevent *bev的read_cb函数被调用以后,应该用一个while循环把bev里面所有的数据都读出来(比如使用bufferevent_read);
2. set_nonblocking的操作应该在bind/listen/connect等操作之后完成;否则这些操作也是nonblocking了……
3. 最好加上 |EV_PERSIST ,免得每次callback的时候都要再enable或者add一次;
4. ssh协议对client/server哪个先说话貌似没要求,比如openssh是等server先说话,而securecrt是自己先说话,所以这个multiplexer的实现很蛋疼,需要设置一个timeout,如果client过了一会儿没说话,就forward给sshd。
5. 对于4,更蛋疼的是,(我用的libevent2.0.17-stable)bufferevent_set_timeouts(bev, NULL, NULL)貌似不能清除timeout(或许是个BUG?),所以使用了一个struct timeval tv = {86400*1024, 0} 来绕过timeout的问题。
大概就这些,代码如下
Feb
22
挑战:某Linux机器A有外网访问权限,但其上运行的ssh服务(22端口)仅对内网开放,希望通过外网的某Linux机器B进行RELAY,实现对机器A的ssh登录。特别地,只要能够进行ssh连接,就可以建立socks代理,实现内网其余机器的访问。
原理:(ssh服务器)A:22 <---- 连接 ~ 连接 ----> 监听B:10001 ~ 监听B:10002 <---- 连接(ssh客户端)
其中的 ~ 表示将两个连接/监听的socket的输入和输出分别连接起来。
简单实现(nc + shell):
1. 在机器B上运行
2. 在机器A上运行
3. 使用ssh客户端连接B:10001即可。
简单实现的主要问题是,一旦ssh客户端断开连接,部分/所有的nc会结束,无法再建立连接。所以需要改进:
1. 写一个死循环脚本来保证nc的运行,例如 for ((;;)); do nc localhost 22 <pipe | nc [B.ip] 10002 >;pipe; done
2. 将该脚本放入 /etc/rc.local ,保证每次开机后自动运行。
还有一个蛋疼的问题是,(在我的测试中)如果ssh客户端被强制断开连接(不是 $exit ),B上面监听10002端口的那个nc不一定会结束。虽然我特意安排了B机器的脚本管道前监听10002,管道后监听10001,希望能利用SIGPIPE来搞定,但是系统似乎抽风。所以还是需要一个机制来保证一旦某个nc结束了,另一个nc也会结束。可能还有一些其他更蛋疼的情况,无法一一列出来。
为了解决nc不结束的蛋疼情况,可以用脚本来实现:记录2个nc的PID,然后定时grep之。如果只剩下1个,就把另一个也kill掉。不过我没有采用这个方案,而是写了一个c程序来处理,pipe出两对fd,fork出两个child,把两对fd dup成两个child的stdin/stdout,child分别exec执行nc,然后wait之,当wait返回以后,就用kill向两个pid送个SIGTERM,结束。然后进入下一轮循环
代码如下(此代码用于B机器,A机器只要稍微修改下exec的参数就行了):
原理:(ssh服务器)A:22 <---- 连接 ~ 连接 ----> 监听B:10001 ~ 监听B:10002 <---- 连接(ssh客户端)
其中的 ~ 表示将两个连接/监听的socket的输入和输出分别连接起来。
简单实现(nc + shell):
1. 在机器B上运行
引用
mkfifo pipe
nc -l -p 10002 < pipe | nc -l -p 10001 > pipe
nc -l -p 10002 < pipe | nc -l -p 10001 > pipe
2. 在机器A上运行
引用
mkfifo pipe
nc localhost 22 < pipe | nc [B.ip] 10002 > pipe
nc localhost 22 < pipe | nc [B.ip] 10002 > pipe
3. 使用ssh客户端连接B:10001即可。
简单实现的主要问题是,一旦ssh客户端断开连接,部分/所有的nc会结束,无法再建立连接。所以需要改进:
1. 写一个死循环脚本来保证nc的运行,例如 for ((;;)); do nc localhost 22 <pipe | nc [B.ip] 10002 >;pipe; done
2. 将该脚本放入 /etc/rc.local ,保证每次开机后自动运行。
还有一个蛋疼的问题是,(在我的测试中)如果ssh客户端被强制断开连接(不是 $exit ),B上面监听10002端口的那个nc不一定会结束。虽然我特意安排了B机器的脚本管道前监听10002,管道后监听10001,希望能利用SIGPIPE来搞定,但是系统似乎抽风。所以还是需要一个机制来保证一旦某个nc结束了,另一个nc也会结束。可能还有一些其他更蛋疼的情况,无法一一列出来。
为了解决nc不结束的蛋疼情况,可以用脚本来实现:记录2个nc的PID,然后定时grep之。如果只剩下1个,就把另一个也kill掉。不过我没有采用这个方案,而是写了一个c程序来处理,pipe出两对fd,fork出两个child,把两对fd dup成两个child的stdin/stdout,child分别exec执行nc,然后wait之,当wait返回以后,就用kill向两个pid送个SIGTERM,结束。然后进入下一轮循环
代码如下(此代码用于B机器,A机器只要稍微修改下exec的参数就行了):
#include <unistd.h>
#include <stdio.h>
#include <stdlib.h>
#include <errno.h>
#include <assert.h>
#include <stdarg.h>
#include <sys/types.h>
#include <sys/wait.h>
void error(const char *fmt, ...)
{
perror("Infomation");
fprintf(stderr, " => ");
va_list ap;
va_start(ap, fmt);
vfprintf(stderr, fmt, ap);
va_end(ap);
}
int main(int argc, char *argv[])
{
int fd_left[2], fd_right[2];
if (pipe(fd_left) < 0 ) {
perror("pipe left failed");
return 1;
}
if (pipe(fd_right) < 0 ) {
perror("pipe right failed");
return 1;
}
pid_t pid1 = fork();
if (pid1 < 0) {
perror("fork1");
return 1;
}
if (pid1 == 0) {
//child
if (dup2(fd_left[0], STDIN_FILENO) < 0) {
error("dup2@1@stdin");
}
if (dup2(fd_right[1], STDOUT_FILENO) < 0) {
error("dup2@1@stdout");
}
execlp("nc", "nc", "-l", "-p", "10001", NULL);
perror("execlp");
return 1;
}
fprintf(stderr, "pid1 = %d\n", pid1);
//parent
pid_t pid2 = fork();
if (pid2 < 0) {
perror("fork2");
return 1;
}
if (pid2 == 0) {
//child
if (dup2(fd_right[0], STDIN_FILENO) < 0) {
error("dup2@1@stdin");
}
if (dup2(fd_left[1], STDOUT_FILENO) < 0) {
error("dup2@1@stdout");
}
execlp("nc", "nc", "-l", "-p", "10002", NULL);
perror("execlp");
return 1;
}
fprintf(stderr, "pid2 = %d\n", pid2);
int status;
pid_t pid = wait(&status);
error("Process[%d] exits\n", pid);
kill(pid1, SIGTERM);
kill(pid2, SIGTERM);
return 0;
}
#include <stdio.h>
#include <stdlib.h>
#include <errno.h>
#include <assert.h>
#include <stdarg.h>
#include <sys/types.h>
#include <sys/wait.h>
void error(const char *fmt, ...)
{
perror("Infomation");
fprintf(stderr, " => ");
va_list ap;
va_start(ap, fmt);
vfprintf(stderr, fmt, ap);
va_end(ap);
}
int main(int argc, char *argv[])
{
int fd_left[2], fd_right[2];
if (pipe(fd_left) < 0 ) {
perror("pipe left failed");
return 1;
}
if (pipe(fd_right) < 0 ) {
perror("pipe right failed");
return 1;
}
pid_t pid1 = fork();
if (pid1 < 0) {
perror("fork1");
return 1;
}
if (pid1 == 0) {
//child
if (dup2(fd_left[0], STDIN_FILENO) < 0) {
error("dup2@1@stdin");
}
if (dup2(fd_right[1], STDOUT_FILENO) < 0) {
error("dup2@1@stdout");
}
execlp("nc", "nc", "-l", "-p", "10001", NULL);
perror("execlp");
return 1;
}
fprintf(stderr, "pid1 = %d\n", pid1);
//parent
pid_t pid2 = fork();
if (pid2 < 0) {
perror("fork2");
return 1;
}
if (pid2 == 0) {
//child
if (dup2(fd_right[0], STDIN_FILENO) < 0) {
error("dup2@1@stdin");
}
if (dup2(fd_left[1], STDOUT_FILENO) < 0) {
error("dup2@1@stdout");
}
execlp("nc", "nc", "-l", "-p", "10002", NULL);
perror("execlp");
return 1;
}
fprintf(stderr, "pid2 = %d\n", pid2);
int status;
pid_t pid = wait(&status);
error("Process[%d] exits\n", pid);
kill(pid1, SIGTERM);
kill(pid2, SIGTERM);
return 0;
}
Jan
12
对于没有GC的语言来说,这实在是最让人头疼的事情了,毕竟内存泄漏是最难处理的问题(之一?),对于一个后台server,即使只是一个小小的泄漏,日积月累,也会导致灾难性的后果。有个传闻说的是,某公司的某下载软件的某后台server,由于有个无法定位的内存泄漏问题,导致服务的内存占用不断增加,以至于只能每隔一段时间重启之。
有人说,C/C++程序员有一半的工作量是花在处理内存泄漏上面,但是很遗憾,内存泄漏仍然屡见不鲜。一旦出现泄漏,能做的事情不多,上述处理方式是消极做法之一,有效,但治标不治本。积极一点的,也不外乎这两个:一是看代码,反复看代码,请别人看代码,请别人反复看代码;或是借助valgrind之类的工具来跟踪内存的分配/释放(而且并不适用于所有程序,例如某些程序寄希望于在其终止时让OS来释放那些只需申请一次且无需释放的内存)。一些额外的测试工作也许能帮助缩小查看代码的范围,但也只能这样了。
既是如此,在程序运行之前,就应该先把好关。
对于C语言,这确实是个比较痛苦的事情。毕竟C语言只是汇编的高级语言封装,语言本身提供的能力很有限。
假定有一个函数申请了多次内存,那么每次遇到错误需要退出的时候,为了避免内存泄漏,必须将其之前申请的所有内存都释放。所以你也许会看到或者写出过这样蛋疼的程序:
于是万恶的goto出场了。为了解决上面的问题,引入goto可以使得每个资源只需要写一份对应的释放代码,例如: 什么意思呢?假定在第一步,给 a 分配内存的时候失败了,那么还没来得及去定义 b 并给其初始化赋值,就跳转到了wtf这儿,而在wtf下面的第二行,却引用了 b 这个变量,对于编译器而言,这便无法处理了。正确的代码应该是:
对变量就近定义的好坏见仁见智了,但是goto毕竟不是个好东西,所以看过内核代码的同学可能会发现另外一种替代性的结构:do-while(0) 。乍一看这个结构似乎没有意义,有点奇怪,但是却很好用,很适合用来消除goto语句,例如上面的代码可以这么做:
但是do-while(0)和goto一样,不是万金油,对于很多较复杂的情况也不能很好的解决,甚至会使得程序更加晦涩难懂。对于do-while(0),如果在这个结构内还有一个循环,循环里面出错想要跳出do-while(0),break就不奏效了(至于为什么,你懂的),这时代码怎么写都恶心,只能羡慕Java里面的break label语法了;而对于比较复杂的资源,比如上文中申请到的内存是 a->b->c 这样嵌套的,那么如何安排内存释放代码,又要让人头疼了。合理的使用goto/do-while(0),将过长的代码拆分成多个函数等都可以起到一定的帮助。
只是很可惜,C语言的能力大概就只能走到这里了。想走得更远,就得借助C++来完成了。虽然C++没有gc,但是由于其OO的特性,使得RAII的实现变得可能。
所谓RAII,即 Resource Acquizition Is Initialization。很晦涩吧?其实具体实现很简单:把资源封装成一个类,在其构造函数中分配,在析构函数中释放。当需要使用的时候,在栈上初始化一个对象,当这个对象生命周期结束的时候,其析构函数会被调用,自动完成资源的释放。对于前面提到的例子,可以把A/B/C封装成一个class,对应的a/b/c就是实例化得到的三个对象,当func函数结束的时候,abc对应的内存就会被释放。同样的方法也适用于锁、互斥量、文件指针等其他类型的资源。下面这段代码以pthread_mutex为例,演示了RAII的使用:
不过程序中因为各种原因常需要使用 new 来分配资源(内存、对象等),这样对应的指针还是得在其生命周期结束的时候被释放,总不能为每一个指针再封装一个struct吧。幸而C++的泛型在这里又为RAII提供了绝佳的方法。实际上在第一版STL里面就包含了一个 auto_ptr 容器,实例化一个auto_ptr的时候可以赋予一个任意类型指针ptr(但是必须是使用new获得的,特别注意:new[]分配的不行),auto_ptr对象将ptr包装起来,并重载了 * 和 -> 两个操作符,使得该对象能像指针一样被使用,并且在该对象被生命周期的时候,其析构函数会delete ptr。 下面是auto_ptr的一个简单实现和使用:
既然说到auto_ptr,为什么不用它来写例子呢?因为auto_ptr的某些特性导致其有大坑,在很多地方不受待见,以至于在 c++11 标准里,auto_ptr被废弃了,因此不建议在项目中使用它了。有兴趣的同学可以去翻看《C++标准程序库》对auto_ptr的介绍。
本来计划写到这里要告一段落了,但是上面的 x_ptr 有坑,无奈只好继续……为什么说有坑呢?举两个例子:
针对func1的问题,可以通过私有化其拷贝函数、拷贝构造函数来禁止x_ptr的拷贝,代码如下
而针对func2的问题,解决方法呢,要么是写一个x_ptr_arr,使用delete[]来处理;要么是在x_ptr的构造函数里加一个flag,用来指定是否是new[]分配的,当然,为了方便,可以设置一个默认值false.....
补充一句,这里的x_ptr其实是boost::scoped_ptr的缩水版了,有兴趣的同学可以自行Google了解更多,关于内存泄漏的话题,这篇大概就说这么多了吧。
最后,感谢Sandy同学的 C++中利用RAII在stack上管理资源I ,本篇有多处参考该文。希望他能抽出时间把 II 给写完吧 :P
有人说,C/C++程序员有一半的工作量是花在处理内存泄漏上面,但是很遗憾,内存泄漏仍然屡见不鲜。一旦出现泄漏,能做的事情不多,上述处理方式是消极做法之一,有效,但治标不治本。积极一点的,也不外乎这两个:一是看代码,反复看代码,请别人看代码,请别人反复看代码;或是借助valgrind之类的工具来跟踪内存的分配/释放(而且并不适用于所有程序,例如某些程序寄希望于在其终止时让OS来释放那些只需申请一次且无需释放的内存)。一些额外的测试工作也许能帮助缩小查看代码的范围,但也只能这样了。
既是如此,在程序运行之前,就应该先把好关。
对于C语言,这确实是个比较痛苦的事情。毕竟C语言只是汇编的高级语言封装,语言本身提供的能力很有限。
假定有一个函数申请了多次内存,那么每次遇到错误需要退出的时候,为了避免内存泄漏,必须将其之前申请的所有内存都释放。所以你也许会看到或者写出过这样蛋疼的程序:
void func(){
void *a = malloc(sizeof(A));
if (NULL == a) {
return;
}
void *b = malloc(sizeof(B));
if (NULL == b) {
free(a);
return;
}
void *c = malloc(sizeof(C));
if (NULL == c) {
free(a);
free(b);
return;
}
......
}
有效,但是不靠谱。当这个函数长达数百行、有多处申请内存的时候,其可维护性是相当低的。当然,使用 alloca 这个非标准的内存分配函数可以在某些情况下解决问题,但是如果申请的内存较大(栈空间不够)、或者分配到的内存被用于较复杂的结构(比如还包含其他资源的指针)、资源不是内存(比如文件指针、锁等同样需要在生命周期结束被释放的资源),alloca就无能为力了。void *a = malloc(sizeof(A));
if (NULL == a) {
return;
}
void *b = malloc(sizeof(B));
if (NULL == b) {
free(a);
return;
}
void *c = malloc(sizeof(C));
if (NULL == c) {
free(a);
free(b);
return;
}
......
}
于是万恶的goto出场了。为了解决上面的问题,引入goto可以使得每个资源只需要写一份对应的释放代码,例如:
void func(){
void *a = malloc(sizeof(A));
if (NULL == a) goto wtf;
void *b = malloc(sizeof(B));
if (NULL == b) goto wtf;
void *c = malloc(sizeof(C));
if (NULL == c) goto wtf;
......
wtf:
if (a != NULL) free(a);
if (b != NULL) free(b);
if (c != NULL) free(c);
}
看起来很棒对不对?但是实际上并不能通过编译,gcc会提示类似这样的错误:void *a = malloc(sizeof(A));
if (NULL == a) goto wtf;
void *b = malloc(sizeof(B));
if (NULL == b) goto wtf;
void *c = malloc(sizeof(C));
if (NULL == c) goto wtf;
......
wtf:
if (a != NULL) free(a);
if (b != NULL) free(b);
if (c != NULL) free(c);
}
引用
cross.c:14: error: jump to label ‘wtf’
cross.c:9: error: from here
cross.c:11: error: crosses initialization of ‘void* c’
cross.c:9: error: from here
cross.c:11: error: crosses initialization of ‘void* c’
void func(){
void *a = NULL, *b = NULL, *c = NULL;
a = malloc(sizeof(A));
if (NULL == a) goto wtf;
b = malloc(sizeof(B));
if (NULL == b) goto wtf;
c = malloc(sizeof(C));
if (NULL == c) goto wtf;
......
wtf:
if (a != NULL) free(a);
if (b != NULL) free(b);
if (c != NULL) free(c);
}
这样一来便要求所有在 goto 之后被用到的变量都必须在第一个goto之前定义,并赋初值。这就类似c89/pascal的做法了,强制要求所有变量在函数的开头定义,失去了变量就近定义的便捷性和一些其他的好处(sandy的说法是“局部性”,但是窃以为变量的就近定义跟局部性关系不大,更多的是在C++中,对象的就近定义可以在一些情况下避免不必要的初始化,并且可能需要之前的一些处理结果)。这儿有个更复杂的例子,作者指出,在驱动/linux内核中大量使用了这种方式来释放资源。注意,稍有不同的是,这个例子有多种资源,函数末尾有多个label,按照资源申请顺序的倒序释放资源(为什么呢,看代码吧~)void *a = NULL, *b = NULL, *c = NULL;
a = malloc(sizeof(A));
if (NULL == a) goto wtf;
b = malloc(sizeof(B));
if (NULL == b) goto wtf;
c = malloc(sizeof(C));
if (NULL == c) goto wtf;
......
wtf:
if (a != NULL) free(a);
if (b != NULL) free(b);
if (c != NULL) free(c);
}
对变量就近定义的好坏见仁见智了,但是goto毕竟不是个好东西,所以看过内核代码的同学可能会发现另外一种替代性的结构:do-while(0) 。乍一看这个结构似乎没有意义,有点奇怪,但是却很好用,很适合用来消除goto语句,例如上面的代码可以这么做:
void func(){
void *a = NULL, *b = NULL, *c = NULL;
do {
a = malloc(sizeof(A));
if (NULL == a) break;
b = malloc(sizeof(B));
if (NULL == b) break;
c = malloc(sizeof(C));
if (NULL == c) break;
......
} while (0);
wtf:
if (a != NULL) free(a);
if (b != NULL) free(b);
if (c != NULL) free(c);
}
既消除了“不法分子”,也达到了避免冗余的目的。对于这个结构,其实还有更多的好处,详见这里。void *a = NULL, *b = NULL, *c = NULL;
do {
a = malloc(sizeof(A));
if (NULL == a) break;
b = malloc(sizeof(B));
if (NULL == b) break;
c = malloc(sizeof(C));
if (NULL == c) break;
......
} while (0);
wtf:
if (a != NULL) free(a);
if (b != NULL) free(b);
if (c != NULL) free(c);
}
但是do-while(0)和goto一样,不是万金油,对于很多较复杂的情况也不能很好的解决,甚至会使得程序更加晦涩难懂。对于do-while(0),如果在这个结构内还有一个循环,循环里面出错想要跳出do-while(0),break就不奏效了(至于为什么,你懂的),这时代码怎么写都恶心,只能羡慕Java里面的break label语法了;而对于比较复杂的资源,比如上文中申请到的内存是 a->b->c 这样嵌套的,那么如何安排内存释放代码,又要让人头疼了。合理的使用goto/do-while(0),将过长的代码拆分成多个函数等都可以起到一定的帮助。
只是很可惜,C语言的能力大概就只能走到这里了。想走得更远,就得借助C++来完成了。虽然C++没有gc,但是由于其OO的特性,使得RAII的实现变得可能。
所谓RAII,即 Resource Acquizition Is Initialization。很晦涩吧?其实具体实现很简单:把资源封装成一个类,在其构造函数中分配,在析构函数中释放。当需要使用的时候,在栈上初始化一个对象,当这个对象生命周期结束的时候,其析构函数会被调用,自动完成资源的释放。对于前面提到的例子,可以把A/B/C封装成一个class,对应的a/b/c就是实例化得到的三个对象,当func函数结束的时候,abc对应的内存就会被释放。同样的方法也适用于锁、互斥量、文件指针等其他类型的资源。下面这段代码以pthread_mutex为例,演示了RAII的使用:
class Mutexer {
private:
pthread_mutex_t *mutex;
public:
Mutexer(pthread_mutex_t *m) { mutex = m; }
~Mutexer() { Unlock(); }
Lock() { pthread_mutex_lock(mutex); }
Unlock() { pthread_mutex_unlock(mutex); }
};
//Global mutex
pthread_mutex_t mutex = PTHREAD_MUTEX_INITIALIZER;
void func() {
Mutexer mtx(&mutex);
mtx::lock();
if (sth. failed) {
return;
}
}
private:
pthread_mutex_t *mutex;
public:
Mutexer(pthread_mutex_t *m) { mutex = m; }
~Mutexer() { Unlock(); }
Lock() { pthread_mutex_lock(mutex); }
Unlock() { pthread_mutex_unlock(mutex); }
};
//Global mutex
pthread_mutex_t mutex = PTHREAD_MUTEX_INITIALIZER;
void func() {
Mutexer mtx(&mutex);
mtx::lock();
if (sth. failed) {
return;
}
}
不过程序中因为各种原因常需要使用 new 来分配资源(内存、对象等),这样对应的指针还是得在其生命周期结束的时候被释放,总不能为每一个指针再封装一个struct吧。幸而C++的泛型在这里又为RAII提供了绝佳的方法。实际上在第一版STL里面就包含了一个 auto_ptr 容器,实例化一个auto_ptr的时候可以赋予一个任意类型指针ptr(但是必须是使用new获得的,特别注意:new[]分配的不行),auto_ptr对象将ptr包装起来,并重载了 * 和 -> 两个操作符,使得该对象能像指针一样被使用,并且在该对象被生命周期的时候,其析构函数会delete ptr。 下面是auto_ptr的一个简单实现和使用:
template <typename T>
class x_ptr
{
private:
T* x;
public:
typedef T ele_type;
explicit x_ptr(T* _x): x(_x) {}
~x_ptr() { delete x; }
T & operator * () { return *x; }
T * operator ->() const { return x; }
};
void func(){
x_ptr<int> p(new int);
*p = 3;
}
代码的最后无需显式调用 delete 需释放分配的那个int,却照样避免了内存的泄漏。class x_ptr
{
private:
T* x;
public:
typedef T ele_type;
explicit x_ptr(T* _x): x(_x) {}
~x_ptr() { delete x; }
T & operator * () { return *x; }
T * operator ->() const { return x; }
};
void func(){
x_ptr<int> p(new int);
*p = 3;
}
既然说到auto_ptr,为什么不用它来写例子呢?因为auto_ptr的某些特性导致其有大坑,在很多地方不受待见,以至于在 c++11 标准里,auto_ptr被废弃了,因此不建议在项目中使用它了。有兴趣的同学可以去翻看《C++标准程序库》对auto_ptr的介绍。
本来计划写到这里要告一段落了,但是上面的 x_ptr 有坑,无奈只好继续……为什么说有坑呢?举两个例子:
void func1() {
x_ptr<int> p(new int), q(new int);
*p = 1;
*q = 2;
p = q;
}
void func2() {
x_ptr<int> p(new int[10]);
}
在 func1 中,由于进行了拷贝(其实拷贝构造也一样),导致 p 对应的那块空间会被泄漏,而 q 对应的那块空间会被释放2次;在 func2 中,x_ptr试图用delete去释放由new[]分配的内存空间,其结果是未定义的(比如不是基本元素而是某个class,程序可能会直接崩溃)。x_ptr<int> p(new int), q(new int);
*p = 1;
*q = 2;
p = q;
}
void func2() {
x_ptr<int> p(new int[10]);
}
针对func1的问题,可以通过私有化其拷贝函数、拷贝构造函数来禁止x_ptr的拷贝,代码如下
template <typename T>
class x_ptr
{
private:
T* x;
x_ptr(const x_ptr&);
x_ptr& operator= (const x_ptr& v);
public:
typedef T ele_type;
explicit x_ptr(T* _x): x(_x) {}
~x_ptr() { delete x; };
T & operator * () { return *x; }
T * operator ->() const { return x; }
};
class x_ptr
{
private:
T* x;
x_ptr(const x_ptr&);
x_ptr& operator= (const x_ptr& v);
public:
typedef T ele_type;
explicit x_ptr(T* _x): x(_x) {}
~x_ptr() { delete x; };
T & operator * () { return *x; }
T * operator ->() const { return x; }
};
而针对func2的问题,解决方法呢,要么是写一个x_ptr_arr,使用delete[]来处理;要么是在x_ptr的构造函数里加一个flag,用来指定是否是new[]分配的,当然,为了方便,可以设置一个默认值false.....
补充一句,这里的x_ptr其实是boost::scoped_ptr的缩水版了,有兴趣的同学可以自行Google了解更多,关于内存泄漏的话题,这篇大概就说这么多了吧。
最后,感谢Sandy同学的 C++中利用RAII在stack上管理资源I ,本篇有多处参考该文。希望他能抽出时间把 II 给写完吧 :P
Dec
23
这程序写了好几次了,干脆贴出来吧~附上exe。
下载文件 (已下载 1692 次)
#include <stdio.h>
#include <stdlib.h>
char str[65536];
int main() {
int i;
for (i = 0; i < 65536; i++) str[i] = '1';
str[65535] = '\0';
i = 0;
while (1) {
i++;
if (i % 3000 == 0) {
sleep(1);
}
puts(str);
}
return 0;
}
#include <stdlib.h>
char str[65536];
int main() {
int i;
for (i = 0; i < 65536; i++) str[i] = '1';
str[65535] = '\0';
i = 0;
while (1) {
i++;
if (i % 3000 == 0) {
sleep(1);
}
puts(str);
}
return 0;
}

Dec
13
@2012.12.31 更多细节参见这篇.
根据官方文档:
也就是说,不管是哪个版本,希望启用MYSQL_OPT_RECONNECT,都应该调用mysql_options()明确设置它。由于一个BUG,5.0.19之前必须在mysql_real_connect()之后设置,而更新的版本则可以在mysql_real_connect之前设置。
这里的坑是,在没有设置了RECONNECT时,mysql_ping()并不会自动重连。
因此最完整的解决方法是:
1. 在使用mysql_real_connect连接数据库之后,再使用mysql_options( &mysql, MYSQL_OPT_RECONNECT, … ) 来设置为自动重连。这样当mysql连接丢失的时候,使用mysql_ping能够自动重连数据库。
示例代码:
2. 在执行查询的之前使用mysql_ping()确保自动重连。(但是这个是必要的吗?找不到相应的说明。。。)
UPDATE@2012.12.30:原来之前有很多误解(认为是5.1.6之后就不需要设置MYSQL_OPT_RECONNECT),多谢@Zind同学提醒,这篇基本上是重写了。。。
根据官方文档:
引用
MYSQL_OPT_RECONNECT (argument type: my_bool *)
Enable or disable automatic reconnection to the server if the connection is found to have been lost. Reconnect has been off by default since MySQL 5.0.3; this option is new in 5.0.13 and provides a way to set reconnection behavior explicitly.
#如果连接丢失,启用或禁用自动重连到Serve,。自5.0.3之后,重连默认被禁用。这个选项是5.0.13新增的,提供了明确设置重连行为的选项。
Note: mysql_real_connect() incorrectly reset the MYSQL_OPT_RECONNECT option to its default value before MySQL 5.0.19. Therefore, prior to that version, if you want reconnect to be enabled for each connection, you must call mysql_options() with the MYSQL_OPT_RECONNECT option after each call to mysql_real_connect(). This is not necessary as of 5.0.19: Call mysql_options() only before mysql_real_connect() as usual.
#注意:mysql_real_connect() 在5.0.19之前错误地重置了MYSQL_OPT_RECONNECT选项为默认值(不启用),因此,在5.0.19之前,如果你希望为连接启用重连,你必须在mysql_real_connect()之后调用mysql_options来设置MYSQL_OPT_RECONNECT。5.0.19之后就不需要这样了,只需要(跟其他选项一样)在mysql_real_connect之前调用即可。
Enable or disable automatic reconnection to the server if the connection is found to have been lost. Reconnect has been off by default since MySQL 5.0.3; this option is new in 5.0.13 and provides a way to set reconnection behavior explicitly.
#如果连接丢失,启用或禁用自动重连到Serve,。自5.0.3之后,重连默认被禁用。这个选项是5.0.13新增的,提供了明确设置重连行为的选项。
Note: mysql_real_connect() incorrectly reset the MYSQL_OPT_RECONNECT option to its default value before MySQL 5.0.19. Therefore, prior to that version, if you want reconnect to be enabled for each connection, you must call mysql_options() with the MYSQL_OPT_RECONNECT option after each call to mysql_real_connect(). This is not necessary as of 5.0.19: Call mysql_options() only before mysql_real_connect() as usual.
#注意:mysql_real_connect() 在5.0.19之前错误地重置了MYSQL_OPT_RECONNECT选项为默认值(不启用),因此,在5.0.19之前,如果你希望为连接启用重连,你必须在mysql_real_connect()之后调用mysql_options来设置MYSQL_OPT_RECONNECT。5.0.19之后就不需要这样了,只需要(跟其他选项一样)在mysql_real_connect之前调用即可。
也就是说,不管是哪个版本,希望启用MYSQL_OPT_RECONNECT,都应该调用mysql_options()明确设置它。由于一个BUG,5.0.19之前必须在mysql_real_connect()之后设置,而更新的版本则可以在mysql_real_connect之前设置。
这里的坑是,在没有设置了RECONNECT时,mysql_ping()并不会自动重连。
因此最完整的解决方法是:
1. 在使用mysql_real_connect连接数据库之后,再使用mysql_options( &mysql, MYSQL_OPT_RECONNECT, … ) 来设置为自动重连。这样当mysql连接丢失的时候,使用mysql_ping能够自动重连数据库。
示例代码:
mysql_init() ...
mysql_real_connect()...
char value = 1;
mysql_options(&mysql, MYSQL_OPT_RECONNECT, (char *)&value);
mysql_real_connect()...
char value = 1;
mysql_options(&mysql, MYSQL_OPT_RECONNECT, (char *)&value);
2. 在执行查询的之前使用mysql_ping()确保自动重连。(但是这个是必要的吗?找不到相应的说明。。。)
UPDATE@2012.12.30:原来之前有很多误解(认为是5.1.6之后就不需要设置MYSQL_OPT_RECONNECT),多谢@Zind同学提醒,这篇基本上是重写了。。。
Nov
23
按照某帖子里的说法,禁用掉WMPNetworkSvc(Windows Media Player Netwroking SharingService)即可正常使用sysprep部署了。
好久没发这么短的Blog了,简直就是微博了,OVER。
好久没发这么短的Blog了,简直就是微博了,OVER。