内容简介:wifidog源码分析Lighttpd1.4.20源码分析之fdevent系统(4) -----连接socket的处理与超时处理
前面讲了lighttpd是怎样使用fdevent系统的,以及监听socket的处理过程。这一篇我们来看一看lighttpd是怎样处理连接socket的。
首先,我们来看看lighttpd是怎样建立和客户端的连接的。前面在讲监听socket的处理过程中其实已经讲解了连接的建立过程。lighttpd监测监听socket的IO事件,如果有可读事件发生,那么表示有新的连接请求,然后调用network.c/network_server_handle_fdevent()来处理连接请求。network_server_handle_fdevent()函数调用connections.c/connection_accept() 接受客户端的请求,建立连接。在建立连接的同时,就得到了连接socket的fd,也就是accept函数的返回值。
建立连接之后,这个连接对应的状态机的状态被设置为CON_STATE_REQUEST_START,就是开始读取客户端发过来的request信息。在从connection_accept函数跳回network_server_handle_fdevent()函数的for循环中后,程序紧接着就调用了一次connection_state_machine()函数,这个函数是根据当前连接的状态机的状态设置状态机的下一个状态,CON_STATE_REQUEST_START的下一个状态是CON_STATE_READ,这个状态表示连接正在读取客户端发送的数据。当连接的状态机被设置成CON_STATE_READ后,在connection_state_machine()函数的最后,有这样一个switch语句:
switch (con->state)
{
case CON_STATE_READ_POST:
case CON_STATE_READ:
case CON_STATE_CLOSE:
fdevent_event_add(srv->ev, &(con->fde_ndx), con->fd, FDEVENT_IN);
break;
case CON_STATE_WRITE:
/*
* request write-fdevent only if we really need it
* - if we have data to write
* - if the socket is not writable yet
*/
if (!chunkqueue_is_empty(con->write_queue) && (con->is_writable == 0) &&(con->traffic_limit_reached == 0))
{
fdevent_event_add(srv->ev, &(con->fde_ndx), con->fd, FDEVENT_OUT);
}
else
{
fdevent_event_del(srv->ev, &(con->fde_ndx), con->fd);
}
break;
default:
fdevent_event_del(srv->ev, &(con->fde_ndx), con->fd);
break;
}
上面这个switch语句将状态处在CON_STATE_READ_POST,CON_STATE_READ和CON_STATE_CLOSE的连接对应的连接socket fd加入到fdevent系统中,并监听可读事件。将处CON_STATE_WRITE状态且有数据要写的连接对应的socket fd加入到fdevent系统中,并监听可写事件。其他状态的连接则把对应的fd从fdevent系统中删除,因为这些连接不会有IO事件发生。这样,连接socket fd就被加入到了fdevent系统中。下面就是等待IO事件的发生。程序在前面已经提到过,如下:
if ((n = fdevent_poll(srv->ev, 1000)) > 0)
{
int revents;
int fd_ndx;
fd_ndx = -1;
do
{
fdevent_handler handler;
void *context;
handler_t r;
fd_ndx = fdevent_event_next_fdndx(srv->ev, fd_ndx);
revents = fdevent_event_get_revent(srv->ev, fd_ndx);
fd = fdevent_event_get_fd(srv->ev, fd_ndx);
handler = fdevent_get_handler(srv->ev, fd);
context = fdevent_get_context(srv->ev, fd);
switch (r = (*handler) (srv, context, revents))
{
case HANDLER_FINISHED:
case HANDLER_GO_ON:
case HANDLER_WAIT_FOR_EVENT:
case HANDLER_WAIT_FOR_FD:
break;
case HANDLER_ERROR:
/*
* should never happen
*/
SEGFAULT();
break;
default:
log_error_write(srv, __FILE__, __LINE__, "d", r);
break;
}
}while (--n > 0);
}
这段程序在前面已经讲解过。对于fdevent系统,它不关心自己处理的fd是连接fd还是监听fd,它所做的就是对于发生了这个fd所希望的IO事件以后,调用这个fd对应的处理函数处理IO事件。连接fd对应的处理函数是connections.c/connection_handle_fdevent()函数。函数的代码如下:
handler_t connection_handle_fdevent(void *s, void *context,int revents)
{
server *srv = (server *) s;
connection *con = context;
//把这个连接加到作业队列中。
joblist_append(srv, con);
if (revents & FDEVENT_IN)
{
con->is_readable = 1;
}
if (revents & FDEVENT_OUT)
{
con->is_writable = 1;
/*
* we don't need the event twice
*/
}
if (revents & ~(FDEVENT_IN | FDEVENT_OUT))
{
/*
* looks like an error 即可读又可写,可能是一个错误。
*/
/*
* FIXME: revents = 0x19 still means that we should read from the queue
*/
if (revents & FDEVENT_HUP)
{
if (con->state == CON_STATE_CLOSE)
{
con->close_timeout_ts = 0;
}
else
{
/*
* sigio reports the wrong event here there was no HUP at all
*/
connection_set_state(srv, con, CON_STATE_ERROR);
}
}
else if (revents & FDEVENT_ERR)
{
connection_set_state(srv, con, CON_STATE_ERROR);
}
else
{
log_error_write(srv, __FILE__, __LINE__, "sd","connection closed: poll() -> ???", revents);
}
}
if (con->state == CON_STATE_READ|| con->state == CON_STATE_READ_POST)
{
connection_handle_read_state(srv, con);
//继续读取数据,直到数据读取完毕
}
// 数据的写回并没有放给状态机去处理。
if (con->state == CON_STATE_WRITE&& !chunkqueue_is_empty(con->write_queue) && con->is_writable)
{
if (-1 == connection_handle_write(srv, con))
{
connection_set_state(srv, con, CON_STATE_ERROR);
log_error_write(srv, __FILE__, __LINE__, "ds", con->fd,"handle write failed.");
}
else if (con->state == CON_STATE_WRITE)
{
//写数据出错,记录当前时间,用来判断连接超时。
con->write_request_ts = srv->cur_ts;
}
}
if (con->state == CON_STATE_CLOSE)
{
/*
* flush the read buffers 清空缓冲区中的数据。
*/
int b;
//获取缓冲区中数据的字节数
if (ioctl(con->fd, FIONREAD, &b))
{
log_error_write(srv, __FILE__, __LINE__, "ss","ioctl() failed", strerror(errno));
}
if (b > 0)
{
char buf[1024];
log_error_write(srv, __FILE__, __LINE__, "sdd","CLOSE-read()", con->fd, b);
//将缓冲区中的数据读取后并丢弃,此时连接已经关闭,数据是无用数据。
read(con->fd, buf, sizeof(buf));
}
else
{
/*
* nothing to read 缓冲区中没有数据。复位连接关闭超时计时。
*/
con->close_timeout_ts = 0;
}
}
return HANDLER_FINISHED;
}
可以看到,connection_handle_fdevent()函数根据当前连接fd所发生的IO事件,对connection结构体中的标记变量赋值,如is_writable,is_readable等,并做一些时间的记录。这些事件所对应的真正的IO处理则交给状态机处理。状态机根据这些标记变量进行相应的动作处理。
这样,对于fdevent系统对于一次连接fd的IO事件就处理结束了。当然,真正的处理工作是由状态机来完成。下面的图简要的描述了fdevent系统对连接fd和监听fd的处理:
下面我们来看一看连接超时的处理。连接超时有三种:读数据超时,写数据超时和关闭超时。处理超时的代码在server.c中的main函数woker进程开始部分:
/**
* alarm函数发出的信号,表示一秒钟已经过去了。
*/
if (handle_sig_alarm)
{
/*
* a new second 新的一秒开始了。。。
*/
#ifdef USE_ALARM
/*
* reset notification 重置
*/
handle_sig_alarm = 0;
#endif
/*
* get current time 当前时间。精确到一秒
*/
min_ts = time(NULL);
/**
* 这里判断和服务器记录的当前时间是否相同。
* 相同,则表示服务器还在这一秒中,继续处理请求等。
* 如果不相同,则进入了一个新的周期(当然周期是一秒)。这就要做一些触发和检查以及清理的动作。
* 如插件的触发连接的超时清理状态缓存等。
* 其中,最主要的工作是检查连接的超时。
*/
if (min_ts != srv->cur_ts)
{
int cs = 0;
connections *conns = srv->conns;
handler_t r;
switch (r = plugins_call_handle_trigger(srv))
{
case HANDLER_GO_ON:
break;
case HANDLER_ERROR:
log_error_write(srv, __FILE__, __LINE__, "s","one of the triggers failed");
break;
default:
log_error_write(srv, __FILE__, __LINE__, "d", r);
break;
}
/*
* trigger waitpid
*/
srv->cur_ts = min_ts;
/*
* cleanup stat-cache 清理状态缓存。每秒钟清理一次。
*/
stat_cache_trigger_cleanup(srv);
/**
* check all connections for timeouts
*/
for (ndx = 0; ndx < conns->used; ndx++)
{
int changed = 0;
connection *con;
int t_diff;
con = conns->ptr[ndx];
//连接的状态是在读
if (con->state == CON_STATE_READ|| con->state == CON_STATE_READ_POST)
{
if (con->request_count == 1) //连接处理一个请求
{
if (srv->cur_ts - con->read_idle_ts >con->conf.max_read_idle)
{
/*
* time - out
*/
connection_set_state(srv, con, CON_STATE_ERROR);
changed = 1;
}
} //这个连接同时处理多个请求
else
{
if (srv->cur_ts - con->read_idle_ts> con->conf.max_keep_alive_idle)
{
/*
* time - out
*/
connection_set_state(srv, con, CON_STATE_ERROR);
changed = 1;
}
}
}
//连接的状态是写
if ((con->state == CON_STATE_WRITE)&& (con->write_request_ts != 0))
{
if (srv->cur_ts - con->write_request_ts> con->conf.max_write_idle)
{
/*
* time - out
*/
#if 1
log_error_write(srv, __FILE__, __LINE__,"sbsosds", "NOTE: a request for",
con->request.uri, "timed outafter writing", con->bytes_written, "bytes. We waited",
(int) con->conf. max_write_idle,
"seconds. If this a problemincrease server.max-write-idle");
#endif
connection_set_state(srv, con, CON_STATE_ERROR);
changed = 1;
}
}
/*
* we don't like div by zero 防止除0。。。
*/
if (0 ==(t_diff = srv->cur_ts - con->connection_start))
t_diff = 1;
/**
* 下面的if语句不是用来判断连接是否超时。
* lighttpd对每个连接设置了一个kbytes_per_second,这个变量设定每个连接在一秒钟内多能传输的最大数据量。
* 如果传送的数据大于这个值,那么这个连接将停止传输数据,被追加到作业队列中等待下一次处理。
* 作者这样做估计是为了平衡各个连接之间的数据传输。
*/
if (con->traffic_limit_reached && (con->conf.kbytes_per_second == 0|| ((con->bytes_written / t_diff)< con->conf.kbytes_per_second * 1024)))
{
/*
* enable connection again
*/
con->traffic_limit_reached = 0;
changed = 1;
}
if (changed)
{
connection_state_machine(srv, con);
}
con->bytes_written_cur_second = 0;
*(con->conf.global_bytes_per_second_cnt_ptr) = 0;
}//end of for( ndx = 0; ...
if (cs == 1)
fprintf(stderr, "\n");
}//end of if (min_ts != srv->cur_ts)...
}//end of if (handle_sig_alarm)...
在这个If语句中,作者的本意是通过alarm信号来判断时间是否到一秒种。handle_sig_alarm就是标记是否已经过了一秒钟。在server.c的信号处理函数sigaction_handler()中可以看到:
case SIGALRM: //超时信号
handle_sig_alarm = 1;
break;
当收到SIGALRM信号时,标记handle_sig_alarm为1。下面的代码是启动计时器。两段代码都被宏包围。说明需要定义宏USE_ALARM才启动计时器。
#ifdef USE_ALARM
struct itimerval interval;
interval.it_interval.tv_sec = 1;
interval.it_interval.tv_usec = 0;
interval.it_value.tv_sec = 1;
interval.it_value.tv_usec = 0;
#endif
#ifdef USE_ALARM
signal(SIGALRM, signal_handler);
if (setitimer(ITIMER_REAL, &interval, NULL))
{
log_error_write(srv, __FILE__, __LINE__, "s", "setting timer failed");
return -1;
}
getitimer(ITIMER_REAL, &interval);
#endif
下面寻找宏USE_ALARM的定义。仍然在server.c文件中:
/*
* IRIX doesn't like the alarm based time() optimization
*/
/*
* #define USE_ALARM
*/
不过,这个唯一的定义被注释掉了。。。
那么,也就是说,作者并没有使用计时器产生SIGALRM信号来判断时间是否过了一秒。其实,上面处理连接超时的代码中,作者通过判断当前时间和服务器记录的当前时间来判断时间是否过了一秒。如果两个时间不一样,那么时间就过了一秒。不使用SIGALRM信号,可以减少很多信号处理,降低程序的复杂度。没有使用SIGALRM信号,那么handle_sig_alarm就一直是1。子进程每循环一次都要比较服务器记录的时间和当前时间。
下面继续看超时处理。在上面的处理程序中,lighttpd通过比较read_idle_ts,write_request_ts和当前时间的差值来判断连接是否读超时或写超时。如果这两个差值分别大于max_read_idle和max_write_idle则表示超时。如果一个连接正在处理多个请求时,读超时是和max_keep_alive_idle比较。这些上限值在配置中设置。
那么,read_idle_ts和write_request_ts又是记录的什么呢?
对于read_idle_ts,在连接进入CON_STATE_REQUEST_START状态时,记录了当前时间。如果连接长时间没有去读取request请求,则也表示连接超时。当连接开始读数据时,read_idle_ts记录开始读数据的时间。这个不多说了。
对于write_request_ts,在处理CON_STATE_WRITE状态时,有对其赋值的语句。在connection_handle_fdevent函数中也有。其实,都是在调用connection_handle_write函数出错并且连接处在CON_STATE_WRITE状态时,记录当前时间。
通过这两个变量可以看出,lighttpd对读和写的超时处理是不一样的。对于读,设定了最长时间,不管读多少数据,一旦时间超了就算超时。而对于写,只有在写出错的时候才开始计算超时。如果没有出错,那么写数据花再多的时间也不算超时。这就有一个问题了,如果客户端上传的数据很多呢?这样没上传完就有可能被判断为超时。其实,lighttpd做为一个web服务器,其假设上传的数据都是有限的。在绝大多数情况下,上传数据都是很小的,也就是http头等,而下载的数据往往很多。因此,这样处理可以提高效率。如果需要上传大量数据,可以修改配置中的超时限制。(PS:这点不太确定,望高手讲解。)
lighttpd每过一秒钟就要轮询连接,检查是否超时。如果连接很多时,这将浪费大量的时间。虽然这样很低效,但是处理简单,程序复杂度低。在真正的使用中,效率也没有想像中的那么差。
至此,lighttpd的fdevent系统就介绍完毕了。从下一篇开始,我们将走进lighttpd的状态机。
本文章由 http://www.wifidog.pro/2015/04/21/wifidog%E6%BA%90%E7%A0%81%E5%88%86%E6%9E%90Lighttpd-socket%E7%9A%84%E5%A4%84%E7%90%86.html 整理编辑,转载请注明出处
以上所述就是小编给大家介绍的《wifidog源码分析Lighttpd1.4.20源码分析之fdevent系统(4) -----连接socket的处理与超时处理》,希望对大家有所帮助,如果大家有任何疑问请给我留言,小编会及时回复大家的。在此也非常感谢大家对 码农网 的支持!
猜你喜欢:- Nginx源码阅读笔记-事件处理模块
- 源码分析(四)—错误及异常处理篇
- Nginx源码阅读笔记-处理HTTP请求
- 注册中心 Eureka 源码解析 —— 任务批处理
- 搭上 Spring Boot 请求处理源码分析专车
- 【Nginx源码研究】初探nginx HTTP处理流程
本站部分资源来源于网络,本站转载出于传递更多信息之目的,版权归原作者或者来源机构所有,如转载稿涉及版权问题,请联系我们。
Tomcat与Java Web开发技术详解
孙卫琴 / 电子工业出版社 / 2004-4-1 / 45.00元
《Tomcat与Java Web开发技术详解》编辑推荐:Jakarta Tomcat服务器是在SUN公司的JSWDK(JavaServer Web DevelopmentKit,SUN公司推出的小型Servlet/JSP调试工具)的基础上发展起来的一个优秀的Java Web应用容器,它是Apache-Jakarta的一个子项目。Tomcat被JavaWorld杂志的编辑选为2001年度最具创新的J......一起来看看 《Tomcat与Java Web开发技术详解》 这本书的介绍吧!