现在开始讲解lighttpd如何处理连接fd.
在
第四节,已经讲解了如何处理服务器负责监听连接的fd,处理连接fd的流程在前半部分与监听fd大体相同:在通过监听fd接收一个新的连接之后,服务器将这个fd加入到事件处理器中, 设置它所感兴趣的IO事件类型, 当IO状态发生变化时, 事件处理器获取被触发的fd,回调函数,事件等,然后通过已经注册的回调函数进行处理.
这里的区别就在于回调函数的不同上.对于监听fd而言, 该回调函数是network.c文件中的network_server_handle_fdevent函数,这个在第四节中已经做了分析;与之对应的,连接fd的回调函数是connections.c文件中的connection_handle_fdevent函数.
// 这个函数是处理接受链接的函数, 与network_server_handle_fdevent对应
handler_t connection_handle_fdevent(void *s, void *context, int revents) {
server *srv = (server *)s;
connection *con = context;
// 添加到server的joblist中
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
*/
#ifdef USE_LINUX_SIGIO
if (srv->ev->in_sigio == 1) {
log_error_write(srv, __FILE__, __LINE__, "sd",
"connection closed: poll() -> HUP", con->fd);
} else {
connection_set_state(srv, con, CON_STATE_ERROR);
}
#else
connection_set_state(srv, con, CON_STATE_ERROR);
#endif
}
} else if (revents & FDEVENT_ERR) {
#ifndef USE_LINUX_SIGIO
log_error_write(srv, __FILE__, __LINE__, "sd",
"connection closed: poll() -> ERR", con->fd);
#endif
connection_set_state(srv, con, CON_STATE_ERROR);
} else {
log_error_write(srv, __FILE__, __LINE__, "sd",
"connection closed: poll() -> ???", revents);
}
}
// 如果连接的状态是READ, 那么处理read状态
if (con->state == CON_STATE_READ ||
con->state == CON_STATE_READ_POST) {
connection_handle_read_state(srv, con);
}
// 如果连接状态是WRITE并却写缓冲区队列中不为空 并且该连接是可写的, 就去处理写状态
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));
}
// 如果还有数据, 打印错误log, 并且读入数据
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,也就是保存连接相关数据的结构体中, 有一个叫state的成员, 顾名思义, 这个成员保存的是一个连接的状态,这里所说的状态与前面提到的IO状态是不同, IO状态是用于表示一个fd可读/可写/出错等, 而这个状态更多的是与协议相关的部分.它是一个枚举类型:
/* the order of the items should be the same as they are processed
* read before write as we use this later */
typedef enum {
CON_STATE_CONNECT, // 连接
CON_STATE_REQUEST_START, // 开始获取请求
CON_STATE_READ, // 处理读
CON_STATE_REQUEST_END, // 请求结束
CON_STATE_READ_POST, // 处理读,但是是POST过来的数据
CON_STATE_HANDLE_REQUEST, // 处理请求
CON_STATE_RESPONSE_START, // 开始回复
CON_STATE_WRITE, // 处理写
CON_STATE_RESPONSE_END, // 回复结束
CON_STATE_ERROR, // 出错
CON_STATE_CLOSE // 连接关闭
} connection_state_t;
为什么需要这些状态?因为lighttpd中采用了所谓"状态机"去处理连接,而这些状态就是状态机中的各种不同状态.
在lighttpd的官方文档中, 对其使用的状态机有一篇文档,在这里:
http://redmine.lighttpd.net/wiki/lighttpd/Docs:InternalHTTPStates
我觉得里面的这幅图非常的直观,学习过编译原理的人一看就可以知道这是状态机的转换图:
现在回到本章的主题中, lighttpd如何处理连接fd.
前面给出的处理连接fd的回调函数,最开始地方有一段代码:
joblist_append(srv, con);
这个函数将一个连接connection结构体放入到joblist中, 后面的部分根据不同的情况设置connection中的state字段,调用的是connection_set_state函数.
现在回到server.c函数中, 第四节中已经结合处理监听fd的流程讲解了这段函数:
// 轮询FD
if ((n = fdevent_poll(srv->ev, 1000)) > 0) {
/* n is the number of events */
int revents;
int fd_ndx;
fd_ndx = -1;
do {
fdevent_handler handler;
void *context;
handler_t r;
// 获得处理这些事件的函数指针 fd等
// 获得下一个fd在fdarray中的索引
fd_ndx = fdevent_event_next_fdndx (srv->ev, fd_ndx);
// 获得这个fd要处理的事件类型
revents = fdevent_event_get_revent (srv->ev, fd_ndx);
// 获取fd
fd = fdevent_event_get_fd (srv->ev, fd_ndx);
// 获取回调函数
handler = fdevent_get_handler(srv->ev, fd);
// 获取处理相关的context(对server是server_socket指针, 对client是connection指针)
context = fdevent_get_context(srv->ev, fd);
/* connection_handle_fdevent needs a joblist_append */
// 进行处理
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);
} else if (n < 0 && errno != EINTR) {
log_error_write(srv, __FILE__, __LINE__, "ss",
"fdevent_poll failed:",
strerror(errno));
}
在server.c文件中, 紧跟着这段代码的是:
// 处理joblist中的连接
for (ndx = 0; ndx < srv->joblist->used; ndx++) {
connection *con = srv->joblist->ptr[ndx];
handler_t r;
connection_state_machine(srv, con);
switch(r = plugins_call_handle_joblist(srv, con)) {
case HANDLER_FINISHED:
case HANDLER_GO_ON:
break;
default:
log_error_write(srv, __FILE__, __LINE__, "d", r);
break;
}
con->in_joblist = 0;
}
简单的说, 这段代码是一个循环, 从joblist中依次取出已经放在这里的connection指针, 再调用
connection_state_machine函数进行处理.
connection_state_machine函数是一个非常重要的函数, 它就是处理连接fd的状态机, 在后面将详细分析这个函数.
现在回顾一下lighttpd处理连接fd的大体框架:前面的流程与处理监听fd大体相同,在与处理连接fd相关的回调函数中, 首先将需要处理的fd相关的connection加入到joblist中, 设置它的state, 后面再轮询joblist, 进入状态机进行处理.
这个过程可能值得商榷, 比如为什么在回调函数中需要将一个connection指针加入到joblist中, 后面再一个循环轮询joblist中的connection,这样不是显得效率低下吗?我们需要注意的是,前面提到的IO事件状态和connection中的成员state是不同的!第一个轮询过程(IO事件处理器的轮询)是根据哪些fd的IO发生了变化被触发而去调用回调函数, 而后面的循环(joblist的轮询)中的connection则不一定都是IO发生变化的!
打一个比方, 一个连接到来, 此时我们把它放入到事件处理器中, 当它可读时被触发, 也就是在第一个轮询中被触发;如果在处理的时候出了问题, 不能继续, 此时我们只需要保存它当前的state字段, 在下一次操作中, 由于它的IO没有发生变化, 那么将不会在第一个轮询也就是IO事件处理器中被处理, 而只会在轮询joblist时被处理, 只需要它的state字段是正确的, 放到状态机处理函数中就可以继续下去.