5
我们知道TCP通过三次握手建立可靠连接,通过四次挥手断开连接,TCP连接是比较昂贵的资源。为什么TCP需要通过三次握手才能建立可靠的连接?两次不行么?断开连接为什么需要四次?TCP连接昂贵在哪里?

三次握手与四次挥手.png

三次握手

客户端:“喂,听得到吗?”
服务端:“我能听到,你能听到我吗?”
客户端:“恩,能听到。”

为什么需要三次握手,对客户端而言,再收到服务端的ACK后,能确定我发的消息服务端能收到,服务端发的消息我也能收到了,那为什么还要第三次握手?这要从服务端考虑,服务端在接收到SYN后只能确定自己能收到客户端发来的消息,如果没有第三次握手,服务端是不确定对方是否能接收到自己这边发送的消息的,这种不确定势必影响到了信道的可靠性。既然三次就已经确保了信道的可靠性,如果在加一次肯定就增加了网络消耗从而影响了建立连接的效率。

四次挥手

客户端:“不说了,挂了吧。”
服务端:“OK!”
服务端:“你要注意身体啊!”
服务端:“拜拜!”
客户端:“拜拜!”

断开连接是释放资源的过程,还是从客户端和服务端两个人的角度去分析挥手过程。

首先建立连接是为了可靠的数据交付,现在连接建立已经有一段时间了,客户端说数据已经发完了,已经没什么要发送了,于是告诉操作系统,嘿,老兄,我数据已经发完了,你可以把我的发送资源释放啦,于是操作系统锁住了发送资源(比如发送队列)准备释放,并标记了TCP连接状态为FIN_WAIT_1,由于数据发送是双方的事情,客户端这边的发送资源已经释放,客户端有义务告知服务端这边的数据已经发送完毕,所以操作系统会发送一条FIN消息到服务端,告知服务端可以释放接收资源了,为了保证服务端确实收到了FIN消息并释放了接收资源,服务端也需要返回一条ACK消息给客户端,如果客户端没收到ACK消息,则重试刚刚的FIN消息。客户端一旦收到ACK消息,则说明服务端已经释放了接收资源,操作系统将TCP连接状态改为FIN_WAIT_2。到这里TCP连接已经关闭一半。

上面的过程只是结束了客户端的数据发送,释放了发送数据需要的资源,但是客户端依然可以接收从服务端发来的数据,服务端只是结束了数据接收并释放相关资源,依然可以放数据,因为服务端处理完接收的数据后要反馈结果给客户端。等结果反馈完后,没有数据要处理了,服务端也要结束发送过程,同样也得告知客户端让其释放接收数据所需要的资源。服务端重复上面的过程。但不同的是,客户端接收到FIN消息并返回ACK消息后需要等一段时间,这是由于担心服务端没有收到ACK又重发了FIN消息。等过了一段时间后并没有收到重发的消息,客户端就会释放所有资源(这里就不管服务端到底有没有收到ACK了,如果一直管下去就是个死循环)。服务端也是一样,重试多次以后也就释放了所有资源(这里不清楚到底是不是释放了资源,也有可能有其他机制)。

从上分析,安全可靠的断开连接至少需要四次,再多一次的意义不大。

昂贵的资源

上面分析可知,三次握手和四次挥手无疑会造成巨大的网络资源和CPU资源的消耗(如果连接没有被复用,这就是一种浪费),另外,客户端和服务端分别要维护各自的发送和接收缓存,各自在操作系统里面的变量(比如文件描述符,操作系统维护的一套数据结构),在阻塞式的网络模型中,服务端还要开启线程来处理这条连接。所以说TCP连接是比较昂贵的资源,需要连接池这种技术来提高它的复用性。

TCP连接的异常断开

以上都是在理想的情况下发生的,理想状态下,一个TCP连接可以被长期保持。但是现实总是很骨感,在保持TCP连接的过程中很可能出现各种意外的情况,比如网络故障,客户端崩溃或者异常重启,在这种情况下,如果服务端没有及时清理这些连接,服务端将发生连接泄露,直至服务端资源耗尽拒绝提供服务(connection refused exception)。因此在实际应用中,服务器端需要采取相应的方法来探测TCP连接是否已经断连。探测的原理就是心跳机制,可以是应用层面的心跳,也可以是第三方的心跳,但是绝大部分类Unix系统均在TCP中提供了相应的心跳检测功能(虽然并不是TCP规范中的一部分)。

客户端程序崩溃或异常退出

当客户端程序因未知原因崩溃或异常退出后,操作系统会给服务端发送一条RST消息,阻塞模型下,服务端内核无法主动通知应用层出错,只有应用层主动调用read()或者write()这样的IO系统调用时,内核才会利用出错来通知应用层对端RSTLinux系统报Connection reset by peer)。非阻塞模型下,服务端select或者epoll会返回sockfd可读,应用层对其进行读取时,read()会报错RST

