TCP 协议面试题

TCP 协议面试题

参考文章

TCP 协议面试灵魂10问

TCP和UDP的区别

TCP是一个面向连接的、基于字节流的传输层协议,提供可靠交付。

UDP是一个面向无连接的、基于报文的传输层协议,尽最大努力交付。

相对于UDP,TCP的三大核心特性:

  • 面向连接:双方通信之前,TCP需要通过三次握手建立连接,而UDP没有建立连接的过程
  • 可靠交付:TCP有确认和超时重传机制,而UDP没有。TCP会精确记录哪些数据发送了、哪些数据被接收了、哪些没有被接收,保证数据按序到达,这是有状态。当意识到丢包了或者网络环境不佳,TCP 会根据具体情况调整自己的行为,控制自己的发送速度或者重发,这是可控制。相应的,UDP 就是无状态不可控的。
  • 面向字节流:UDP 的数据传输是基于数据报的,这是因为仅仅只是继承了 IP 层的特性,而 TCP 为了维护状态,将一个个 IP 包变成了字节流,字节流数据需要手动控制数据边界。

TCP 与 UDP 的区别

  1. TCP 面向连接,UDP 是无连接的;
  2. TCP 提供可靠的服务,也就是说,通过 TCP 连接传送的数据,无差错,不丢失,不重复,且按序到达;UDP 尽最大努力交付,即不保证可靠交付
  3. TCP 的逻辑通信信道是全双工的可靠信道;UDP 则是不可靠信道
  4. 每一条 TCP 连接只能是点到点的;UDP 支持一对一,一对多,多对一和多对多的交互通信
  5. TCP 面向字节流(可能出现黏包问题),实际上是 TCP 把数据看成一连串无结构的字节流;UDP 是面向报文的(不会出现黏包问题)
  6. UDP 没有拥塞控制,因此网络出现拥塞不会使源主机的发送速率降低(对实时应用很有用,如 IP 电话,实时视频会议等)
  7. TCP 首部开销20字节;UDP 的首部开销小,只有 8 个字节
TCPUDP
面向连接的传输层协议无连接的
每一条TCP连接只能有两个端点,每一条TCP连接只能是点对点的(一对一)支持一对一、一对多、多对一和多对多的交互通信
提供可靠交付的服务:确认和超时重传机制使用尽最大努力交付
有流量控制(通过设置接收窗口大小)和拥塞控制(拥塞控制算法:慢开始、拥塞避免、快重床、快恢复)没有拥塞控制
面向字节流面向报文
20个字节的首部首部开销小,只有8个字节

TCP的优点: 可靠,稳定 。TCP的可靠体现在TCP在传递数据之前,会有三次握手来建立连接,而且在数据传递时,有确认、窗口、重传、拥塞控制机制,在数据传完后,还会断开连接用来节约系统资源。

TCP的缺点: 慢,效率低,占用系统资源高,易被攻击。 TCP在传递数据之前,要先建连接,这会消耗时间,而且在数据传递时,确认机制、重传机制、拥塞控制机制等都会消耗大量的时间,而且要在每台设备上维护所有的传输连接,事实上,每个连接都会占用系统的CPU、内存等硬件资源。 而且,因为TCP有确认机制、三次握手机制,这些也导致TCP容易被人利用。

UDP的优点: 速度快,比TCP稍安全。 UDP没有TCP的握手、确认、窗口、重传、拥塞控制等机制,UDP是一个无状态的传输协议,所以它在传递数据时非常快。没有TCP的这些机制,UDP较TCP被攻击者利用的漏洞就要少一些。

UDP的缺点: 不可靠,不稳定。 因为UDP没有TCP那些可靠的机制,在数据传递时,如果网络质量不好,就会很容易丢包。

什么时候应该使用TCP:当对网络通讯质量有要求的时候,需要数据要准确无误的传递给对方。往往用于一些要求可靠的应用,比如HTTP、HTTPS、FTP等传输文件的协议,POP、SMTP等邮件传输的协议。

