TCP
TCP头部的报文结构
-
序号:seq序号,占32位,用来标识从TCP源端向目的端发送的字节流,发起方发送数据时对此进行标记。
-
确认号:ack序号,占32位,只有ACK标志位为1时,确认序号字段才有效,ack=seq+1。
序号是本TCP报文数据部分的首字节序号,确认号是成功接收别人的TCP报文,并期待接收的下一个TCP报文中数据部分的首字节的序号。
TCP Flags
- URG:紧急指针标志
- ACK:确认序号标志
- PSH:push标志
- RST:重置连接标志
- SYN:同步序号,用于建立连接过程
- FIN:finish标志,用于释放连接
需要注意的是:
- 不要将确认序号ack与标志位中的ACK搞混了。
- 确认方ack = 发起方seq + 1,两端配对。
三次握手
TCP是一种面向连接的、可靠的、基于字节流的传输层通信协议,在发送数据前,通信双方必须在彼此间建立一条连接。所谓的“连接”,其实是客户端和服务端保存的一份关于对方的信息,如ip地址、端口号等。
TCP可以看成是一种字节流,它会处理IP层或以下的层的丢包、重复以及错误问题。在连接的建立过程中,双方需要交换一些连接的参数。这些参数可以放在TCP头部。当一个连接被建立或被终止时,交换的报文段只包含TCP头部,而没有数据。
“握手”是为了建立连接,流程图如下:
对于面试中的回答,可以这样说:
-
刚开始客户端处于 closed 状态,服务端处于 listen 状态。
-
第一次握手:客户端给服务端发一个 SYN 报文,并指明客户端的初始化序列号 ISN©。此时客户端处于 SYN_Send 状态。
-
第二次握手:服务器收到客户端的 SYN 报文之后,会以自己的 SYN 报文作为应答,并且也是指定了自己的初始化序列号 ISN(s),同时会把客户端的 ISN + 1 作为 ACK 的值,表示自己已经收到了客户端的 SYN,此时服务器处于 SYN_REVD 的状态。
-
第三次握手:客户端收到 SYN 报文之后,会发送一个 ACK 报文,当然,也是一样把服务器的 ISN + 1 作为 ACK 的值,表示已经收到了服务端的 SYN 报文,此时客户端处于 establised 状态。
-
服务器收到 ACK 报文之后,也处于 establised 状态,此时,双方以建立起了连接。
对于seq和ack的值,个人有个不成熟的记忆方法:
即:
若上一段请求报文有seq0,本段请求报文的 ack1 = seq0 + 1。
若上一段请求报文有ack0,本段请求报文的 seq1 = ack0。
(1和0只是标记两段请求报文)
千万不要用大白话回答!
TCP 三次握手异常情况实战分析
TCP 第一次握手 SYN 丢包
当客户端发起的 TCP 第一次握手 SYN 包,在超时时间内没收到服务端的 ACK,就会在超时重传 SYN 数据包,每次超时重传的 RTO 是翻倍上涨的,直到 SYN 包的重传次数到达 tcp_syn_retries
值后,客户端不再发送 SYN 包。
TCP 第二次握手 SYN、ACK 丢包
- 客户端发起 SYN 后,由于第二次握手 SYN、ACK 丢包,客户端是无法收到服务端的 SYN、ACK 包,当发生超时后,就会重传 SYN 包。
- 服务端收到客户的 SYN 包后,就会回 SYN、ACK 包,但是客户端一直没有回 ACK,服务端在超时后,重传了 SYN、ACK 包,接着一会,客户端超时重传的 SYN 包又抵达了服务端,服务端收到后,超时定时器就重新计时,然后回了 SYN、ACK 包,所以相当于服务端的超时定时器只触发了一次,又被重置了。
- 最后,客户端 SYN 超时重传次数达到了 5 次(tcp_syn_retries 默认值 5 次),就不再继续发送 SYN 包了。
所以,我们可以发现,当第二次握手的 SYN、ACK 丢包时,客户端会超时重发 SYN 包,服务端也会超时重传 SYN、ACK 包。
简单地说:
当 TCP 第二次握手 SYN、ACK 包丢了后,客户端 SYN 包会发生超时重传,服务端 SYN、ACK 也会发生超时重传。
客户端 SYN 包超时重传的最大次数,是由 tcp_syn_retries 决定的,默认值是 5 次;服务端 SYN、ACK 包时重传的最大次数,是由 tcp_synack_retries 决定的,默认值是 5 次。
TCP 第三次握手 ACK 丢包
在建立 TCP 连接时,如果第三次握手的 ACK 服务端无法收到,则服务端就会短暂处于 SYN_RECV
状态,而客户端会处于 ESTABLISHED
状态。
由于服务端一直收不到 TCP 第三次握手的 ACK,则会一直重传 SYN、ACK 包,直到重传次数超过 tcp_synack_retries
值(默认值 5 次)后,服务端就会断开 TCP 连接。
而客户端则会有两种情况:
- 如果客户端没发送数据包,一直处于
ESTABLISHED
状态,然后经过较长一段时间才可以发现一个「死亡」连接,于是客户端连接就会断开连接。 - 如果客户端发送了数据包,一直没有收到服务端对该数据包的确认报文,则会一直重传该数据包,直到重传次数超过
tcp_retries2
值(默认值 15 次)后,客户端就会断开 TCP 连接。
三次握手常见面试题
三次握手有什么作用?
- 确认双方的接收能力、发送能力是否正常。
- 指定自己的初始化序列号,为后面的可靠传送做准备。
- 如果是 https 协议的话,三次握手这个过程,还会进行数字证书的验证以及加密密钥的生成。
(ISN)是固定的吗?
三次握手的一个重要功能是客户端和服务端交换ISN(Initial Sequence Number), 以便让对方知道接下来接收数据的时候如何按序列号组装数据。
如果ISN是固定的,攻击者很容易猜出后续的确认号,因此 ISN 是动态生成的。
什么是半连接队列?
服务器第一次收到客户端的 SYN 之后,就会处于 SYN_RCVD 状态,此时双方还没有完全建立其连接,服务器会把此种状态下请求连接放在一个队列里,我们把这种队列称之为半连接队列。
当然还有一个全连接队列,就是已经完成三次握手,建立起连接的就会放在全连接队列中。如果队列满了就有可能会出现丢包现象。
这里再补充一点关于SYN-ACK 重传次数的问题:
服务器发送完SYN-ACK包,如果未收到客户确认包,服务器进行首次重传,等待一段时间仍未收到客户确认包,进行第二次重传。如果重传次数超过系统规定的最大重传次数,系统将该连接信息从半连接队列中删除。
注意,每次重传等待的时间不一定相同,一般会是指数增长,例如间隔时间为 1s,2s,4s,8s…
三次握手过程中可以携带数据吗?
第一次、第二次握手不可以携带数据,而第三次握手是可以携带数据的。
为什么这样呢?大家可以想一个问题,假如第一次握手可以携带数据的话,如果有人要恶意攻击服务器,那他每次都在第一次握手中的 SYN 报文中放入大量的数据,因为攻击者根本就不理服务器的接收、发送能力是否正常,然后疯狂着重复发 SYN 报文的话,这会让服务器花费很多时间、内存空间来接收这些报文。也就是说,第一次握手可以放数据的话,其中一个简单的原因就是会让服务器更加容易受到攻击了。
而对于第三次的话,此时客户端已经处于 established 状态,也就是说,对于客户端来说,他已经建立起连接了,并且也已经知道服务器的接收、发送能力是正常的了,所以能携带数据页没啥毛病。
首次握手有什么隐患?
会出现SYN超时。
-
服务端收到客户端的SYN,回复SYN-ACK的时候未收到ACK确认。
-
于是服务端不断重试直到超时,Linux默认等待63秒才断开连接。
防护措施:
-
SYN队列满后,通过
tcp_syncookies
参数会发SYN cookie【源端口+目标端口+时间戳组成】。 -
若为正常连接则Client会回发SYN Cookie,直接建立连接。
建立连接,Client出现故障怎么办?
利用保活机制:
- 向对方发送保活探测报文,如果未收到响应则继续发送。
- 当尝试次数达到保活探测数仍然未收到响应则中断连接。
为什么一定要进行三次握手?两次不行吗?
首先我们要弄明白三次握手的目的:
第一次握手:客户端发包,服务端收到了。
这样服务端会得出结论:客户端的发送能力、服务端的接收能力是正常的。
第二次握手:服务端发包,客户端收到了。
这样客户端会得出结论:客户端的发送、接收能力是正常的,服务端的发送、接收能力也是正常的。
但是要注意,此时服务端并不能确认客户端的接受能力是否是正常的。
于是就有了第三次握手:客户端发包,服务端收到了。
这样服务端就能得出最终结论:客户端的发送、接收能力是正常的,服务端的发送、接收能力也是正常的。
如果用了两次握手,则有可能会发生下面这种情况:
客户端向服务端发出一个连接请求,但由于某种原因该连接请求报文丢失了,于是客户端又向服务端发送了一个连接请求,这次一切正常,建立连接后进行数据传输,然后释放连接,注意:客户端一共发送了两个连接请求报文段,一个丢失,一个到达服务端。但如果丢失的那个请求在某个网络节点滞留了,延迟了一会才到达服务端,这时服务端就会误以为客户端又发送了一次新的连接请求,而由于只有两次握手,此时服务端只要发出确认就能够建立新的连接了。但此时客户端会忽略服务端发来的确认,不进行数据传输,那么服务端就会一直等待客户端发送数据,造成资源的浪费。
四次挥手
-
第一次挥手:客户端发送一个FIN,用来关闭客户端和服务端的数据传送,客户端进入FIN_WAIT_1状态。
-
第二次挥手:服务端收到FIN后,发送一个ACK给客户端,确认序号 ack = seq + 1,服务端进入CLOSE_WAIT状态。
-
第三次挥手:服务端发送一个FIN,用来关闭服务端和客户端的数据传送,服务端进入LAST_ACK状态。
-
第四次挥手:客户端收到FIN后,客户端进入TIME_WAIT状态,接着发送一个ACK给服务端,确认序号 ack = seq + 1,服务端进入CLOSED状态,完成四次挥手。
注意客户端发出确认报文后不是立马释放TCP连接,而是要经过2MSL(最长报文段寿命的2倍时长)后才释放TCP连接。而服务端一旦收到客户端发出的确认报文就会立马释放TCP连接,所以服务端结束TCP连接的时间要比客户端早一些。
四次挥手常见面试题
为什么挥手需要四次?
- 关闭连接时,客户端向服务端发送
FIN
时,仅仅表示客户端不再发送数据了但是还能接收数据 - 服务器收到客户端的
FIN
报文时,先回一个ACK
应答报文,而服务端可能还有数据需要处理和发送,等服务端不再发送数据时,才发送FIN
报文给客户端来表示同意现在关闭连接,所以服务端的ACK
和FIN
一般都会分开发送,从而比三次握手导致多了一次
为什么会有TIME_WAIT状态,且超过2MSL?
主动发起关闭连接的一方,才会有 TIME-WAIT 状态。
TIME-WAIT 作用:等待足够的时间以确保最后的 ACK 能让被动关闭方接收,从而帮助其正常关闭
原因:
1.防止关闭后又建立的新连接接收到旧连接的数据包
-
假设TIME_WAIT等待过短,那么被复用的端口可能会建立新的TCP连接
-
此时旧的连接在四次握手前可能有一个旧的数据包刚刚到达(图中SEQ=301)
-
这样新的连接就会处理旧的服务端数据包,产生数据错乱等严重问题。
-
经过
2MSL
这个时间,足以让两个方向上的数据包都被丢弃,使得原来连接的数据包在网络中都自然消失,再出现的数据包一定都是新建立连接所产生的。
-
2.等待足够的时间以确保最后的 ACK 能让被动关闭方接收,从而帮助其正常关闭
- 如果服务端发送了FIN后,没有接收到客户端的ACK报文,则会超时重传再次发送FIN。
- 所以需要客户端等待2MSL,确保服务端接收到了ACK报文。
服务器出现大量CLOSE_WAIT状态的原因?
对方关闭socket连接,而我方忙于读或写,没有及时关闭连接。
解决措施:
- 检查代码,特别是释放资源的代码。
- 检查配置,特别是处理请求的线程配置。
看到这里,希望读者对于以下几个问题都有了答案。
- 请画出三次握手和四次挥手的示意图
- 为什么连接的时候是三次握手?
- 什么是半连接队列?
- ISN(Initial Sequence Number)是固定的吗?
- 三次握手过程中可以携带数据吗?
- 如果第三次握手丢失了,客户端服务端会如何处理?
- SYN攻击是什么?
- 挥手为什么需要四次?
- 四次挥手释放连接时,等待2MSL的意义?
参考资料:
- 剑指Java面试-Offer直通车
- https://mp.weixin.qq.com/s/jTDU-zxP1INTYLpGLypjXQ
- https://zhuanlan.zhihu.com/p/63690137
- https://zhuanlan.zhihu.com/p/86426969