首页 计算机网络 - 传输层1
文章
取消

计算机网络 - 传输层1

计算机网络 - 传输层1

传输层

传输层的功能

  1. 传输层提供进程与进程之间的逻辑通信。使用网络层的服务为应用层提供通信服务
  2. 复用和分用

    • 复用:应用层所有的应用进程都可以通过传输层传输到网络层。 - 分用:传输层从网络层收到的数据可以交付给不同的应用进程。
  3. 对收到的报文进行差错检测

TCP

三次握手QQ截图20220623022220

ACK不消耗seq,所以如果自己的上一个包是ACK的话(报文不携带数据),这次的seq还是上次的seq

图里说的上一次自己是ACK则这次seq就是上一次的seq,指的是纯ACK包。也就是报文不传输数据的时候

这里ack = seq + 1 里面的1其实是接收到的数据的大小。 但是握手阶段不存在这个数据,为了更好理解为何握手/挥手阶段seq/ack还是要+1,我们统一给她一个解释:这个握手阶段除了ack以外的包所搭载的数据叫ghost byte,大小为1。第二种解释是数据大小依旧为0。但是FIN/SYN按照一个字节算。所以+1

ack确认号是我们下一次希望收到的发送数据的第一个字节的序号 。当丢包发生的时候。ack确认号就是接收方所期待的序号最小的没拿到的数据包。所以这种确认机制可以实现累计确认。

所以在握手阶段 我们客户端首次发送的seq = x,则我们下一次希望收到的发送数据的第一个字节的序号 应为x+1 所以服务器发回的ack = x + 1

所以ack计算方式是 ack(out) = seq(last_time_in) + size_of_received_data + (in)SYN/FIN(1)

` seq序列号是本次所发送数据的第一个字节的序号。假如我们发送 1 2 3 所以seq1 接着我们发送4 5 6 所以seq4 也就是上一次序号+上一次自己的数据量 = 1 + 3 = 4`

seq的计算方式是 seq(out) = 上一次自己的seq + 上一次自己的size_of_data + 上一次自己的SYN/FIN(1)

一般来讲,对方发回的ack就是自己下一次的seq

因为采用了延迟确认,客户端连续多次发送数据后,服务器传回的ack将会是最后一次接收到的客户端报文的seq + 收到的data。如下图。最后一次接收到的seq是5121,证明5121之前的数据包都拿到了,大小是1024。所以截止至客户端新发送数据之前,服务器回复的每一次ack都是5121+1024 = 6145

注意大小写。小写ack是序号,大写ACK指的是这个包是不是ACK

所以说ACK = 1代表是确认包,所以里面会带有ack确认序号。也就是希望对面继续发送的数据的第一个字节的序号。

序列号seq解决乱序问题。ack应答码解决丢包问题

QQ截图20220623195418

延迟确认

简单的说,Delay Ack就是延时发送ACK,在收到数据包的时候,会检查是否需要发送ACK,如果需要的话,进行快速ACK还是延时ACK,在无法使用快速确认的条件下,就会使用Delay Ack。

TCP在何时发送ACK的时候有如下规定:

1.当有响应数据发送的时候,ACK会随着数据一块发送

2.如果没有响应数据,ACK就会有一个延迟,以等待是否有响应数据一块发送,但是这个延迟一般在40ms~500ms之间,一般情况下在40ms左右,如果在40ms内有数据发送,那么ACK会随着数据一块发送,对于这个延迟的需要注意一下,这个延迟并不是指的是收到数据到发送ACK的时间延迟,而是内核会启动一个定时器,每隔200ms就会检查一次,比如定时器在0ms启动,200ms到期,180ms的时候data来到,那么200ms的时候没有响应数据,ACK仍然会被发送,这个时候延迟了20ms.

3.如果在等待发送ACK期间,第二个数据又到了,这时候就要立即发送ACK!

优点:减少了数据段的个数,提高了发送效率

缺点:过多的delay会拉长RTT

累计确认

累计确认指的是TCP当前的ack应答码一定是保证了这个应答码之前的所有数据已经全部收到了。

  • 所以在丢包的时候,我们回复的ack应答码是丢失的数据的第一个字节(接收方所期待的序号最小的没拿到的数据包)。
  • 所以如果没有丢包而是丢了ACK应答报文,则由于滑动窗口的存在,所以我们可以用这种累计确认(下一次应答)来进行确认,而且不会进行数据重发。

QQ截图20220805034441

四次挥手 (主动关闭的一方才有timewait)

QQ截图20220623024124

  • 当客户端发送链接释放报文段之后,客户端停止发送数据。主动关闭TCP链接。也就是第一次FIN。之后客户端进入 FIN_WAIT_1 状态。(此时仅仅表示客户端不需要发送数据了,但还可以接受数据)
  • 服务器接受到客户端发送的释放报文段后,回送一个ack报文段。客户到服务器的这个方向的连接也就被释放了。此时是半关闭状态。服务端进入 CLOSED_WAIT 状态。(此时表示为服务端知道了客户端不发送了,但是服务端可能还要发送
  • 客户端收到服务端的 ACK 应答报文后,之后进入 FIN_WAIT_2 状态。
  • 服务器发送/处理完剩余数据后,也发送链接释放报文段。主动关闭TCP链接。也就是第二次FIN。之后服务端进入 LAST_ACK 状态。(服务端处理完剩下的数据后,才发送FIN表示我也完事儿了。可以彻底关闭
  • 客户端接受到后,回送一个ack报文段。进入 TIME_WAIT 状态。服务器收到了 ACK 应答报文后,就进入了 CLOSED 状态,至此服务端已经完成连接的关闭。
  • 再等到时间等待计时器设置的2MSL(最长报文段寿命)后,客户端进入 CLOSED 状态,链接彻底关闭。

如果客户端发送的确认报文段

注意事项

  1. 主动关闭连接的,才有 TIME_WAIT 状态
  2. 四次挥手之所以为四次就是因为可以半关闭状态。也就是服务器可能还要发送一些数据给客户端。理论上四次挥手也可以换成三次。如果服务器在收到客户端的 FIN 时没有更多数据或根本没有数据要发送,则可以将 ack 和 fin 合并为一个包。 或者,因为延迟确认的特性,可以把ACK,FIN(第二次和第三次挥手)和要发的数据一起发过去,这样也是三次挥手。
  3. 需要三次握手而不是两次握手的原因之一是:为了防止已失效的连接请求报文段突然又传送到了服务端,因而产生错误。
    • 假设客户端先发了一个序列号为90的SYN请求报文, 但是它在网络中某个节点被阻塞了, 然后客户端又发送了另一个新的SYN请求报文, 序列号为100, 但是旧的请求报文比新的先到达服务端, 然后服务端回返回一个SYN ACK报文, 其中ACK确认号为91, 这显然是错的, 客户端收到该ACK报文之后, 发现确认号为91而不是101, 判定这是一个历史连接, 客户端就发送一个RST报文回去告知服务端, 终止这次连接。
  4. 在连接建立后,所有传送的报文段都必须把ACK置为1。
  5. TCP 不会为没有数据的ACK包进行重传。当没有数据的ACK 包丢失了,就由对方重传对应的报文
  6. 握手阶段的前两次不可以携带数据。第三次ACK可以携带数据。也就是SYN=1的报文段不可以携带数据。
  7. 挥手阶段的FIN包可以携带数据

挥手比握手多一次的原因就是,握手阶段,前两次无法发送数据,所以响应和发送(SYN和ACK)可以要连起来。挥手阶段一方关闭连接了另一方还可能发送。所以响应和发送(FIN和ACK)是分开的

握手丢失会发生什么?

第一次握手丢失了,会发生什么? – 客户端超时重传SYN,超过次数断开。

客户端想和服务端建立 TCP 连接的时候,首先第一个发的就是 SYN 报文,然后进入到 SYN_SENT 状态。

在这之后,如果客户端迟迟收不到服务端的 SYN-ACK 报文(第二次握手),就会触发「超时重传」机制,重传 SYN 报文。

不同版本的操作系统可能超时时间不同,有的 1 秒的,也有 3 秒的,这个超时时间是写死在内核里的,如果想要更改则需要重新编译内核,比较麻烦。重传的 SYN 报文的序列号都是一样的

当客户端在 1 秒后没收到服务端的 SYN-ACK 报文后,客户端就会重发 SYN 报文,那到底重发几次呢?

在 Linux 里,客户端的 SYN 报文最大重传次数由 tcp_syn_retries内核参数控制,这个参数是可以自定义的,默认值一般是 5。

通常,第一次超时重传是在 1 秒后,第二次超时重传是在 2 秒,第三次超时重传是在 4 秒后,第四次超时重传是在 8 秒后,第五次是在超时重传 16 秒后。没错,每次超时的时间是上一次的 2 倍

当第五次超时重传后,会继续等待 32 秒,如果服务端仍然没有回应 ACK,客户端就不再发送 SYN 包,然后断开 TCP 连接。

所以,总耗时是 1+2+4+8+16+32=63 秒,大约 1 分钟左右。

当第一次握手一直丢失时,发生的过程如下图

QQ截图20220824195815

  • 当客户端超时重传 3 次 SYN 报文后,由于 ` tcp_syn_retries 为 3,已达到最大重传次数,于是再等待一段时间(时间为上一次超时时间的 2 倍),如果还是没能收到服务端的第二次握手(SYN-ACK `报文),那么客户端就会断开连接

第二次握手丢失了,会发生什么? – 客户端超时重传SYN,服务端超时重传SYN-ACK,超过次数断开

当服务端收到客户端的第一次握手后,就会回 SYN-ACK 报文给客户端,这个就是第二次握手,此时服务端会进入 SYN_RCVD 状态。

第二次握手的 SYN-ACK 报文其实有两个目的 :

  • 第二次握手里的 ACK, 是对第一次握手的确认报文;
  • 第二次握手里的 SYN,是服务端发起建立 TCP 连接的报文;

所以,如果第二次握手丢了,就会发送比较有意思的事情,具体会怎么样呢?

因为第二次握手报文里是包含对客户端的第一次握手的 ACK 确认报文,所以,如果客户端迟迟没有收到第二次握手,那么客户端就觉得可能自己的 SYN 报文(第一次握手)丢失了,于是客户端就会触发超时重传机制,重传 SYN 报文

然后,因为第二次握手中包含服务端的 SYN 报文,所以当客户端收到后,需要给服务端发送 ACK 确认报文(第三次握手),服务端才会认为该 SYN 报文被客户端收到了。

那么,如果第二次握手丢失了,服务端就收不到第三次握手,于是服务端这边会触发超时重传机制,重传 SYN-ACK 报文

在 Linux 下,SYN-ACK 报文的最大重传次数由 tcp_synack_retries内核参数决定,默认值是 5。

因此,当第二次握手丢失了,客户端和服务端都会重传:

  • 客户端会重传 SYN 报文,也就是第一次握手,最大重传次数由 tcp_syn_retries内核参数决定;
  • 服务端会重传 SYN-ACK 报文,也就是第二次握手,最大重传次数由 tcp_synack_retries 内核参数决定。

当第二次握手一直丢失时,发生的过程如下图

QQ截图20220824195907

  • 当客户端超时重传 1 次 SYN 报文后,由于 ` tcp_syn_retries 为 1,已达到最大重传次数,于是再等待一段时间(时间为上一次超时时间的 2 倍),如果还是没能收到服务端的第二次握手(SYN-ACK` 报文),那么客户端就会断开连接。
  • 当服务端超时重传 2 次 SYN-ACK 报文后,由于 tcp_synack_retries 为 2,已达到最大重传次数,于是再等待一段时间(时间为上一次超时时间的 2 倍),如果还是没能收到客户端的第三次握手(ACK 报文),那么服务端就会断开连接。

