前言
这段时间面试官都挺忙的,频频出现在博客文章标题,虽然我不是特别想蹭热度,但是实在想不到好的标题了-。-,蹭蹭就蹭蹭 :)
事实上我在阿里面试的时候确实被问到了这个问题,HTTP、HTTPS、TCP/IP、Socket通信、三次握手四次挥手过程?当时虽然思路正确,可惜最终也并不算完全答对
结束后花了一段时间整理了下思路,参考和查阅了一下资料,整理如下:
来源:编程充电宝
作者:在所不辞
问题描述
http服务器返回你的请求就关闭了(主动关闭)
你能否讲解一下TCP
的三次握手与四次挥手呢?
面试官如果从整体到局部入手,那我们就先讲讲整个三次握手和四次挥手的过程,但不要忘记,讲的同时应该适当体现你对该知识点掌握的深度和广度,具体怎么说,我们后面慢慢道来。
三次握手
所谓的握手
即一次发包到接收的过程,可能从客户端发送到服务端,也可能从服务端发送到客户端。
过程描述
先上一张TCP报文结构
图,待会我们会回来看这张图:
TCP报文结构
先上三次握手的流程图:
三次握手
接下来我们来详细讲解下上图的过程:
- 客户主机发起连接请求,设置
SYN
标志位为1
,同时客户端随机
选择了一个初始序号client_isn
,并且存放在TCP报文
字段的序号
中,如下图:
第一次握手:SYN报文
- 接下来,当服务端接收到该报文后,会为其分配
TCP 缓存和变量
(这使得TCP容易受到被称为SYN 洪泛攻击
的拒绝服务攻击)紧接着,服务端会返回一个SYNACK 报文
到客户端,其中SYN
标志位为1
,确认号
设置为client_isn + 1
,并且选一个自己的初始序号server_isn
,并放置在序号
字段中,如下图:
第二次握手:SYNACK报文
- 当收到服务器发来的
SYNACK
报文段后,客户端也需要给该连接分配缓存和变量,然后再次发送一个确认报文给服务端,其中,SYN
标志位设置为0
,将确认号
设置为server_isn + 1
,另外,此次报文可以携带负载数据:
第三次握手:ACK报文
细节拓展
- 三次握手的状态转换图(建议达到能默写下来的熟练程度)
三次握手状态图
- 服务器为什么要使用特殊的初始序号
server_isn
?这为什么是必要的呢?
这个细节和问题深究第3题
是一致的,服务器使用特定的初始序列号 server_isn
(从源
和目的地IP
和端口
的散列
中获取)可以用来抵御SYN洪水攻击。具体为什么,以及什么是SYN 洪泛攻击
,问题深究部分我们会再详谈。
为了下文描述方便,我们将三次握手分别称为:SYN 报文
、SYNACK 报文
、ACK 报文
问题深究
1.为什么要三次握手而不是两次?
简单来说,三次握手的目的是为了让双方验证各自的接收能力和发送能力。
- 第一次握手,A 发送
SYN
到B
,B
接收到了后,能确认什么呢? 显然,B
能确认A
的发送
能力和B
的接收
能力; - 第二次握手,
B
发送SYNACK
到A
,A
接收到后,能确认什么呢?A
能确认B
的发送能力和A
自己的接收能力,此外,A
收到了SYNACK
,那么说明前面A
发的SYN
成功到达B
的手中,所以也能确认A
自己的发送
能力和B
的接收
能力;至此,A
已经确认了双方各自的发送能力和接收能力都是OK
的,因此转为ESTABLISHED
状态; - 第三次握手,
A
发送ACK
到B
,B
接收后,能确认什么呢?
直接的,B
能确认A
的发送
能力和B
的接收
能力,另外由于B
能收到ACK
说明前面发送的SYNACK
已经成功被接受了,说明能确认A
的接收
能力和B
的发送
能力。
如果使用两次握手,就不能确认上述所说的四种能力,那么就会导致问题。
假定不采用第三次报文握手,那么只要B发出确认,新的连接就建立了。
现假定一种异常情况,即A
发出的SYN
报文段并没有丢失,而是在某些网络节点长时间滞留了,以致延误到连接释放后的某个时间才到达B
。本来这是一个早已失效的报文段。但B
收到此失效的连接请求报文段后,却误以为是A
又发出一次新的连接请求,于是就向A
发出确认报文段,同意建立连接。
由于现在A
并没有发出建立连接的请求,因此不会理睬B
的确认,也不会向B
发送数据,但B
却以为新的运输连接已经建立了,并一直等待A
发来的数据。B
的许多资源就这样白白浪费了。
2.两个TCP建立请求相互之间同时发起时会发生什么?建立几个连接?
首先理解题意,我们知道三次握手
正是建立TCP连接
的过程,我们假设 A 是客户端,B 是服务端:
rfc793-同时启动
这里先给大家讲一下上图中一些符号的含义:
- 右箭头 (-->) :从 A 发送到 B 的 TCP 报文段,且 B 接收到了;
- 左箭头 (<--) :从 B 发送到 A 的 TCP 报文段,且 A 接收到了;
- 省略号 (…) :TCP 报文段仍在网络中(delayed);
- 丢失 ("XXX") :TCP 报文段丢失或者被拒绝。
- 注释会放在括号中;
- TCP 状态代表了处于中间的报文段到达之后的状态(AFTER);
- 报文段的内容只显示了序列号(SEQ)、控制符(CTL)和 ACK,其余内容被省略。
接下来我们详细来看看上图中,两个请求同时相互发起时,两个TCP
均会经历如下状态的转换:
同步请求的状态转换
上述的状态转换图可以跟前面的三次握手的转换图进行对比理解,同时发起的两个请求最终只会建立一个连接
。
SYN-RECEIVED
跟SYN-RCVD
是一样的,前者rcf793
中的描述方法,后者是《计算机网络-自顶向下方法》中的使用方法。
3. 客户端正在和服务端建立 TCP 连接,然而当服务器变 SYN-RCVD 后,此时一个旧的 SYN 报文 又到达了,服务器会如何处理?
其实这道题更加深挖了TCP 建立连接
的过程,我们可以在rfc793
中了解到详细信息。
rfc793-RST
从上图可以看到,第三行就是旧的SYN 连接
到达服务器时,第四行是服务器照常返回,第五行是客户端给服务端发送RST 报文
,将服务端重置为LISTEN
。
我们需要从上图了解到的一点是,服务端在SYN_RECEIVED
状态下,接收到旧的SYN 报文
时是不能作出判断的,而是照常返回,当客户端接收到该报文后发现异常,才会发送RST 报文
,重置连接。
关于RST 报文
,我一开始也很疑惑,直到看到rfc793 原文
:
rfc793-page33
确实说明了TCP B
不能检测这个旧的SYN 报文
是否正确,所以正常返回。而客户端收到会进行检测,发现是旧的报文,就会返回RST 报文
。
4.第三次握手失败了怎么办?
这个问题在网上找到的答案质量参差不齐,翻阅了rfc793
,仔细研究后,最终整理出以下答案:
首先考虑失败的情况:
ACK报文丢失导致第三次握手失败
当客户端收到服务端的SYNACK
应答后,其状态变为ESTABLISHED
,并会发送ACK
包给服务端,准备发送数据了。如果此时ACK
在网络中丢失(如上图所示),过了超时计时器后,那么服务端会重新发送SYNACK
包,重传次数根据/proc/sys/net/ipv4/tcp_synack_retries
来指定,默认是5
次。如果重传指定次数到了后,仍然未收到ACK
应答,那么一段时间后,Server
自动关闭这个连接。
问题就在这里,客户端已经认为连接建立,而服务端则可能处在SYN-RCVD
或者CLOSED
,接下来我们需要考虑这两种情况下服务端的应答:
- 服务端处于
CLOSED
,当接收到连接已经关闭的请求时,服务端会返回RST 报文
,客户端接收到后就会关闭连接,如果需要的话则会重连,那么那就是另一个三次握手了。 - 服务端处于
SYN-RCVD
,此时如果接收到正常的ACK 报文
,那么很好,连接恢复,继续传输数据;如果接收到写入数据等请求呢?注意了,此时写入数据等请求也是带着ACK 报文
的,实际上也能恢复连接,使服务器恢复到ESTABLISHED
状态,继续传输数据。
这个结论也可以在STACKFLOW
上找到验证:
What if a TCP handshake segment is lost?
上图圈住的部分:
总的来说,如果一个ACK 报文
丢失了,但它的下一个数据包没有丢失,那么连接正常,否则,连接会被重置。
5.知道SYN攻击吗?如何防范?
所谓SYN 洪泛攻击
,就是利用SYNACK 报文
的时候,服务器会为客户端请求分配缓存,那么黑客(攻击者),就可以使用一批虚假的ip
向服务器大量地发建立TCP 连接
的请求,服务器为这些虚假ip
分配了缓存后,处在SYN_RCVD
状态,存放在半连接队列
中;另外,服务器发送的请求又不可能得到回复(ip都是假的,能回复就有鬼了),只能不断地重发请求
,直到达到设定的时间/次数后,才会关闭。
服务器不断为这些半开连接
分配资源(但从未使用),导致服务器的连接资源被消耗殆尽,不过所幸,我们可以使用SYN Cookie
进行有效地防御。
所谓的SYN Cookie
防御系统,与前面接收到SYN 报文
就分配缓存不同,此时暂不分配资源;同时利用SYN 报文
的源
和目的地IP
和端口
,以及服务器存储的一个秘密数
,使用它们进行散列,得到server_isn
,然后附着在SYNACK 报文
中发送给客户端,接下来就是对ACK 报文
进行判断,如果其返回的ack
字段正好等于server_isn + 1
,说明这是一个合法的ACK
,那么服务器才会为其生成一个具有套接字的全开的连接。
SYN Cookie 防御
当然这种方案也有一定缺点
,最明显的就是服务器不保存连接的半开状态
,就丧失
了重发SYN-ACK消息
的能力,这一方面会降低正常用户的连接成功率,另一方面会导致某些情况下正常通信的双方会对连接是否成功打开产生误解,如客户端发给服务端的第三次握手消息(ACK
)半路遗失,客户端认为连接成功了,服务端认为没收到ACK
,连接没成功,这种情况就需要上层应用采取策略特别处理了。
6.(ISN)是固定的吗?
不固定,client_isn
是随机生成的,而server_isn
则需要根据SYN 报文
中的源、ip和端口
,加上服务器本身的密码数
进行相同的散列得到,显然这也不是固定的。
7.三次握手过程中可以携带数据吗?
讲过程的时候其实已经讲了,第三次握手是可以携带数据的
,而前两次不行。
8. 关于 https 的认证过程?
限于篇幅,此处暂时不讲,留意后续文章。
四次挥手
和握手
类似,每次挥手
也代表一次报文的发出和接收。
过程描述
因为自顶向上这本书里面的图比较简略,这里采用谢希仁版本的计算机网络中的图:
计算机网络-谢希仁-四次挥手
首先,当前客户端和服务器的状态都为ESTABLISHED
,接下来我们详细讲解下上图的过程:
- 客户主机发起连接释放的请求,设置
FIN
为1
,当然,序号seq
也会带上,这里假设为u
;发送完毕后,客户端进入FIN-WAIT-1
状态。
第一次挥手:FIN报文
- 服务端接收到
FIN 报文
后,会返回一个ACK 报文
回去,此时设置ACK
为1
,确认号
为u + 1
;表明自己接受到了客户端关闭连接的请求,但还没有准备好关闭连接。发送完毕后,服务器端进入CLOSE-WAIT
状态,客户端接收到这个确认包之后,进入FIN-WAIT-2
状态,等待服务器端关闭连接。
第二次挥手:ACK报文
- 服务器端准备好关闭连接时,向客户端发送结束连接请求,
FIN
置为1
;发送完毕后,服务器端进入LAST-ACK
状态,等待来自客户端的最后一个ACK
。
第三次挥手:FIN报文
- 客户端接收到服务端传来的
FIN 报文
后,知道服务器已经准备好关闭了,发送一个确认包,并进入TIME-WAIT
状态,等待可能出现的要求重传的ACK 报文
;服务器端接收到这个确认包之后,关闭连接,进入CLOSED
状态。
客户端等待了某个固定时间(两个最大段生命周期,2MSL
,2 Maximum Segment Lifetime)之后,没有收到服务器端的ACK
,认为服务器端已经正常关闭连接,于是自己也关闭连接,进入CLOSED
状态。
第四次挥手:ACK报文
这里我们再来看下rfc793
中关于四次挥手的简单例子:
rfc793-正常的关闭例子
细节拓展
- 四次挥手重要的是
TIME-WAIT
状态,为什么需要这个状态呢?
要确保服务器是否已经收到了我们的ACK 报文
,如果没有收到的话,服务器会重新发FIN 报文
给客户端,那么客户端再次收到FIN 报文
之后,就知道之前的 ACK 报文
丢失了,就会再次发送ACK 报文
。
问题深究
1.为什么握手只要三次,挥手却要四次?
关键就在中间两步。
- 建立连接时,当服务器收到客户端的
SYN 报文
后,可以直接发送SYNACK 报文
。其中ACK
是用来应答的,SYN
是用来同步的。 - 但是关闭连接时,当服务器收到
FIN 报文
时,很可能并不会立即关闭SOCKET
,所以只能先回复一个ACK 报文
,告诉客户端,“你发的FIN 报文
我收到了”。只有等到服务器所有的报文都发送/接收完了,我才能发送FIN 报文
,因此不能一起发送,需要四次握手。
2.为什么 TIME_WAIT 状态需要经过 2MSL 才能转换到 CLOSE 状态?
- 第一,为了保证客户端发送的最后一个
ACK 报文
能够到达服务器。我们必须假设网络是不可靠的,ACK 报文
可能丢失。如果服务端发出FIN 报文
后没有收到ACK 报文
,就会重发FIN 报文
,此时处于TIME-WAIT
状态的客户端就会重发ACK 报文
。当然,客户端也不能无限久的等待这个可能存在的FIN 报文
,因为如果服务端正常接收到了ACK 报文
后是不会再发FIN 报文
的。因此,客户端需要设置一个计时器,那么等待多久最合适呢?所谓的MSL
(Maximum Segment Lifetime)指一个报文在网络中最大的存活时间,2MSL就是一个发送和一个回复所需的最大时间。如果直到2MSL
时间后,客户端都没有再次收到FIN 报文
,那么客户端推断ACK 报文
已经被服务器成功接收,所以结束TCP 连接
。 - 第二,防止已失效的连接请求报文段出现在新的连接中。客户端在发送完最后一个
ACK 报文
后,再经过时间2MSL
,就可以使由于网络不通畅产生的滞留报文段失效。这样下一个新的连接中就不会出现旧的连接请求报文。