异步编程经典 读skipfish源码
skipfish是google开源出来的一款安全检测工具(google真是好啊,开源了好多东东)。作者是个geek,写起程序来相当精干老道,项目主页上列举其特点之一:
High speed: pure C code, highly optimized HTTP handling, minimal CPU footprint – easily achieving 2000 requests per second with responsive targets.
一个单线程的程序如何能达到这个性能我们就来分析一下源码吧。
先从高处俯瞰一下整个程序:
整个程序分为一下几个模块:
http模块(http_client.c),数据管理模块(database.c),爬虫兼攻击模块(crawler.c,易做图ysis.c)和报表模块(report.c)。
其中http模块主要负责http回话的的处理,包括url解析,请求的发送,响应的解析。这个模块没用三方库来处理,所有功能都自己实现,相当牛,也相当高效。
数据管理模块主要是管理爬虫需要的站点树和检查出的错误,不多说。
爬虫兼攻击模块负责在url里插入攻击向量,以及html解析和攻击结果的检查。
报表模块是最后生成网页报表的模块,就是把数据模块里的数据输出,不解释。
好,我们来仔细分析http模块和攻击模块。
我们仔细想下整个程序的性能问题就可以发现,攻击往往是顺序进行的。而网页的下载却有快有慢(更内容多少,即时网速都有关)。所以,如果将http的处理逻辑串到攻击逻辑里面必然会造成一会儿网卡忙cpu闲,一会儿cpu闲而网卡忙。这个问题在我前面一篇文章讨论过。解决方法毫无疑问,异步化,下面我们就来看看这两个逻辑是如何异步交互的。
从攻击的角度来看:一个url需要往往都要经过至少以下一些检查步骤:
1.url的类型检查
2.xss测试
3.sql注入测试
从http的角度来看,一个url意味着建立连接,发送请求,接收响应。
从http入手,如何将网卡的性能发挥到100%,并发!skipfish的并发采用了最简单的poll,你可能疑问为什么不用epoll,答案很简单,并发量不多(skipfish默认并发是40个连接),因为http的请求主要是下载,所以一个连接需要下载很多东西,几十个连接流量已经不小了。这不是关键,不多说了。整个skipfish的主循环也在http模块中:u32 next_from_queue(void)。在main函数中有如下代码:
while ((next_from_queue() && !stop_soon) || (!show_once++)) {
…
}
很显然,这个函数就是这个程序的引擎。函数的意思也很明了:http请求队列里是否还有请求。这里引出了很重要的东西:请求队列。代码如下:
/* Request queue descriptor: */
struct queue_entry {
struct http_request* req; /* Request descriptor */
struct http_response* res; /* Response descriptor */
struct conn_entry* c; /* Connection currently used */
struct queue_entry* prev; /* Previous queue entry */
struct queue_entry* next; /* Next queue entry */
u8 retrying; /* Request being retried? */
};
static struct queue_entry* queue;
代码很简单,一个双向链表。里面存放着请求响应对,和关联的网络连接。整个http模块所干的事情,基本也就是next_from_queue函数干的事情,说得简单一点,就是从网卡上取数据往请求队列里面的响应体里面填。填完之后呢?看代码,在next_from_queue函数里:
if (!p)
p = __DFL_ck_alloc(sizeof(struct pollfd) * max_connections);
while (c) {
p[i].fd = c->fd;
p[i].events = POLLIN | POLLERR | POLLHUP;
if (c->write_len - c->write_off || c->SSL_rd_w_wr)
p[i].events |= POLLOUT;
p[i].revents = 0;
c = c->next;
i++;
}
poll(p, conn_cur, 100);
这里填充poll相关的结构体并调用poll,这是整个程序除了gethostbyname以外唯一会阻塞的地方(没流量啥都干不了当然阻塞啦)。下面开始处理所有连接上的数据,代码被我删掉很多,仅留下了关键点。
c = conn;
for (i=0;i<conn_cur;i++) {
if (p[i].revents & (POLLERR|POLLHUP)) {
if (c->q) {
if (c->write_len - c->write_off || !c->read_len) {
c->q->res->state = STATE_CONNERR;
keep = c->q->req->callback(c->q->req, c->q->res);
req_errors_net++;
req_errors_cur++;
} else {
if (parse_response(c->q->req, c->q->res, c->read_buf,
c->read_len, 0) != 2) {
c->q->res->state = STATE_OK;
keep = c->q->req->callback(c->q->req, c->q->res);
if (req_errors_cur <= max_fail)
req_errors_cur = 0;
} else {
c->q->res->state = STATE_CONNERR;
keep = c->q->req->callback(c->q->req, c->q->res);
req_errors_net++;
req_errors_cur++;
}
}
}
destroy_unlink_conn(c, keep);
} else
以上就是当连接错误或者连接关闭的时候的处理,我们看到,这里主要做的事情就是调用parse_response来解析已获得的数据,如果解析没问题,说明网页已经传好了,就调用回调函数req->callback(c->q->req, c->q->res);,我们会经常看到这样的调用,而callback是http_request结构体里面的一个函数指针,它的值在请求发出之前就已经设置好了。我们后面再看。
if (((p[i].revents & POLLIN) && !c->SSL_wr_w_rd) ||
((p[i].revents & POLLOUT) && c->SSL_rd_w_wr)) {
if (c->q) {
if (c->proto == PROTO_HTTPS) {
read_res = SSL_read(c->srv_ssl, c->read_buf + c->read_len,
READ_CHUNK);
} else {
read_res = read(c->fd, c->read_buf + c->read_len, READ_CHUNK);
if (read_res <= 0) goto network_error;
}
c->read_buf[c
补充:综合编程 , 安全编程 ,