第三次握手丢失了,会发生什么? – 服务端超时重传SYN-ACK,超过次数断开。因为客户端的纯ACK(不带数据的ACK)是不会重传的。如果此时客户端发送数据,则服务器会发送RST表示断开连接。

客户端收到服务端的 SYN-ACK 报文后,就会给服务端回一个 ACK 报文,也就是第三次握手,此时客户端状态进入到 ESTABLISH 状态。

因为这个第三次握手的 ACK 是对第二次握手的 SYN 的确认报文,所以当第三次握手丢失了,如果服务端那一方迟迟收不到这个确认报文,就会触发超时重传机制,重传 SYN-ACK 报文,直到收到第三次握手,或者达到最大重传次数。

当第三次握手一直丢失时,发生的过程如下图

QQ截图20220824200047

  • 服务端超时重传 2 次 SYN-ACK 报文后,由于 tcp_synack_retries 为 2,已达到最大重传次数,于是再等待一段时间(时间为上一次超时时间的 2 倍),如果还是没能收到客户端的第三次握手(ACK 报文),那么服务端就会断开连接。

挥手丢失会发生什么?

第一次挥手丢失了,会发生什么? –客户端超时重传FIN,超过次数直接close

当客户端(主动关闭方)调用 close 函数后,就会向服务端发送 FIN 报文,试图与服务端断开连接,此时客户端的连接进入到 FIN_WAIT_1 状态。

正常情况下,如果能及时收到服务端(被动关闭方)的 ACK,则会很快变为 FIN_WAIT2状态。

如果第一次挥手丢失了,那么客户端迟迟收不到被动方的 ACK 的话,也就会触发超时重传机制,重传 FIN 报文,重发次数由 tcp_orphan_retries 参数控制。

当客户端重传 FIN 报文的次数超过 tcp_orphan_retries 后,就不再发送 FIN 报文,直接进入到 close 状态。

当第一次挥手一直丢失时,发生的过程如下图

QQ截图20220824203900

  • 当客户端超时重传 3 次 FIN 报文后,由于 tcp_orphan_retries 为 3,已达到最大重传次数,于是再等待一段时间(时间为上一次超时时间的 2 倍),如果还是没能收到服务端的第二次挥手(ACK报文),那么客户端就会断开连接。

第二次挥手丢失了,会发生什么?– 客户端超时重传FIN,超过次数直接close。因为服务器的纯ACK不会重传

当服务端收到客户端的第一次挥手后,就会先回一个 ACK 确认报文,此时服务端的连接进入到 CLOSE_WAIT 状态。

在前面我们也提了,ACK 报文是不会重传的,所以如果服务端的第二次挥手丢失了,客户端就会触发超时重传机制,重传 FIN 报文,直到收到服务端的第二次挥手,或者达到最大的重传次数。

这里提一下,当客户端收到第二次挥手,也就是收到服务端发送的 ACK 报文后,客户端就会处于 FIN_WAIT2 状态,在这个状态需要等服务端发送第三次挥手,也就是服务端的 FIN 报文。

QQ截图20220824204227

对于 close 函数关闭的连接,由于无法再发送和接收数据,所以FIN_WAIT2 状态不可以持续太久,而 tcp_fin_timeout 控制了这个状态下连接的持续时长,默认值是 60 秒。

这意味着对于调用 close 关闭的连接,如果在 60 秒后还没有收到 FIN 报文,客户端(主动关闭方)的连接就会直接关闭。

但是注意,如果主动关闭方使用 shutdown 函数关闭连接且指定只关闭发送方向,而接收方向并没有关闭,那么意味着主动关闭方还是可以接收数据的。如果主动关闭方一直没收到第三次挥手,那么主动关闭方的连接将会一直处于 FIN_WAIT2 状态(tcp_fin_timeout 无法控制 shutdown 关闭的连接)。

QQ截图20220824204236

当第二次挥手一直丢失时,发生的过程如下图

QQ截图20220824204001

  • 当客户端超时重传 2 次 FIN 报文后,由于 tcp_orphan_retries 为 2,已达到最大重传次数,于是再等待一段时间(时间为上一次超时时间的 2 倍),如果还是没能收到服务端的第二次挥手(ACK 报文),那么客户端就会断开连接。

第三次挥手丢失了,会发生什么?– 服务端超时重传FIN,超过次数直接close

当服务端(被动关闭方)收到客户端(主动关闭方)的 FIN 报文后,内核会自动回复 ACK,同时连接处于 CLOSE_WAIT 状态,顾名思义,它表示等待应用进程调用 close 函数关闭连接。

此时,内核是没有权利替代进程关闭连接,必须由进程主动调用 close 函数来触发服务端发送 FIN 报文。

服务端处于 CLOSE_WAIT 状态时,调用了 close 函数,内核就会发出 FIN 报文,同时连接进入 LAST_ACK 状态,等待客户端返回 ACK 来确认连接关闭。

如果客户端迟迟收不到这个 ACK,服务端就会重发 FIN 报文,重发次数仍然由 tcp_orphan_retries 参数控制,这与客户端重发 FIN 报文的重传次数控制方式是一样的。

当第三次挥手一直丢失时,发生的过程如下图

QQ截图20220824204407

  • 当服务端重传第三次挥手报文的次数达到了 3 次后,由于 tcp_orphan_retries 为 3,达到了重传最大次数,于是再等待一段时间(时间为上一次超时时间的 2 倍),如果还是没能收到客户端的第四次挥手(ACK报文),那么服务端就会断开连接。
  • 客户端因为是通过 close 函数关闭连接的,处于 FIN_WAIT_2 状态是有时长限制的,如果 tcp_fin_timeout 时间内还是没能收到服务端的第三次挥手(FIN 报文),那么客户端就会断开连接。

第四次挥手丢失了,会发生什么?– 服务端超时重传FIN,超过次数断开。因为客户端的纯ACK不会重传。

当客户端收到服务端的第三次挥手的 FIN 报文后,就会回 ACK 报文,也就是第四次挥手,此时客户端连接进入 TIME_WAIT 状态。

在 Linux 系统,TIME_WAIT 状态会持续 2MSL 后才会进入关闭状态。

然后,服务端(被动关闭方)没有收到 ACK 报文前,还是处于 LAST_ACK 状态。

如果第四次挥手的 ACK 报文没有到达服务端,服务端就会重发 FIN 报文,重发次数仍然由前面介绍过的 tcp_orphan_retries 参数控制。

当第四次挥手一直丢失时,发生的过程如下图

QQ截图20220824204725

  • 当服务端重传第三次挥手报文达到 2 时,由于 tcp_orphan_retries 为 2, 达到了最大重传次数,于是再等待一段时间(时间为上一次超时时间的 2 倍),如果还是没能收到客户端的第四次挥手(ACK 报文),那么服务端就会断开连接。
  • 客户端在收到第三次挥手后,就会进入 TIME_WAIT 状态,开启时长为 2MSL 的定时器,如果途中再次收到第三次挥手(FIN 报文)后,就会重置定时器,当等待 2MSL 时长后,客户端就会断开连接。

关于 TIME_WAIT

为什么需要 TIME_WAIT 状态?

需要 TIME-WAIT 状态,主要是两个原因:

  • 防止历史连接中的数据,被后面相同四元组的连接错误的接收。

    服务端在关闭连接之前发送的报文,被网络延迟了。接着,服务端以相同的四元组重新打开了新连接,前面被延迟的报文这时抵达了客户端,而且该数据报文的序列号刚好在客户端接收窗口内,因此客户端会正常接收这个数据报文,但是这个数据报文是上一个连接残留下来的,这样就产生数据错乱等严重的问题。为了防止历史连接中的数据,被后面相同四元组的连接错误的接收,因此 TCP 设计了 TIME_WAIT 状态,状态会持续 2MSL 时长,这个时间足以让两个方向上的数据包都被丢弃,使得原来连接的数据包在网络中都自然消失,再出现的数据包一定都是新建立连接所产生的。

  • 保证「被动关闭连接」的一方,能被正确的关闭

    如果客户端(主动关闭方)最后一次 ACK 报文(第四次挥手)在网络中丢失了,那么按照 TCP 可靠性原则,服务端(被动关闭方)会重发 FIN 报文。假设客户端没有 TIME_WAIT 状态,而是在发完最后一次回 ACK 报文就直接进入 CLOSED 状态,如果该 ACK 报文丢失了,服务端则重传的 FIN 报文,而这时客户端已经进入到关闭状态了,在收到服务端重传的 FIN 报文后,就会回 RST 报文。服务端收到这个 RST 并将其解释为一个错误(Connection reset by peer),这对于一个可靠的协议来说不是一个优雅的终止方式。

    简单来说就是,如果第四次握手丢了,服务端会超时重传FIN,如果没有TIME_WAIT(客户端直接close)则客户端会回复一个RST。不够优雅。

    为了防止这种情况出现,客户端必须等待足够长的时间确保对端收到 ACK,如果对端没有收到 ACK,那么就会触发 TCP 重传机制,服务端会重新发送一个 FIN,这样一去一来刚好两个 MSL 的时间。

