写在前面
◆ ◆ ◆ ◆
有一次去浦东的一家互联网公司面试java后端开发。当天下午聊了技术、部门经理、HR,然后让回去等通知。当天晚上HR打电话过来聊薪资,巴拉巴拉说了一堆什么公司制度啦,这个年限只能匹配这个薪资,给的薪水比预先的低了2K,于是也直接跟HR说了达不到预期,不想考虑。
过了三天HR说要不再来公司试一下,公司来了个从阿里挖来的CTO,聊得好了说不定可以提薪。于是又去面试了一下,java基础,Spring框架,JVM,优化,项目问了一堆,情况还行。
最后他问了一个开放性问题,说让我思考十分钟,然后再回答。但是小明同学立马就回答了。回答结束了,他把HR叫进来,说了一句“在原先给他的薪资基础上再涨3K……”
最后,小明同学也就入职了这家公司……
问题
◆ ◆ ◆ ◆
题目是:想象在一条宽阔的大河两边有两支军队,河中心有一个岛屿,上面有一群反贼。两支军队只有在同时进攻,才能拿下反贼。但是军队都没有任何交流方式,只能派士兵去对面报信。士兵过河一定要经过岛屿,可能就会被杀死。要如何保证两支军队能够同时进攻岛屿?
分析
◆ ◆ ◆ ◆
经过分析,其实就是一个可靠消息传递的问题。两端需要及时通信,并且保证可靠传输。士兵过河被杀死的过程,可以理解为数据丢失过程。要保证两端建立可靠消传输……这不就是TCP三次握手嘛!
连接三次握手
◆ ◆ ◆ ◆
在TCP/IP协议中,TCP协议提供可靠的连接服务,采用三次握手建立一个连接: 第一次握手:建立连接时,客户端发送syn包(syn=j)到服务器,并进入SYN_SEND状态,等待服务器确认;(SYN:同步序列编号) 第二次握手:服务器收到syn包,必须确认客户的SYN(ack=j+1),同时自己也发送一个SYN包(syn=k),即SYN+ACK包,此时服务器进入SYN_RECV状态; 第三次握手:客户端收到服务器的SYN+ACK包,向服务器发送确认包ACK(ack=k+1),此包发送完毕,客户端和服务器进入ESTABLISHED状态,完成三次握手;
完成三次握手,客户端与服务器开始传送数据;
在此期间,若规定时间内没有收到回复,则重新发送数据包。
过程如下图:
断开四次挥手
◆ ◆ ◆ ◆
由于TCP连接时全双工的,因此,每个方向都必须要单独进行关闭,这一原则是当一方完成数据发送任务后,发送一个FIN来终止这一方向的连接,收到一个FIN只是意味着这一方向上没有数据流动了,即不会再收到数据了,但是在这个TCP连接上仍然能够发送数据,直到这一方向也发送了FIN。首先进行关闭的一方将执行主动关闭,而另一方则执行被动关闭:
第一次挥手:Client发送一个FIN,用来关闭Client到Server的数据传送,Client进入FIN_WAIT_1状态。 第二次挥手:Server收到FIN后,发送一个ACK给Client,确认序号为收到序号+1(与SYN相同,一个FIN占用一个序号),Server进入CLOSE_WAIT状态。 第三次挥手:Server发送一个FIN,用来关闭Server到Client的数据传送,Server进入LAST_ACK状态。 第四次挥手:Client收到FIN后,Client进入TIME_WAIT状态,接着发送一个ACK给Server,确认序号为收到序号+1,Server进入CLOSED状态,完成四次挥手。
简单的理解为:
1、C说:我数据发完了,要准备断开啦 2、S说:行,我知道你发完了 3、S说:数据我收完了,可以断开了 4、C说:好的,断开连接
实际中还会出现同时发起主动关闭的情况,如下图:
关注点
◆ ◆ ◆ ◆
以上只是基本原理,另外还有一些关注点,也要注意下:
因为当Server端收到Client端的SYN连接请求报文后,可以直接发送SYN+ACK报文。其中ACK报文是用来应答的,SYN报文是用来同步的。但是关闭连接时,当Server端收到FIN报文时,很可能并不会立即关闭Socket,所以只能先回复一个ACK报文,告诉Client端,"你发的FIN报文我收到了"。只有等到我Server端所有的报文都发送完了,我才能发送FIN报文,因此不能一起发送。故需要四步握手。
2.为什么不能用两次握手进行连接?
三次握手完成两个重要的功能,既要双方做好发送数据的准备工作(双方都知道彼此已准备好),也要允许双方就初始序列号进行协商,这个序列号在握手过程中被发送和确认。
现在把三次握手改成仅需要两次握手,死锁是可能发生的。作为例子,考虑计算机S和C之间的通信,假定C给S发送一个连接请求分组,S收到了这个分组,并发送了确认应答分组。按照两次握手的协定,S认为连接已经成功地建立了,可以开始发送数据分组。可是,S的应答分组在传输中被丢失的情况下,将不知道S 是否已准备好,不知道S建立什么样的序列号,C甚至怀疑S是否收到自己的连接请求分组。在这种情况下,C认为连接还未建立成功,将忽略S发来的任何数据分组,只等待连接确认应答分组。而S在发出的分组超时后,重复发送同样的分组。这样就形成了死锁。