旁白
今天面了下鹅场的后台技术开发,面试官问了我关于TCP连接协议的3次握手和断开连接协议的4次握手的问题。因为之前看过TCP的连接和断开处理,但是印象没有那么深刻。我想主要原因是一直重复简单的记忆,却没有深刻地理解。所以,在此将搜集一些这方面的资料整理一下,可以加深印象,同时也分享给有需要的博友。
声明
因为主要是整理这方面的资料,会涉及到一些博友的原创,所以,在文章最后会给原文链接。
正文
TCP概念
TCP(Transmission Control Protocol 传输控制协议)是一种面向连接的、可靠的、基于字节流的传输层通信协议。在简化的计算机网络OSI模型中,它完成第四层传输层所指定的功能,用户数据报协议(UDP)是同一层内另一个重要的传输协议。在因特网协议族(Internet protocol suite)中,TCP层是位于IP层之上,应用层之下的中间层。不同主机的应用层之间经常需要可靠的、像管道一样的连接,但是IP层不提供这样的流机制,而是提供不可靠的包交换。
应用层向TCP层发送用于网间传输的、用8位字节表示的数据流,然后TCP把数据流分区成适当长度的报文段(通常受该计算机连接的网络的数据链路层的最大传输单元(MTU)的限制)。之后TCP把结果包传给IP层,由它来通过网络将包传送给接收端实体的TCP层。TCP为了保证不发生丢包,就给每个包一个序号,同时序号也保证了传送到接收端实体的包的按序接收。然后接收端实体对已成功收到的包发回一个相应的确认(ACK);如果发送端实体在合理的往返时延(RTT)内未收到确认,那么对应的数据包就被假设为已丢失将会被进行重传。TCP用一个校验和函数来检验数据是否有错误;在发送和接收时都要计算校验和。
TCP功能
当应用层向TCP层发送用于网间传输的、用8位字节表示的数据流,TCP则把数据流分割成适当长度的报文段,最大传输段大小(MSS)通常受该计算机连接的网络的数据链路层的最大传送单元(MTU)限制。之后TCP把数据包传给IP层,由它来通过网络将包传送给接收端实体的TCP层。
TCP为了保证报文传输的可靠,就给每个包一个序号,同时序号也保证了传送到接收端实体的包的按序接收。然后接收端实体对已成功收到的字节发回一个相应的确认(ACK);如果发送端实体在合理的往返时延(RTT)内未收到确认,那么对应的数据(假设丢失了)将会被重传。
在拥塞控制上,采用广受好评的TCP拥塞控制算法(也称AIMD算法)。该算法主要包括三个主要部分:1)加性增、乘性减;2)慢启动;3)对超时事件做出反应。
TCP协议
tcp连接的状态 图1
建立连接协议-三次握手
在TCP/IP协议中,TCP协议提供可靠的连接服务,采用三次握手建立一个连接,如上图1所示。 (SYN包表示标志位syn=1,ACK包表示标志位ack=1,SYN+ACK包表示标志位syn=1,ack=1)
1)第一次握手:建立连接时,客户端A主动向服务器B发送SYN包(SEQ_NUMBER=j),并进入SYN_SEND状态,等待服务器B确认;
2)第二次握手:服务器B收到客户端A发送的SYN包,必须确认客户端A的SYN(ACK_NUMBER=j+1),同时自己也发送一个SYN包(SEQ_NUMBER=k)包,即SYN+ACK包,此时服务器进入SYN_RECV状态;
3)第三次握手:客户端A收到服务器B的SYN+ACK包,向服务器B发送确认包ACK(ACK_NUMBER=k+1),此包发送完毕,客户端A和服务器B进入ESTABLISHED状态,完成三次握手。
三次握手如下图2所示:
TCP三次握手建立连接 图2
断开连接-四次握手
断开连接不分客户端或者服务器了,任何一方都可以调用close或者closesocket之类的函数主动终止一个连接。我们假设,在正常情况下(客户端主动断开连接),主动一方调用close函数断开连接时的处理过程。我们知道TCP连接时全双工的(关于全双工,半双工,单工等通信方式在此不做讲述,请自搜索之。),因此每个方向都必须单独进行关闭。其原则是:客户端发送一个FIN来终止这个方向的连接,当服务器收到FIN报文时,会发送ACK确认报文。因为TCP是全双工的,可以想象一下TCP连接上有两条数据通路,当发送FIN报文时,表示发送FIN的一端就不能发送数据,也就是关闭了其中一条数据通路。被动关闭的一端发送ACK后,应用层通常会检测到这个连接即将断开,然后被动断开的应用层调用close关闭连接。
四次握手的进行顺序如下:
1)客户端A发送一个FIN,用来关闭client到服务器B的数据传送;
2)服务器B收到客户端发送的FIN,就发送一个ACK包,确认序号为收到的序号加1,和SYN一样,一个FIN占用一个序号;
3)服务器B关闭与客户端A的连接,发送一个FIN给客户端A;
4)客户端A发送一个ACK报文进行确认,并将确认序号设置为收到的序号加1.
TCP关闭连接的四次握手,如下图3所示:
TCP四次握手断开连接 图3
下面再用一个更加形象的图表示一下TCP断开连接的四次握手,如图4所示:
图4
- ESTABLISHED:表示连接已经建立了。
- FIN_WAIT_1: FIN_WAIT_1和FIN_WAIT_2状态的真正含义都是表示等待对方的FIN报文。而这两种状态的区别是:FIN_WAIT_1状态实际上是当SOCKET在ESTABLISHED状态时,它想主动关闭连接,向对方发送了FIN报文,此时该SOCKET即进入到FIN_WAIT_1状态。而当对方回应ACK报文后,则进入到FIN_WAIT_2状态,当然在实际的正常情况下,无论对方何种情况下,都应该马上回应ACK报文,所以FIN_WAIT_1状态一般是比较难见到的,而FIN_WAIT_2状态还有时常常可以用netstat看到。
- FIN_WAIT_2:上面已经详细解释了这种状态,实际上FIN_WAIT_2状态下的SOCKET,表示半连接,也即有一方要求close连接,但另外还告诉对方,我暂时还有点数据需要传送给你,稍后再关闭连接。
- TIME_WAIT:请参考下节中的TIME_WAIT的作用。
那么问题来了
1、TIME_WAIT的作用是什么?
TIME_WAIT的作用
1)可靠地实现TCP全双工连接的终止
在进行关闭连接时,最后的ACK是主动关闭(假设是客户端)的一方发出的,如果这个最终的ACK丢失,那么服务器将重发最终的FIN。因此客户端必须维护状态信息,允许服务端重新发送最终的ACK报文。如果不维持这个状态信息,那么客户端将响应RST分节,服务器将此分节解释成一个错误(在java中会抛出connection reset的SocketException)。因而,要实现TCP全双工连接的正常终止,必须处理终止序列四个分节中任何一个分节的丢失情况,主动关闭的客户端必须维持状态信息进入TIME_WAIT状态。
2)允许老的重复分节在网络中消逝
TCP分节可能由于路由器异常而“迷途”,在迷途期间,TCP发送端可能因确认超时而重发这个分节,迷途的分节在路由器修复后也会被送到最终目的地,这个原来的迷途分节就称为lost duplicate。在关闭一个TCP连接后,马上又重新建立起一个相同的IP地址和端口之间的TCP连接,后一个连接被称为前一个连接的化身(incarnation),那么有可能出现这种情况,前一个连接的迷途重复分组在前一个连接终止后出现,从而被误解成从属于新的化身。为了避免这个情况,TCP不允许处于TIME_WAIT状态的连接启动一个新的化身,因为TIME_WAIT状态持续2MSL,就可以保证当成功建立一个TCP连接的时候,来自连接先前化身的重复分组已经在网络中消逝。
TIME_WAIT的优缺点:
短时间的多次请求可能会导致服务器上存在很多的TIME_WAIT,那么导致端口被用尽,无法继续访问应用程序。
2、为什么建立连接协议是三次握手,而关闭连接却是四次握手呢?(面试官问我的问题,回答没有这个答案好)
这是因为服务端的LISTEN状态下的SOCKET当收到SYN报文的连接请求后,它可以把ACK和SYN(ACK表示应答,SYN表示同步)放在一个报文里来发送。但关闭连接时,当收到对方的FIN报文通知,它仅仅表示对方没有数据发送,但未必所有的数据都全部发送完了,有可能部分数据在缓冲区中或者网络延迟导致部分数据服务器没有接收到。所以你可能未必会马上会关闭SOCKET,也即你可能还需要发送一些数据给对方之后,再发送FIN报文给对方来表示你同意现在可以关闭连接了,所以它这里的ACK报文和FIN报文多数情况下都是分开发送的。
3、 为什么不能用两次握手进行连接?
我们知道,3次握手完成两个重要的功能,既要双方做好发送数据的准备工作(双方都知道彼此已准备好),也要允许双方就初始序列号进行协商,这个序列号在握手过程中被发送和确认。
现在把三次握手改成仅需要两次握手,死锁是可能发生的。作为例子,考虑计算机S和C之间的通信,假定C给S发送一个连接请求分组,S收到了这个分组,并发送了确认应答分组。按照两次握手的协定,S认为连接已经成功地建立了,可以开始发送数据分组。可是,C在S的应答分组在传输中被丢失的情况下,将不知道S是否已准备好,不知道S建立什么样的序列号,C甚至怀疑S是否收到自己的连接请求分组。在这种情况下,C认为连接还未建立成功,将忽略S发来的任何数据分组,只等待连接确认应答分组。而S在发出的分组超时后,重复发送同样的分组。这样就形成了死锁。