电子说
在 TCP 协议中,默认情况下,当我们调用 close() 函数关闭套接口时,TCP 走四次挥手进行断开链路,但是要是若缓冲区还有数据未发送到对端时,系统将尝试把这些数据发送给对端。四次挥手的过程导致我们在 TIME_WAIT 状态下无法复用端口。有些情况下我们不需要 TIME_WAIT, 而是想快速断开连接,从而避免 socket 的堆积。
这个时候我们可以使用 SO_LINGER 套接字选项
struct linger {
int l_onoff;
int l_linger;
}
2)若 l_onoff 为非 0 且 l_linger 为 0,那么当 close 某个连接时 TCP 将终止该连接。也即是TCP将丢弃保留在套接字发送缓冲区中的任何数据,并发送RST报文给对端,不再走四次挥手,从而避免了 TCP 的 TIME_WAIT 状态。但是依然存在以下可能性:在 2 MSL 秒内创建该连接的另一个化身,导致来自刚被终止的连接上的旧的重复分节被不正确的传递到新的化身上。
3)若 l_onoff 为非 0 值且 l_linger 也为非 0 值,那么当套接字关闭时内核将拖延一段时间关闭,也即是若在套接字的发送缓冲区中还有残留数据,那么进程将投入睡眠,直到数据发送完且均被对端确认或者滞留时间到。若套接字被设置成非阻塞型,那么它将不等待 close 完成,即是滞留时间不为 0 也是如此。当使用 SO_LINGER 选项时,应用程序检查 close 的返回值很重要,因为若在数据发送完并被确认前延滞时间到的话,close 将返回 EWOULDBLOCK 错误,且套接字发送缓冲区中的任何残留数据都被丢弃。
通过下面实现进行验证。
首先 server 端使用 nc 进行监听一个TCP 指定端口。
客户端使用如下代码
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
int main(int argc, char *argv[])
{
struct sockaddr_in peer;
struct linger linger;
int ret;
int sock = socket(AF_INET, SOCK_STREAM, 0);
memset(&peer, 0, sizeof(peer));
peer.sin_family = AF_INET;
inet_pton(AF_INET, argv[1], &peer.sin_addr);
peer.sin_port = htons(atoi(argv[2]));
memset(&linger, 0, sizeof(linger));
linger.l_onoff = 1;
linger.l_linger = 0;
ret = setsockopt(sock, SOL_SOCKET, SO_LINGER, &linger, sizeof(linger));
if (ret) {
printf("Fail to set linger\\n");
exit(1);
}
ret = connect(sock, (const struct sockaddr *)&peer, sizeof(peer));
if (ret) {
printf("Fail to connect.\\n", strerror(errno));
exit(1);
}
printf("Connect successfully\\n");
close(sock);
printf("Done\\n");
return 0;
}
通过抓包分析来看,调用 close 后,客户端直接发送了 RST 报文端开了连接。
19:22:13.101476 IP 17.15.220.199 > localhost.localdomain : Flags [S], seq 12771346 ..
19:22:13.101509 IP localhost.localdomain > 17.15.220.199 : Flags [S .], seq 1277234 ..
19:22:13.101732 IP 17.15.220.199 > localhost.localdomain : Flags [.], ack ...
19:22:13.101912 IP 17.15.220.199 > localhost.localdomain : Flags [R .] ...
在 tcp_close 中查看具体实现
/*
内核并并不关心有多少数据未被用户进程读取,内核关心的是有没有数据未被读取,
若有数据未被读取而丢弃(data_was_unread>0),则给对方发送rst报文
若没有数据未被用户进程读取,也即是全部数据都被用户进程读取了(data_was_unread==0),则相对对端发送fin报文
*/
if (data_was_unread) {
/* Unread data was tossed, zap the connection. */
NET_INC_STATS_USER(LINUX_MIB_TCPABORTONCLOSE);
/*发送rst报文前设置状态为TCP_CLOSE,这时没有TIME_WAIT状态,没有FIN_WAIT_1状态,说明此时时不正常关闭的。
所以可得,在编写程序时,在关闭连接前,一定要保证所有接收到的数据被读取,否则连接会不正常关闭*/
tcp_set_state(sk, TCP_CLOSE);
//发送rst报文,之所以不是fin报文,是因为关闭时还有未读的数据属于异常情况,fin表示一切正常情况
tcp_send_active_reset(sk, GFP_KERNEL);
} else if (sock_flag(sk, SOCK_LINGER) && !sk->sk_lingertime) {
/* Check zero linger _after_ checking for unread data. */
/*调用tcp_disconnect断开、删除并释放已建立连接但未被accept的传输控制块,同时
删除并释放已接收在接收队列(包括失序队列)上的段以及发送队列上的段*/
sk->sk_prot->disconnect(sk, 0);// tcp_disconnect
NET_INC_STATS_USER(LINUX_MIB_TCPABORTONDATA);
} else if (tcp_close_state(sk)) { //若未读字节数为0,则调用tcp_close_state根据sk当前状态来设置sk下一状态,比如当前状态为TCP_ESTABLISHED,则下一状态为TCP_FIN_WAIT1,该方法的返回确定是否发送fin报文给对方
/*
从上面的代码段可以看到,当有数据还未读取时,说明是异常关闭,直接发送 RST 报文给对端。若接收缓冲区中数据都已经读取完了,判断 SOCK_LINGER 套接字选项,若 l_linger 为 0,则调用 tcp_disconnect 给对端发送 RST 报文,同时释放接收和发送队列上的数据。
审核编辑:刘清
全部0条评论
快来发表一下你的评论吧 !