哪些情况下,会收到来自对端的RST消息呢。

  1. connect一个不存在的端口,客户端会收到一条RST,报错Connection refused
  2. 程序崩溃或异常退出,会向对端发送。
  3. 对端断电重启,send数据时会收到来自对端的RST
  4. close(sockfd)时,直接丢弃接收缓冲区未读取的数据,并给对方发一个RST。这个是由SO_LINGER选项来控制的;

TCP socket在任何状态下,只要收到RST包,即可释放连接资源。

客户端断电或网络异常

如果客户端断电或网络异常,并且连接通道内没有任何数据交互,服务端是感知不到客户端掉线的,此时需要借助心跳机制来感知这种状况,一般的做法是,服务端往对端发送一个心跳包并启动一个超时定时器,如果能正确收到对端的回应,说明在线,如果超时,可以进行一系列操作,比如重试、关闭连接等等。

keep alive or heart beart

借鉴一下大神的文章

很多人都知道TCP并不会去主动检测连接的丢失,这意味着,如果双方不产生交互,那么如果网络断了或者有一方机器崩溃,另外一方将永远不知道连接已经不可用了。检测连接是否丢失的方法大致有两种:keepaliveheart-beat

Keepalive是很多的TCP实现提供的一种机制,它允许连接在空闲的时候双方会发送一些特殊的数据段,并通过响应与否来判断连接是否还存活着(所谓keep~~alive)。我曾经写过一篇关于keepalive的blog ,但后来我也发现,其实keepalive在实际的应用中并不常见。为何如此?这得归结于keepalive设计的初衷。Keepalive适用于清除死亡时间比较长的连接。
比如这样的场景:一个用户创建tcp连接访问了一个web服务器,当用户完成他执行的操作后,很粗暴的直接拨了网线。这种情况下,这个tcp连接已经断开了,但是web服务器并不知道,它会依然守护着这个连接。如果web server设置了keepalive,那么它就能够在用户断开网线的大概几个小时以后,确认这个连接已经中断,然后丢弃此连接,回收资源。
采用keepalive,它会先要求此连接一定时间没有活动(一般是几个小时),然后发出数据段,经过多次尝试后(每次尝试之间也有时间间隔),如果仍没有响应,则判断连接中断。可想而知,整个周期需要很长的时间。
所以,如前面的场景那样,需要一种方法能够清除和回收那些在系统不知情的情况下死去了很久的连接,keepalive是非常好的选择。
但是,在大部分情况下,特别是分布式环境中,我们需要的是一个能够快速或者实时监控连接状态的机制,这里,heart-beat才是更加合适的方案。
Heart-beat(心跳),按我的理解,它的原理和keepalive非常类似,都是发送一个信号给对方,如果多次发送都没有响应的话,则判断连接中断。它们的不同点在于,keepalivetcp实现中内建的机制,是在创建tcp连接时通过设置参数启动keepalive机制;而heart-beat则需要在tcp之上的应用层实现。一个简单的heart-beat实现一般测试连接是否中断采用的时间间隔都比较短,可以很快的决定连接是否中断。并且,由于是在应用层实现,因为可以自行决定当判断连接中断后应该采取的行为,而keepalive在判断连接失败后只会将连接丢弃。
关于heart-beat,一个非常有趣的问题是,应该在传输真正数据的连接中发送心跳信号,还是可以专门创建一个发送“心跳”信号的连接。比如说,AB两台机器之间通过连接m来传输数据,现在为了能够检测AB之间的连接状态,我们是应该在连接m中传输心跳信号,还是创建新的连接n来专门传输心跳呢?我个人认为两者皆可。如果担心的是端到端的连接状态,那么就直接在该条连接中实现心跳。但很多时候,关注的是网络状况和两台主机间的连接状态,这种情况下, 创建专门的心跳连接也未尝不可。

Socket感知连接断开

正常情况

客户端正常关闭连接:

//发送FIN消息,说明客户端已经没有数据发送,服务端read时会返回-1或者null
socket.shutdownOutput();
//默认的SO_LINGER参数,客户端发送FIN消息,服务端read时会返回-1或者null
socket.close();
//设置了立即关闭,客户端发送RST消息,服务端`read`时会报`connection rest by peer`。
socket.close();

非正常情况

  • 客户端程序崩溃或异常退出:服务端read时会报connection rest by peer
  • 断电重启:服务端发送心跳信息时,会收到客户端的RST消息,调用read时会报connection rest by peer
  • 断电或网络中断:服务端发送心跳信息后超时。

临风
1.3k 声望24 粉丝