为什么 TIME_WAIT 等待的时间是 2MSL?

MSL 是 Maximum Segment Lifetime,报文最大生存时间,它是任何报文在网络上存在的最长时间,超过这个时间报文将被丢弃。因为 TCP 报文基于是 IP 协议的,而 IP 头中有一个 TTL 字段,是 IP 数据报可以经过的最大路由数,每经过一个处理他的路由器此值就减 1,当此值为 0 则数据报将被丢弃,同时发送 ICMP 报文通知源主机。

MSL 与 TTL 的区别: MSL 的单位是时间,而 TTL 是经过路由跳数。所以 MSL 应该要大于等于 TTL 消耗为 0 的时间,以确保报文已被自然消亡。

TTL 的值一般是 64,Linux 将 MSL 设置为 30 秒,意味着 Linux 认为数据报文经过 64 个路由器的时间不会超过 30 秒,如果超过了,就认为报文已经消失在网络中了

TIME_WAIT 等待 2 倍的 MSL,比较合理的解释是: 网络中可能存在来自发送方的数据包,当这些发送方的数据包被接收方处理后又会向对方发送响应,所以一来一回需要等待 2 倍的时间

比如,如果被动关闭方没有收到断开连接的最后的 ACK 报文,就会触发超时重发 FIN 报文,另一方接收到 FIN 后,会重发 ACK 给被动关闭方, 一来一去正好 2 个 MSL。

可以看到 2MSL时长 这其实是相当于至少允许报文丢失一次。比如,若 ACK 在一个 MSL 内丢失,这样被动方重发的 FIN 会在第 2 个 MSL 内到达,TIME_WAIT 状态的连接可以应对。

2MSL 的时间是从客户端接收到 FIN 后发送 ACK 开始计时的。如果在 TIME-WAIT 时间内,因为客户端的 ACK 没有传输到服务端,客户端又接收到了服务端重发的 FIN 报文,那么 2MSL 时间将重新计时

在 Linux 系统里 2MSL 默认是 60 秒,那么一个 MSL 也就是 30 秒。Linux 系统停留在 TIME_WAIT 的时间为固定的 60 秒

半连接队列和全连接队列

在服务端当中,对socket执行bind方法可以绑定监听端口,然后执行listen方法后,就会进入监听(LISTEN)状态。内核会为每一个处于LISTEN状态的socket 分配两个队列,分别叫半连接队列和全连接队列

QQ截图20220629093515

  • 半连接队列(SYN队列),服务端收到第一次握手后,会将这个连接对应的文件描述符加入到这个队列中,队列内的文件描述符对应的连接都处于SYN_RCVD 状态。
  • 全连接队列(ACCEPT队列),在服务端收到第三次握手后,会将半连接队列的文件描述符取出,放到全连接队列中。队列里的文件描述符对应的连接都处于 ESTABLISHED状态。这里面的文件描述符,就等着服务端执行accept()后被取出了。

全连接队列(icsk_accept_queue)是个链表,而半连接队列(syn_table)是个哈希表

所以建立连接的过程中根本不需要accept() 参与, 执行accept()只是为了从全连接队列里取出对应的文件描述符。

listen函数的backlog控制的是全连接队列(ACCEPT队列)队列的大小。

为什么半连接队列要设计成哈希表

先对比下全连接里队列,他本质是个链表,因为也是线性结构,说它是个队列也没毛病。它里面放的都是已经建立完成的连接,这些连接正等待被取走。而服务端取走连接的过程中,并不关心具体是哪个连接,只要是个连接就行,所以直接从队列头取就行了。这个过程算法复杂度为O(1)

半连接队列却不太一样,因为队列里的都是不完整的连接,嗷嗷等待着第三次握手的到来。那么现在有一个第三次握手来了,则需要从队列里把相应IP端口的连接取出,如果半连接队列还是个链表,那我们就需要依次遍历,才能拿到我们想要的那个连接,算法复杂度就是O(n)。

而如果将半连接队列设计成哈希表,那么查找半连接的算法复杂度就回到O(1)了。因此出于效率考虑,全连接队列被设计成链表,而半连接队列被设计为哈希表。

全连接队列满了会怎么样?

如果队列满了,服务端还收到客户端的第三次握手ACK,默认当然会丢弃这个ACK。但除了丢弃之外,还有一些附带行为,这会受 tcp_abort_on_overflow 参数的影响。

  • tcp_abort_on_overflow设置为 0,全连接队列满了之后,会丢弃这个第三次握手ACK包,并且开启定时器,重传第二次握手的SYN+ACK,如果重传超过一定限制次数,还会把对应的半连接队列里的连接给删掉。也就是丢弃链接

QQ截图20220629094504

  • tcp_abort_on_overflow设置为 1,全连接队列满了之后,就直接发RST给客户端,效果上看就是连接断了。

QQ截图20220629094528

半连接队列要是满了会怎么样 (SYN攻击)

一般是丢弃,但这个行为可以通过 tcp_syncookies 参数去控制。但比起这个,更重要的是先了解下半连接队列为什么会被打满。

首先我们需要明白,一般情况下,半连接的”生存”时间其实很短,只有在第一次和第三次握手间,如果半连接都满了,说明服务端疯狂收到第一次握手请求,如果是线上游戏应用,能有这么多请求进来,那说明你可能要富了。但现实往往比较骨感,你可能遇到了SYN Flood攻击

所谓SYN Flood攻击,可以简单理解为,攻击方模拟客户端疯狂发第一次握手请求过来,在服务端憨憨地回复第二次握手过去之后,客户端死活不发第三次握手过来,这样做,可以把服务端半连接队列打满,从而导致正常连接不能正常进来。

那这种情况怎么处理?有没有一种方法可以绕过半连接队列

有,tcp_syncookies派上用场了。它被设置为1的时候,客户端发来第一次握手SYN时,服务端不会将其放入半连接队列中,而是直接生成一个cookies,这个cookies会跟着第二次握手,发回客户端。客户端在发第三次握手的时候带上这个cookies,服务端验证到它就是当初发出去的那个,就会建立连接并放入到全连接队列中。可以看出整个过程不再需要半连接队列的参与。

QQ截图20220629094704

cookies方案为什么不直接取代半连接队列?

目前看下来syn cookies方案省下了半连接队列所需要的队列内存,还能解决 SYN Flood攻击,那为什么不直接取代半连接队列?

凡事皆有利弊,cookies方案虽然能防 SYN Flood攻击,但是也有一些问题。因为服务端并不会保存连接信息,所以如果传输过程中数据包丢了,也不会重发第二次握手的信息。

另外,编码解码cookies,都是比较耗CPU的,利用这一点,如果此时攻击者构造大量的第三次握手包(ACK包),同时带上各种瞎编的cookies信息,服务端收到ACK包以为是正经cookies,憨憨地跑去解码(耗CPU),最后发现不是正经数据包后才丢弃。

这种通过构造大量ACK包去消耗服务端资源的攻击,叫ACK攻击,受到攻击的服务器可能会因为CPU资源耗尽导致没能响应正经请求。

全连接队列,半连接队列总结

  • 每一个socket执行listen时,内核都会自动创建一个半连接队列和全连接队列。
  • 第三次握手前,TCP连接会放在半连接队列中,直到第三次握手到来,才会被放到全连接队列中。
  • accept方法只是为了从全连接队列中拿出一条连接,本身跟三次握手几乎毫无关系
  • 出于效率考虑,虽然都叫队列,但半连接队列其实被设计成了哈希表,而全连接队列本质是链表。
  • 全连接队列满了,再来第三次握手也会丢弃,此时如果tcp_abort_on_overflow=1,还会直接发RST给客户端。
  • 半连接队列满了,可能是因为受到了SYN Flood攻击,可以设置tcp_syncookies,绕开半连接队列。
  • 客户端没有半连接队列和全连接队列,但有一个全局hash,可以通过它实现自连接或TCP同时打开。

杂项

  • 同步位SYN = 1 时表明这是一个连接请求/接受报文。

  • 紧急位URG = 1 的时候表明此报文段有紧急数据。不用在缓存队列中排队。配合紧急指针使用。

  • 紧急指针指向的是紧急数据在此报文段中的末位位置。假如紧急指针 = 100,则1~100为紧急数据。

  • 复位RST = 1的时候表明TCP连接出现严重错误。需要释放连接再重新连接

  • 推送位PSH = 1时表明接收方应尽快将数据交付给应用程序,不需要等到缓存填满再交付。

  • 终止位FIN = 1时表明此报文段发送方数据已经发送完毕,需要释放连接
  • bind()设置套接字的本地(源)地址。这是接收数据包的地址。套接字发送的数据包将此作为源地址,因此其他主机将知道将其数据包发送回哪里。 这句话的意思就是你接收到了数据包之后,bind告诉系统你发到这个端口和ip的数据包要发给某个特定的文件描述符。所以只有接受需求的时候才会用bind。客户端不写bind是因为内核自动选择了一个端口。 如果不需要接收,则套接字源地址是无用的。像 TCP 这样的协议需要启用接收才能正确发送,因为当一个或多个数据包到达时,目标主机会发回确认(即确认)。
  • 被动接收的一端都要绑定一个端口,主动发送的一端一般都是系统分配端口
  • 标准TCP只有单播因为是点对点,只有UDP有多播和组播
  • 调用send函数仅仅是把数据拷贝到发送的缓存区。具体发送不是应用程序可控的。所以说send就算不返回错误也有可能发送失败。
  • 如果服务器重启,大量客户端重新连接进来,如果服务器处理accept队列不及时将会发生连接被拒绝的错误。

