针对面试,知道往往是不够的,重要的是你能否在面试的场景下,利用你的回答突显你的优点,刻意练习+费曼技巧,助你拿到大厂 offer,后续的文章中我也会讲述我如何在大二就进入腾讯实习。
你能否讲解一下TCP
的三次握手与四次挥手呢?
面试官如果从整体到局部入手,那我们就先讲讲整个三次握手和四次挥手的过程,但不要忘记,讲的同时应该适当体现你对该知识点掌握的深度和广度,具体怎么说,我们后面慢慢道来。
所谓的握手
即一次发包到接收的过程,可能从客户端发送到服务端,也可能从服务端发送到客户端。
先上一张TCP报文结构
图,待会我们会回来看这张图:
先上三次握手的流程图:
接下来我们来详细讲解下上图的过程:
客户主机发起连接请求,设置SYN
标志位为1
,同时客户端随机
选择了一个初始序号client_isn
,并且存放在TCP报文
字段的序号
中,如下图:
接下来,当服务端接收到该报文后,会为其分配TCP 缓存和变量
(这使得TCP容易受到被称为SYN 洪泛攻击
的拒绝服务攻击)紧接着,服务端会返回一个SYNACK 报文
到客户端,其中SYN
标志位为1
,确认号
设置为client_isn + 1
,并且选一个自己的初始序号server_isn
,并放置在序号
字段中,如下图:
当收到服务器发来的SYNACK
报文段后,客户端也需要给该连接分配缓存和变量,然后再次发送一个确认报文给服务端,其中,SYN
标志位设置为0
,将确认号
设置为server_isn + 1
,另外,此次报文可以携带负载数据:
这个细节和问题深究第3题
是一致的,服务器使用特定的初始序列号 server_isn
(从源
和目的地IP
和端口
的散列
中获取)可以用来抵御SYN洪水攻击。具体为什么,以及什么是SYN 洪泛攻击
,问题深究部分我们会再详谈。
为了下文描述方便,我们将三次握手分别称为:
SYN 报文
、SYNACK 报文
、ACK 报文
简单来说,三次握手的目的是为了让双方验证各自的接收能力和发送能力。
第一次握手,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
的许多资源就这样白白浪费了。
首先理解题意,我们知道三次握手
正是建立TCP连接
的过程,我们假设 A 是客户端,B 是服务端:
这里先给大家讲一下上图中一些符号的含义:
接下来我们详细来看看上图中,两个请求同时相互发起时,两个TCP
均会经历如下状态的转换:
上述的状态转换图可以跟前面的三次握手的转换图进行对比理解,同时发起的两个请求最终只会建立一个连接
。
SYN-RECEIVED
跟SYN-RCVD
是一样的,前者rcf793
中的描述方法,后者是《计算机网络-自顶向下方法》中的使用方法。
TCP 连接
,然而当服务器变为SYN-RCVD
后,此时一个旧的SYN 报文
又到达了,服务器会如何处理?其实这道题更加深挖了TCP 建立连接
的过程,我们可以在rfc793
中了解到详细信息。
从上图可以看到,第三行就是旧的SYN 连接
到达服务器时,第四行是服务器照常返回,第五行是客户端给服务端发送RST 报文
,将服务端重置为LISTEN
。
我们需要从上图了解到的一点是,服务端在SYN_RECEIVED
状态下,接收到旧的SYN 报文
时是不能作出判断的,而是照常返回,当客户端接收到该报文后发现异常,才会发送RST 报文
,重置连接。
关于RST 报文
,我一开始也很疑惑,直到看到rfc793 原文
:
确实说明了TCP B
不能检测这个旧的SYN 报文
是否正确,所以正常返回。而客户端收到会进行检测,发现是旧的报文,就会返回RST 报文
。
这个问题在网上找到的答案质量参差不齐,翻阅了rfc793
,仔细研究后,最终整理出以下答案:
首先考虑失败的情况:
当客户端收到服务端的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
上找到验证:
上图圈住的部分:
总的来说,如果一个
ACK 报文
丢失了,但它的下一个数据包没有丢失,那么连接正常,否则,连接会被重置。
所谓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-ACK消息
的能力,这一方面会降低正常用户的连接成功率,另一方面会导致某些情况下正常通信的双方会对连接是否成功打开产生误解,如客户端发给服务端的第三次握手消息(ACK
)半路遗失,客户端认为连接成功了,服务端认为没收到ACK
,连接没成功,这种情况就需要上层应用采取策略特别处理了。
不固定,client_isn
是随机生成的,而server_isn
则需要根据SYN 报文
中的源、ip和端口
,加上服务器本身的密码数
进行相同的散列得到,显然这也不是固定的。
讲过程的时候其实已经讲了,第三次握手是可以携带数据的
,而前两次不行。
限于篇幅,此处暂时不讲,留意后续文章。欢迎大家关注我的公众号【编程充电宝】,你的支持,是我持续输出的最大动力~
和握手
类似,每次挥手
也代表一次报文的发出和接收。
因为自顶向上这本书里面的图比较简略,这里采用谢希仁版本的计算机网络中的图:
首先,当前客户端和服务器的状态都为ESTABLISHED
,接下来我们详细讲解下上图的过程:
客户主机发起连接释放的请求,设置FIN
为1
,当然,序号seq
也会带上,这里假设为u
;发送完毕后,客户端进入 FIN-WAIT-1
状态。
服务端接收到FIN 报文
后,会返回一个ACK 报文
回去,此时设置ACK
为1
,确认号
为u + 1
;表明自己接受到了客户端关闭连接的请求,但还没有准备好关闭连接。发送完毕后,服务器端进入 CLOSE-WAIT
状态,客户端接收到这个确认包之后,进入 FIN-WAIT-2
状态,等待服务器端关闭连接。
服务器端准备好关闭连接时,向客户端发送结束连接请求,FIN
置为1
;发送完毕后,服务器端进入 LAST-ACK
状态,等待来自客户端的最后一个ACK
。
客户端接收到服务端传来的FIN 报文
后,知道服务器已经准备好关闭了,发送一个确认包,并进入 TIME-WAIT
状态,等待可能出现的要求重传的ACK 报文
;服务器端接收到这个确认包之后,关闭连接,进入 CLOSED
状态。
客户端等待了某个固定时间(两个最大段生命周期,2MSL
,2 Maximum Segment Lifetime)之后,没有收到服务器端的 ACK
,认为服务器端已经正常关闭连接,于是自己也关闭连接,进入 CLOSED
状态。
这里我们再来看下rfc793
中关于四次挥手的简单例子:
TIME-WAIT
状态,为什么需要这个状态呢?要确保服务器是否已经收到了我们的ACK 报文
,如果没有收到的话,服务器会重新发FIN 报文
给客户端,那么客户端再次收到FIN 报文
之后,就知道之前的 ACK 报文
丢失了,就会再次发送ACK 报文
。
关键就在中间两步。
SYN 报文
后,可以直接发送SYNACK 报文
。其中ACK
是用来应答的,SYN
是用来同步的。FIN 报文
时,很可能并不会立即关闭SOCKET
,所以只能先回复一个ACK 报文
,告诉客户端,“你发的FIN 报文
我收到了”。只有等到服务器所有的报文都发送/接收完了,我才能发送FIN 报文
,因此不能一起发送,需要四次握手。ACK 报文
能够到达服务器。我们必须假设网络是不可靠的,ACK 报文
可能丢失。如果服务端发出FIN 报文
后没有收到ACK 报文
,就会重发FIN 报文
,此时处于TIME-WAIT
状态的客户端就会重发ACK 报文
。当然,客户端也不能无限久的等待这个可能存在的FIN 报文
,因为如果服务端正常接收到了ACK 报文
后是不会再发FIN 报文
的。因此,客户端需要设置一个计时器,那么等待多久最合适呢?所谓的MSL
(Maximum Segment Lifetime)指一个报文在网络中最大的存活时间,2MSL就是一个发送和一个回复所需的最大时间。如果直到2MSL
时间后,客户端都没有再次收到FIN 报文
,那么客户端推断ACK 报文
已经被服务器成功接收,所以结束TCP 连接
。ACK 报文
后,再经过时间2MSL
,就可以使由于网络不通畅产生的滞留报文段失效。这样下一个新的连接中就不会出现旧的连接请求报文。以下书籍和博文为本文的撰写提供不少帮助:
计算机网络 (第7版) 谢希仁
计算机网络-自顶向下方法 第七版
rfc793 原文连接:https://tools.ietf.org/rfc/rfc793.txt
https://hit-alibaba.github.io/interview/basic/network/TCP.html Github【HIT-Alibaba/interview】
https://stackoverflow.com/questions/16259774/what-if-a-tcp-handshake-segment-is-lost StackFlow
https://mp.weixin.qq.com/s/8t_KFtrrBkFyZKPJg_y6pw 公众号【苦逼的码农】
https://www.cnblogs.com/Andya/p/7272462.html 博客园 【Andya】
https://www.cnblogs.com/popduke/p/5823801.html 博客园 【YonnyHao】
https://blog.csdn.net/scuzoutao/article/details/81774100 csdn博主【邹啊涛】
https://blog.csdn.net/qq_38950316/article/details/81087809 csdn博主 【青柚_】
读者们也可将上述书籍和博文作为延伸阅读。