什么时候应该使用UDP:对网络通讯质量要求不高的时候,要求网络通讯速度能尽量的快。比如,日常生活中,常见使用UDP协议的应用如下: 直播视频、视频通话、TFTP ……

TCP三次握手

TCP 的三次握手,需要确认双方的两样能力: 发送的能力接收的能力。于是便会有下面的三次握手的过程:

image-20210306000936263

握手三次,改变双方状态四次。

  1. 最开始双方都处于CLOSED状态,然后服务器被动打开,开始监听某个端口,服务器进入了LISTEN状态,然后客户端主动打开,发送SYN请求报文,客户端进入SYN-SENT状态。

  2. 服务器接收到客户发送的SYN,返回SYN和ACK(对客户SYN的应答),服务器进入SYN-RCVD状态。

  3. 客户端接受到服务器的(SYN,ACK),再发送一个ACK(对服务器SYN的应答)给服务器,客户端进入ESTABLISHED状态。

  4. 服务器收到客户端的ACK,进入ESTABLISHED状态。

SYN需要消耗一个序列号,下次发送ACK的序列号要加1。

规则:凡是需要对端确认的,一定消耗TCP报文的序列号。

SYN 需要对端的确认, 而 ACK 并不需要,因此 SYN 消耗一个序列号而 ACK 不需要。

为什么不是两次握手,根本原因是:无法确认客户端的接收能力。分析如下:如果是两次,你现在发了 SYN 报文想握手,但是这个包滞留在了当前的网络中迟迟没有到达,TCP 以为这是丢了包,于是重传,两次握手建立好了连接。看似没有问题,但是连接关闭后,如果这个滞留在网路中的包到达了服务端呢?这时候由于是两次握手,服务端只要接收到然后发送相应的数据包,就默认建立连接,但是现在客户端已经断开了。这就带来了连接资源的浪费。

为什么不算四次握手,根本原因是:三次握手的目的是确认双方发送接收的能力,四次握手也可以,但为了解决问题,三次就足够了,再多用处就不大了。

三次握手过程中可以携带数据吗:第三次握手的时候,可以携带。前两次握手不能携带数据。如果前两次握手能够携带数据,那么一旦有人想攻击服务器,那么他只需要在第一次握手中的 SYN 报文中放大量数据,那么服务器势必会消耗更多的时间内存空间去处理这些数据,增大了服务器被攻击的风险。第三次握手的时候,客户端已经处于ESTABLISHED状态,并且已经能够确认服务器的接收、发送能力正常,这个时候相对安全了,可以携带数据。

同时被动打开:双方同时发SYN报文被动打开(没有调用listen监听端口),这是一个可能会发生的情况。

  • 在发送方给接收方发SYN报文的同时,接收方也给发送方发SYN报文,两者的状态都变为SYN-SENT
  • 在各自收到对方的SYN后,双方发送对应的SYN + ACK,两者状态都变为SYN-REVD
  • 双方接收到对应的SYN + ACK中的ACK后,两者状态一起变为ESTABLISHED,无需再为SYN + ACK中的SYN发送ACK

image-20210306162018852

TCP四次挥手

image-20210306152034474

刚开始双方处于ESTABLISHED状态。

客户端要断开了,向服务器发送 FIN 报文,发送后客户端变成了FIN-WAIT-1状态。这时候客户端同时也变成了half-close(半关闭)状态,即无法向服务端发送报文,只能接收。

服务端接收到FIN后向客户端发送ACK应答,变成了CLOSED-WAIT状态。

客户端接收到了服务端的确认,变成了FIN-WAIT-2状态。

随后,服务端向客户端发送FIN,自己进入LAST-ACK状态,客户端收到服务端发来的FIN后,发送 ACK 给服务端,并进入TIME-WAIT状态。客户端需要等待2 个 MSL(Maximum Segment Lifetime,报文最大生存时间), 在这段时间内如果客户端没有收到服务端的重发请求(会刷新2MSL),那么表示 ACK 成功到达,挥手结束,否则客户端重发 ACK。