为什么TCP、UDP套接字服务器端需要绑定端口号客户端不需要?

  1. IP地址和端口号是用来标识具体某一台主机上的具体一个进程的。也就是说,端口号可以用来标识主机上的某一个进程。
  2. 因此,操作系统需要对端口号进行管理,并且计算机中的端口号是有限的。
  3. 如果不进行绑定,操作系统会随机生成一个端口号给服务器。如果操作系统给服务器分配这个端口号的同时,有其他程序也准备使用这个端口号或者说端口号已经被使用,则可能会导致服务器一直启动不起来。
  4. 其次,服务器运行起来就不会在停止了,我们将服务器端的端口号绑定有助于有规划的对主机中的端口号进行使用。
  5. 客户端需要主动向服务器端发送请求,因此客户端就需要知道服务器端的IP地址和端口号,如果不绑定让系统随机生成,客户端将无法知道服务器端的端口号,即使知道也需要每次都去获取。
  6. 对于客户端来说,服务器端并不需要主动给客户端发送数据,客户端是主动的而服务器端是被动的。客户端给服务器端发送数据时,会将自己的IP地址和端口号一起发送过去,服务器端可以方便的找到客户端。
  7. 同时,客户端并不是一直运行的,只需要每次系统随机分配即可。
  8. 因此,服务器端需要绑定而客户端不需要绑定。

TCP (传输控制协议)和 UDP (用户数据报协议)的区别

  1. 连接

    • TCP 是面向连接的,传输数据前先要建立连接。有拥塞控制,流量控制等。

    • UDP 是无连接的。没有拥塞控制,流量控制等。

  2. 数据类型(传输方式)

    • TCP是字节流。流式传输,没有边界
    • UDP是报文流。一个包一个包传输,可能丢失或乱序。
3. 服务对象
1
2
3
4
5
6
7
8
 - TCP只支持一对一
 - UDP支持一对一,一对多,多对一和多对多。
 	4. 可靠性
 - TCP 是可靠交付数据的,数据可以无差错、不丢失、不重复、按需到达。
 - UDP 是尽最大努力交付,不保证可靠交付数据。
 	5. 报文首部开销
 - TCP 首部长度较长,会有一定的开销,首部在没有使用「选项」字段时是 20 个字节,如果使用了「选项」字段则会变长的。
 - UDP 首部只有 8 个字节,并且是固定不变的,开销较小。

TCP保证可靠性

  1. 数据分段:在发送端对数据进行分段,在接收端重组,由TCP确认分段的大小并控制分段和重组
  2. 确认应答(ACK):接收端->发送端(最后一个字节的序号+1)
  3. 超时重传:发送端:维护一个超时定时器,如果在定时器超时之后没有收到想要的确认,重发分段
  4. 滑动窗口:接收端一次接收发送端最大能发送数据的大小,保证处理能力不一样的两端不会数据(缓冲区)溢出
  5. 失序处理:接收端将数据整理,把数据以正确的顺序交给应用层。
  6. 重复处理:包因为延迟(超时时长定义有问题),导致接收端收到了重复的包,要丢弃重复的数据。
  7. 数据校验:首部和尾部序号的校验,如果不对,会导致超时重传。

MSS

  • MSS是TCP的最大报文长度
  • MSS不包括TCP头。也就是纯数据
    • 举个例子。MTU是1500,标准情况下,MTU = IP头(20) + TCP头(20) + TCP数据(1460)
    • 这里的MSS就是1460
  • MSS主要作用是为了避免TCP报文在IP层分片。包括和滑动窗口配合与对方协商自己可以接受多少数据。在TCP的分段中,每一个分段包都有自己的TCP头部,也方便重组。
  • 如果已经在TCP分段好的数据仍然大于MTU则还需要IP分片。
  • MSS在三次握手阶段和每次发送的时候都会传递给对方。然后双方按照二者最小的那一个值做为通信的MSS值。这就叫MSS协商。
  • MSS大于了协商值就会在TCP层分段

MTU

  • 首先我们应该还记得,IP是网络层,TCP是传输层。TCP把数据打包好给IP的网络层,IP网络层再次打包好放到数据链路层。这个MTU(最大传输单元)就是数据链路层的。它规定了一个有多大。
  • 一般来说它的大小是1500
  • MTU = IP头+(TCP/UDP头+TCP/UDP数据)
  • IP头大小没有特殊情况是20字节
  • MTU大于1500了就要在IP层分片。

IP层叫分片,TCP叫分段。

不传递MSS会怎么样

没有接收到对方的MSS,本测按照默认值发送。(536字节)

因为IP层的最小重组缓冲区是576,那么576-20(tcp头)-20(ip头)就是536

v2-8fc147d1e13653975c3ddd6101baee7d_r

QQ截图20220804211738

为什么既然 IP 层会分片,为什么 TCP 层还需要 MSS 呢?

