通讯流程总览
关于TCP协议的一些相关概念可以看我这篇博客网络基础
下图是基于TCP协议的客户端/服务器程序的一般流程:
下面我们结合TCP协议的通信流程,来初步认识一下三次握手和四次挥手,以及建立连接和断开连接与各个网络接口之间的对应关系。
TCP建立连接(三次握手)
情景联系:
第一次握手:A给B打电话说,你可以听到我说话吗?
第二次握手:B收到了A的信息,然后对A说:我可以听得到你说话啊,你能听到我说话吗?
第三次握手:A收到B信息,然后说可以的,我要给你发信息啦!
在三次握手之后,A和B都能确定这么一件事:我说的话,你能听到;你说的话,我也能听到。这样,就可以开始正常通信了。
当服务器完成套接字创建、绑定以及监听的初始化动作之后,就可以调用accept函数阻塞等待客户端发起请求连接了。
服务器初始化:
- 调用socket,创建文件描述符。
- 调用bind,将当前的文件描述符和IP/PORT绑定在一起,如果这个端口已经被其他进程占用了,就会bind失败。
- 调用listen,声明当前这个文件描述符作为一个服务器的文件描述符,为后面的accept做好准备。
- 调用accept,并阻塞,等待客户端连接到来。
TCP三次握手的流程
假设一开始客户端和服务端都处于CLOSED的状态。然后先是服务端主动监听某个端口,处于LISTEN状态;
【第一个报文】:客户端会随机初始化序列号(client_isn),将此序列号置于TCP首部的序列号字段中,同时将SYN标志位置为1,表示该报文为SYN报文。接下来就将第一个SYN报文发送给服务端,表示向服务端发起连接,该报文不包含应用层数据,之后客户端处于 SYN-SENT 状态。
【第二个报文】:服务端收到客户端的SYN报文后,首先服务端也随机初始化自己的序列号(server_isn),将此序号填入TCP首部的序号字段中,其次把TCP首部的确认应答号字段填入 client_isn + 1,接着把 SYN 和 ACK 标志位置为1。最后把该报文发给客户端,该报文也不包含应用层数据,之后服务端处于 SYN-RCVD 状态。
【第三个报文】:客户端收到服务端报文后,还要向服务端回应最后一个报文,首先应答报文 TCP 首部 ACK 标志位置为1,其次确认应答号字段填入 server_isn + 1,最后把报文发送给服务端,这次报文可以携带客户端到服务器的数据,之后客户端处于 ESTABLISHED 状态。
- 最后服务器收到客户端的应答报文后,也进入 ESTABLISHED 状态。
为什么是三次握手
为什么TCP连接确立需要三次握手,而不是两次?还是四次?这是一个经常能被问到的问题。接下来就几个方面分析为什么需要三次握手的原因:
避免历史连接(主要原因)
上面文字出自 RFC 793,三次握手的主要原因是为了防止旧的重复连接初始化造成混乱。
网络环境是比较复杂的,它不一定能保证我们先发送的数据包就一定能再我们期望的时间内送达,它可能半路 Poor Gay 了。也有可能超时后再抵达服务端,那么这时的TCP就会产生以下的一种情况:
如上图所示,如果一个SYN报文再超时后没有得到响应,客户端可能再次发送一个新的SYN请求,而这时旧的SYN请求可能比新的SYN请求先达到服务器。如果此时没有第三次连接来确认此次连接是否是历史连接的话,那么双方可能会建立两个链接?造成数据混乱。而如果是三次连接的话,客户端就有机会再去确认或者中止掉错误的连接,防止历史连接初始化了连接。
避免资源浪费
上面我们在介绍避免历史连接的时候,就已经提及到了如果没有三次握手,无法确认客户端收到了服务端发送的建立连接的 ACK 确认序号信号。
假如我们现在是两次握手,如果有网络阻塞等原因造成旧的连接SYN请求还没抵达服务端,就已经达到了超时时间,那么客户端就会再次发送请求SYN报文,之后如果两次请求都能达到服务端的话,由于服务端现在只有两次握手,无法确定当前的SYN就是客户端想要的连接,只能一收到 SYN 就返回一个 ACK 报文再建立起一个连接,这么做就会造成资源的浪费。看下图所示:
小结,不能使用两次握手和四次握手的原因:
两次握手:无法防止历史连接的建立,会造成双方资源的浪费,也无法可靠的同步双方序列号;
四次握手:三次握手就已经理论上最少可靠连接建立,所以不需要使用更多的通信次数。
TCP传输过程的控制
如何提高数据传输的可靠性
从上面我们聊TCP连接的建立过程,我们可以知道,当客户端的数据达到接收主机的时候,服务端主机会返回一个已收到消息的通知。这个消息就是前面提到的应答消息(ACK)。这个消息机制具体的实现就是,每次当接收端收到对端发送过来的消息时,都会将对端消息中的序列号+1,作为自己消息发送的应答号。
TCP通过肯定的确认应答(ACK)实现可靠的数据传输。当发送端将数据发送之后等待对端的确认应答。如果有确认应答,说明数据已经成功到达对端。反之,则数据丢失的可能性很大。当在一定时间内没有收到确认应答,发送端就认为数据已经丢失,那么就会进行重新发送。由此,即使产生了丢包,仍然能够保证数据能够到达对端,实现可靠传输。这个过程如下所示:
当然还有另外一种情况,就是主机B已经收到了数据,丢失确认应答的消息在传输过程中丢失,那么此时主机A在一段时间内没有收到确认应答消息,也会认为主机B没有收到消息,从而再发送一次,如下图:
这种情况在传输并不鲜见,如果一直收到主机A发送的重复数据,对于主机B来说,它必须去放弃一些重复的包,这就需要我们上面所提到的序列号了。根据序列号判断这个数据包先前是否收到过,如果收到过就放弃,如果没有收到就保留。序列号的生成有其独特算法,这里就不做赘述了。
超时重发如何确定
超时重传机制是用来确保TCP传输的可靠性的重要手段之一,我们在上面已经提及过多次:每次发送数据包时,发送的数据报都有seq号,接收端收到数据后,会回复 ACK 进行确认,表示某 seq 号数据已经收到。发送端在发送了某个seq包后,等待一段时间,如果没有收到对应的 ACK 回复,就认为该报文丢失,会重传这个数据包。中间等待的这段时间我们称为超时时间。那么如何来确定这个超时时间呢?
比较理想的方式就是定义一个固定值的最小时间,它能保证“确认应答一定能在这个时间内返回”。但是这样却有很大的弊端,因为在长距离的通信时(访问外网)时,延迟肯定就大,那么如果把值设置得太大,那么短距离的通信就很不友好了(隔了那么就才能知道这个包丢了)。如果值设置得小了,那么每次长距离访问都被判定为丢包???所以这样设置固定值是不可取的,需要根据网络的延迟,动态设置超时时间。
TCP终止连接(四次挥手)
四次挥手:
A:“喂,我不说了 (FIN)。”A->FIN_WAIT1
B:“我知道了(ACK)。等下,上一句还没说完。Balabala…..(传输数据)”B->CLOSE_WAIT | A->FIN_WAIT2
B:”好了,说完了,我也不说了(FIN)。”B->LAST_ACK
A:”我知道了(ACK)。”A->TIME_WAIT | B->CLOSED
A等待2MSL,保证B收到了消息,否则重说一次”我知道了”,A->CLOSED
这样,通过四次挥手,可以把该说的话都说完,并且A和B都知道自己没话说了,对方也没花说了,然后就挂掉电话(断开链接)了 。
TCP四次挥手的过程
- 现在客户端与服务端都处在连接建立的状态,假设此时客户端想要关闭连接;
- 【第一个报文】:客户端发送一个 FIN 报文,用来关闭客户端到服务端的数据传送,也就是客户端告诉被服务端:我已经不会再给你发数据了(当然,在 FIN 包之前发送出去的数据,如果没有收到对应的 ACK 报文,客户端依旧会重发这些数据),但此时客户端还可以接受数据;
- 【第二个报文】:服务端收到 FIN 报文后,发送一个 ACK 给对方,确认序号为收到序号 + 1,此时服务端进入 CLOSED_WAIT 状态。客户端接收到 ACK 报文后,进入 FIN_WAIT_2 状态;
- 【第三个报文】:服务端发送一个 FIN 报文,用来关闭被动关闭方到主动关闭方的数据传送,也就是告诉主动关闭方,我的数据也发送完了,不会再给你发数据了,接下来服务端进入 LAST_ACK 状态;
- 【第四个报文】:客户端收到 FIN 报文之后,发送一个 ACK 给服务端,确认应答号为收到序号 + 1,此时客户端进入 TIME_WAIT 状态;
- 服务端收到 FIN 报文后,就直接进入了 CLOSED 状态,连接资源将被释放,四次挥手到此结束;
- 客户端在经过 2MSL 时间后,自动进入CLOSED 状态,至此客户端也完成了连接的关闭。
为什么需要四次挥手
再来回顾下四次挥手双方发 FIN 包的过程,就能理解为什么需要四次了:
- 关闭连接时,客户端向服务端发送 FIN 时,仅仅表示客户端不再发送数据了但是还能接收数据。
- 服务器收到客户端的 FIN 报文时,先回一个 ACK 应答报文,而服务端可能还有数据需要处理和发送,等服务端不再发送数据时,才发送 FIN 报文给客户端来表示同意现在关闭连接。
从上面过程可知,服务端通常需要等待完成数据的发送和处理,所以服务端的 ACK 和 FIN 一般都会分开发送,从而比三次握手导致多了一次。