深入浅出TCP之半关闭与CLOSE_WAIT

终止一个连接要经过4次握手。这由TCP的半关闭(half-close)造成的。既然一个TCP连接是全双工(即数据在两个方向上能同时传递,可理解为两个方向相反的独立通道),因此每个方向必须单独地进行关闭。
 
这原则就是当一方完成它的数据发送任务后就能发送一个FIN来终止这个方向连接。当一端收到一个FIN,内核让read返回0来通知应用层另一端已经终止了向本端的数据传送。发送FIN通常是应用层对socket进行关闭的结果。
例如:TCP客户端发送一个FIN,用来关闭从客户到服务器的数据传送。
 
    半关闭对服务器究竟有什么影响呢?先看看下面的TCP状态转化图
 
 
                                  tcp状态装换图
 
    客户端主动关闭时,发出FIN包,收到服务器的ACK,客户端停留在FIN_WAIT2状态。而服务端收到FIN,发出ACK后,停留在COLSE_WAIT状态。
    这个CLOSE_WAIT状态非常讨厌,它持续的时间非常长,服务器端如果积攒大量的COLSE_WAIT状态的socket,有可能将服务器资源耗尽,进而无法提供服务。
    那么,服务器上是怎么产生大量的失去控制的COLSE_WAIT状态的socket呢?我们来追踪一下。
    一个很浅显的原因是,服务器没有继续发FIN包给客户端。
    服务器为什么不发FIN,可能是业务实现上的需要,现在不是发送FIN的时机,因为服务器还有数据要发往客户端,发送完了自然就要通过 系统调用发FIN了,这个场景并不是上面我们提到的持续的COLSE_WAIT状态,这个在受控范围之内。
    那么究竟是什么原因呢,咱们引入两个系统调用close(sockfd)和shutdown(sockfd,how)接着往下分析。
    在这儿,需要明确的一个概念---- 一个进程打开一个socket,然后此进程再派生子进程的时候,此socket的sockfd会被继承。socket是系统级的对象,现在的结果是,此socket被两个进程打开,此socket的引用计数会变成2。
 
    继续说上述两个系统调用对socket的关闭情况。
    调用close(sockfd)时,内核检查此fd对应的socket上的引用计数。如果引用计数大于1,那么将这个引用计数减1,然后返回。如果引用计数等于1,那么内核会真正通过发FIN来关闭TCP连接。
    调用shutdown(sockfd,SHUT_RDWR)时,内核不会检查此fd对应的socket上的引用计数,直接通过发FIN来关闭TCP连接。
 
     现在应该真相大白了,可能是服务器的实现有点问题,父进程打开了socket,然后用派生子进程来处理业务,父进程继续对网络请求进行监听,永远不会终止。客户端发FIN过来的时候,处理业务的子进程的read返回0,子进程发现对端已经关闭了,直接调用close()对本端进行关闭。实际上,仅仅使socket的引用计数减1,socket并没关闭。从而导致系统中又多了一个CLOSE_WAIT的socket。。。
 
如何避免这样的情况发生?
子进程的关闭处理应该是这样的:
shutdown(sockfd, SHUT_RDWR);
close(sockfd);
这样处理,服务器的FIN会被发出,socket进入LAST_ACK状态,等待最后的ACK到来,就能进入初始状态CLOSED。
 
补充一下shutdown()的函数说明
linux系统下使用shutdown系统调用来控制socket的关闭方式
int shutdown(int sockfd,int how);
参数 how允许为shutdown操作选择以下几种方式:
SHUT_RD:关闭连接的读端。也就是该套接字不再接受数据,任何当前在套接字接受缓冲区的数据将被丢弃。进程将不能对该套接字发出任何读操作。对TCP套接字该调用之后接受到的任何数据将被确认然后被丢弃。
SHUT_WR:关闭连接的写端。
SHUT_RDWR:相当于调用shutdown两次:首先是以SHUT_RD,然后以SHUT_WR
注意:
在多进程中如果一个进程中shutdown(sfd, SHUT_RDWR)后其它的进程将无法进行通信. 如果一个进程close(sfd)将不会影响到其它进程.




1. 对close的正确理解
 一般我们关闭一个socket,是调用close函数。close函数首先将socket fd的reference减一,若reference依旧大于0,则该socket端口的状态保持不变;若reference等于0,则首先将sender buffer中的数据全部发送出去,并将receive buffer中的数据全部丢弃,最后发送FIN,执行主动关闭。对当前process而言,当一个socket fd执行close后,再对其做read,会返回EOF;执行write,会返回SIGPIPE.
socket提供了SO_LINGER选项,允许我们改变close的行为,从而到达某些特别的操纵目的。默认情况下,close会立即返回,tcp协议栈首先会将send buffer中的数据先发送出去,然后开始执行主动关闭。
struct linger {
int l_onoff;
int l_linger;
};
(a) l_onoff = 0,l_linger参数将被忽略,等同于close的默认方式;
(b) l_onoff = 1, l_linger = 0; close会立即返回,tcp协议栈将send buffer和receive buffer中的数据全部情况,同时立即发送RST。采用这种方式将绕过正常的4次握手关闭,可避免进入2MSL TIME_WAIT状态。
(c) l_onoff = 1, l_linger = xx; close会被block住,当send buffer里面的数据全部被发送并被ack,则close返回,否则直到xx超时。若超时,则close返回EWOULDBLOCK,同时send buffer里面的所有数据将被丢弃。这个功能只对blocked fd有效,对non-blocked fd无效。
第3种方式相对于第1种而言,优点是可以确切直到send buffer里面的数据有没有被tcp协议栈完整的发送出去。因为第1种的方式,close立即返回,应用层无法知晓是否send buffer中的数据已经被对方协议栈完整收到。采用第3种,应用层可以清楚直到对方协议栈是否完整收到了数据。虽然3比1进步了一些,但依旧无法确保对方应用层已经完整收到了数据。要想确认对方应用端收到了数据,方案只有两个:
(1) 在应用层上设计协议,比如对方收到完整的数据后,发送一个确认的数据包过来。那么发送方只有在收到这个确认数据包之后,才会调用close。
(2) 采用shutdown做半关闭,并通过read等待读取到EOF。在正常情况下,对方只有在数据完整收到后才会执行close操作,发送方才能读取到EOF。

2. close与shutdown 的区别
(1) 
shutdown与socket描述符没有关系,即使调用shutdown(fd, SHUT_RDWR)也不会关闭fd,最终还需close(fd)。
(2) shutdown无论当前socket fd的reference值是否为0,都会对该socket执行主动关闭
(3)  在调用了SHUT_WR后再执行write操作, 会触发SIGPIPE,但可以对该fd正常read;close之后对当前process无论read&write都不允许;  

3. 半关闭的API支持
A:
write;
shutdown(WR)
read;
close;

B:
read;
write;
close;

4. 其它
blocked read:

n=read(fd, buf, MAXLINE)
n > 0,表示这次成功读到了多少个字节
若n < 0 errno = EINTR,代表数据没有读完,是中断导致的,需要再次读

n < 0 && errno != EINTR,则代表出错,比如收到RST,或其它各种错误
n=0,代表对方端发送FIN。 由于TCP是流式的,因此n=0仅会在对方开始关闭链接close或shutdown时才会出现

  • 1
    点赞
  • 7
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值