Lighttpd1.4.20源码分析之状态机(4) 错误处理和连接关闭

  Lighttpd所要处理的错误分为两种。一种是http协议规定的错误,如404错误。另一种就是服务器运行过程中的错误,如write错误。

  对于http协议规定的错误,lighttpd返回相应的错误提示文件。其实对于lighttpd而言,这不算错误。在返回错误提示文件后,相当于顺利的完成了一次请求,只是结果和客户端想要的不一样而已。

    对于服务器运行中的错误,状态机会直接进入CON_STATE_ERROR状态。大部分的情况下,这种错误都是由客户端提前断开连接所造成的。比如你不停的刷新页面,在你刷新的时候,前一次的连接没有完成,但被浏览器强行断开,这时,服务器就会出现连接错误。对于服务器而言,刷新前后的两个连接是不相干的。因此,服务器在接收后一个连接的时候仍然会继续处理前一次的连接。而这时前一次的连接已经断开,这就产生了连接错误。  

进入CON_STATE_ERROR状态后,如果前面的处理已经得到了结果。也就是http_status不为空。那么调用plugins_call_handle_request_done告诉插件请求处理结束。

/*
* even if the connection was drop we still have to write it to the
* access log
*/
if (con->http_status)
{
plugins_call_handle_request_done(srv, con);
}

    接着,如果使用了ssl,关闭ssl连接。关闭ssl连接的代码很长,但大部分都是错误处理。再朝后,如果连接模式不是DIRECT,调用plugins_call_handle_connection_close告诉插件连接已经关闭。到此,如果连接没有设置keep_alive,那么关闭连接并做一些清理工作之后就直接结束状态机的运行了。
    如果设置了keep_alive,此时可能是服务器首先关闭连接的。调用shutdown关闭连接的读和写。如果关闭没有出错,状态机进入CON_STATE_CLOSE状态。

1     /*
2 * close the connection
3 */
4 if ((con->keep_alive == 1) && (0 == shutdown(con->fd, SHUT_WR)))
5 {
6 con->close_timeout_ts = srv->cur_ts;
7 connection_set_state(srv, con, CON_STATE_CLOSE);、
8 if (srv->srvconf.log_state_handling)
9 {
10 log_error_write(srv, __FILE__, __LINE__, "sd",
11 "shutdown for fd", con->fd);
12 }
13 }

    上面的代码中有这么一句:con->close_timeout_ts = srv->cur_ts;将close_timeout_ts的值设置为当前时间。这时干吗用的?不着急,继续完后走。
    进入CON_STATE_ERROR状态之后,如果是keep_alive,且缓冲区中有数据还没读,那么把这些数据读出来然后直接丢弃。如果没有数据可读,直接设置close_time_ts为0。接着执行下面的代码:

1 if (srv->cur_ts - con->close_timeout_ts > 1)
2 {
3 connection_close(srv, con);
4 if (srv->srvconf.log_state_handling)
5 {
6 log_error_write(srv, __FILE__, __LINE__, "sd", "connection closed for fd", con->fd);
7 }
8 }

    除了输出日志,这段代码就是调用了connection_close。connection_close做最后的清理工作,包括调用close,将对应的connection放回缓冲池中等。前面刚刚说过,在CON_STATE_ERROR中设置了close_time_ts为cur_ts。在出了CON_STATE_ERROR后,进入CON_STATE_CLOSE,这段时间cur_ts是没有改变的。如果前面的代码中测试缓冲区中没有数据可读,此时const_time_ts是等于cur_ts。也就是说状态机还在CON_CLOSE_STATE这个状态,然后就退出了状态机的大while循环。服务器进入了connection_state_mechine最后面的那个switch语句。在这个switch中,连接对应的fd被加入到fdevent系统中并监听读入事件。
    这个时候连接都已经关闭了还有什么能读的啊?别急!细心的读者应该注意到了,到这个时候,服务器仅仅是对连接调用了shutdown函数,没有调用close函数。首先说一下shutdown和close的区别。close作用在socket fd上和作用在其他fd上的效果差不多,都是减少引用计数,当计数为0的时候释放资源,关闭连接。shutdown函数则是针对socket fd的专门函数。shutdown可以关闭socket连接的一个方向,也就是可以只关闭写或者只关闭读(socket连接是全双工的)。另外一个区别就是,如果连接是多个进程共享的,那么在一个进程中调用close不影响其他进程使用连接,因为仅仅是引用计数减少1。而如果一个进程调用了shutdown ,那么,不管三七二十一,系统直接咔嚓掉这个连接,其他进程看到的也是关闭的连接。还有一个区别,调用了close的socket fd,read,write函数不能再从这个fd上读取或发送数据。而调用了shutdown的socket fd,如果缓冲区中还有数据可读,由于此时对于进程而言,socket fd没有关闭,read依然可以从这个fd读数据。由于shutdown仅仅是关闭了连接,不会进行资源的释放,要想释放连接占用的资源,还必须调用close函数。
    对于keep_alive的连接,关闭是服务器主动发起的。根据TCP协议,主动发起关闭的一方,其连接将进入TIME_WAIT状态,此时连接所占用的资源没有释放。更悲剧的是要在这个状态待很长时间。。。(默认4分钟)对于高并发的服务器,如果服务器主动关闭大量连接,那么服务器的资源很快就会被用光(端口,内存等),新的连接将无法建立。关于这个TIME_WAIT状态,读者可自行google之,网上有很多介绍的文章以及处理办法。虽然这个TIME_WAIT状态看似会给我们带来很多麻烦,但是,如果没有这个状态我们会更麻烦。。。《UNIX Network Programming Volum1》这本神作中有详细的介绍,有兴趣的读者可以看看。既然麻烦不可避免,那么就尽量将麻烦的影响见到最低。连接占用的资源主要就是端口和内存。端口是没办法的,占用就占用了。但是,内存还是可以节省一点的。前面说了,shutdown之后,我们依然可以把缓冲区的数据读出来。那么,我们把这些数据读出来以后,缓冲区所占用的内存就可以释放了。从而降低了内存的使用。
    在CON_STATE_CLOSE中,下面的代码把缓冲区的数据读了出来:

1 if (ioctl(con->fd, FIONREAD, &b))
2 {
3 log_error_write(srv, __FILE__, __LINE__, "ss", "ioctl() failed", strerror(errno));
4 }
5 if (b > 0)
6 {
7 char buf[1024];
8 log_error_write(srv, __FILE__, __LINE__, "sdd", "CLOSE-read()", con->fd, b);
9
10 read(con->fd, buf, sizeof(buf));
11 }
12 else
13 {
14 /*
15 * nothing to read
16 */
17
18 con->close_timeout_ts = 0;
19 }

    如果没有数据可读,那么设置close_timeout_ts会使连接在后面的代码中被关闭。如果有数据可读,读取数据之后,连接依然处在CON_STATE_CLOSE状态中,连接对应的fd被加入到fdevent系统中监听读事件。如果缓冲区中还有数据,那么在connection_handle_fdevent 函数中,也有上面这段代码,再次运行之。如果数据没有读完,服务器将会重复的在connection_handle_fdevent函数中运行上述代码直到数据读完。随着close_timeout_ts被设置为0,在下次joblist的调度中,状态机将会关闭连接,清理所有资源。
    至此,连接正式关闭。之所以这么麻烦,就是由于连接是服务器主动关闭的,会有TIME_WAIT状态的问题。这样处理仅仅在内存上有了一些节省。但是具体效果怎样,这要看系统内核是如何处理连接缓冲区的。如果各位读者了解这方面的内容,还请不吝赐教!
    另外,shutdown之后内核中的缓冲区到底会怎样笔者并不是很确定。上面的内容只是根据代码的推断。还是那句话,如果给为读者有什么高见,还行不吝赐教啊。

posted @ 2011-05-05 00:03  kernel@hcy  阅读(...)  评论(...编辑  收藏