应用层如何强制发送RST报文进行断开连接
在TCP协议中,默认情况下,当我们调用close()函数关闭套接口时,TCP走四次挥手进行断开链路,但是要是若缓冲区还有数据未发送到对端时,系统将尝试把这些数据发送给对端。四次挥手的过程导致我们在TIMEWAIT状态下无法复用端口。有些情况下我们不需要TIMEWAIT,而是想快速断开连接,从而避免socket的堆积。
这个时候我们可以使用SOLINGER套接字选项
structlinger{
intlonoff;
intllinger;
}
1)若lonoff为0,表示关闭该选项。llinger值被忽略,也即是走TCP的默认设置。
2)若lonoff为非0且llinger为0,那么当close某个连接时TCP将终止该连接。也即是TCP将丢弃保留在套接字发送缓冲区中的任何数据,并发送RST报文给对端,不再走四次挥手,从而避免了TCP的TIMEWAIT状态。但是依然存在以下可能性:在2MSL秒内创建该连接的另一个化身,导致来自刚被终止的连接上的旧的重复分节被不正确的传递到新的化身上。
3)若lonoff为非0值且llinger也为非0值,那么当套接字关闭时内核将拖延一段时间关闭,也即是若在套接字的发送缓冲区中还有残留数据,那么进程将投入睡眠,直到数据发送完且均被对端确认或者滞留时间到。若套接字被设置成非阻塞型,那么它将不等待close完成,即是滞留时间不为0也是如此。当使用SOLINGER选项时,应用程序检查close的返回值很重要,因为若在数据发送完并被确认前延滞时间到的话,close将返回EWOULDBLOCK错误,且套接字发送缓冲区中的任何残留数据都被丢弃。
通过下面实现进行验证。
首先server端使用nc进行监听一个TCP指定端口。
客户端使用如下代码includesystypes。hincludesyssocket。hincludenetinetin。hincludeunistd。hincludestdlib。hincludestdio。hincludestring。hincludeerrno。hincludesystime。hincludesystypes。hincludesyssocket。hincludenetinetin。hincludenetinettcp。hintmain(intargc,charargv〔〕){structsockaddrinpeer;structlingerlinger;intret;intsocksocket(AFINET,SOCKSTREAM,0);memset(peer,0,sizeof(peer));peer。sinfamilyAFINET;inetpton(AFINET,argv〔1〕,peer。sinaddr);peer。sinporthtons(atoi(argv〔2〕));memset(linger,0,sizeof(linger));linger。lonoff1;linger。llinger0;retsetsockopt(sock,SOLSOCKET,SOLINGER,linger,sizeof(linger));if(ret){printf(Failtosetlinger);exit(1);}retconnect(sock,(conststructsockaddr)peer,sizeof(peer));if(ret){printf(Failtoconnect。,strerror(errno));exit(1);}printf(Connectsuccessfully);close(sock);printf(Done);return0;}
通过抓包分析来看,调用close后,客户端直接发送了RST报文端开了连接。
19:22:13。101476IP17。15。220。199localhost。localdomain:Flags〔S〕,seq12771346。。
19:22:13。101509IPlocalhost。localdomain17。15。220。199:Flags〔S。〕,seq1277234。。
19:22:13。101732IP17。15。220。199localhost。localdomain:Flags〔。〕,ack。。。
19:22:13。101912IP17。15。220。199localhost。localdomain:Flags〔R。〕。。。
在tcpclose中查看具体实现内核并并不关心有多少数据未被用户进程读取,内核关心的是有没有数据未被读取,若有数据未被读取而丢弃(datawasunread0),则给对方发送rst报文若没有数据未被用户进程读取,也即是全部数据都被用户进程读取了(datawasunread0),则相对对端发送fin报文if(datawasunread){Unreaddatawastossed,zaptheconnection。NETINCSTATSUSER(LINUXMIBTCPABORTONCLOSE);发送rst报文前设置状态为TCPCLOSE,这时没有TIMEWAIT状态,没有FINWAIT1状态,说明此时时不正常关闭的。所以可得,在编写程序时,在关闭连接前,一定要保证所有接收到的数据被读取,否则连接会不正常关闭tcpsetstate(sk,TCPCLOSE);发送rst报文,之所以不是fin报文,是因为关闭时还有未读的数据属于异常情况,fin表示一切正常情况tcpsendactivereset(sk,GFPKERNEL);}elseif(sockflag(sk,SOCKLINGER)!sksklingertime){Checkzerolingeraftercheckingforunreaddata。调用tcpdisconnect断开、删除并释放已建立连接但未被accept的传输控制块,同时删除并释放已接收在接收队列(包括失序队列)上的段以及发送队列上的段skskprotdisconnect(sk,0);tcpdisconnectNETINCSTATSUSER(LINUXMIBTCPABORTONDATA);}elseif(tcpclosestate(sk)){若未读字节数为0,则调用tcpclosestate根据sk当前状态来设置sk下一状态,比如当前状态为TCPESTABLISHED,则下一状态为TCPFINWAIT1,该方法的返回确定是否发送fin报文给对方
从上面的代码段可以看到,当有数据还未读取时,说明是异常关闭,直接发送RST报文给对端。若接收缓冲区中数据都已经读取完了,判断SOCKLINGER套接字选项,若llinger为0,则调用tcpdisconnect给对端发送RST报文,同时释放接收和发送队列上的数据。