前言
TCP协议是大厂面试必问的知识点。整理了15道非常经典的TCP面试题,希望大家都找到理想的offer呀
1. 讲下TCP三次握手流程
开始客户端和服务器都处于CLOSED状态,然后服务端开始监听某个端口,进入LISTEN状态
- 第一次握手(SYN=1, seq=x),发送完毕后,客户端进入 SYN_SEND 状态
- 第二次握手(SYN=1, ACK=1, seq=y, ACKnum=x+1), 发送完毕后,服务器端进入 SYN_RCVD 状态。
- 第三次握手(ACK=1,ACKnum=y+1),发送完毕后,客户端进入 ESTABLISHED 状态,当服务器端接收到这个包时,也进入 ESTABLISHED 状态,TCP 握手,即可以开始数据传输。
2.TCP握手为什么是三次,不能是两次?不能是四次?
TCP握手为什么是三次呢?为了方便理解,我们以谈恋爱为例子:两个人能走到一起,最重要的事情就是相爱,就是我爱你,并且我知道,你也爱我,接下来我们以此来模拟三次握手的过程:
为什么握手不能是两次呢?
如果只有两次握手,女孩子可能就不知道,她的那句我也爱你,男孩子是否收到,恋爱关系就不能愉快展开。
为什么握手不能是四次呢?
因为握手不能是四次呢?因为三次已经够了,三次已经能让双方都知道:你爱我,我也爱你。而四次就多余了。
3. 讲下TCP四次挥手过程
- 第一次挥手(FIN=1,seq=u),发送完毕后,客户端进入FIN_WAIT_1 状态
- 第二次挥手(ACK=1,ack=u+1,seq =v),发送完毕后,服务器端进入CLOSE_WAIT 状态,客户端接收到这个确认包之后,进入 FIN_WAIT_2 状态
- 第三次挥手(FIN=1,ACK1,seq=w,ack=u+1),发送完毕后,服务器端进入LAST_ACK 状态,等待来自客户端的最后一个ACK。
- 第四次挥手(ACK=1,seq=u+1,ack=w+1),客户端接收到来自服务器端的关闭请求,发送一个确认包,并进入 TIME_WAIT状态,等待了某个固定时间(两个最大段生命周期,2MSL,2 Maximum Segment Lifetime)之后,没有收到服务器端的 ACK ,认为服务器端已经正常关闭连接,于是自己也关闭连接,进入 CLOSED 状态。服务器端接收到这个确认包之后,关闭连接,进入 CLOSED 状态。
4. TCP挥手为什么需要四次呢?
举个例子吧!
★小明和小红打电话聊天,通话差不多要结束时,小红说“我没啥要说的了”,小明回答“我知道了”。但是小明可能还会有要说的话,小红不能要求小明跟着自己的节奏结束通话,于是小明可能又叽叽歪歪说了一通,最后小明说“我说完了”,小红回答“知道了”,这样通话才算结束。”
5. TIME-WAIT 状态为什么需要等待 2MSL
2MSL,2 Maximum Segment Lifetime,即两个最大段生命周期
- 1个 MSL 保证四次挥手中主动关闭方最后的 ACK 报文能最终到达对端
- 1个 MSL 保证对端没有收到 ACK 那么进行重传的 FIN 报文能够到达
6.TCP 和 UDP 的区别
- TCP面向连接((如打电话要先拨号建立连接);UDP是无连接的,即发送数据之前不需要建立连接。
- TCP要求安全性,提供可靠的服务,通过TCP连接传送的数据,不丢失、不重复、安全可靠。而UDP尽最大努力交付,即不保证可靠交付。
- TCP是点对点连接的,UDP一对一,一对多,多对多都可以
- TCP传输效率相对较低,而UDP传输效率高,它适用于对高速传输和实时性有较高的通信或广播通信。
- TCP适合用于网页,邮件等;UDP适合用于视频,语音广播等
- TCP面向字节流,UDP面向报文
7. TCP报文首部有哪些字段,说说其作用
- 16位端口号:源端口号,主机该报文段是来自哪里;目标端口号,要传给哪个上层协议或应用程序
- 32位序号:一次TCP通信(从TCP连接建立到断开)过程中某一个传输方向上的字节流的每个字节的编号。
- 32位确认号:用作对另一方发送的tcp报文段的响应。其值是收到的TCP报文段的序号值加1。
- 4位头部长度:表示tcp头部有多少个32bit字(4字节)。因为4位最大能标识15,所以TCP头部最长是60字节。
- 6位标志位:URG(紧急指针是否有效),ACk(表示确认号是否有效),PSH(缓冲区尚未填满),RST(表示要求对方重新建立连接),SYN(建立连接消息标志接),FIN(表示告知对方本端要关闭连接了)
- 16位窗口大小:是TCP流量控制的一个手段。这里说的窗口,指的是接收通告窗口。它告诉对方本端的TCP接收缓冲区还能容纳多少字节的数据,这样对方就可以控制发送数据的速度。
- 16位校验和:由发送端填充,接收端对TCP报文段执行CRC算法以检验TCP报文段在传输过程中是否损坏。注意,这个校验不仅包括TCP头部,也包括数据部分。这也是TCP可靠传输的一个重要保障。
- 16位紧急指针:一个正的偏移量。它和序号字段的值相加表示最后一个紧急数据的下一字节的序号。因此,确切地说,这个字段是紧急指针相对当前序号的偏移,不妨称之为紧急偏移。TCP的紧急指针是发送端向接收端发送紧急数据的方法。
8. TCP 是如何保证可靠性的
- 首先,TCP的连接是基于三次握手,而断开则是四次挥手。确保连接和断开的可靠性。
- 其次,TCP的可靠性,还体现在有状态;TCP会记录哪些数据发送了,哪些数据被接受了,哪些没有被接受,并且保证数据包按序到达,保证数据传输不出差错。
- 再次,TCP的可靠性,还体现在可控制。它有报文校验、ACK应答、超时重传(发送方)、失序数据重传(接收方)、丢弃重复数据、流量控制(滑动窗口)和拥塞控制等机制。
9. TCP 重传机制
超时重传
TCP 为了实现可靠传输,实现了重传机制。最基本的重传机制,就是超时重传,即在发送数据报文时,设定一个定时器,每间隔一段时间,没有收到对方的ACK确认应答报文,就会重发该报文。
这个间隔时间,一般设置为多少呢?我们先来看下什么叫RTT(Round-Trip Time,往返时间)。
RTT就是,一个数据包从发出去到回来的时间,即数据包的一次往返时间。超时重传时间,就是Retransmission Timeout ,简称RTO。
RTO设置多久呢?
- 如果RTO比较小,那很可能数据都没有丢失,就重发了,这会导致网络阻塞,会导致更多的超时出现。
- 如果RTO比较大,等到花儿都谢了还是没有重发,那效果就不好了。
一般情况下,RTO略大于RTT,效果是最好的。一些小伙伴会问,超时时间有没有计算公式呢?有的!有个标准方法算RTO的公式,也叫Jacobson / Karels 算法。我们一起来看下计算RTO的公式
1. 先计算SRTT(计算平滑的RTT)
- SRTT = (1 - α) * SRTT + α * RTT //求 SRTT 的加权平均
2. 再计算RTTVAR (round-trip time variation)
- RTTVAR = (1 - β) * RTTVAR + β * (|RTT - SRTT|) //计算 SRTT 与真实值的差距
3. 最终的RTO
- RTO = µ * SRTT + ∂ * RTTVAR = SRTT + 4·RTTVAR
其中,α = 0.125,β = 0.25, μ = 1,? = 4,这些参数都是大量结果得出的最优参数。
但是,超时重传会有这些缺点:
- 当一个报文段丢失时,会等待一定的超时周期然后才重传分组,增加了端到端的时延。
- 当一个报文段丢失时,在其等待超时的过程中,可能会出现这种情况:其后的报文段已经被接收端接收但却迟迟得不到确认,发送端会认为也丢失了,从而引起不必要的重传,既浪费资源也浪费时间。
并且,TCP有个策略,就是超时时间间隔会加倍。超时重传需要等待很长时间。因此,还可以使用快速重传机制。
快速重传
快速重传机制,它不以时间驱动,而是以数据驱动。它基于接收端的反馈信息来引发重传。
一起来看下快速重传流程:
快速重传流程
发送端发送了 1,2,3,4,5,6 份数据:
- 第一份 Seq=1 先送到了,于是就 Ack 回 2;
- 第二份 Seq=2 也送到了,假设也正常,于是ACK 回 3;
- 第三份 Seq=3 由于网络等其他原因,没送到;
- 第四份 Seq=4 也送到了,但是因为Seq3没收到。所以ACK回3;
- 后面的 Seq=4,5的也送到了,但是ACK还是回复3,因为Seq=3没收到。
- 发送端连着收到三个重复冗余ACK=3的确认(实际上是4个,但是前面一个是正常的ACK,后面三个才是重复冗余的),便知道哪个报文段在传输过程中丢失了,于是在定时器过期之前,重传该报文段。
- 最后,接收到收到了 Seq3,此时因为 Seq=4,5,6都收到了,于是ACK回7.
但快速重传还可能会有个问题:ACK只向发送端告知最大的有序报文段,到底是哪个报文丢失了呢?并不确定!那到底该重传多少个包呢?
是重传 Seq3 呢?还是重传 Seq3、Seq4、Seq5、Seq6 呢?因为发送端并不清楚这三个连续的 ACK3 是谁传回来的。”
带选择确认的重传(SACK)
为了解决快速重传的问题:应该重传多少个包? TCP提供了SACK方法(带选择确认的重传,Selective Acknowledgment)。
SACK机制就是,在快速重传的基础上,接收端返回最近收到的报文段的序列号范围,这样发送端就知道接收端哪些数据包没收到,酱紫就很清楚该重传哪些数据包啦。SACK标记是加在TCP头部选项字段里面的。
SACK机制
如上图中,发送端收到了三次同样的ACK=30的确认报文,于是就会触发快速重发机制,通过SACK信息发现只有30~39这段数据丢失,于是重发时就只选择了这个30~39的TCP报文段进行重发。
D-SACK
D-SACK,即Duplicate SACK(重复SACK),在SACK的基础上做了一些扩展,,主要用来告诉发送方,有哪些数据包自己重复接受了。DSACK的目的是帮助发送方判断,是否发生了包失序、ACK丢失、包重复或伪重传。让TCP可以更好的做网络流控。来看个图吧:
D-SACK简要流程
10. 聊聊TCP的滑动窗口
TCP 发送一个数据,需要收到确认应答,才会发送下一个数据。这样有个缺点,就是效率会比较低。
这就好像我们面对面聊天,你说完一句,我应答后,你才会说下一句。那么,如果我在忙其他事情,没有能够及时回复你。你说完一句后,要等到我忙完回复你,你才说下句,这显然很不现实。”
为了解决这个问题,TCP引入了窗口,它是操作系统开辟的一个缓存空间。窗口大小值表示无需等待确认应答,而可以继续发送数据的最大值。
TCP头部有个字段叫win,也即那个16位的窗口大小,它告诉对方本端的TCP接收缓冲区还能容纳多少字节的数据,这样对方就可以控制发送数据的速度,从而达到流量控制的目的。
通俗点讲,就是接受方每次收到数据包,在发送确认报文的时候,同时告诉发送方,自己的缓存区还有多少空余空间,缓冲区的空余空间,我们就称之为接受窗口大小。这就是win。”
TCP 滑动窗口分为两种: 发送窗口和接收窗口。发送端的滑动窗口包含四大部分,如下:
- 已发送且已收到ACK确认
- 已发送但未收到ACK确认
- 未发送但可以发送
- 未发送也不可以发送
- 虚线矩形框,就是发送窗口。
- SND.WND: 表示发送窗口的大小,上图虚线框的格子数就是14个。
- SND.UNA: 一个绝对指针,它指向的是已发送但未确认的第一个字节的序列号。
- SND.NXT:下一个发送的位置,它指向未发送但可以发送的第一个字节的序列号。
接收方的滑动窗口包含三大部分,如下:
- 已成功接收并确认
- 未收到数据但可以接收
- 未收到数据并不可以接收的数据
- 虚线矩形框,就是接收窗口。
- REV.WND: 表示接收窗口的大小,上图虚线框的格子就是9个。
- REV.NXT:下一个接收的位置,它指向未收到但可以接收的第一个字节的序列号。
11. 聊聊TCP的流量控制
TCP三次握手,发送端和接收端进入到ESTABLISHED状态,它们即可以愉快地传输数据啦。
但是发送端不能疯狂地向接收端发送数据,因为接收端接收不过来的话,接收方只能把处理不过来的数据存在缓存区里。如果缓存区都满了,发送方还在疯狂发送数据的话,接收方只能把收到的数据包丢掉,这就浪费了网络资源啦。
TCP 提供一种机制可以让发送端根据接收端的实际接收能力控制发送的数据量,这就是流量控制。”
TCP通过滑动窗口来控制流量,我们看下流量控制的简要流程吧:
首先双方三次握手,初始化各自的窗口大小,均为 400 个字节。
TCP的流量控制
- 假如当前发送方给接收方发送了200个字节,那么,发送方的SND.NXT会右移200个字节,也就是说当前的可用窗口减少了200 个字节。
- 接受方收到后,放到缓冲队列里面,REV.WND =400-200=200字节,所以win=200字节返回给发送方。接收方会在 ACK 的报文首部带上缩小后的滑动窗口200字节
- 发送方又发送200字节过来,200字节到达,继续放到缓冲队列。不过这时候,由于大量负载的原因,接受方处理不了这么多字节,只能处理100字节,剩余的100字节继续放到缓冲队列。这时候,REV.WND = 400-200-100=100字节,即win=100返回发送方。
- 发送方继续干活,发送100字节过来,这时候,接受窗口win变为0。
- 发送方停止发送,开启一个定时任务,每隔一段时间,就去询问接受方,直到win大于0,才继续开始发送。
12. TCP的拥塞控制
拥塞控制是作用于网络的,防止过多的数据包注入到网络中,避免出现网络负载过大的情况。它的目标主要是最大化利用网络上瓶颈链路的带宽。它跟流量控制又有什么区别呢?流量控制是作用于接收者的,根据接收端的实际接收能力控制发送速度,防止分组丢失的。
我们可以把网络链路比喻成一根水管,如果我们想最大化利用网络来传输数据,那就是尽快让水管达到最佳充满状态。
发送方维护一个拥塞窗口cwnd(congestion window) 的变量,用来估算在一段时间内这条链路(水管)可以承载和运输的数据(水)的数量。它大小代表着网络的拥塞程度,并且是动态变化的,但是为了达到最大的传输效率,我们该如何知道这条水管的运送效率是多少呢?
一个比较简单的方法就是不断增加传输的水量,直到水管快要爆裂为止(对应到网络上就是发生丢包),用 TCP 的描述就是:
★只要网络中没有出现拥塞,拥塞窗口的值就可以再增大一些,以便把更多的数据包发送出去,但只要网络出现拥塞,拥塞窗口的值就应该减小一些,以减少注入到网络中的数据包数。”
实际上,拥塞控制主要有这几种常用算法
- 慢启动
- 拥塞避免
- 拥塞发生
- 快速恢复
慢启动算法
慢启动算法,表面意思就是,别急慢慢来。它表示TCP建立连接完成后,一开始不要发送大量的数据,而是先探测一下网络的拥塞程度。由小到大逐渐增加拥塞窗口的大小,如果没有出现丢包,每收到一个ACK,就将拥塞窗口cwnd大小就加1(单位是MSS)。每轮次发送窗口增加一倍,呈指数增长,如果出现丢包,拥塞窗口就减半,进入拥塞避免阶段。
- TCP连接完成,初始化cwnd = 1,表明可以传一个MSS单位大小的数据。
- 每当收到一个ACK,cwnd就加一;
- 每当过了一个RTT,cwnd就增加一倍; 呈指数让升
为了防止cwnd增长过大引起网络拥塞,还需设置一个慢启动阀值ssthresh(slow start threshold)状态变量。当cwnd到达该阀值后,就好像水管被关小了水龙头一样,减少拥塞状态。即当cwnd >ssthresh时,进入了拥塞避免算法。
拥塞避免算法
一般来说,慢启动阀值ssthresh是65535字节,cwnd到达慢启动阀值后
- 每收到一个ACK时,cwnd = cwnd + 1/cwnd
- 当每过一个RTT时,cwnd = cwnd + 1
显然这是一个线性上升的算法,避免过快导致网络拥塞问题。
拥塞发生
当网络拥塞发生丢包时,会有两种情况:
- RTO超时重传
- 快速重传
如果是发生了RTO超时重传,就会使用拥塞发生算法
- 慢启动阀值sshthresh = cwnd /2
- cwnd 重置为 1
- 进入新的慢启动过程
这真的是辛辛苦苦几十年,一朝回到解放前。其实还有更好的处理方式,就是快速重传。发送方收到3个连续重复的ACK时,就会快速地重传,不必等待RTO超时再重传。
image.png
慢启动阀值ssthresh 和 cwnd 变化如下:
- 拥塞窗口大小 cwnd = cwnd/2
- 慢启动阀值 ssthresh = cwnd
- 进入快速恢复算法
快速恢复
快速重传和快速恢复算法一般同时使用。快速恢复算法认为,还有3个重复ACK收到,说明网络也没那么糟糕,所以没有必要像RTO超时那么强烈。
正如前面所说,进入快速恢复之前,cwnd 和 sshthresh已被更新:
- - cwnd = cwnd /2
- - sshthresh = cwnd
然后,真正的快速算法如下:
- cwnd = sshthresh + 3
- 重传重复的那几个ACK(即丢失的那几个数据包)
- 如果再收到重复的 ACK,那么 cwnd = cwnd +1
- 如果收到新数据的 ACK 后, cwnd = sshthresh。因为收到新数据的 ACK,表明恢复过程已经结束,可以再次进入了拥塞避免的算法了。
13. 半连接队列和 SYN Flood 攻击的关系
TCP进入三次握手前,服务端会从CLOSED状态变为LISTEN状态,同时在内部创建了两个队列:半连接队列(SYN队列)和全连接队列(ACCEPT队列)。
什么是半连接队列(SYN队列) 呢? 什么是全连接队列(ACCEPT队列) 呢?回忆下TCP三次握手的图:
三次握手
- TCP三次握手时,客户端发送SYN到服务端,服务端收到之后,便回复ACK和SYN,状态由LISTEN变为SYN_RCVD,此时这个连接就被推入了SYN队列,即半连接队列。
- 当客户端回复ACK, 服务端接收后,三次握手就完成了。这时连接会等待被具体的应用取走,在被取走之前,它被推入ACCEPT队列,即全连接队列。
SYN Flood是一种典型的DoS (Denial of Service,拒绝服务) 攻击,它在短时间内,伪造不存在的IP地址,向服务器大量发起SYN报文。当服务器回复SYN+ACK报文后,不会收到ACK回应报文,导致服务器上建立大量的半连接半连接队列满了,这就无法处理正常的TCP请求啦。
- 主要有 syn cookie和SYN Proxy防火墙等方案应对。
- syn cookie:在收到SYN包后,服务器根据一定的方法,以数据包的源地址、端口等信息为参数计算出一个cookie值作为自己的SYNACK包的序列号,回复SYN+ACK后,服务器并不立即分配资源进行处理,等收到发送方的ACK包后,重新根据数据包的源地址、端口计算该包中的确认序列号是否正确,如果正确则建立连接,否则丢弃该包。
SYN Proxy防火墙:服务器防火墙会对收到的每一个SYN报文进行代理和回应,并保持半连接。等发送方将ACK包返回后,再重新构造SYN包发到服务器,建立真正的TCP连接。
14. Nagle 算法与延迟确认
Nagle算法
如果发送端疯狂地向接收端发送很小的包,比如就1个字节,那么亲爱的小伙伴,你们觉得会有什么问题呢?
TCP/IP协议中,无论发送多少数据,总是要在数据前面加上协议头,同时,对方接收到数据,也需要发送ACK表示确认。为了尽可能的利用网络带宽,TCP总是希望尽可能的发送足够大的数据。Nagle算法就是为了尽可能发送大块数据,避免网络中充斥着许多小数据块。”
Nagle算法的基本定义是:任意时刻,最多只能有一个未被确认的小段。所谓“小段”,指的是小于MSS尺寸的数据块,所谓“未被确认”,是指一个数据块发送出去后,没有收到对方发送的ACK确认该数据已收到。
Nagle算法的实现规则:
- 如果包长度达到MSS,则允许发送;
- 如果该包含有FIN,则允许发送;
- 设置了TCP_NODELAY选项,则允许发送;
- 未设置TCP_CORK选项时,若所有发出去的小数据包(包长度小于MSS)均被确认,则允许发送;
- 上述条件都未满足,但发生了超时(一般为200ms),则立即发送。
延迟确认
如果接受方刚接收到发送方的数据包,在很短很短的时间内,又接收到第二个包。那么请问接收方是一个一个地回复好点,还是合并一起回复好呢?
★接收方收到数据包后,如果暂时没有数据要发给对端,它可以等一段时再确认(Linux上默认是40ms)。如果这段时间刚好有数据要传给对端,ACK就随着数据传输,而不需要单独发送一次ACK。如果超过时间还没有数据要发送,也发送ACK,避免对端以为丢包。”
但是有些场景不能延迟确认,比如发现了乱序包、接收到了大于一个 frame 的报文,且需要调整窗口大小等。
一般情况下,Nagle算法和延迟确认不能一起使用,Nagle算法意味着延迟发,延迟确认意味着延迟接收,酱紫就会造成更大的延迟,会产生性能问题。
15. TCP的粘包和拆包
TCP是面向流,没有界限的一串数据。TCP底层并不了解上层业务数据的具体含义,它会根据TCP缓冲区的实际情况进行包的划分,所以在业务上认为,一个完整的包可能会被TCP拆分成多个包进行发送,也有可能把多个小的包封装成一个大的数据包发送,这就是所谓的TCP粘包和拆包问题。
TCP的粘包和拆包
为什么会产生粘包和拆包呢?
- 要发送的数据小于TCP发送缓冲区的大小,TCP将多次写入缓冲区的数据一次发送出去,将会发生粘包;
- 接收数据端的应用层没有及时读取接收缓冲区中的数据,将发生粘包;
- 要发送的数据大于TCP发送缓冲区剩余空间大小,将会发生拆包;
- 待发送数据大于MSS(最大报文长度),TCP在传输前将进行拆包。即TCP报文长度-TCP头部长度>MSS。
解决方案:
发送端将每个数据包封装为固定长度
在数据尾部增加特殊字符进行分割
将数据分为两部分,一部分是头部,一部分是内容体;其中头部结构大小固定,且有一个字段声明内容体的大小。
参考资料
[1]TCP 的那些事儿(下): https://coolshell.cn/articles/11609.html
[2]面试头条你需要懂的 TCP 拥塞控制原理: https://zhuanlan.zhihu.com/p/76023663
[3]30张图解:TCP 重传、滑动窗口、流量控制、拥塞控制发愁: https://zhuanlan.zhihu.com/p/133307545
[4]TCP协议灵魂之问,巩固你的网路底层基础: https://juejin.cn/post/6844904070889603085
[5]TCP粘包和拆包: https://blog.csdn.net/ailunlee/article/details/95944377
本文转载自微信公众号「捡田螺的小男孩」,可以通过以下二维码关注。转载本文请联系捡田螺的小男孩公众号。