首页 > 代码库 > 【整理】close 和 shutdown 的原理
【整理】close 和 shutdown 的原理
http://stackoverflow.com/questions/14740852/linux-socket-close-vs-shutdown
shutdown(sd, SHUT_WR)
发送FIN给对端,对端会响应一个ACK(协议层-内核所做). 未来任何尝试写这个socket会发生错误。但是对端仍然可以可以读,因为本端关闭写时,可能由于协议层的策略(滑动窗口、拥塞窗口、angle算法等)导致发送延迟,如果有待发送的消息,那么要尽力保证这些消息都发出去的。所以,会在最后一个报文中加入FIN标志,同时,关闭用于减少网络中小报文的angle算法,向连接对端发送消息。如果没有待发送的消息,则构造一个报文,仅含有FIN标志位,发送出去关闭连接;对端可以继续写数据给本端(主动端),因为对端收到FIN(收到FIN之前其实还有主动端缓冲区待发送消息)并响应ACK了(在响应ACK之前可能还有数据发送给本端),响应ACK后就进入CLOSE_WAIT状态,这个状态一般比较短暂,如果存在大量的CLOSE_WAIT,说明程序未关闭socket或者并发量太大来不及,或者你用的网络库有bug。
shutdown(sd, SHUT_RD)
不发送任何网络消息:他只是限制本地API对于随后的socket的读取直接返回EOS. 在已经在读操作被shutdown的socket上接收数据的行为(内核的行为, 表明对端要发送数据过来的行为)是依赖于系统的: Unix会确认它并且丢弃它; Linux会确认它并且缓冲它,最终会使得sender停止运行(发送rst给sender); Windows会发出一个RST, 此时sender继续写会报错“Connection reset by peer”- 注:本端(主动端)发出fin进入FIN_WAIT1,收到对端ACK(对端发送ACK之前可能还有数据要发送给本端)进入FIN_WAIT2状态,收到对端FIN,发送ACK则进入TIME_WAIT,在2MSL内可以重发最后的ACK防止最后的ACK丢包;对端收到主动端的fin,如果有数据要发送给主动端,就发送数据,然后发送ACK,如果没有就直接发送ACK给主动端,然后对端进入CLOSE_WAIT状态,此时程序要保证及时关闭socket,不然会close_wait泄漏造成服务挂掉,当关闭socket时发送FIN给主动端,对端进入LASK_ACK状态,主动端收到FIN并ACK给对端,主动端进入TIME_WAIT,对端关闭(TIMEWAIT存在的原因就是防止最后一个ack丢包,在2MSL内可重传ACK)。
close函数:
参数sockfd为套接字描述符,成功返回0,失败返回-1;该函数的功能是关闭套接字描述符引用计数,当计数大于0时什么都不干,当计数等于0时触发TCP/IP的四次挥手,即主动关闭方发送FIN。
如果在调用close以前,TCP协议栈的发送队列中有已排队等候发送的数据,则协议栈尝试将数据发送出去,发送完毕后根据套接字描述引用计数来决定是否关闭连接。
如果在调用close以后,且套接字描述符引用计数为0,则在其上调用write或者read函数则会产生错误码为9即EBADF(bad file descriptor 和Broken Pipe不一样,Broken Pipe表示文件描述符还合法,但是连接状态非法)的错误。调用close函数时,TCP协议栈对发送队列中已排队等候发送数据的处理流程受SO_LINGER选项的影响,该选项关联的数据结构如下:
struct linger{
int l_onoff; //0=off,nonzero=on
int l_linger; //linger time
};
l_onoff是选项开关,如果是0则关闭选项并且忽略参数l_linger,如果是非零则打开选项,默认是0;l_linger是延迟关闭连接时间,只有l_onoff打开时即为非零时,该参数的值才有效。这几个参数与close时TCP协议栈对待发数据的处理流程如下表:表1 待发数据处理流程
l_onoff
l_linger
close行为
发送队列
TCP协议栈
零
忽略
立即返回
保持直至发送完成
接管套接字并保证将数据发送至对端
非零
零
立即返回
立即放弃
直接发送RST,自身立即复位,不用经过2MSL状态,对端收到复位错误码
非零
非零
阻塞直到l_linger时间超时或数据发送完成(套接字必须设置为阻塞)
在超时时间段内保持尝试发送,若超时则立即放弃
超时则同第二种情况,若发送完成则皆大欢喜
当应用程序在调用close()函数关闭TCP连接时,Linux内核的默认行为是将套接口发送队列里的原有数据(比如之前残留的数据)以及新加入 的数据(比如函数close()产生的FIN标记,如果发送队列没有残留之前的数据,那么这个FIN标记将单独产生一个新数据包)发送出去并且销毁套接口 (并非把相关资源全部释放,比如只是把内核对象sock标记为dead状态等,这样当函数close()返回后,TCP发送队列的数据包仍然可以继续由内 核协议栈发送,但是一些相关操作就会受到影响和限制,比如对数据包发送失败后的重传次数)后立即返回。这需要知道两点:
第一,当应用程序获得 close()函数的返回值时,待发送的数据可能还处在Linux内核的TCP发送队列里,因为当我们调用write()函数成功写出数据时,仅表示这些 数据被Linux内核接收放入到发送队列,如果此时立即调用close()函数返回后,那么刚才write()的数据限于TCP本身的拥塞控制机制(比如 发送窗口、接收窗口等等),完全有可能还呆在TCP发送队列里而未被发送出去;当然也有可能发送出去一些,毕竟在调用函数close()时,进入到 Linux内核后有一次数据包的主动发送机会,即:
tcp_close() -> tcp_send_fin() -> __tcp_push_pending_frames() -> tcp_write_xmit()第二,所有这些数据的发送,最终却并不一定能全部被对端确认(即数据包到了对端TCP协议栈的接收队列),只能做到TCP协议本身提供的一定程度的 保证,比如TCP协议的重传机制(并且受close()函数影响,重传机制弱化,也就是如果出现类似系统资源不足这样的问题,调用过close()函数进 行关闭的套接口所对应的这些数据会优先丢弃)等,因为如果网络不好可能导致TCP协议放弃继续重传或在意外收到对端发送过来的数据时连接被重置导致未成功 发送的数据全部丢失(后面会看到这种情况)。
【整理】close 和 shutdown 的原理