释放TCP连接时,A要经历2MSL时间的TIME-WAIT状态,理由是:

  • 确保确保四次挥手中主动关闭方最后的 ACK 报文最终能达到对端,确保被动关闭方因未收到最后的 ACK 报文而重传的 (FIN, ACK)能够到达对端
  • 防止旧连接的重复分组被新连接接收

为什么是四次挥手而不是三次:因为服务端在接收到FIN, 往往不会立即返回FIN, 必须等到服务端所有的数据都发送完毕了,才能发FIN。因此先发一个ACK表示已经收到客户端的FIN,延迟一段时间才发FIN。这就造成了四次挥手。如果是三次挥手则等于说服务端将ACKFIN的发送合并为一次挥手,这个时候长时间的延迟可能会导致客户端误以为FIN没有到达客户端,从而让客户端不断的重发FIN

同时关闭会怎样

image-20210306161847754

半连接队列和SYN Flood攻击

三次握手前,服务端的状态从CLOSED变为LISTEN, 同时在内部创建了两个队列:半连接队列全连接队列,即SYN队列ACCEPT队列

  • 半连接队列:当客户端发送SYN到服务端,服务端收到以后回复ACKSYN,状态由LISTEN变为SYN_RCVD,此时这个连接就被推入了SYN队列,也就是半连接队列
  • 全连接队列:当客户端返回ACK, 服务端接收后,三次握手完成。这个时候连接等待被具体的应用取走,在被取走之前,它会被推入另外一个 TCP 维护的队列,也就是全连接队列(Accept Queue)

SYN Flood 攻击原理 SYN Flood 属于典型的 DoS/DDoS 攻击。其攻击的原理很简单,服务器端的资源分配是在二次握手时分配的,而客户端的资源是在完成三次握手时分配的,所以服务器容易受到SYN洪泛攻击,SYN攻击就是用客户端在短时间内伪造大量不存在的 IP 地址,并向服务端疯狂发送SYN

对于服务端而言,会产生两个危险的后果:

  1. 处理大量的SYN包并返回对应ACK, 势必有大量连接处于SYN_RCVD状态,从而占满整个半连接队列,正常的SYN请求因为队列满而被丢弃,无法处理正常的请求
  2. 由于是不存在的 IP,服务端长时间收不到客户端的ACK,会导致服务端不断重发数据,直到耗尽服务端的资源

如何应对 SYN Flood 攻击?

  1. 增加 SYN 连接,也就是增加半连接队列的容量。
  2. 减少 SYN + ACK 重试次数,避免大量的超时重发。
  3. 利用 SYN Cookie 技术,在服务端接收到SYN后不立即分配连接资源,而是根据这个SYN计算出一个Cookie,连同第二次握手回复给客户端,在客户端回复ACK的时候带上这个Cookie值,服务端验证 Cookie 合法之后才分配连接资源。
  4. 降低主机的等待时间使主机尽快的释放半连接的占用,短时间受到某IP的重复SYN则丢弃后续请求。

listen(int sockfd, int backlog)中的backlog参数用于限制未决连接的数量。

unp指出,backlog应该用于指定某个给定套接字上内核为之排队的最大已完成连接数:当已完成队列满的时候,内核将不再为该套接字接受新的客户连接请求。这样实现的好处是,应用程序不需要因为服务器需要处理大量连接请求(不管正常客户请求还是黑客攻击)而设置一个巨大的backlog值。即使在这样的解释下,传统的backlog值为5不够大的情形依然发生。

TCP报文头部字段

image-20210306215209087

唯一标识一个连接:TCP连接四元组——源 IP、源端口、目标 IP 和目标端口。

TCP 报文怎么没有源 IP 和目标 IP 呢?这是因为在 IP 层就已经处理了 IP 。TCP 只需要记录两者的端口即可。

序号是一个长为 4 个字节,也就是 32 位的无符号整数,表示范围为 0 ~ 2^32 - 1。如果到达最大值了后就循环到0。序列号在 TCP 通信的过程中有两个作用:

  1. 在 SYN 报文中交换彼此的初始序列号。
  2. 保证数据包按正确的顺序组装。