首先,我们上次提到过。TCP报文在TCP层分段的时候,每一段都会带着自己的TCP头(因为是给数据分段,MSS也是数据大小不包括TCP头,所以每一段数据切割好之后都会被加上TCP头做为一个独立报文。如果IP分片的话IP是不会给报文接TCP头的。只会给自己加上IP头。

而且 IP层没有超时重传机制。需要依靠TCP。

假如现在丢包了。因为TCP没分段,是交给了IP分片。所以整个数据只有一个TCP头(一个TCP序列号)。一旦丢包需要把整个一大块传给了IP的TCP报文全部重传(这里虽然可能由于数据过大,IP给数据分为了多片,但是这一堆分片的数据依旧是属于一个TCP报文的因为只占一个TCP序列号。举个例子就是说假如有6000字节,TCP没分段,给了IP,IP分成了4*1500。如果其中的一个1500丢了,整个6000全部重传)。

如果TCP也分段了。因为避免了IP重新分片。所以IP拿到了这个报文直接就发出去了,而且这每一个小报文段都带有TCP的序列号。如果丢包了只需要传递丢的这一小块数据就可以了。因为每一小段都是完整报文段,有自己的序列号。

所以说TCP报文头没有IP报文头的(禁止分片/更多分片)的这个标志。因为IP分片的话数据是瞎瘠薄切。里面可能会有不完整的TCP报文段。可能切在中间了。所以到了之后需要合起来。TCP的数据不会被瞎瘠薄切,每一段都是完整的报文。所以他用序列号就可以了。(推测)

MSS一定能避免IP层分片吗? – 不一定

不一定,因为不一定每一段路由的MTU都是1500,有可能小于1500.这时候可能需要再次IP分片。

UDP不分段,所以只能依靠IP层分片

TCP-IP协议栈

QQ截图20220804192446

IP是网络层协议

IP报文头一般是20字节。除了可变部分以外。

IP分段 注意传输层TCP里面叫分段,网络层IP层叫分片。

网络层发现报文(IP头+(TCP/UDP头+TCP/UDP数据))大小大于1500,则需要分片。

TCP 超时重传

指的是发送方在超过一定时间之内没有收到对方的ACK应答包的时候,会重发该数据。

有两种情况会触发超时重传

  • 数据包丢失
  • 确认应答包丢失

QQ截图20220804223402

RTT

数据发送时刻到接收到确认的时刻的差值就是RTT

QQ截图20220804223448

  • 当超时时间 RTO 较大时,重发就慢,丢了老半天才重发,没有效率,性能差;
  • 当超时时间 RTO 较小时,会导致可能并没有丢就重发,于是重发的就快,会增加网络拥塞,导致更多的超时,更多的超时导致更多的重发。

所以超时重传时间 RTO 的值应该略大于报文往返 RTT 的值

快重传机制(Fast transmit属于拥塞控制。注意不是快恢复。)注意快重传是数据驱动的,超时重传是时间驱动的!一个是看应答码,一个是算时间。

TCP应答确认一定是有序的。即发送1234,如果2丢了则就算收到了34那么应答码ack也依旧是2,当丢包发生的时候。ack确认号就是接收方所期待的序号最小的没拿到的数据包。

QQ截图20220805031036

在上图,发送方发出了 1,2,3,4,5 份数据:

  • 第一份 Seq1 先送到了,于是就 Ack 回 2;
  • 结果 Seq2 因为某些原因没收到,Seq3 到达了,于是还是 Ack 回 2;
  • 后面的 Seq4 和 Seq5 都到了,但还是 Ack 回 2,因为 Seq2 还是没有收到;
  • 发送端收到了三个 Ack = 2 的确认,知道了 Seq2 还没有收到,就会在定时器过期之前,重传丢失的 Seq2。
  • 最后,收到了 Seq2,此时因为 Seq3,Seq4,Seq5 都收到了,于是 Ack 回 6 。

所以,快速重传的工作方式是当收到三个相同的 ACK 报文时,会在定时器过期之前,重传丢失的报文段。

快速重传机制只解决了一个问题,就是超时时间的问题,但是它依然面临着另外一个问题。就是重传的时候,是重传一个,还是重传所有的问题。

举个例子,假设发送方发了 6 个数据,编号的顺序是 Seq1 ~ Seq6 ,但是 Seq2、Seq3 都丢失了,那么接收方在收到 Seq4、Seq5、Seq6 时,都是回复 ACK2 给发送方,但是发送方并不清楚这连续的 ACK2 是接收方收到哪个报文而回复的, 那是选择重传 Seq2 一个报文,还是重传 Seq2 之后已发送的所有报文呢(Seq2、Seq3、 Seq4、Seq5、 Seq6) 呢?

  • 如果只选择重传 Seq2 一个报文,那么重传的效率很低。因为假如 Seq3 报文也丢失了,还得在后续收到三个重复的 ACK3 才能触发重传。
  • 如果选择重传 Seq2 之后已发送的所有报文,虽然能同时重传已丢失的 Seq2 和 Seq3 报文,但是 Seq4、Seq5、Seq6 的报文是已经被接收过了,对于重传 Seq4 ~Seq6 折部分数据相当于做了一次无用功,浪费资源。

可以看到,不管是重传一个报文,还是重传已发送的报文,都存在问题。

为了解决不知道该重传哪些 TCP 报文,于是就有 SACK 方法。

SACK 方法(选择性确认

这种方式需要在 TCP 头部「选项」字段里加一个 SACK 的东西,它可以将已收到的数据的信息发送给「发送方」,这样发送方就可以知道哪些数据收到了,哪些数据没收到,知道了这些信息,就可以只重传丢失的数据

说白了,就是一个数据段告诉对方我收到的是什么,如果触发了快重传则发送方看一眼这个SACK数据段缺哪块,就发送哪块。

如下图,发送方收到了三次同样的 ACK 确认报文,于是就会触发快速重发机制,通过 SACK 信息发现只有 200~299 这段数据丢失,则重发时,就只选择了这个 TCP 段进行重复。

QQ截图20220805031827

Duplicate SACK (D-SACK)重复选择性确认

和SACK的主要区别是使用了 SACK 来告诉「发送方」有哪些数据被重复接收了。

使用场景1:丢包

QQ截图20220805033509

  • 「接收方」发给「发送方」的两个 ACK 确认应答都丢失了,所以发送方超时后,重传第一个数据包(3000 ~ 3499)
  • 于是「接收方」发现数据是重复收到的,于是回了一个 SACK = 3000~3500,告诉「发送方」 3000~3500 的数据早已被接收了,因为 ACK 都到了 4000 了,已经意味着 4000 之前的所有数据都已收到,所以这个 SACK 就代表着 D-SACK
  • 这样「发送方」就知道了,数据没有丢,是「接收方」的 ACK 确认报文丢了。

使用场景2:延迟

QQ截图20220805033922

  • 数据包(1000~1499) 被网络延迟了,导致「发送方」没有收到 Ack 1500 的确认报文。
  • 而后面报文到达的三个相同的 ACK 确认报文,就触发了快速重传机制,但是在重传后,被延迟的数据包(1000~1499)又到了「接收方」;
  • 所以「接收方」回了一个 SACK=1000~1500,因为 ACK 已经到了 3000,所以这个 SACK 是 D-SACK,表示收到了重复的包。
  • 这样发送方就知道快速重传触发的原因不是发出去的包丢了,也不是因为回应的 ACK 包丢了,而是因为网络延迟了。

总结:

  1. 可以让「发送方」知道,是发出去的包丢了,还是接收方回应的 ACK 包丢了;
  2. 可以知道是不是「发送方」的数据包被网络延迟了;
  3. 可以知道网络中是不是把「发送方」的数据包给复制了;

TCP滑动窗口

如果每一次发送数据都要进行一次ACK未免效率有点低。所以有了滑动窗口的概念。

窗口大小就是指无需等待确认应答,而可以继续发送数据的最大值。这个值在报文头会写。就是window字段。这玩意和MSS没啥关系。因为滑动窗口是操作系统的缓存区大小

假设窗口大小为 3 个 TCP 段,那么发送方就可以「连续发送」 3 个 TCP 段,并且中途若有 ACK 丢失,可以通过「下一个确认应答进行确认」。如下图:

QQ截图20220805034441

通常窗口的大小是由接收方的窗口大小来决定的。但是也会针对网络拥塞情况进行适当缩小、

发送方滑动窗口分为四个部分 SWND

QQ截图20220805034735

  • #1 是已发送并收到 ACK确认的数据:1~31 字节(这部分可以删除)
  • #2 是已发送但未收到 ACK确认的数据:32~45 字节
  • #3 是未发送但总大小在接收方处理范围内(接收方还有空间):46~51字节
  • #4 是未发送但总大小超过接收方处理范围(接收方没有空间):52字节以后

在下图,当发送方把数据「全部」都一下发送出去后,可用窗口的大小就为 0 了,表明可用窗口耗尽,在没收到 ACK 确认之前是无法继续发送数据了。

QQ截图20220805034928

在下图,当收到之前发送的数据 32~36 字节的 ACK 确认应答后,如果发送窗口的大小没有变化,则滑动窗口往右边移动 5 个字节,因为有 5 个字节的数据被应答确认,接下来 52~56 字节又变成了可用窗口,那么后续也就可以发送 52~56 这 5 个字节的数据了。

QQ截图20220805035006

所以滑动窗口就是确认多少,滑动多少。

接收方的窗口分为三个部分 RWND

QQ截图20220805035154

  • #1 + #2 是已成功接收并确认的数据(等待应用进程读取);
  • #3 是未收到数据但可以接收的数据;
  • #4 未收到数据并不可以接收的数据;

接收方未按序收到的报文在什么位置?

QQ截图20220805054344

  • 最左边报文一定是尚未收到的报文
  • 如果31没到,32,33先到,就先把32,33缓存在接收缓冲区中,并且会发送3个对31的确认报文(快重传),当收到了31后,那么窗口就继续滑动到34的位置.
  • 注意ack包的确认号一定是下一个期望收到的数据的起始号。所以这里传回的ack不会是34而是31

TCP流量控制

我们刚才说了滑动窗口的主要作用是不用每次都等到ACK之后才能再次发送。但是滑动窗口的大小也不是固定的。因为可能我现在比较忙,处理不过来,如果我来不及确认,但是你一直在那发,就会出现超时重传。

举个例子,你让我拆快递,没有滑动窗口就是我拆完一个,告诉你我拆完了,你再给我一个。有滑动窗口就是你给我了一张桌子,只要桌子没满你就一直在那放。但是桌子是固定大小的,可能我出去上了个厕所,虽然桌子没满但是我没在,你一直在这放导致很多快递来不及拆,你以为快递被人偷了就重新买了又放在了桌子上,很消耗资源。所以我让这个桌子是变长的。

所以,TCP 提供一种机制可以让「发送方」根据「接收方」的实际接收能力控制发送的数据量,也就是避免「发送方」的数据填满「接收方」的缓存。这就是所谓的流量控制。

  • 滑动窗口的总大小一般情况下是固定的,但是可用窗口是变化的。所以接收方的大小就是 总大小 - 已接收的但是未被处理的数据的大小。一旦数据已被处理,则可用窗口会扩大(滑动)。
  • 发送方的大小就是总大小 - 已发送但未确认的数据的大小。一旦收到ACK确认则可用窗口会扩大(滑动)
  • 每一次接收端发送ACK的时候都会把自己的接受窗口大小rwnd告知对方,这样的话对方就可以动态调整自己的发送窗口大小。
  • 一旦接收端发送的rwnd为0,就发生了窗口关闭

窗口关闭

当接收方发送的ACK报文附带的rwnd为0的时候,就发生窗口关闭。

当接收方处理完了数据,rwnd又有空间的时候,会再次发送一个ACK报文,告诉发送方自己的窗口大小,(可以发送数据)。但是一旦这个ACK报文丢了,就会出现死锁。因为发送方等着这个ACK才能发,接受方已经发了ACK迟迟没拿到数据

这时候就有一个东西叫做持续计时器。只要 TCP 连接一方收到对方的零窗口通知,就启动持续计时器。

如果持续计时器超时,就会发送窗口探测 ( Window probe ) 报文(这个报文携带1字节数据),而对方在确认这个探测报文时,给出自己现在的接收窗口大小。

  • 如果接收窗口仍然为 0,那么收到这个报文的一方就会重新启动持续计时器;
  • 如果接收窗口不是 0,那么死锁的局面就可以被打破了。

窗口探测的次数一般为 3 次,每次大约 30-60 秒(不同的实现可能会不一样)。如果 3 次过后接收窗口还是 0 的话,有的 TCP 实现就会发 RST 报文来中断连接。

就算接收方的rwnd为0,接受方也必须接受零窗口探测报文段(ACK),确认报文段和携带紧急数据的报文段。

QQ截图20220805054019

窗口过小导致数据传输效率问题

假如说应用程序读取数据很少,则每一次的窗口的大小会越来越小。这样假如告诉你我窗口是1字节你再给我发1字节这样非常浪费资源。由此,我们可以有两个选择。

  • 一个是接收方窗口小于某个值的时候,就当做0处理,发送rwnd为0
    • 当「窗口大小」小于 min( MSS,缓存空间/2 ) ,也就是小于 MSS 与 1/2 缓存大小中的最小值时,就会向发送方通告窗口为 0,也就阻止了发送方再发数据过来。
    • 等到接收方处理了一些数据后,窗口大小 >= MSS,或者接收方缓存空间有一半可以使用,就可以把窗口打开让发送方发送数据过来。
  • 另外一个是让发送方避免发送过小的数据包。(Nagle

Nagle算法

  • 如果包长度达到MSS(或含有Fin包),立刻发送,否则等待下一个包到来;如果下一包到来后两个包的总长度超过MSS的话,就会进行拆分发送;
  • 等待超时(一般为200ms),第一个包没到MSS长度,但是又迟迟等不到第二个包的到来,则立即发送。
  • 一般telnet和ssh这种小包交互的时候会关闭这个算法。

TCP拥塞控制 拥塞窗口CWND(Congestion Window)

在网络出现拥堵时,如果继续发送大量数据包,可能会导致数据包时延、丢失等,这时 TCP 就会重传数据,但是一重传就会导致网络的负担更重,于是会导致更大的延迟以及更多的丢包,这个情况就会进入恶性循环被不断地放大….

所以,TCP 不能忽略网络上发生的事,它被设计成一个无私的协议,当网络发送拥塞时,TCP 会自我牺牲,降低发送的数据量。

于是,就有了拥塞控制,控制的目的就是避免「发送方」的数据填满整个网络。

拥塞窗口 cwnd发送方维护的一个的状态变量,它会根据网络的拥塞程度动态变化的

  • cwnd = n意思是可以发送n个报文段(n个MSS)

我们在前面提到过发送窗口 swnd 和接收窗口 rwnd 是约等于的关系,那么由于加入了拥塞窗口的概念后,此时发送窗口的值是swnd = min(cwnd, rwnd),也就是拥塞窗口和接收窗口中的最小值。

拥塞窗口 cwnd 变化的规则:

  • 只要网络中没有出现拥塞,cwnd 就会增大;
  • 但网络中出现了拥塞,cwnd 就减少;

如何判断网络是否拥塞了呢?

很简单,超时重传了就会认为网络出现拥塞

有哪些拥塞控制算法?

  1. 慢启动
  2. 拥塞避免
  3. 拥塞发生
    • 超时重传
    • 快重传
  4. 快恢复 (注意不是快重传)

慢启动(指数增长)

TCP 在刚建立连接完成后,首先是有个慢启动的过程,这个慢启动的意思就是一点一点的提高发送数据包的数量,如果一上来就发大量的数据,这不是给网络添堵吗?(TCP链接完成后无法确认网络状况是否良好,一次发了一大堆万一丢了又得重传很脑瘫。所以一开始一点一点地尝试发)

  • 慢启动算法规则:当发送方每收到一个 ACK,拥塞窗口 cwnd 的大小就会加 1。注意是每次收到一个ACK。所以是指数增长

例子:

这里假定拥塞窗口 cwnd 和发送窗口 swnd 相等,下面举个栗子:

  • 连接建立完成后,一开始初始化 cwnd = 1,表示可以传一个 MSS 大小的数据。
  • 当收到一个 ACK 确认应答后,cwnd 增加 1,于是一次能够发送 2 个
  • 当收到 2 个的 ACK 确认应答后, cwnd 增加 2,于是就可以比之前多发2 个,所以这一次能够发送 4 个
  • 当这4 个的 ACK **确认到来的时候,每个确认 cwnd 增加 1, **4 个确认 cwnd 增加 4,于是就可以比之前多发 4 个,所以这一次能够发送 8 个

QQ截图20220809060754

cwnd直到涨到慢启动阈值(Slow Start thresh)的时候会发生状态改变

  • cwnd < ssthresh 时,使用慢启动算法。
  • cwnd >= ssthresh 时,就会使用「拥塞避免算法」。

拥塞避免算法(“加法增大”,线性增长)

顾名思义,结合前面我们提到过当拥塞窗口 cwnd 「超过」慢启动门限 ssthresh 就会进入拥塞避免算法,我们可以理解为:我们为了效率,肯定希望一次尽可能发送更多的数据包。但是为了不干扰他人,又不能像之前一样指数增长,那我们采用折中的方法,当收到一个 ACK 时,cwnd 增加 1。也就是线性增长

但是,就这么一直增长,迟早要进入拥堵阶段,也就是发生了丢包。这时候就需要重传了。当触发了重传机制的时候,也就是进入了下一个阶段:「拥塞发生」

拥塞发生

当网络出现拥塞,也就是会发生数据包重传,重传机制主要有两种:

  • 超时重传
  • 快速重传

发生超时重传的拥塞发生算法:

  • ssthresh 设为 拥塞窗口的一半(cwnd/2)。
  • cwnd 恢复为 cwnd 初始化值,这里假定 cwnd 初始化值 1
  • 然后进入到慢启动阶段。

因为慢启动会极大地减少数据流,所以会发生严重的网络卡顿。所以会有快重传的拥塞发生算法

QQ截图20220809062600

发生快速重传的拥塞发生算法:

前面我们讲过「快速重传算法」。当接收方发现丢了一个中间包的时候,ACK会发送丢了的那个包的序列号。如果接收端发现收到了三次前一个包的 ACK,于是发送端就会快速地重传,不必等待超时再重传。TCP 认为这种情况不严重,因为大部分没丢,只丢了一小部分,则 ssthreshcwnd 变化如下:

  • cwnd = cwnd/2 ,也就是设置为原来的一半;(注意区别,超时重传的时候cwnd会恢复为默认值)
  • ssthresh = cwnd/2;
  • 进入快速恢复算法

快恢复

快速重传和快速恢复算法一般同时使用,快速恢复算法是认为,你还能收到 3 个重复 ACK 说明网络也不那么糟糕,所以没有必要像 RTO 超时(重传超时)那么强烈。

正如前面所说,进入快速恢复之前,cwndssthresh 已被更新了:这俩都是原来cwnd的一半。

然后进入快恢复阶段:

  • 拥塞窗口 cwnd = ssthresh + 3 ( 3 的意思是快速重传时已经确认接收到了 3 个重复的ACK数据包);
  • 重传丢失的数据包;
  • 如果再收到重复的 ACK,那么 cwnd 增加 1;
  • 如果收到新数据的 ACK 后,把 cwnd 设置为第一步中的 ssthresh 的值,原因是该 ACK 确认了新的数据,说明从 duplicated ACK 时的数据都已收到,该恢复过程已经结束,可以回到恢复之前的状态了,也即再次进入拥塞避免状态;

QQ截图20220809064515

为什么快恢复长得和理论的图不太一样?

  1. 在快速恢复的过程中,首先 ssthresh = cwnd/2,然后 cwnd = ssthresh + 3,表示网络可能出现了阻塞,所以需要减小 cwnd 以避免,加 3 代表快速重传时已经确认接收到了 3 个重复的ACK数据包;
  2. 随后继续重传丢失的数据包,如果再收到重复的 ACK,那么 cwnd 增加 1。加 1 代表每个收到的重复的 ACK 包,都已经离开了网络。这个过程的目的是尽快将丢失的数据包发给目标。
  3. 如果收到新数据的 ACK 后,把 cwnd 设置为第一步中的 ssthresh 的值,恢复过程结束。

首先,快速恢复是拥塞发生后慢启动的优化,其首要目的仍然是降低 cwnd 来减缓拥塞,所以必然会出现 cwnd 从大到小的改变。

其次,过程2(cwnd逐渐加1)的存在是为了尽快将丢失的数据包发给目标,从而解决拥塞的根本问题(三次相同的 ACK 导致的快速重传),所以这一过程中 cwnd 反而是逐渐增大的。

拥塞控制和流量控制的区别

  • 流量控制,的目的就是避免「发送方」的数据填满「接收方」的缓存,但是并不知道网络的中发生了什么。理解为数据包可到达,但是避免接收方来不及处理
    • rwnd窗口由接收方调整
  • 拥塞控制,的目的就是避免「发送方」的数据填满整个网络。理解为避免数据包可能会发生不可到达,避免耗尽网络资源
    • cwnd窗口由发送方调整。

面试题相关

没有accept能否建立TCP链接? – 可以

上文的全连接队列和半连接队列提到了。第一次握手会放入半连接,三次握手后放入全连接。

accept的目的仅仅是从全连接队列取出一条连接而已。

accept 底层实现

  • accept()函数,就使用来 从 全连接队列 中 的队首 (队头) 位置取出来一项 (每一项都是一个已经完成三次握手的TCP连接),返回给进程。
  • accept会以文件描述符的形式返回一个套接字。这个套接字就是我们项目里的读写文件描述符。
  • 如果已完成连接队列是空的呢?
    • 那么accept()会一致卡在这里【休眠】等待,一直到已完成队列中有一项时才会被唤醒。

没有listen能否建立TCP链接? – 不可以

服务端如果只 bind 了 IP 地址和端口,而没有调用 listen 的话,然后客户端对服务端发起了连接建立,服务端会回 RST 报文。

listen底层实现

维护一个半连接队列和一个全连接队列。listen调用后,监听到第一次握手就放入半连接队列,三次握手完毕后放入全连接队列。

FIN 报文一定得调用关闭连接的函数,才会发送吗? – 不一定

如果进程退出了,不管是不是正常退出,还是异常退出(如进程崩溃),内核都会发送 FIN 报文,与对方完成四次挥手。

TCP的第三次握手能不能携带数据? – 可以

QQ截图20220805001644

上图可以看到,第二次我握手的时候客户端已经切换到ESTABLISHED所以可以携带数据。

客户端发送完第三次握手后,是不是不管服务器有没有收到,直接就发送数据? – 是的

不然不就是四次握手了么

如果因为各种原因,服务端并未收到客户端发来的第三次握手包,那客户端后续发送的数据,服务端如何处理?

如果第三次握手包服务器没有收到,就直接发送数据,服务器将这个携带应用数据的包当做第三次握手(前提是这一个包中携带有ACK标记)。

为什么三次握手必须由客户端发起

因为客户端是主动端,服务端是被动端。

服务端不知道客户端的IP地址。放到项目里就是我们监听的时候,客户端发送的数据包是含有自己的ip和端口地址的。所以我们可以利用。

为什么要进行三次握手,两次或四次行不行

三次握手能防止历史连接的建立,能减少双方不必要的资源开销,能帮助双方同步初始化序列号。序列号能够保证数据包不重复、不丢弃和按序传输。

  • 「两次握手」:无法防止历史连接的建立,会造成双方资源的浪费,也无法可靠的同步双方序列号;
    • 详细点就是说假如有一个客户端的历史已经失效的报文段突然到达,则服务端以为是客户端想要建立链接。如果是两次握手的话,服务端再发送一次就可以建立连接了。但是客户端并没有需求,造成了浪费也有可能发送错误数据。、
    • 第二点就是SYN SYNACK ACK这三个阶段中,客户端向服务端发送SYN,则服务端收到了并且也向客户端发送了SYN,然后客户端再次ACK才能保证双方都收到了序列号。不然无法保证客户端也受到了服务端的SYN报文。
  • 「四次握手」:三次握手就已经理论上最少可靠连接建立,所以不需要使用更多的通信次数。

为什么要四次挥手,三次两次行不行?

四次挥手之所以为四次就是因为可以半关闭状态。也就是服务器可能还要发送一些数据给客户端。理论上四次挥手也可以换成三次。如果服务器在收到客户端的 FIN 时没有更多数据或根本没有数据要发送,则可以将 ack 和 fin 合并为一个包。或者,因为延迟确认的特性,可以把ACK,FIN(第二次和第三次挥手)和要发的数据一起发过去,这样也是三次挥手。

服务器收到客户端的 FIN 报文时,内核会马上回一个 ACK 应答报文,但是服务端应用程序可能还有数据要发送,所以并不能马上发送 FIN 报文,而是将发送 FIN 报文的控制权交给服务端应用程序

  • 如果服务端应用程序有数据要发送的话,就发完数据后,才调用关闭连接的函数;
  • 如果服务端应用程序没有数据要发送的话,可以直接调用关闭连接的函数,

从上面过程可知,是否要发送第三次挥手的控制权不在内核,而是在被动关闭方(上图的服务端)的应用程序,因为应用程序可能还有数据要发送,由应用程序决定什么时候调用关闭连接的函数,当调用了关闭连接的函数,内核就会发送 FIN 报文了,所以服务端的 ACK 和 FIN 一般都会分开发送。

两次挥手是自连接。

为什么每次建立 TCP 连接时,初始化的序列号都要求不一样呢?

  • 为了防止历史报文被下一个相同四元组的连接接收(主要方面)
  • 为了安全性,防止黑客伪造的相同序列号的 TCP 报文被对方接收

初始化序列号不一样是防止历史报文被相同四元组接受,那么为什么还需要TIME_WAIT

因为序列号会被复用,不是无限的。

TIME_WAIT 过多有什么危害?

  • 第一是占用系统资源,比如文件描述符、内存资源、CPU 资源、线程资源等;
  • 第二是占用端口资源,端口资源也是有限的,一般可以开启的端口为 32768~61000,也可以通过 net.ipv4.ip_local_port_range参数指定范围。

如果已经建立了连接,但是客户端突然出现故障了怎么办? - TCP KEEPALIVE

TCP 有一个机制是保活机制KEEPALIVE。这个机制的原理是这样的:

定义一个时间段,在这个时间段内,如果没有任何连接相关的活动,TCP 保活机制会开始作用,每隔一个时间间隔,发送一个探测报文,该探测报文包含的数据非常少,如果连续几个探测报文都没有得到响应,则认为当前的 TCP 连接已经死亡,系统内核将错误信息通知给上层应用程序。

如果开启了 TCP 保活,需要考虑以下几种情况:

  • 第一种,对端程序是正常工作的。当 TCP 保活的探测报文发送给对端, 对端会正常响应,这样 TCP 保活时间会被重置,等待下一个 TCP 保活时间的到来。
  • 第二种,对端程序崩溃并重启。当 TCP 保活的探测报文发送给对端后,对端是可以响应的,但由于没有该连接的有效信息,会产生一个 RST 报文,这样很快就会发现 TCP 连接已经被重置。
  • 第三种,是对端程序崩溃,或对端由于其他原因导致报文不可达。当 TCP 保活的探测报文发送给对端后,石沉大海,没有响应,连续几次,达到保活探测次数后,TCP 会报告该 TCP 连接已经死亡

我们项目里有自己的超时检测机制,这个是HTTP应用层的的不是TCP。TCP的保活和HTTP保活不一样。我们的保活机制就是定时器,没有新的读写操作(请求)就调用关闭。

HTTP keepalive 和 TCP keepalive如何理解

  • TCP的保活是没有通信的时候,这时候也没断开连接,我们看一下对端是否活着。所以这个周期比较长。
  • HTTP的keepalive是因为在关闭的时候,每一次请求答复后浏览器也就是客户端会主动关闭这个TCP链接。而如果打开了keepalive,则不会主动关闭这个链接。

所以

  • TCP keepalive是用来检测的
  • HTTP keep-alive 是用来设置生效(开关)的。关闭也就是直接收到请求就断开连接。如果打开了就不主动断开连接。

HTTP不带keepalive的话谁主动关闭? 推测

我们知道HTTP的keepalive必须两端都开启才能生效。

以下是个人测试:

如果服务器关闭了keepalive,浏览器开启,则是浏览器主动发起FIN。

通过知乎https://zhuanlan.zhihu.com/p/224595048 得到:如果浏览器关闭了keepalive,服务器开启,则是服务器主动发起FIN。

那么可以推测出,是哪方关闭了,就是开启的那方主动关闭连接。

那么如果都关闭呢?不知道了。

QQ截图20220925035850

accept(服务端)/connect(客户端)的成功返回 发生在三次握手的哪一步?

  • 客户端在第二次握手后

  • 服务端在第三次握手后

简单记住就是只要收到了对方的ACK就可以了。客户端是第二次握手收到服务端的ACK,服务端是第三次收到了客户端的ACK。

很多closewait状态是什么原因

在被动关闭连接情况下,在已经接收到FIN,回复了ACK,但是还没有发送自己的FIN的时刻,连接处于CLOSE_WAIT状态。所以原因可能是我方正在忙着读或者写,还没有及时处理断开连接的操作。也就是我方没有及时close。应该读到错误的时候自己也调用关闭。

很多FIN_WAIT_2状态是什么原因

和上面一样。被动关闭是closewait,主动关闭对应的就是fin_wait_2

shutdown和close的区别

  • shutdown()函数可以选择关闭全双工连接的读通道或者写通道,如果两个通道同时关闭,则这个连接不能再继续通信。shutdown()只会关闭连接,但是不会释放占用的文件描述符。所以即使使用了SHUT_RDWR类型调用shutdown()关闭连接,也仍然要调用close()来释放连接占用的文件描述符。如果有多进程/多线程共享同一个 socket,shutdown 则不管引用计数,直接使得该 socket 不可用,然后发出 FIN 报文,如果有别的进程企图使用该 socket,将会受到影响。
  • close()函数会同时关闭全双工连接的读写通道,除了关闭连接外,还会释放套接字占用的文件描述符。如果有多进程/多线程共享同一个 socket,如果有一个进程调用了 close 关闭只是让 socket 引用计数 -1,并不会导致 socket 不可用,同时也不会发出 FIN 报文,其他进程还是可以正常读写该 socket,直到引用计数变为 0,才会发出 FIN 报文。

粗暴关闭 优雅关闭

如果客户端是用 close 函数来关闭连接,那么在 TCP 四次挥手过程中,如果收到了服务端发送的数据,由于客户端已经不再具有发送和接收数据的能力,所以客户端的内核会回 RST 报文给服务端,然后内核会释放连接,这时就不会经历完成的 TCP 四次挥手,所以我们常说,调用 close 是粗暴的关闭。当服务端收到 RST 后,内核就会释放连接,当服务端应用程序再次发起读操作或者写操作时,就能感知到连接已经被释放了:

  • 如果是读操作,则会返回 RST 的报错,也就是我们常见的Connection reset by peer。
  • 如果是写操作,那么程序会产生 SIGPIPE 信号,应用层代码可以捕获并处理信号,如果不处理,则默认情况下进程会终止,异常退出。

相对的,shutdown 函数因为可以指定只关闭发送方向而不关闭读取方向,所以即使在 TCP 四次挥手过程中,如果收到了服务端发送的数据,客户端也是可以正常读取到该数据的,然后就会经历完整的 TCP 四次挥手,所以我们常说,调用 shutdown 是优雅的关闭。

客户端调用 close 了,断开的流程是什么?

服务端的read会读到EOF。这时候可以调用close了(服务端发送FIN)。

  • 客户端主动调用关闭连接的函数,于是就会发送 FIN 报文,这个 FIN 报文代表客户端不会再发送数据了,进入 FIN_WAIT_1 状态
  • 服务端收到了 FIN 报文,然后马上回复一个 ACK 确认报文,此时服务端进入 CLOSE_WAIT 状态。在收到 FIN 报文的时候,TCP 协议栈会为 FIN 包插入一个文件结束符 EOF 到接收缓冲区中,服务端应用程序可以通过 read 调用来感知这个 FIN 包,这个 EOF 会被放在已排队等候的其他已接收的数据之后(末尾),所以必须要继续读取read 接收缓冲区已接收的数据
    • 调用 socket 的close方法后,缓冲区中未发送完的数据不会丢。因为内核会把FIN包用EOF替换后插入到缓冲区的末尾。
  • 接着,当服务端在 read 数据的时候,最后自然就会读到 EOF,接着 read() 就会返回 0,这时服务端应用程序如果有数据要发送的话,就发完数据后才调用关闭连接的函数,如果服务端应用程序没有数据要发送的话,可以直接调用关闭连接的函数,这时服务端就会发一个 FIN 包,这个 FIN 报文代表服务端不会再发送数据了,之后处于 LAST_ACK 状态;
  • 客户端接收到服务端的 FIN 包,并发送 ACK 确认包给服务端,此时客户端将进入 TIME_WAIT 状态;
  • 服务端收到 ACK 确认包后,就进入了最后的 CLOSE 状态;
  • 客户端经过 2MSL 时间之后,也进入 CLOSE 状态;

QQ截图20220805021651

整理一下

  • 浏览器关闭了页面,浏览器会调用close。发送FIN
  • 服务器接收到了FIN包,TCP协议栈会把这个FIN包换成EOF结束符然后放到对应客户端读写描述符的接收缓冲区中。
    • 我们知道每一个socket都是文件描述符。
    • 我们知道每个socket都有一个自己的缓冲区。
  • 通过读取(可能是epoll,可能是read,可能是select等等)我们能读取到这个EOF,我们就知道了客户端想要关闭,不会再发送数据了。我们就可以这边准备进行关闭。
  • 我们读取到了EOF可以调用close进行关闭了。

服务端关闭了,客户端继续写会强制退出

客户端关闭了写端,服务端继续写没问题,关闭了读写则服务器继续写也会触发异常

客户端拔掉了网线会怎么样?

首先,拔掉网线这个动作并不会影响 TCP 连接的状态。

TCP 连接在 Linux 内核中是一个名为 struct socket 的结构体,该结构体的内容包含 TCP 连接的状态等信息。当拔掉网线的时候,操作系统并不会变更该结构体的任何内容,所以 TCP 连接的状态也不会发生改变。

假设拔掉网线后有数据传输

在客户端拔掉网线后,服务端向客户端发送的数据报文会得不到任何的响应,在等待一定时长后,服务端就会触发超时重传机制,重传未得到响应的数据报文。

  • 如果在服务端重传报文的过程中,客户端刚好把网线插回去了,由于拔掉网线并不会改变客户端的 TCP 连接状态,并且还是处于 ESTABLISHED 状态,所以这时客户端是可以正常接收服务端发来的数据报文的,然后客户端就会回 ACK 响应报文。此时,客户端和服务端的 TCP 连接依然存在的,就感觉什么事情都没有发生。

  • 但是,如果如果在服务端重传报文的过程中,客户端一直没有将网线插回去,服务端超时重传报文的次数达到一定阈值后,内核就会判定出该 TCP 有问题,然后通过 Socket 接口告诉应用程序该 TCP 连接出问题了,于是服务端的 TCP 连接就会断开

    • 而等客户端插回网线后,如果客户端向服务端发送了数据,由于服务端已经没有与客户端相同四元祖的 TCP 连接了,因此服务端内核就会回复 RST 报文,客户端收到后就会释放该 TCP 连接。
    • 此时,客户端和服务端的 TCP 连接都已经断开了。

假设拔掉网线后,没有数据传输

针对拔掉网线后,没有数据传输的场景,还得看是否开启了 TCP keepalive 机制 (TCP 保活机制)。

如果没有开启 TCP keepalive 机制,在客户端拔掉网线后,并且双方都没有进行数据传输,那么客户端和服务端的 TCP 连接将会一直保持存在。(因为我压根不知道你断开了没有。)

而如果开启了 TCP keepalive 机制,在客户端拔掉网线后,即使双方都没有进行数据传输,在持续一段时间后,TCP 就会发送探测报文:

  • 如果对端是正常工作的。当 TCP 保活的探测报文发送给对端, 对端会正常响应,这样 TCP 保活时间会被重置,等待下一个 TCP 保活时间的到来。
  • 如果对端主机崩溃,或对端由于其他原因导致报文不可达。当 TCP 保活的探测报文发送给对端后,石沉大海,没有响应,连续几次,达到保活探测次数后,TCP 会报告该 TCP 连接已经死亡

所以,TCP 保活机制可以在双方没有数据交互的情况,通过探测报文,来确定对方的 TCP 连接是否存活。

输入网址到显示网页的过程

首先浏览器将URL通过DNS协议(UDP连接)解析为IP地址

交给TCP协议,建立链接

IP层将TCP报文封装。发送

到达对端后用ARP/RARP进行寻址。

然后找到目标客户端。

成功建立链接。

发出HTTP请求,

用到的协议:HTTP/S,DNS(UDP), TCP, IP, MAC, ARP, RARP

粘包怎么办

其实严谨一点不能叫解决粘包。应该叫实现功能。你硬要用字节流协议去做数据报协议的功能,你肯定要自己去解决怎么读取消息的问题。

我们提到过,调用了send仅仅是把内容拷贝到内核缓冲区中而已,具体什么时候发送是TCP决定的。TCP是字节流,我们不能认为一个用户消息对应一个TCP报文,即 TCP不保证每一个报文是一个完整的数据报。所以可能会发生粘包。尤其是有些小包数据会有Nagle算法优化导致多包一起发出去。或者是一个大报文被拆成了几个小报文。这样就可能我分了五次发送 1 2 3 4 5然后变成一个12345发出去。但是应用程序不知道怎么拆开。不知道是1 2还是1 23 45 还是…

所以总结原因:

  • TCP面向字节流,一个用户消息不一定是一个TCP报文。一个报文可能有多个消息或者是多个报文对应一个消息。所以是因为:
    • 多个小消息合并为大包发送(发送方粘包)
    • 大包拆分为小包(发送方粘包)
    • 接收端不及时接受导致多个包在一起。(接收方粘包)
    • 说白了就是不知道边界在哪。

要解决这个问题,要交给应用程序

粘包的问题出现是因为不知道一个用户消息的边界在哪,如果知道了边界在哪,接收方就可以通过边界来划分出有效的用户消息。

一般有三种方式分包的方式:

  • 固定长度的消息;
  • 特殊字符作为边界;
  • 自定义消息结构。

固定长度的消息:

这种是最简单方法,即每个用户消息都是固定长度的,比如规定一个消息的长度是 64 个字节,当接收方接满 64 个字节,就认为这个内容是一个完整且有效的消息。

但是这种方式灵活性不高,实际中很少用。

特殊字符作为边界(HTTP就是这玩意)

我们可以在两个用户消息之间插入一个特殊的字符串,这样接收方在接收数据时,读到了这个特殊字符,就把认为已经读完一个完整的消息。

自定义消息结构

我们可以自定义一个消息结构,由包头和数据组成,其中包头包是固定大小的,而且包头里有一个字段来说明紧随其后的数据有多大。

比如这个消息结构体,首先 4 个字节大小的变量来表示数据长度,真正的数据则在后面。当接收方接收到包头的大小(比如 4 个字节)后,就解析包头的内容,于是就可以知道数据的长度,然后接下来就继续读取数据,直到读满数据的长度,就可以组装成一个完整到用户消息来处理了。

https://juejin.cn/post/7031925832167718943

服务端挂了,客户端的TCP链接还在吗?

如果是服务端进程崩溃 – 触发四次挥手

TCP 的连接信息是由内核维护的,所以当服务端的进程崩溃后,内核需要回收该进程的所有 TCP 连接资源,于是内核会发送第一次挥手 FIN 报文,后续的挥手过程也都是在内核完成,并不需要进程的参与,所以即使服务端的进程退出了,还是能与客户端完成 TCP四次挥手的过程。

如果是服务端主机宕机 – 无法进行四次挥手。需要靠后续数据判断。

当服务端的主机发生了宕机,是没办法和客户端进行四次挥手的,所以在服务端主机发生宕机的那一时刻,客户端是没办法立刻感知到服务端主机宕机了,只能在后续的数据交互中来感知服务端的连接已经不存在了

如果宕机后,客户端会发送数据: – 触发超时重传后,重传次数达到阈值,内核判定链接有问题,主动断开。

在服务端主机宕机后,客户端发送了数据报文,由于得不到响应,在等待一定时长后,客户端就会触发超时重传机制,重传未得到响应的数据报文。

当重传次数达到达到一定阈值后,内核就会判定出该 TCP 连接有问题,然后通过 Socket 接口告诉应用程序该 TCP 连接出问题了,于是客户端的 TCP 连接就会断开。

如果宕机后,客户端一直不发送数据: – 需要看是否开启了TCP 的keepalive。没有开启则不会检测,开启了由于有探测报文则可以检测到。

  • 如果没有开启 TCP keepalive 机制,在服务端主机发送宕机后,如果客户端一直不发送数据,那么客户端的 TCP 连接将一直保持存在,所以我们可以得知一个点,在没有使用 TCP 保活机制,且双方不传输数据的情况下,一方的 TCP 连接处在 ESTABLISHED 状态时,并不代表另一方的 TCP 连接还一定是正常的。

  • 而如果开启了 TCP keepalive 机制,在服务端主机发送宕机后,即使客户端一直不发送数据,在持续一段时间后,TCP 就会发送探测报文,探测服务端是否存活:

    • 如果对端是正常工作的。当 TCP 保活的探测报文发送给对端, 对端会正常响应,这样 TCP 保活时间会被重置,等待下一个 TCP 保活时间的到来。
    • 如果对端主机崩溃,或对端由于其他原因导致报文不可达。当 TCP 保活的探测报文发送给对端后,石沉大海,没有响应,连续几次,达到保活探测次数后,TCP 会报告该 TCP 连接已经死亡

    所以,TCP keepalive 机制可以在双方没有数据交互的情况,通过探测报文,来确定对方的 TCP 连接是否存活。

如果宕机后马上重启服务器, 重启服务器后客户端此时发送了数据: –由于原链接不存在,会返回RST。 客户端收到RST后断开连接。

如果服务端主机宕机后,然后马上重启了服务端,重启完成后,如果这时客户端发送了数据,由于服务端之前的连接信息已经不存在( TCP 连接的数据结构已经丢失了),所以会回 RST 报文给客户端,客户端收到 RST 报文后,就断开连接。

客户端挂了,服务端的TCP链接还在吗?

和上面的服务端一样。

UDP和TCP哪个快

  • 因为UDP不保证数据可以被准确无误的送达,所以没有那些重传和校验机制。自然在一般情况下UDP比TCP更快
  • 但是一般情况下,UDP还是要做重传机制。这时候问题来了。如果现在我需要传一个特别大的数据包但是丢包了呢
    • TCP里,它内部会根据MSS的大小分段,这时候进入到IP层之后,每个包大小都不会超过MTU,因此IP层一般不会再进行分片。这时候发生丢包了,只需要重传每个MSS分段就够了。
    • 但对于UDP,其本身并不会分段,如果数据过大,到了IP层,就会进行分片。此时发生丢包的话,再次重传,就会重传整个大数据包
  • 对于上面这种情况,使用UDP就比TCP要慢
  • 当然,解决起来也不复杂。这里的关键点在于是否实现了数据分段机制,使用UDP的应用层如果也实现了分段机制的话,那就不会出现上述的问题了

单判断协议在哪一层

  • 知道mac,是链路层

  • 而且知道ip ,是网络层

  • 而且知道端口,是传输层

    • 显然,icmp工作在网络层
本文由作者按照 CC BY 4.0 进行授权

C++ STL - 2 - 迭代器设计思路。萃取。

计算机网络 - 数据链路层1