之前讲了TCP与UDP的各自特点以及TCP是如何解决丢包失序等一系列问题的(浅谈网络中的分层与两个主要的协议:TCP和UDP协议),今天就来讲一讲TCP建立连接的三次握手与断开连接的四次挥手。
TCP建立连接:三次握手
先来看TCP三次握手的时序图:
结合这张图,我们可以看到TCP三次挥手的详细步骤(左边为客户端,右边为服务端):
- 服务端首先主动监听某个端口,进入LISTEN状态。
- 客户端会发送一个SYN包,并指定客户端发包的初始序号为x(seq=x),并进入SYN-SENT状态,等待服务端的ACK包。
- 服务端会发送一个SYN+ACK包,并指定服务端发包的初始序号为y(seq=y),并进入SYN-RCVD状态,等待客户端的ACK。
- 当SYN-SENT状态的客户端收到服务端针对其SYN包的ACK后,并对服务端同时发过来的SYN回复一个ACK,这时它就认为连接已经确立了,进入了ESTABLISHED状态。
- 当SYN-ACK状态的服务端收到了ACK之后,也确立了连接,进入了ESTABLISHED状态。 以上便是我们常说的TCP的三次握手了,相信刚看完该过程的小伙伴都会有这样的疑惑:为什么是三次,而不是两次,也不是四次五次呢?
问题:为什么TCP是三次握手,而不是两次,或是四次五次呢?
我觉得这就体现了TCP是一个十分“平等”的协议了,如果是两次握手,那么我们就可以看到,只有客户端发出的SYN包得到了ACK,而服务端也就只是回复了一下,并没有人对他进行回复。也就是说,客户端通过发出一条SYN信息并接收到ACK才可以确认连接成立,而服务端却只是发出了一个包并没有得到任何回复,就要让它确立连接成立,这显然是不对等的! 爱情还强调双向奔赴呢,代码的世界当然也要讲究付出有回应啊!万一客户端在发出一个SYN包之后跑路了,而这对于服务端来说是未知的,而后它又傻乎乎的建立了连接,浪费了资源。所以TCP规定要三次握手,这样对双方来说都能够“付出有回应”。
那又有人要问了,为什么不是四次五次握手呢?四次五次握手当然是OK的,甚至几百上千次握手只要你愿意也是行得通的,但这样也就别建立连接了,一直握手吧。三次握手是最合适的次数了,因为它确保了双方都能发出一个SYN包并得到回复,四次五次更多次握手按照信息论来讲也不会使连接建立成功的概率加大多少,所以三次握手是足够的了。
TCP断开连接:四次挥手
TCP四次挥手时序图:
照例先梳理一下四次挥手的详细步骤:
- 客户端主动发起结束请求,发送一个FIN包,并进入FIN-WAIT-1状态。
- 服务端接收到FIN包,回复一个ACK包,并进入CLOSED-WAIT状态,这个时候服务器B处于半关闭状态,还可以发数据,但不会再接收到数据了。
- 客户端收到ACK包,进入FIN-WAIT-2状态。
- 服务端想结束了连接了,主动发送一个FIN+ACK包,并进入LAST-ACK状态
- 客户端接收到了FIN+ACK包,回复一个ACK包,进入TIME-WAIT状态,等待 2 MSL(MSL:最大报文生存时间)后进入CLOSED状态
- 服务端在收到ACK之后,进入CLOSED状态
那么一样的,我们还是有几个针对该过程的疑问。
为什么客户端要两个FIN-WAIT状态呢?
我们再用上述所说的“平等观念”来看就解释得通了,一个TCP连接是端到端的,双方都是平等的关系,怎么能够单方面结束连接呢?离婚还得双方都签离婚协议书不是吗?所以客户端发了一个结束连接的请求,并得到服务端“知道了”的ACK之后,进入FIN-WAIT-2状态,这个时候等待的便是服务端处理完了它的事情,也发出一个“想结束”的请求,双方都想结束,这个连接才可以结束。当然,如果这个时候服务端直接跑路,客户端是不知道的,他就永远会在这边等,TCP是没有对这个状态的处理的,不过一些操作系统是有的,如Linux可以设置tcp_fin_timeout参数来对该状态设置最大等待时间。
为什么客户端最后要有一个TIME-WAIT状态,并时间为2 MSL呢?
首先,这个TIME-WAIT状态是为了确保服务端能够接收到最后一个客户端发出的ACK的,若是服务端没收到该ACK,会重发FIN+ACK包,这个时候处于TIME-WAIT便能意识到该重发ACK了,重发后重新设置已经等待的时间为0。了解了该状态的作用便能够知道为什么是时间为2 MSL了,他是由刚发出ACK包最大报文生存时间加上服务端可能会重发的FIN+ACK包的最大报文生存时间得到的,能够确保它收到服务端重发的包或者确保服务端已经关闭了。
最后附上著名TCP状态机图
结合时序图来看能有更加深刻的认识!