ISNInitial Sequence Number(初始序列号),在三次握手的过程当中,双方会用过SYN报文来交换彼此的 ISN。ISN 并不是一个固定的值,而是每 4 ms 加一,溢出则回到 0,这个算法使得猜测 ISN 变得很困难。那为什么要这么做?如果 ISN 被攻击者预测到,要知道源 IP 和源端口号都是很容易伪造的,当攻击者猜测 ISN 之后,直接伪造一个 RST 后,就可以强制连接关闭的,这是非常危险的。而动态增长的 ISN 大大提高了猜测 ISN 的难度。

6个标识位: URG 紧急指针,告诉接收TCP模块紧要指针域指着紧要数据。 ACK 置1时表示确认号(为合法,为0的时候表示数据段不包含确认信息,确认号被忽略。 PSH 置1时请求的数据段在接收方得到后就可直接送到应用程序,而不必等到缓冲区满时才传送。 RST 置1时重建连接。如果接收到RST位时候,通常发生了某些错误。 SYN 置1时用来发起一个连接。 FIN 置1时表示发端完成发送任务。用来释放连接,表明发送方已经没有数据发送了。

窗口大小占用两个字节,也就是 16 位,但实际上是不够用的。因此 TCP 引入了窗口缩放的选项,作为窗口缩放的比例因子,这个比例因子的范围在 0 ~ 14,比例因子可以将窗口的值扩大为原来的 2 ^ n 次方。

校验和占用两个字节,防止传输过程中数据包有损坏,如果遇到校验和有差错的报文,TCP 直接丢弃之,等待重传。

可选项:可选项的格式如下:

img

常用的可选项有以下几个:

  • TimeStamp: TCP 时间戳,后面详细介绍。
  • MSS: 指的是 TCP 允许的从对方接收的最大报文段。
  • SACK: 选择确认选项。
  • Window Scale:窗口缩放选项。

TCP 快速打开(TCP Fast Open, 即TFO)原理

使用SYN Cookie 实现 TFO。

TCP快速打开(TCP Fast Open,TFO)是对TCP的一种简化握手手续的拓展,用于提高两端点间连接的打开速度。简而言之,就是在TCP的三次握手过程中传输实际有用的数据。这个扩展最初在Linux系统实现,Linux服务器,Linux系统上的Chrome浏览器,或运行在Linux上的其他支持的软件。

通过握手开始时的SYN包中的TFO cookie来验证一个之前连接过的客户端。如果验证成功,它可以在三次握手最终的ACK包收到之前就开始发送数据,这样便跳过了一个绕路的行为,更在传输开始时就降低了延迟。这个加密的Cookie被存储在客户端,在一开始的连接时被设定好。然后每当客户端连接时,这个Cookie被重复返回。

img

https://www.cnblogs.com/Serverlessops/p/12249539.html

TCP报文中时间戳的作用?

timestamp是 TCP 报文首部的一个可选项,一共占 10 个字节,格式如下:

$kind(1 字节) + length(1 字节) + info(8 个字节)$

其中 kind = 8, length = 10, info 有两部分构成: timestamptimestamp echo,各占 4 个字节。

TCP 的时间戳主要解决两大问题:

  • 计算往返时延 RTT(Round-Trip Time)
  • 防止序列号的回绕问题

计算往返时延 比如现在ab发送一个报文s1ba回复ACK报文s2,那么:

  1. ab发送报文时,timestamp中存放的时间戳就是a主机此时的内核时间ta1
  2. ba回复报文时,timestamp中存放的就是b主机此时的时间tb1timestamp echo的值为从s1报文解析出来的时间ta1
  3. a收到b回复的s2报文之后,此时a主机的内核时间为ta2,而在s2报文的timestamp echo存放的是ta1

最终:RTT = ta2 - ta1

上面的RTT是从a主机计算出来,但是此时如果a主机再回复一个报文给b主机,那么在b主机一端便可以得到一个tb2 - tb1的值,也能计算出RTT

防止序列号回绕

如果出现了序列号相同的包,那应该怎么办呢?

虽然说序列号seq number占用了32位,可以表达的范围为0~2^32-1,这能用的完吗?但是你别忘了初始化序列号是不一定的,而且如果C-S发送数据的时间够长,那肯定是可以用完的。此时序列号便会出现回绕。

利用timestamp可以很好的解决此问题。因为每次发包时都会将内核的时间记录在报文内部,那么2个包的序列号即使相同,时间戳也不可能相同,这样就可以区分了。

TCP 的超时重传时间是如何计算

TCP 具有超时重传机制,即间隔一段时间没有等到数据包的回复时,重传这个数据包。

这个重传间隔也叫做超时重传时间(Retransmission TimeOut, 简称RTO),它的计算跟上一节提到的 RTT 密切相关。

两种主要的计算方法:

  • 经典方法
  • 标准方法

经典方法 经典方法引入了一个新的概念——SRTT(Smoothed round trip time,即平滑往返时间),每产生一次新的 RTT. 就根据一定的算法对 SRTT 进行更新,具体而言,计算方式如下(SRTT 初始值为0):

$SRTT = (α * SRTT) + ((1 - α) * RTT)$,其中,α 是平滑因子,建议值是0.8,范围是0.8 ~ 0.9

拿到 SRTT,就可以计算 RTO 的值了:

$RTO = min(ubound, max(lbound, β * SRTT))$,β 是加权因子,一般为1.3 ~ 2.0lbound 是下界,ubound 是上界。

其实这个算法过程还是很简单的,但是也存在一定的局限,就是在 RTT 稳定的地方表现还可以,而在 RTT 变化较大的地方就不行了,因为平滑因子 α 的范围是0.8 ~ 0.9, RTT 对于 RTO 的影响太小。

标准方法 为了解决经典方法对于 RTT 变化不敏感的问题,后面又引出了标准方法,也叫Jacobson / Karels 算法。

一共有三步:

  • 第一步: 计算SRTT,公式如下:$SRTT = (1 - α) * SRTT + α * RTT$

    注意这个时候的 α跟经典方法中的α取值不一样了,建议值是1/8,也就是0.125

  • 第二步: 计算RTTVAR(round-trip time variation)这个中间变量。

    $$RTTVAR = (1 - β) * RTTVAR + β * (|RTT - SRTT|)$$

    β 建议值为 0.25。这个值是这个算法中出彩的地方,也就是说,它记录了最新的 RTT 与当前 SRTT 之间的差值,给我们在后续感知到 RTT 的变化提供了抓手。

  • 第三步: 计算最终的RTO:$RTO = µ * SRTT + ∂ * RTTVAR$

    µ建议值取1, 建议值取4

这个公式在 SRTT 的基础上加上了最新 RTT 与它的偏移,从而很好的感知了 RTT 的变化,这种算法下,RTO 与 RTT 变化的差值关系更加密切。

TCP 的流量控制

对于发送端和接收端而言,TCP 需要把发送的数据放到发送缓存区, 将接收的数据放到接收缓存区。而流量控制索要做的事情,就是在通过接收缓存区的大小,控制发送端的发送。如果对方的接收缓存区满了,就不能再继续发送了。

流量控制就是让发送方的发送速率不要太快,要让接收方来得及接收。

要具体理解流量控制,首先需要了解滑动窗口的概念。

TCP 滑动窗口分为两种:

  • 发送窗口,包含四大部分:
    • 已发送且已确认
    • 已发送但未确认
    • 未发送但可以发送
    • 未发送也不可以发送
  • 接收窗口

TCP的窗口单位是字节,不是报文段。发送方的发送窗口不能超过接收方给出的接收窗口的数值。

利用滑动窗口机制可以很方便的在TCP连接上实现对发送方的流量控制

image-20201031203010356

以上处理的不足,存在死锁问题

  1. B向A发送了零窗口的报文段后不久,B的接收缓存又有了一些存储空间。于是B向A发送了rwnd = 400的报文段,然而这个报文段在传送过程中丢失了。
  2. A一直等待收到B发送的非零窗口的通知,而B也一直等待A发送的数据,如果没有其他措施,这种相互等待的死锁局面将一直持续下去。

解决方法

  1. TCP为每一个连接设有一个持续计时器。

  2. 只要TCP连接的一方收到对方的零窗口通知,就启动持续计时器。

  3. 若持续计时器设置的时间到期,就发送一个零窗口探测报文段(仅携带1字节的数据),而对方就在确认这个探测报文时给出了现在的窗口值。

  4. 如果窗口值仍然是零,那么收到这个报文段的一方就重新设置持续计时器。

  5. 如果窗口不是零,那么死锁的僵局就可以打破了。

https://www.cnblogs.com/ppzhang/p/10506237.html

TCP 的拥塞控制

在某段时间,若对网络中某一资源的需求超过了该资源所能提供的可用部分,网络性能就要变坏,这种情况叫做拥塞

所谓拥塞控制就是防止过多的数据注入到网络中,这样可以使网络中的路由器或链路不致过载。

拥塞控制的前提是:网络能够承受现有的网络负荷。拥塞控制是一个全局性的过程,设计到所有的主机、所有的路由器,以及降低网络传输性能有关的所有因素。

相反,流量控制往往是指点对点通信量的控制,是个端到端的问题。流量控制所要做的就是抑制发送端发送数据的速率,以便使接收端来得及接收。

image-20201031213929383

拥塞控制的两大方面:

  • 开环控制:设计网络时事先将有关发生拥塞的因素考虑周到,力求网络在工作时不产生拥塞。但一旦整个系统运行起来,就不再中途进行改正了。
  • 闭环控制:基于反馈环路的概念,主要有几种措施:
    • 监测网络系统以便检测到拥塞在何时、何处发生
    • 把拥塞发生的信息传送到可采取行动的地方
    • 调整网络系统的运行以解决出现的问题

TCP进行拥塞控制的算法有四种:

  • 慢开始
  • 拥塞避免
  • 快重传
  • 快恢复

发送方维持一个叫做拥塞窗口cwnd的状态变量,拥塞窗口的大小取决于网络的拥塞程度,并且动态地在变化。当接收方总是有足够大的缓存空间时,发送方让自己的发送窗口等于拥塞窗口。

判断网络拥塞的依据就是出现了超时。

image-20201101170414504

相关变量:(用报文段的个数作为窗口大小的单位

  • cwnd:拥塞窗口大小
  • SMSS:发送方的最大报文段
  • ssthresh:慢开始门限,用来防止cwnd增长过大引起网络拥塞
    • 当$cwnd \lt ssthresh$,使用慢开始算法
    • 当$cwnd \gt ssthresh$,停止使用慢开始算法而改用拥塞避免算法
    • 当$cwnd = ssthresh$,既可使用慢开始算法,也可使用拥塞避免算法
  • 传输轮次:一个传输轮次所经历的时间其实就是往返时间RTT。强调:把拥塞窗口cwnd所允许发送的报文段都连续发送出去,并收到了对已发送的最后一个字节的确认。

慢开始算法:每经过一个传输轮次,拥塞窗口cwnd就加倍。

拥塞避免算法:每经过一个传输轮次,拥塞窗口cwnd就加一。拥塞避免并非完全能够避免了拥塞,是说把拥塞窗口控制为按线性规律增长,使网络比较不容易出现拥塞。

快重传算法可以让发送方尽早知道发生了个别报文段的丢失。快重传算法首先要求对方不要等待自己发送数据时才进行捎带确认,而是要立即发送确认,即使收到了失序的报文段也要立即发出对已收到的报文段的重复确认。

image-20201101171524869

发送方知道只是丢失了个别的报文段时,不启动慢开始,而是执行快恢复算法:

  • ssthresh = cwnd/2
  • cwnd = ssthresh
  • 执行拥塞避免算法

在拥塞避免阶段,拥塞窗口是按照线性规律增大的,常称为加法增大AI

一旦出现超时或3个重复确认,就要把门限值设置为当前拥塞窗口值的一半,并大大减小拥塞窗口的数值,常称为乘法减小MD

二者合一起就是AIMD算法。

image-20201101171933600

接收方根据自己的接收能力设定了接收方窗口rwnd,并把该窗口值写入TCP首部中的窗口字段,传给发送方。接收方窗口又称为通知窗口。

发送方的发送窗口一定不能够超过对方给出的接收方窗口值rwnd,则:

发送方窗口的上限值=Min[rwnd, cwnd]
  • 当$rwnd \lt cwnd$:接收方的接收能力限制发送方窗口的最大值
  • 当$rwnd \gt cwnd$:网络的拥塞程度限制发送方窗口的最大值
  • rwnd和cwnd中数值较小的一个,控制了发送方发送数据的速率

网络层的策略对TCP拥塞控制影响最大的就是路由器的分组丢弃策略。最简单的情况下是使用尾部丢弃策略,路由器的尾部丢弃往往会导致一连串的分组丢失,使发送方出现超时重传,使TCP进入慢开始状态,尾部丢弃可能同时影响很多条TCP连接,使很多TCP连接在同一时间突然都进入慢开始状态,这在TCP术语中称为全局同步。全局同步使得全网的通信量突然下降很多,而在网络恢复后,其通信量又突然增大很多。

为了避免网络中的全局同步现象,提出了主动队列管理AQM,AQM早期的实现方案是随机早期检测RED

Nagle 算法和延迟确认

Nagle 算法 试想一个场景,发送端不停地给接收端发很小的包,一次只发 1 个字节,那么发 1 千个字节需要发 1000 次。这种频繁的发送是存在问题的,不光是传输的时延消耗,发送和确认本身也是需要耗时的,频繁的发送接收带来了巨大的时延。而避免小包的频繁发送,这就是 Nagle 算法要做的事情。具体来说,Nagle 算法的规则如下:

  • 当第一次发送数据时不用等待,就算是 1byte 的小包也立即发送

  • 后面发送满足下面条件之一就可以发了:

    • 数据包大小达到最大段大小(Max Segment Size, 即 MSS)
    • 之前所有包的 ACK 都已接收到

延迟确认 试想这样一个场景,当我收到了发送端的一个包,然后在极短的时间内又接收到了第二个包,那我是一个个地回复,还是稍微等一下,把两个包的 ACK 合并后一起回复呢?延迟确认(delayed ack)所做的事情,就是后者,稍稍延迟,然后合并 ACK,最后才回复给发送端。TCP 要求这个延迟的时延必须小于500ms,一般操作系统实现都不会超过200ms。不过需要主要的是,有一些场景是不能延迟确认的,收到了就要马上回复:

  • 接收到了大于一个 frame 的报文,且需要调整窗口大小
  • TCP 处于 quickack 模式(通过tcp_in_quickack_mode设置)
  • 发现了乱序包

两者一起使用会怎样? 前者意味着延迟发,后者意味着延迟接收,会造成更大的延迟,产生性能问题。

如何理解 TCP 的 keep-alive

大家都听说过 http 的 keep-alive, 不过 TCP 层面也是有keep-alive机制,而且跟应用层不太一样。试想一个场景,当有一方因为网络故障或者宕机导致连接失效,由于 TCP 并不是一个轮询的协议,在下一个数据包到达之前,对端对连接失效的情况是一无所知的。这个时候就出现了 keep-alive, 它的作用就是探测对端的连接有没有失效。

在 Linux 下,可以这样查看相关的配置:sudo sysctl -a | grep keepalive

// 每隔 7200 s 检测一次
net.ipv4.tcp_keepalive_time = 7200
// 一次最多重传 9 个包
net.ipv4.tcp_keepalive_probes = 9
// 每个包的间隔重传间隔 75 s
net.ipv4.tcp_keepalive_intvl = 75

不过,现状是大部分的应用并没有默认开启 TCP 的keep-alive选项,为什么?站在应用的角度:

  • 7200s 也就是两个小时检测一次,时间太长
  • 时间再短一些,也难以体现其设计的初衷, 即检测长时间的死连接