四、Tcp篇

1 TCP三次握手与四次挥手面试题

1.1 TCP基本认识

什么是 TCP √!O

TCP 是面向连接的、可靠的、基于字节流的传输层通信协议。

img

  • 面向连接:指的是TCP会维护用于保证可靠性和流量控制的某些状态信息。这些状态信息就是连接,包括Socket、序列号、确认号和窗口大小。Socket用于把当前主机接入网络;序列号用来确保数据有序;确认号用来保证数据完整性;窗口大小用来做流量控制。
  • 可靠的:无论的网络链路中出现了怎样的链路变化,TCP 都可以保证一个报文一定能够到达接收端;
  • 基于字节流的:TCP不知道消息的边界,发送端有可能将单个消息分成多个报文发送,也可能将多个消息合并成一个报文发送。因此,必须在应用层定义自己的消息结构。

TCP头格式有哪些√!O

序列号:在建立连接时由计算机生成的随机数作为其初始值,用来解决网络包乱序问题。

确认应答号:接收方用确认号来告诉发送方自己下一次「期望」收到的数据的序列号,发送端收到这个确认应答以后可以认为在这个序号以前的数据都已经被正常接收。用来解决丢包的问题。
控制位:

  • ACK:该位为 1 时,「确认应答」的字段变为有效,TCP 规定除了最初建立连接时的 SYN 包之外该位必须设置为 1
  • RST:该位为 1 时,表示 TCP 连接中出现异常必须强制断开连接。
  • SYN:该位为 1 时,表示希望建立连接,并在其「序列号」的字段进行序列号初始值的设定。
  • FIN:该位为 1 时,表示今后不会再有数据发送,希望断开连接。当通信结束希望断开连接时,通信双方的主机之间就可以相互交换 FIN 位为 1 的 TCP 段。

为什么需要 TCP 协议, TCP 工作在哪一层√!OP

因为IP层是不可靠的,可能会出现丢包,乱序等问题,当出现错误时,ip 层会选择丢弃。它不保证网络包的可靠交付,只保证尽最大努力交付。

如果需要保障网络数据包的可靠性,那么就需要由上层(传输层)的 TCP 协议来负责。

如何唯一确定一个 TCP 连接呢√!O

TCP 四元组可以唯一的确定一个连接,四元组包括如下:[源ip地址,源端口,目标地址,目标端口]

有一个 IP 的服务端监听了一个端口,它的 TCP 的最大连接数是多少√

服务端通常固定在某个本地端口上监听,等待客户端的连接请求。因此,客户端 IP 和 端口是可变的,其理论值计算公式如下:
最大TCP连接数 = 客户端IP数 × 客户端的端口数
对 IPv4,客户端的 IP 数最多为 232 次方,客户端的端口数最多为 216 次方,也就是服务端单机最大 TCP 连接数,约为 248 次方。
当然,服务端最大并发 TCP 连接数远不能达到理论上限,会受以下因素影响:

  • 文件描述符限制,单个进程可打开的文件描述符数量是有限制的,一般默认是1024。
  • 内存限制,每个 TCP 连接都要占用一定内存,操作系统的内存是有限的,如果内存资源被占满后,会发生 OOM异常。

UDP和TCP有什么区别呢?分别的应用场景是√!2

  • TCP可以实现可靠传输,UDP仅保证尽最大努力交付。
  • TCP必须建立1v1的连接,而UDP不需要建立连接,可以1对多。
  • TCP首部开销较大,20字节,UDP固定,就8字节。
  • **TCP是基于字节流的无边界的,而 UDP是面向消息的有边界的。**具体来说,当UDP发送数据包时,它不会基于MSS进行分片,而是将一整个数据包交给IP层去处理。当UDP 接收方接收数据时,每个UDP报文在缓冲区被封装为一个数据结构,该数据结构包含了UDP报文的各个字段。然后UDP接收缓冲区会按照接收的顺序将它们构造为一个队列,应用层就可以每次从队头取出一个完整的消息。

应用场景:

  • 由于TCP是面向连接的,能保证数据的可交付,因此常用于FTP文件传输,HTTP协议等。
  • 而UDP是面向无连接的,他可以随时发送数据,因此经常用于音视频传输等等。

TCP和UDP可以使用同一个端口吗√!2

可以的,传输层的「端口号」的作用,是为了区分同一个主机上同一传输层协议中不同应用程序的数据包。
而 UDP 和 TCP 是完全不同的两个网络模块,当应用程序收到数据包后,可以在 IP 包头的「协议号」字段知道该数据包是TCP还是UDP,所以可以根据这个信息确定将数据包交给哪个模块去处理。

1.2 TCP 连接建立

TCP 三次握手过程是怎样的√!O

一开始,客户端与服务端都处于close状态,先是服务端主动监听某个端口,处于listen状态:

  • 第一次握手:客户端随机初始化序列号,并将SYN标志置为1,表示是SYN报文,然后将此报文发送给服务端,表示向服务端发起连接,该报文不含应用层数据,之后客户端处于SYN-SENT状态;
  • 第二次握手:服务端收到客户端的SYN报文后,首先也随机初始化一个序列号填入序号字段中,然后把客户端发来的序列号+1作为首部中的确认号,最后将SYN和ACK标志字段置为1,表示是SYN+ACK报文。然后将该报文发给客户端。这时服务端处于SYN-RCVD状态。
  • 第三次握手:客户端收到SYN+ACK报文后,将确认应答号置为服务端序号+1,将ACK字段置为1表示该报文是个ACK报文。第三次握手时报文是可以携带数据的(前两次是不可以携带数据的)。最终将该报文发给服务器端。之后进入established状态。服务端收到该报文后,也进入established状态。
    在这里插入图片描述

为什么是三次握手?不是两次、四次√!1

首先,建立全双工通信最少需要三次握手。具体来说:

  • 原因一:同步双方初始序列号:TCP的通信的双方,都必须维护一个自己的序列号,用于确保消息的有序性。如果是两次握手的话,那么只能保证只有一方的初始序列号能被对方成功接收;
  • 原因二:避免建立历史连接和资源浪费:如果客户端由于网络阻塞先后发起了两次SYN报文的话,假设只有两次握手,那么服务器端无法分辨哪个是最新的SYN报文,因此都会建立连接,但最终只有一个能用于通信。因此建立无效就会造成资源浪费。

可以发现,如果是三次握手,当服务端发完SYN+ACK报文后,进入的是SYN_RCVD状态而不是established状态,并且可以确认这个连接是否是正确的连接以及同步一个正确的序列号。

假设客户端发了一个序号为100的SYN报文,此时客户端宕机然后重启了,所以客户端又发了一个序号为200的SYN报文,但序号为100的SYN报文先到达了服务端,服务端不知道这是旧SYN报文,并且假设只有两次握手,那么服务端直接就建立了连接,建立连接意味着可以发送数据,但当服务端的SYN+ACK报文到达客户端后,客户端从确认号得知这不是自己想建立的连接,所以会发送RST报文终止连接。当过一段之间后,序号为200的SYN报文到达了服务端,才能建立正确的连接。可见,两次握手导致服务端白白的建立了一次连接

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

  • 因为如果IP层有一个数据包的大小超过了MTU,那么IP层就要对该数据包进行分片,然后到目的主机再进行组装。但如果在某个过程中丢了一个分片,那么IP层就会丢掉整个数据包;
  • 因此当一个IP分片丢失后,接收方的IP层就无法组装一个完整的TCP报文,并且TCP也不会回复ACK。因此发送方迟迟收不到ACK确认报文,就会发生超时重传(整个TCP头部+首部)。可见这样是非常没有效率的;
  • 所以只要TCP报文不超过MSS的话,那么IP层就不需要分片,而且就算某个分片丢了,也不用重传整个TCP报文。

第一次握手丢失了,会发生什么 √!1

  • 如果第一次握手丢失了,如果客户端迟迟收不到服务端的 SYN-ACK 报文(第二次握手),就会触发「超时重传」机制,重传 SYN 报文,而且重传的 SYN 报文的序列号都是一样的;
  • 当到达超时重传次数后仍然没有收到服务端的ACK报文,那么客户端会主动断开连接;
  • 在 Linux 里,客户端的 SYN 报文最大重传次数由 tcp_syn_retries内核参数控制,这个参数是可以自定义的,默认值一般是 5。
root@ubuntu:/usr/local/nginx/sbin# cat /proc/sys/net/ipv4/tcp_syn_retries
5

第二次握手丢失了,会发生什么 √!1

由于第二次握手中包含了对第一次握手的ACK报文 和 第二次握手里的 SYN报文,所以,如果第二次握手丢了:

  • 由于客户端迟迟没有收到第二次握手,那么就会触发触发超时重传机制,重传 SYN 报文;
    -又因为第二次握手中包含服务端的 SYN 报文,但是第二次握手丢了,所以服务端也收不到第三次握手的ACK报文,因此服务端也会重传SYN+ACK报文。

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

第三次握手丢失了,会发生什么√

客户端收到服务端的 SYN-ACK 报文后,就会给服务端回一个 ACK 报文,也就是第三次握手,此时客户端状态进入到 ESTABLISH 状态。
因为这个第三次握手的 ACK 是对第二次握手的 SYN 的确认报文,所以当第三次握手丢失了,如果服务端那一方迟迟收不到这个确认报文,就会触发超时重传机制,重传 SYN-ACK 报文,直到收到第三次握手,或者达到最大重传次数。如果到达最大重传次数还没有收到,那么服务端会主动断开连接。

什么是 SYN 攻击?如何避免 SYN 攻击√

我们都知道 TCP 连接建立是需要三次握手,假设攻击者短时间伪造不同 IP 地址的 SYN 报文,服务端每接收到一个 SYN 报文,就进入SYN_RCVD 状态,但服务端发送出去的 ACK + SYN 报文,无法得到未知 IP 主机的 ACK 应答,久而久之就会占满服务端的半连接队列,使得服务端不能为正常用户服务。
先说一下,什么是 TCP 半连接和全连接队列。在 TCP 三次握手的时候,Linux 内核会维护两个队列,分别是:

  • 半连接队列:当服务端接收到客户端的 SYN 报文时,会创建一个半连接对象,然后将其加入半连接队列;
  • 全连接队列:接着发送 SYN + ACK 给客户端,等待客户端回应 ACK 报文;服务端接收到 ACK 报文后,从半连接取出一个半连接对象,然后创建一个新的连接对象放入到全连接队列;
  • 应用通过调用 accpet() socket 接口,从「 Accept 队列」取出连接对象。

不管是半连接队列还是全连接队列,都有最大长度限制,超过限制时,默认情况都会丢弃报文。

SYN 攻击方式最直接的表现就会把 TCP 半连接队列打满,这样当 TCP 半连接队列满了,后续再在收到 SYN 报文就会丢弃,导致客户端无法和服务端建立连接。

避免 SYN 攻击方式,可以有以下四种方法:

  • 调大 netdev_max_backlog:当网卡接收数据包的速度大于内核处理的速度时,会有一个队列保存这些数据包。控制该队列的最大值如下参数,默认值是 1000,我们要适当调大该参数的值,比如设置为 10000:
  • 增大 TCP 半连接队列;
  • 开启 tcp_syncookies:开启 syncookies 功能就可以在不使用 SYN 半连接队列的情况下成功建立连接,相当于绕过了 SYN 半连接来建立连接。
    net.ipv4.tcp_syncookies 参数主要有以下三个值:
    0 值,表示关闭该功能;
    1 值,表示仅当 SYN 半连接队列放不下时,再启用它;
    2 值,表示无条件开启功能;
    那么在应对 SYN 攻击时,只需要设置为 1 即可。
  • 减少 SYN+ACK 重传次数:当服务端受到 SYN 攻击时,就会有大量处于 SYN_REVC 状态的 TCP 连接,处于这个状态的 TCP 会重传 SYN+ACK ,当重传超过次数达到上限后,就会断开连接。那么针对 SYN 攻击的场景,我们可以减少 SYN-ACK 的重传次数,以加快处于 SYN_REVC 状态的 TCP 连接断开。

1.3 TCP 连接断开

TCP 四次挥手过程是怎样√

首先双方都处于Established 状态,

  • 当客户端打算关闭连接时,它会发送一个 FIN 报文,进入 FIN_WAIT_1 状态;
  • 服务端收到该报文后,就向客户端发送 ACK 应答报文,接着服务端进入 CLOSE_WAIT 状态。
  • 客户端收到服务端的 ACK 应答报文后,之后进入 FIN_WAIT_2 状态;
  • 等待服务端处理完数据后,也向客户端发送 FIN 报文,之后服务端进入 LAST_ACK 状态;
  • 客户端收到服务端的 FIN 报文后,回复一个 ACK 报文后,之后进入 TIME_WAIT 状态;
  • 服务端收到了 ACK 应答报文后,就进入了 CLOSE 状态,至此服务端已经完成连接的关闭。
  • 客户端在经过 2MSL 时间后,自动进入 CLOSE 状态,至此客户端也完成连接的关闭。
  • 可以看到,每个方向都需要一个 FIN 和一个 ACK,因此通常被称为四次挥手。
    这里一点需要注意是:主动关闭连接的,才有 TIME_WAIT 状态。
    在这里插入图片描述

为什么挥手需要四次???

  • 当客户端要关闭连接时,首先向向服务端发送 FIN 报文,仅仅表示客户端不再发送数据了但是还能接收数据。
  • 服务端收到客户端的 FIN 报文时,先回一个 ACK 应答报文,而服务端可能还有数据需要处理和发送,等服务端不再发送数据时,才发送 FIN 报文给客户端来表示同意现在关闭连接。
  • 当客户端接收到服务端地FIN报文时,回复一个ACK应答报文。
  • 从上面过程可知,服务端通常需要等待完成数据的发送和处理,所以服务端的 ACK 和 FIN 一般都会分开发送,因此是需要四次挥手。
    但是在特定情况下,四次挥手是可以变成三次挥手的:

第一次挥手丢失了,会发生什么√

当客户端(主动关闭方)调用 close 函数后,就会向服务端发送 FIN 报文,试图与服务端断开连接,此时客户端的连接进入到 FIN_WAIT_1 状态。
正常情况下,如果能及时收到服务端(被动关闭方)的 ACK,则会很快变为 FIN_WAIT2状态。
如果第一次挥手丢失了,那么客户端迟迟收不到被动方的 ACK 的话,也就会触发超时重传机制,重传 FIN 报文,重发次数由 tcp_orphan_retries 参数控制。如果到达重传次数后还是没能收到服务端的第二次挥手(ACK报文),那么客户端就会断开连接。

第二次挥手丢失了,会发生什么√

  • 当服务端收到客户端的第一次挥手后,就会先回一个 ACK 确认报文,此时服务端的连接进入到 CLOSE_WAIT 状态。
  • 在前面我们也提了,ACK 报文是不会重传的,所以如果服务端的第二次挥手丢失了,客户端就会触发超时重传机制,重传 FIN 报文,直到收到服务端的第二次挥手,或者达到最大的重传次数。如果都没有,那么客户端就会直接断开连接。
  • 但与第一次挥手丢失不同,第一次挥手丢失,服务端压根没有收到客户端的FIN报文,因此始终是established状态;但是第二次挥手丢失说明服务端已经接收到了客户端的FIN报文,进入到了close_wait状态。

第三次挥手丢失了,会发生什么√

当服务端(被动关闭方)收到客户端(主动关闭方)的 FIN 报文后,内核会自动回复 ACK,同时连接处于 CLOSE_WAIT 状态,顾名思义,它表示等待应用进程调用 close 函数关闭连接。
此时,内核是没有权利替代进程关闭连接,必须由进程主动调用 close 函数来触发服务端发送 FIN 报文。
服务端处于 CLOSE_WAIT 状态时,调用了 close 函数,内核就会发出 FIN 报文,同时连接进入 LAST_ACK 状态,等待客户端返回 ACK 来确认连接关闭。
如果迟迟收不到这个 ACK,服务端就会重发 FIN 报文,重发次数仍然由 tcp_orphan_retries 参数控制,这与客户端重发 FIN 报文的重传次数控制方式是一样的。如果到达重传次数仍没有收到客户端的ACK,那么服务端会直接断开连接。

第四次挥手丢失了,会发生什么√

  • 当客户端收到服务端的第三次挥手的 FIN 报文后,就会回 ACK 报文,也就是第四次挥手,此时客户端连接进入 TIME_WAIT 状态。
  • 在 Linux 系统,TIME_WAIT 状态会持续 2MSL 后才会进入关闭状态。但如果第四次挥手的ACK报文丢失,导致再次收到第三次挥手(FIN 报文)后,就会重置定时器。
  • 然后,服务端(被动关闭方)没有收到 ACK 报文前,还是处于 LAST_ACK 状态。
  • 如果第四次挥手的 ACK 报文没有到达服务端,服务端就会重发 FIN 报文,重发次数仍然由前面介绍过的 tcp_orphan_retries 参数控制。

为什么需要 TIME_WAIT 状态√!O

**首先,主动发起关闭的一方,才会有TIME-WAIT状态。**因为主动关闭方在关闭前一定没有要发送的数据了,所以被动关闭方不要TIME-WAIT状态。
需要 TIME-WAIT 状态,主要是两个原因:
第一个原因是防止历史连接中的数据,被后面相同四元组的连接 错误的接收。假设再上一次连接时有一个数据包被网络延迟了,然后再该连接关闭后没有TIME-WAIT状态,那么再次连接后,被延迟的数据包就有可能被错误接收。因此TIME-WAIT的作用就是等待2MSL的时长,使得原来连接的数据包在网络中都自然消失。
第二个原因就是保证「被动关闭连接」的一方,能被正确的关闭。什么意思呢。因为如果主动关闭方最后一次ACK报文在网络中丢失了,那么按照 TCP 可靠性原则,被动关闭方会重传第三次握手报文。但如果此时客户端没有TIME-WAIT状态直接进入到关闭状态,那么在收到服务端重传的FIN报文后,就会回RST报文,对于一个可靠的协议来说不是一个优雅的方式。因此TIME-WAIT状态是能够确保连接被正确的关闭。

为什么 TIME_WAIT 等待的时间是 2MSL√!1

  • MSL 是 Maximum Segment Lifetime,报文最大生存时间,它是任何报文在网络上存在的最长时间,超过这个时间报文将被丢弃。
  • **TIME_WAIT 等于 2 倍的 MSL,意味着至少允许报文丢失一次。**比如, 如果主动关闭方的第四挥手报文在一个 MSL 内丢失,这样被动关闭方重发的第三次挥手报文会在第 2 个 MSL 内到达。
  • TIME_WAIT状态是从第四挥手报文发出开始计时的,如果第四次挥手报文丢失了,那么一定还会收到重传的第三次挥手报文,这个时候就要重传第四次挥手报文,并重新开始计时。

就是主动关闭方发送ACK报文后,自己启动TIME_WAIT记时的同时,也会启动超时重传的计时,当一定时间内没有收到第四握手报文时,就会重传,两个计时器都会重新计时,所以历史报文一定会在2MSL内自然消亡。

TIME_WAIT 过多有什么危害?

如何优化 TIME_WAIT?

服务器出现大量 TIME_WAIT 状态的原因有哪些?

服务器出现大量 CLOSE_WAIT 状态的原因有哪些?

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

首先,在客户端发生故障的情况下,如果服务端一直不会发送数据给客户端,那么服务端是永远无法感知到客户机宕机这个事件的,也就是服务端的 TCP 连接将一直处于 ESTABLISH 状态,占用着系统资源。
所以为了避免这个机制,TCP设置了一种保活机制,这个机制的原理是这样的:
定义一个时间段,在这个时间段内,如果没有任何连接相关的活动,那么服务端会每隔一短时间发送一个探测报文,如果连续几个探测报文都没有得到响应,则认为当前连接已经死亡,系统内核将错误信息通知给上层应用程序。
需要注意的是,应用程序需要通过socket接口设置SO_KEEPALIVE 选项才能启动保活机制。
在 Linux 内核可以有对应的参数可以设置保活时间、保活探测的次数、保活探测的时间间隔,以下都为默认值:

net.ipv4.tcp_keepalive_time=7200
net.ipv4.tcp_keepalive_intvl=75  
net.ipv4.tcp_keepalive_probes=9

tcp_keepalive_time=7200:表示保活时间是 7200 秒(2小时),也就 2 小时内如果没有任何连接相关的活动,则会启动保活机制
tcp_keepalive_intvl=75:表示每次检测间隔 75 秒;
tcp_keepalive_probes=9:表示检测 9 次无响应,认为对方是不可达的,从而中断本次的连接。
在这里插入图片描述
也就是说在 Linux 系统中,最少需要经过 2 小时 11 分 15 秒才可以发现一个「死亡」连接。

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

  • 第一种,对端程序是正常工作的。当 TCP 保活的探测报文发送给对端, 对端会正常响应,这样 TCP 保活时间会被重置,等待下一个 TCP 保活时间的到来。
  • 第二种,对端主机宕机并重启。当 TCP 保活的探测报文发送给对端后,对端是可以响应的,但由于没有该连接的有效信息,会产生一个 RST 报文,这样很快就会发现 TCP 连接已经被重置。
  • 第三种,是对端主机宕机(注意不是进程崩溃,进程崩溃后操作系统在回收进程资源的时候,会发送 FIN 报文,而主机宕机则是无法感知的,所以需要 TCP 保活机制来探测对方是不是发生了主机宕机),或对端由于其他原因导致报文不可达。当 TCP 保活的探测报文发送给对端后,石沉大海,没有响应,连续几次,达到保活探测次数后,TCP 会报告该 TCP 连接已经死亡。
  • TCP 保活的这个机制检测的时间是有点长,我们可以自己在应用层实现一个心跳机制。比如,web 服务软件一般都会提供 keepalive_timeout 参数,用来指定 HTTP 长连接的超时时间。如果设置了 HTTP 长连接的超时时间是 60 秒,web 服务软件就会启动一个定时器,如果客户端在完成一个 HTTP 请求后,在 60 秒内都没有再发起新的请求,定时器的时间一到,就会触发回调函数来释放该连接。

如果已经建立了连接,但是服务端的进程崩溃会发生什么?

1.4 Socket编程

客户端是什么时候得知连接socket的ip的端口号的?

TCP连接的三次握手过程如下:

客户端向服务器发送 syn 报文,请求建立连接。

服务器接收到 syn 报文后,使用已有的监听套接字返回 syn-ack 报文。此时服务器还没分配连接套接字。

客户端收到 syn-ack 后,发送 ack 报文完成三次握手。

那么,关于你问的问题:

服务器是在三次握手的第二次,即返回 syn-ack 报文时,通过 syn-ack 报文头中的源IP和源端口告诉客户端自己的连接套接字地址。

比如 syn-ack 报文头为:

源IP:服务器IP
源端口:服务器随机分配的连接套接字端口
目的IP:客户端IP
目的端口:客户端原始请求的端口

这样客户端就知道连接已建立,且知道应该用哪个IP和端口与服务器相应的连接套接字通信。

服务器返回syn-ack时才真正分配用于此连接的套接字,并通过报文头告知客户端。三次握手建立连接时就是通过修改和传递报文头中的IP和端口来完成 sockets 在两端的关联。

所以答案是:服务器在三次握手的第二次syn-ack报文中,通过修改报文头告诉客户端它分配的连接套接字地址。

您提出了一个好问题。客户端收到服务器返回的syn-ack报文头中的ip地址和端口号是它未曾见过的,那么客户端是如何处理它的?

请允许我进一步解释:

TCP三次握手建立连接时,客户端和服务器都没有事先约定好对方的IP和端口。

客户端仅是主动发起一个请求,希望连接到服务器默认监听的端口,如80或443端口。

服务器响应syn-ack报文时,向客户端告知它给这个特定连接分配的IP和端口。

尽管客户端未见过这个IP和端口,但是它知道这是服务器为了建立此连接而分配的地址。

客户端相信并接收这个地址,因为它来自于服务器对客户端syn报文的合法响应。

客户端从此就知晓应使用这个新学习到的地址与服务器相应连接套接字通信。

通过接下来的三次握手 ack报文,客户端正式与服务器建立了必要的连接关联。

所以总体来说,客户端之所以能处理新的IP和端口,是基于TCP三次握手建立连接的语义和规范来进行的。它相信并采纳服务器提供的新地址,以完成两边sockets的关联。

针对 TCP 应该如何 Socket 编程?√

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-T4XRcKgY-1689757076878)(https://cdn.xiaolincoding.com//mysql/other/format,png-20230309230545997.png)]

  • 服务端和客户端初始化 socket,得到文件描述符;
  • 服务端调用 bind,将 socket 绑定在指定的 IP 地址和端口;
  • 服务端调用 listen,进行监听;
  • 服务端调用 accept,等待客户端连接;
  • 客户端调用 connect,向服务端的地址和端口发起连接请求;
  • 服务端 accept 返回用于传输的 socket 的文件描述符;
  • 客户端调用 write 写入数据;服务端调用 read 读取数据;
  • 客户端断开连接时,会调用 close,那么服务端 read 读取数据的时候,就会读取到了 EOF,待处理完数据后,服务端调用 close,表示连接关闭。

这里需要注意的是,服务端调用 accept 时,连接成功了会返回一个已完成连接的 socket,后续用来传输数据。

所以,监听的 socket 和真正用来传送数据的 socket,是「两个」 socket,一个叫作监听 socket,一个叫作已完成连接 socket

成功连接建立之后,双方开始通过 read 和 write 函数来读写数据,就像往一个文件流里面写东西一样。

listen() 的参数 backlog 有何意义?

Linux内核中会维护两个队列:

  • 半连接队列(SYN 队列):接收到一个 SYN 建立连接请求,处于 SYN_RCVD 状态;
  • 全连接队列(Accpet 队列):已完成 TCP 三次握手过程,处于 ESTABLISHED 状态;

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-nNY0nY6B-1689757076879)(https://cdn.xiaolincoding.com//mysql/other/format,png-20230309230542373.png)]

int listen (int socketfd, int backlog);
  • 参数一 socketfd 为 socketfd 文件描述符

  • 参数二 backlog,这参数在历史版本有一定的变化

  • 在早期 Linux 内核 backlog 是 SYN 队列大小,也就是未完成的队列大小。

    在 Linux 内核 2.2 之后,backlog 变成 accept 队列,也就是已完成连接建立的队列长度,所以现在通常认为 backlog 是 accept 队列。

    但是上限值是内核参数 somaxconn 的大小,也就说 accpet 队列长度 = min(backlog, somaxconn)。

说一下五种I\O模型?

首先,任何一个I\O操作都分为两个阶段,数据准备数据存取

  • 数据准备根据系统I\O操作的就绪状态,分为阻塞非阻塞两种方式;
  • 数据读写根据应用程序和内核的交互方式分为同步异步两种方式。

在处理 IO 的时候,阻塞和非阻塞都是同步 IO。只有使用了特殊的 API 才是异步 IO。
在这里插入图片描述

I\O blocking I\O 同步阻塞

当用户进程调用了read系统调用时,内核就开始了IO的第一个阶段:准备数据(对于网络IO来说,很多时候数据在一开始还没有到达。比如,还没有收到一个完整的TCP包。这个时候内核就要等待足够的数据到来)。这个过程需要等待,也就是说数据被拷贝到操作系统内核的缓冲区中是需要一个过程的。而在用户进程这边,整个进程会被阻塞。第二个阶段:当内核一直等到数据准备好了,它就会将数据从kernel中拷贝到用户内存,然后 内核返回结果,用户进程才解除block的状态,重新运行起来。

blocking IO的特点就是在IO执行的两个阶段(等待数据、复制数据)都被阻塞了。

优点:能够及时返回数据,无延迟。

缺点:用户性能可能会降低。

在这里插入图片描述

I/O non-blocking 同步 非阻塞

当用户进程发出read操作时,如果kernel中的数据还没有准备好,那么它并不会block用户进程,而是立刻返回一个error,说明可能是内部错误。如果继续判断errno == EAGAIN,说明是非阻塞返回,数据还没有准备好。如果返回0,说明客户端关闭了连接无法获取数据;如果返回的值大于0,说明数据已经存储到了内核空间,可以拷贝到用户进程了。需要注意,拷贝数据整个过程,进程仍然是属于阻塞的状态

nonblocking IO的特点是用户进程需要不断的主动询问kernel数据准备好了没有。其进程在等待数据阶段不会阻塞,在复制数据阶段仍然会阻塞。

优点:能够在等待数据阶段干其他活了(包括提交其他任务,也就是 “后台” 可以有多个任务在同时执行)。

缺点:任务完成的响应延迟增大了,因为每过一段时间才去轮询一次read操作,而任务可能在两次轮询之间的任意时间完成。这会导致整体数据吞吐量的降低。
在这里插入图片描述

IO多路复用 IO multiplexing 同步 阻塞/非阻塞

由于同步非阻塞方式需要不断主动轮询,轮询占据了很大一部分过程,轮询会消耗大量的CPU时间,而 “后台” 可能有多个任务在同时进行,人们就想到了循环查询多个任务的完成状态每次只轮询检查一个任务很亏,所以循环检查多个任务,只要有任何一个任务完成,就去处理它。

当用户进程调用了select,那么整个进程会被block,而同时,内核会“监视”所有select负责的socket,当任何一个socket中的数据准备好了,select就会返回。这个时候用户进程再调用read操作(同步操作),将数据从kernel拷贝到用户进程。如果没有设置timeont参数,那么如果没有任何一个socket的数据准备好,那么进程将被阻塞,就是阻塞模式;如果设置了timeout参数,那么同样前提下,select会返回一次,变成了非阻塞模式

**优点:**当需要同时处理多个客户端接入请求时,可以利用多线程或者I/O多路复用技术进行处理。I/O多路复用技术通过把多个I/O的阻塞复用到一个select的阻塞上,从而使得系统在单线程的情况下可以同时处理多个客户端请求。与传统的多线程/多进程模型比,I/O多路复用的最大优势是系统开销小,系统不需要创建新的额外进程或者线程,也不需要维护这些进程和线程的运行,降底了系统的维护工作量,节省了系统资源。

**缺点:**每次去轮询多个I\O的数据就绪状态肯定是消耗性能的。
在这里插入图片描述

信号驱动I/O signal-driven 同步 非阻塞

信号驱动式I/O:首先我们允许Socket进行信号驱动IO,并安装一个信号处理函数,进程继续运行并不阻塞。当数据准备好时,进程会收到一个SIGIO信号,可以在信号处理函数中调用I/O操作函数处理数据。过程如下图所示:

也就是说,数据准备过程是非阻塞模式,但是数据读取过程还是同步的,因为调用的是read系统调用。
在这里插入图片描述

异步非阻塞 IO(asynchronous IO)

相对于同步IO,异步IO不是顺序执行。用户进程进行aio_read系统调用之后,无论内核数据是否准备好,都会直接返回给用户进程,然后用户态进程可以去做别的事情。等到socket数据准备好了,内核直接复制数据给进程,然后从内核向进程发送通知IO两个阶段,进程都是非阻塞的。

用户进程发起aio_read操作之后,立刻就可以开始去做其它的事。而另一方面,从kernel的角度,当它受到一个asynchronous read之后,首先它会立刻返回,所以不会对用户进程产生任何block。然后,kernel会等待数据准备完成,然后将数据拷贝到用户内存,当这一切都完成之后,kernel会给用户进程发送一个signal或执行一个基于线程的回调函数来完成这次 IO 处理过程,告诉它read操作完成了。

在 Linux 中,通知的方式是 “信号”:

如果这个进程正在用户态忙着做别的事(例如在计算两个矩阵的乘积),那就强行打断之,调用事先注册的信号处理函数,这个函数可以决定何时以及如何处理这个异步任务。由于信号处理函数是突然闯进来的,因此跟中断处理程序一样,有很多事情是不能做的,因此保险起见,一般是把事件 “登记” 一下放进队列,然后返回该进程原来在做的事

如果这个进程正在内核态忙着做别的事,例如以同步阻塞方式读写磁盘,那就只好把这个通知挂起来了,等到内核态的事情忙完了,快要回到用户态的时候,再触发信号通知

如果这个进程现在被挂起了,例如无事可做 sleep 了,那就把这个进程唤醒,下次有 CPU 空闲的时候,就会调度到这个进程,触发信号通知。

在这里插入图片描述

2 TCP重传、滑动窗口、流量控制、拥塞控制

TCP如何实现可靠传输√!O

  • 首先,每个TCP报头都包含一个序列号和确认号,序列号可以保证接收的报文有序,确认号可以保证报文的完整性;其次,TCP还可以根据首部的校验和来验证这个报文是否有差错;
  • 超时重传 : 当发送方发送一个报文之后,它会启动一个定时器,等待接收端回复ACK。如果发送端在一个往返时延(RTT)内未收到ACK,那么发送端就认为该数据包丢失了,然后进行超时重传;
  • 流量控制: TCP 连接的双方在内核缓冲区中都有读写缓冲区,接收端只允许发送端发送自己能够接收的数据量。当接收方的缓冲区大小不足时,会通过报头的窗口大小字段让发送端降低发送速率。
  • 拥塞控制:流量控制的作用只是为了避免发送方的数据填满接收方的接收缓存。但是流量控制并不能根据整个网络的环境来做出什么调整。因此拥塞控制的作用是当网络出现拥塞时,让每个TCP发送方降低发送速率。

重传机制√

TCP 实现可靠传输的方式之一,是通过序列号与确认应答。

在 TCP 中,当发送端的数据到达接收主机时,接收端主机会返回一个确认应答消息,表示已收到消息。

但在错综复杂的网络,丢包是不能避免的事情,所以 TCP 针对数据包丢失的情况,会用重传机制解决。

接下来说说常见的重传机制:

  • 超时重传
  • 快速重传
  • SACK
  • D-SACK
超时重传

重传机制的其中一个方式,就是在发送数据时,设定一个定时器,当超过指定的时间后没有收到对方的 ACK 确认应答报文,就会重发该数据,也就是我们常说的超时重传

TCP 会在以下两种情况发生超时重传:

  • 数据包丢失,也就是数据包就没发送到接收端,因此更不可能收到确认应答。
  • 确认应答丢失,数据包到达了接收端,但是发回给发送端的确认应答丢失了。

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-EWNFJRmr-1689757161159)(https://cdn.xiaolincoding.com/gh/xiaolincoder/ImageHost2/%E8%AE%A1%E7%AE%97%E6%9C%BA%E7%BD%91%E7%BB%9C/TCP-%E5%8F%AF%E9%9D%A0%E7%89%B9%E6%80%A7/5.jpg?image_process=watermark,text_5YWs5LyX5Y-377ya5bCP5p6XY29kaW5n,type_ZnpsdHpoaw,x_10,y_10,g_se,size_20,color_0000CD,t_70,fill_0)]

超时时间应该设置为多少呢?

RTT(Round-Trip Time 往返时延) 指的是数据发送时刻到接收到确认的时刻的差值,也就是包的往返时间。

超时重传时间是以 RTO (Retransmission Timeout 超时重传时间)表示。

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

根据上述的两种情况,我们可以得知,超时重传时间 RTO 的值应该略大于报文往返 RTT 的值

如果超时重发的数据,再次超时的时候,又需要重传的时候,TCP 的策略是超时间隔加倍。

也就是每当遇到一次超时重传的时候,都会将下一次超时时间间隔设为先前值的两倍。两次超时,就说明网络环境差,不宜频繁反复发送。

超时触发重传存在的问题是,超时周期可能相对较长。那是不是可以有更快的方式呢?

于是就可以用「快速重传」机制来解决超时重发的时间等待。

快速重传

TCP 还有另外一种快速重传(Fast Retransmit)机制,它不以时间为驱动,而是以数据驱动重传

快速重传机制,是如何工作的呢?其实很简单,一图胜千言。

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-JPr6sJf4-1689757161159)(https://cdn.xiaolincoding.com/gh/xiaolincoder/ImageHost2/%E8%AE%A1%E7%AE%97%E6%9C%BA%E7%BD%91%E7%BB%9C/TCP-%E5%8F%AF%E9%9D%A0%E7%89%B9%E6%80%A7/10.jpg?image_process=watermark,text_5YWs5LyX5Y-377ya5bCP5p6XY29kaW5n,type_ZnpsdHpoaw,x_10,y_10,g_se,size_20,color_0000CD,t_70,fill_0)]

在上图,发送方发出了 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 方法

还有一种实现重传机制的方式叫:SACK( Selective Acknowledgment), 选择性确认

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

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

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-u2JGBE5U-1689757161159)(https://cdn.xiaolincoding.com/gh/xiaolincoder/ImageHost2/%E8%AE%A1%E7%AE%97%E6%9C%BA%E7%BD%91%E7%BB%9C/TCP-%E5%8F%AF%E9%9D%A0%E7%89%B9%E6%80%A7/11.jpg?image_process=watermark,text_5YWs5LyX5Y-377ya5bCP5p6XY29kaW5n,type_ZnpsdHpoaw,x_10,y_10,g_se,size_20,color_0000CD,t_70,fill_0)]

如果要支持 SACK,必须双方都要支持。在 Linux 下,可以通过 net.ipv4.tcp_sack 参数打开这个功能(Linux 2.4 后默认打开)。

Duplicate SACK

Duplicate SACK 又称 D-SACK,其主要使用了 SACK 来告诉「发送方」有哪些数据被重复接收了。

下面举例两个栗子,来说明 D-SACK 的作用。

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-BtPO3MRh-1689757161160)(https://cdn.xiaolincoding.com/gh/xiaolincoder/ImageHost2/%E8%AE%A1%E7%AE%97%E6%9C%BA%E7%BD%91%E7%BB%9C/TCP-%E5%8F%AF%E9%9D%A0%E7%89%B9%E6%80%A7/12.jpg?image_process=watermark,text_5YWs5LyX5Y-377ya5bCP5p6XY29kaW5n,type_ZnpsdHpoaw,x_10,y_10,g_se,size_20,color_0000CD,t_70,fill_0)]

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

栗子二号:网络延时

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-1l3cJSCu-1689757161160)(https://cdn.xiaolincoding.com/gh/xiaolincoder/ImageHost2/%E8%AE%A1%E7%AE%97%E6%9C%BA%E7%BD%91%E7%BB%9C/TCP-%E5%8F%AF%E9%9D%A0%E7%89%B9%E6%80%A7/13.jpg?image_process=watermark,text_5YWs5LyX5Y-377ya5bCP5p6XY29kaW5n,type_ZnpsdHpoaw,x_10,y_10,g_se,size_20,color_0000CD,t_70,fill_0)]

注意看,当ACK为 3000, 且没有SACK字段的时候,说明不存在没有接收到的数据包。

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

可见,D-SACK 有这么几个好处:

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

在 Linux 下可以通过 net.ipv4.tcp_dsack 参数开启/关闭这个功能(Linux 2.4 后默认打开)。

SACK的作用就是告诉接收方,哪些数据时已经收到的了,ack之前就是连续收到的,SACK字段就是后面收到的,还没有回复ack的。

D-SACK的作用就是告诉发送方:

  • 如果第一次收到ACK,并且带有D-SACK字段,那就说明是接收方回应的ACK丢了;
  • 如果第二次收到同样的ACK,并且带有D-SACK字段,就说明数据包是被网络延迟了。

SACK字段的值比ACK的值大,D-SACK字段的值比ACK的值小。

滑动窗口√

  • 如果每次都是发送方发送一个数据包,等到接收方回复ACK之后才能发送下一个数据包的话,这样效率太低下了。所以,这样的传输方式有一个缺点:数据包的往返时间越长,通信的效率就越低。
  • 为解决这个问题,TCP 引入了窗口这个概念。即使在往返时间较长的情况下,它也不会降低网络通信的效率。
  • 那么有了窗口,就可以指定窗口大小,窗口大小就是指无需等待确认应答,而可以继续发送数据的最大值
  • 窗口的实现实际上是操作系统开辟的一个缓存空间,发送方主机在等到确认应答返回之前,必须在缓冲区中保留已发送的数据。如果按期收到确认应答,此时数据就可以从缓存区清除。
    举例:
    假设窗口大小为 3 个 TCP 段,那么发送方就可以「连续发送」 3 个 TCP 段,并且中途若有 ACK 丢失也没关系,比如第2个报文端的ACK丢失了,当收到第三个报文段并回复ack为3的ACK报文,就说明前三个报文段都收到了。这就做累计确认或累积应答。

窗口大小由哪一方决定√

TCP 头里有一个字段叫 Window,也就是窗口大小。
这个字段是接收端告诉发送端自己还有多少缓冲区可以接收数据。于是发送端就可以根据这个接收端的处理能力来发送数据,而不会导致接收端处理不过来。

发送方发送的数据大小不能超过接收方的窗口大小,否则接收方就无法正常接收到数据。

发送方的滑动窗口√

发送方的滑动窗口分为4个部分:

  • 已发送并收到 ACK确认的数据;
  • 已发送但未收到 ACK确认的数据;
  • 未发送但还在发送窗口内的数据;
  • 未发送但还在发送窗口外的数据;

程序是如何表示发送方的四个部分的呢√

TCP 滑动窗口方案使用三个指针来跟踪在四个传输类别中的每一个类别中的字节。其中两个指针是绝对指针(指特定的序列号),一个是相对指针(需要做偏移)。

  • SND.WND:表示发送窗口的大小(大小是由接收方指定的);
  • SND.UNASend Unacknoleged):是一个绝对指针,它指向的是已发送但未收到确认的第一个字节的序列号,也就是 #2 的第一个字节。
  • SND.NXT也是一个绝对指针,它指向未发送但可发送范围的第一个字节的序列号,也就是 #3 的第一个字节。
  • 指向 #4 的第一个字节是个相对指针,它需要 SND.UNA 指针加上 SND.WND 大小的偏移量,就可以指向 #4 的第一个字节了。
    那么可用窗口大小的计算就可以是:

可用窗口大小 = SND.WND -(SND.NXT - SND.UNA)
在这里插入图片描述

接收方的滑动窗口√

接收方的滑动窗口根据处理的情况划分成三个部分:

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

其中三个接收部分,使用两个指针进行划分:

  • RCV.WND:表示接收窗口的大小,它会通告给发送方。
  • RCV.NXT:是一个指针,它指向期望从发送方发送来的下一个数据字节的序列号,也就是 #3 的第一个字节。
  • 指向 #4 的第一个字节是个相对指针,它需要 RCV.NXT 指针加上 RCV.WND 大小的偏移量,就可以指向 #4 的第一个字节了。
    在这里插入图片描述

接收窗口和发送窗口的大小是相等的吗√

  • 并不是完全相等,接收窗口的大小是约等于发送窗口的大小的。
  • 因为滑动窗口并不是一成不变的。比如,当接收方的应用进程读取数据的速度非常快的话,这样的话接收窗口可以很快的就空缺出来。那么新的接收窗口大小,是通过 TCP 报文中的 Windows 字段来告诉发送方。那么这个传输过程是存在时延的,所以接收窗口和发送窗口是约等于的关系。

滑动窗口的大小过大或者过小会产生什么问题√

窗口过大

  • 由于窗口过大,可能大量数据同时发送到网络中,会导致网络拥塞,进而导致数据包的丢失和重传,降低网络传输性能;

窗口过小

  • 由于窗口过小,所以会导致无法充分利用网络带宽,导致数据传输速率较慢
  • 另外由于窗口过小,所以发送方可能会出现频繁的等待现象,增加了数据传输的延迟

在实际应用中,TCP使用拥塞控制算法来检测网络拥塞,并自动调整滑动窗口的大小,以适应不同的网络条件。

流量控制?

操作系统缓冲区与滑动窗口的关系?

拥塞控制√!

  • 流量控制的作用只是为了避免发送方的数据填满接收方的接收缓存。但是流量控制并不能根据整个网络的环境来做出什么调整。
  • 而拥塞控制是当网络出现拥堵时,可能会出现数据包延迟、丢失等,如果这个时候发送方再重传数据,那么会进一步加重网络的负担,进入恶性循环。
  • 因此,当网络出现拥塞时,TCP需要降低发送的数据量。拥塞控制的目的是避免发送方的数据填满整个网络。

拥塞窗口√!

  • 拥塞窗口 cwnd是发送方维护的一个的状态变量,当网络没有拥塞时,cwnd就会变大;反之就会变小。
  • 是否出现拥塞,就是根据发送方是否进行超时重传来判定的。
  • 拥塞窗口cwnd的初始值通常是1个MSS(1460字节),cwnd的单位是字节。

拥塞控制四种算法√!

慢启动算法

  • TCP刚建立连接时,首先是有个慢启动的过程,也就是一开始先一点一点提高发送数据包的数量。慢启动的规则就是只要收到一个ACK,拥塞窗口cwnd就会加1(第二次发两个收到两个ACK,那么cwnd就变成了4)。
  • 由于慢启动算法,发包的个数是呈指数增长,这肯定是不行的,因此当大于慢启动门限ssthresh (slow start threshold)时,就启动拥塞避免算法。

拥塞避免算法

  • 当拥塞窗口 cwnd 「超过」慢启动门限 ssthresh 就会进入拥塞避免算法。一般来说 ssthresh 的大小是 65535 字节。
  • 拥塞避免算法的规则是,每收到一个ACK,那么cwnd就增加1/cwnd,意思就是收到cwnd个ACK后,cwnd的大小才能加1。比如在慢启动算法中,假设cwnd为8,那么再收到8个ACK的话,那么cwnd就变成了16;而在拥塞避免算法中,收到8个ACK后,cwnd变为了9。可见拥塞避免算法的目的是让cwnd从指数增长变成了线性增长。(缓慢增长嘛,防止网络突然拥塞)

拥塞发生
那么只要cwnd一直增加,网络总是会出现拥塞的,也就是会发生数据包重传,重传机制主要有两种:超时重传、快速重传。

  • 当发生了「超时重传」,则就会使用拥塞发生算法。这个时候,ssthresh 和 cwnd 的值会发生变化:
    • ssthresh 设为 cwnd/2;
    • cwnd 重置为初始值;
  • 而如果使用「快速重传」的话,说明此时的网络拥塞并不严重,因此ssthresh 和 cwnd 变化如下:
    • cwnd = cwnd/2 ,也就是设置为原来的一半;
    • ssthresh = cwnd;
    • 进入快速恢复算法。

快速恢复
快速重传和快速恢复算法一般同时使用,快速恢复算法是认为,你还能收到 3 个重复 ACK 说明网络也不那么糟糕,所以没有必要像超时重传那样反应过于强烈。当前只需要尽快将丢失的数据包发给接收方。

  • 进入快速恢复之前,cwnd 和 ssthresh 已被更新了:
    • cwnd = cwnd/2 ,也就是设置为原来的一半;
    • ssthresh = cwnd;
  • 然后,进入快速恢复算法如下:
    • 拥塞窗口 cwnd = ssthresh + 3 ( 3 的意思是3个重复的ACK确认有 3 个数据包被收到了);
    • 重传丢失的数据包;
    • 如果再收到重复的 ACK,那么 cwnd 增加 1;(加 1 代表每个收到的重复的 ACK 包,都已经离开了网络。这一步目的是为了尽快将丢失的数据包发给目标,从而解决拥塞的根本问题(三次相同的 ACK 导致的快速重传))
    • 如果收到新数据的 ACK 后,说明之前丢失的数据包已经送到,恢复过程已结束。因此把 cwnd 设置为第一步中的 ssthresh 的值,再次进入拥塞避免状态;

3 TCP实战抓包分析

在这里插入图片描述

tcpdump 和 Wireshark 有什么区别?

tcpdump 和 Wireshark 就是最常用的网络抓包和分析工具,更是分析网络性能必不可少的利器。
tcpdump 仅支持命令行格式使用,常用在 Linux 服务器中抓取和分析网络包。
Wireshark 除了可以抓包外,还提供了可视化分析网络包的图形页面。
所以,这两者实际上是搭配使用的,先用 tcpdump 命令在 Linux 服务器上抓包,接着把抓包的文件拖出到 Windows 电脑后,用 Wireshark 可视化分析。
当然,如果你是在 Windows 上抓包,只需要用 Wireshark 工具就可以。

tcpdump 在 Linux 下如何抓包?

假设我们要抓取下面的 ping 的数据包:

jixu@ubuntu:~/Desktop$ ping -I ens37 -c 3 183.232.231.174
PING 183.232.231.174 (183.232.231.174) from 192.168.2.109 ens37: 56(84) bytes of data.
From 192.168.2.104 icmp_seq=1 Redirect Network(New nexthop: 1.2.168.192)
64 bytes from 183.232.231.174: icmp_seq=1 ttl=47 time=71.2 ms
64 bytes from 183.232.231.174: icmp_seq=1 ttl=47 time=71.2 ms (DUP!)
64 bytes from 183.232.231.174: icmp_seq=1 ttl=46 time=71.2 ms (DUP!)
64 bytes from 183.232.231.174: icmp_seq=1 ttl=46 time=71.2 ms (DUP!)
From 192.168.2.104 icmp_seq=2 Redirect Network(New nexthop: 1.2.168.192)

--- 183.232.231.174 ping statistics ---
2 packets transmitted, 1 received, +3 duplicates, +2 errors, 50% packet loss, time 1001ms
rtt min/avg/max/mdev = 71.153/71.153/71.153/0.000 ms

要抓取上面的 ping 命令数据包,首先我们要知道 ping 的数据包是 icmp 协议,接着在使用 tcpdump 抓包的时候,就可以指定只抓 icmp 协议的数据包:
在这里插入图片描述
那么当 tcpdump 抓取到 icmp 数据包后, 输出格式如下:
在这里插入图片描述在这里插入图片描述TCPdump常用用法:

首先,先来看看常用的选项类
在这里插入图片描述
接下来,我们再来看看常用的过滤表用法,在上面的 ping 例子中,我们用过的是 icmp and host 183.232.231.174,表示抓取 icmp 协议的数据包,以及源地址或目标地址为 183.232.231.174 的包。其他常用的过滤选项,我也整理成了下面这个表格。
在这里插入图片描述
说了这么多,你应该也发现了,tcpdump 虽然功能强大,但是输出的格式并不直观。

所以,在工作中 tcpdump 只是用来抓取数据包,不用来分析数据包,而是把 tcpdump 抓取的数据包保存成 pcap 后缀的文件,接着用 Wireshark 工具进行数据包分析。

Wireshark 工具如何分析数据包?

Wireshark 除了可以抓包外,还提供了可视化分析网络包的图形页面,同时,还内置了一系列的汇总分析工具。

比如,拿上面的 ping 例子来说,我们可以使用下面的命令,把抓取的数据包保存到 ping.pcap 文件:
在这里插入图片描述
接着把 ping.pcap 文件拖到电脑,再用 Wireshark 打开它。打开后,你就可以看到下面这个界面:
在这里插入图片描述
是吧?在 Wireshark 的页面里,可以更加直观的分析数据包,不仅展示各个网络包的头部信息,还会用不同的颜色来区分不同的协议,由于这次抓包只有 ICMP 协议,所以只有紫色的条目。
接着,在网络包列表中选择某一个网络包后,在其下面的网络包详情中,可以更清楚的看到,这个网络包在协议栈各层的详细信息。比如,以编号 1 的网络包为例子:
在这里插入图片描述
可以在数据链路层,看到 MAC 包头信息,如源 MAC 地址和目标 MAC 地址等字段;
可以在 IP 层,看到 IP 包头信息,如源 IP 地址和目标 IP 地址、TTL、IP 包长度、协议等 IP 协议各个字段的数值和含义;
可以在 ICMP 层,看到 ICMP 包头信息,比如 Type、Code 等 ICMP 协议各个字段的数值和含义;
从 ping 的例子中,我们可以看到网络分层就像有序的分工,每一层都有自己的责任范围和信息,上层协议完成工作后就交给下一层,最终形成一个完整的网络包。
在这里插入图片描述

解密 TCP 三次握手和四次挥手

本次例子,我将使用我自己的一个聊天服务器作为例子:

jixu@ubuntu:~/Desktop$ sudo tcpdump -i any tcp and host 127.0.0.1 and port 8000 -w chat.pacp
tcpdump: listening on any, link-type LINUX_SLL (Linux cooked v1), capture size 262144 bytes
^C10 packets captured
20 packets received by filter
0 packets dropped by kernel

然后启动我的服务器作为例子。
最后,回到终端一,按下 Ctrl+C 停止 tcpdump,并把得到的 chat.pcap 取出到电脑。
使用 Wireshark 打开 chat.pcap 后,你就可以在 Wireshark 中,看到如下的界面:
在这里插入图片描述
最开始的 3 个包就是 TCP 三次握手建立连接的包,中间是数据传输过程,而最后的 3 个包则是 TCP 断开连接的挥手包。

你可能会好奇,为什么三次握手连接过程的 Seq 是 0 ?
实际上是因为 Wireshark 工具帮我们做了优化,它默认显示的是序列号 seq 是相对值,而不是真实值。
如果你想看到实际的序列号的值,可以右键菜单, 然后找到「协议首选项」,接着找到「Relative Seq」后,把它给取消。

这其实跟我们书上看到的 TCP 三次握手和四次挥手很类似,作为对比,你通常看到的 TCP 三次握手和四次挥手的流程,基本是这样的:
在这里插入图片描述
为什么抓到的 TCP 挥手是三次,而不是书上说的四次?
当被动关闭方(上图的服务端)在 TCP 挥手过程中,「没有数据要发送」并且「开启了 TCP 延迟确认机制」,那么第二和第三次挥手就会合并传输,这样就出现了三次挥手。

而通常情况下,服务器端收到客户端的 FIN 后,很可能还没发送完数据,所以就会先回复客户端一个 ACK 包,稍等一会儿,完成所有数据包的发送后,才会发送 FIN 包,这也就是四次挥手了。

TCP 三次握手异常情况实战分析

4 TCP 半连接队列和全连接队列

5 如何优化TCP?

https://mp.weixin.qq.com/s?__biz=MjM5Njg5NDgwNA==&mid=2247489331&idx=1&sn=869e5d5cab557db5348f237ca05c8d59&chksm=a6e3160891949f1eaa3700adcc48743299fac3c336dab1d295f9295b951e1597b819710555d8&scene=178&cur_album_id=1532487451997454337#rd

将以三个角度来阐述提升 TCP 的策略,分别是:

  • TCP 三次握手的性能提升;
  • TCP 四次挥手的性能提升;
  • TCP 数据传输的性能提升;

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-ib4f3uV8-1689757190644)(https://cdn.xiaolincoding.com/gh/xiaolincoder/ImageHost/%E8%AE%A1%E7%AE%97%E6%9C%BA%E7%BD%91%E7%BB%9C/TCP-%E5%8F%82%E6%95%B0/3.jpg)]

TCP 三次握手的性能提升

TCP 是面向连接的、可靠的、双向传输的传输层通信协议,所以在传输数据之前需要经过三次握手才能建立连接。

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-hiW508ls-1689757190644)(https://cdn.xiaolincoding.com/gh/xiaolincoder/ImageHost/%E8%AE%A1%E7%AE%97%E6%9C%BA%E7%BD%91%E7%BB%9C/TCP-%E5%8F%82%E6%95%B0/4.jpg)]

那么,三次握手的过程在一个 HTTP 请求的平均时间占比 10% 以上,在网络状态不佳、高并发或者遭遇 SYN 攻击等场景中,如果不能有效正确的调节三次握手中的参数,就会对性能产生很多的影响。

如何正确有效的使用这些参数,来提高 TCP 三次握手的性能,这就需要理解「三次握手的状态变迁」,这样当出现问题时,先用 netstat 命令查看是哪个握手阶段出现了问题,再来对症下药,而不是病急乱投医。

客户端和服务端都可以针对三次握手优化性能。主动发起连接的客户端优化相对简单些,而服务端需要监听端口,属于被动连接方,其间保持许多的中间状态,优化方法相对复杂一些。

所以,客户端(主动发起连接方)和服务端(被动连接方)优化的方式是不同的,接下来分别针对客户端和服务端优化。

客户端优化

三次握手建立连接的首要目的是「同步序列号」。

只有同步了序列号才有可靠传输,TCP 许多特性都依赖于序列号实现,比如流量控制、丢包重传等,这也是三次握手中的报文称为 SYN 的原因,SYN 的全称就叫 Synchronize Sequence Numbers(同步序列号)。

SYN_SENT 状态的优化

客户端作为主动发起连接方,首先它将发送 SYN 包,于是客户端的连接就会处于 SYN_SENT 状态。

客户端在等待服务端回复的 ACK 报文,正常情况下,服务器会在几毫秒内返回 SYN+ACK ,但如果客户端长时间没有收到 SYN+ACK 报文,则会重发 SYN 包,重发的次数由 tcp_syn_retries 参数控制,默认是 5 次:

jixu@jixu-ubuntu:~/Desktop$ sudo su
root@jixu-ubuntu:/home/jixu/Desktop# cat /proc/sys/net/ipv4/tcp_syn_retries # 查看
6
root@jixu-ubuntu:/home/jixu/Desktop# echo 5 > /proc/sys/net/ipv4/tcp_syn_retries # 设置

我们可以根据网络的稳定性和目标服务器的繁忙程度修改 SYN 的重传次数,调整客户端的三次握手时间上限。比如内网中通讯时,就可以适当调低重试次数,尽快把错误暴露给应用程序。

服务端优化

当服务端收到 SYN 包后,服务端会立马回复 SYN+ACK 包,表明确认收到了客户端的序列号,同时也把自己的序列号发给对方。

此时,服务端出现了新连接,状态是 SYN_RCV。在这个状态下,Linux 内核就会建立一个「半连接队列」来维护「未完成」的握手信息,当半连接队列溢出后,服务端就无法再建立新的连接。

SYN 攻击,攻击的是就是这个半连接队列。

如何查看由于 SYN 半连接队列已满,而被丢弃连接的情况?

root@jixu-ubuntu:/home/jixu/Desktop# netstat -s | grep "SYNs to LISTEN"
root@jixu-ubuntu:/home/jixu/Desktop# netstat -s | grep "SYNs to LISTEN"

上面输出的数值是累计值,表示共有多少个 TCP 连接因为半连接队列溢出而被丢弃。隔几秒执行几次,如果有上升的趋势,说明当前存在半连接队列溢出的现象

如何调整 SYN 半连接队列大小????

要想增大半连接队列,不能只单纯增大 tcp_max_syn_backlog 的值,还需一同增大 somaxconn 和 backlog,也就是增大 accept 队列。否则,只单纯增大 tcp_max_syn_backlog 是无效的。

6 如何理解TCP是面向字节流的协议√!1

嗯要说明这个问题的话我先回答一下为什么说UDP协议是面向报文的。

  • 当用户消息通过 UDP 协议传输时,操作系统不会对消息进行拆分,在组装好 UDP 头部后就交给网络层来处理,所以每个UDP报文就是一个用户的消息边界。
  • 当UDP接收方收到之后,其接收缓冲区使用队列来组织的,每个元素就是一个UDP报文,这样当用户调用recvfrom系统调用读取数据时,就会从队列里取出一个数据,,然后从内核里拷贝给用户缓冲区。

而TCP不一样,TCP在发送消息时,一条消息可能会被内核分成多个TCP报文发送;也可能将多条消息组合成一个TCP报文发送;所以TCP接收方收到消息之后,它无法确定这是不是一条完整的消息,因此必须定义应用层协议来解析报文。

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

  • 主要原因是因为,防止历史报文被下一个相同四元组的连接接收。
  • 但是原则上,正常四次挥手断开连接的话,由于存在TIME_WAIT状态的存在,历史报文都会自动消失,但是关键就在于,连接不是每次都能正常关闭的。
  • 假设在一个连接中,服务端断电重启之后收到了之前连接的客户端的数据包,由于服务端内核中已经没有维护这个连接了,所以内核只能回复RST。那么这个时候发送端就不存在RST了。这个时候发送方的历史报文就有可能被下一个连接接收。
  • 因此,为了解决这种情况,我们就需要让每个初始化序列号都是随机的,具体是根据一个计时器和根据四元组生成的哈希值生成。但是这种情况还是会发生回绕的,为了解决回绕问题,收发双方会维护最近一次收到数据包的时间戳,如果发现新收到的数据包的时间戳不是递增的,那么就表示该数据包是过期的,就会直接丢弃这个数据包。

12 TCP连接,一端断电和进程崩溃有什么区别√!

大背景,没有开启keepalive选项,没有数据交互 。一端断电和进程崩溃有什么区别

keepalive

  • 先来说一下什么是keepalive。其实就是TCP保活机制。具体流程为如果设置的保活时间(tcp_keepalive_time)内没有任何连接相关的活动,那么就启动保活机制,每隔一个时间间隔(tcp_keepalive_intvl)发送一次探测报文,如果连续检测(tcp_keepalive_probes)次数都无响应,则认为对方是不可达的。
  • 如果对端程序是正常工作的。当 TCP 保活的探测报文发送给对端, 对端会正常响应,这样 TCP 保活时间会被重置,等待下一个 TCP 保活时间的到来。
  • 如果对端主机崩溃(注意不是进程崩溃,进程崩溃操作系统会负责断开连接),或对端由于其他原因导致报文不可达。当 TCP 保活的探测报文发送给对端后,石沉大海,没有响应,连续几次,达到保活探测次数后,TCP 会报告该 TCP 连接已经死亡。
  • 但是TCP的 keepalive 检测时间太长了,要两个小时才能发现连接断开了,因为它属于传输层的兜底机制,一般应用层会实现自己的心跳检测机制来探测连接是否存活。

如果是进程崩溃

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

如果是一端断电

  • 客户端主机断电崩溃了,服务端是无法感知到的,在加上服务端没有开启 TCP keepalive,又没有数据交互的情况下,服务端的 TCP 连接是会一直处于 ESTABLISHED 连接状态,直到服务端重启进程。
  • 所以,我们可以得知一个点,在没有使用 TCP 保活机制且双方不传输数据的情况下,一方的 TCP 连接处在 ESTABLISHED 状态,并不代表另一方的连接还一定正常。

那么再来看一下,有数据传输一端断电进程崩溃的区别。

如果是进程崩溃

同上,内核会负责连接的断开。

一端断电

  • 如果是客户端宕机,那么服务端超时重传报文的次数达到一定阈值后,内核就会判定出该连接异常,直接断开连接。
  • 如果服务端在超时重传的过程中,客户端主机迅速重启了,但是由于客户端内核中已经没有之前TCP连接的数据结构了,因此会直接回复RST报文。
  • 那 TCP 的数据报文具体重传几次呢?在 Linux 系统中,提供一个叫 tcp_retries2 配置项,默认值是 15,如下:
    jixu@jixu-ubuntu:~/Desktop$ cat /proc/sys/net/ipv4/tcp_retries2
    15
    
  • 不过 tcp_retries2 设置了 15 次,并不代表 TCP 超时重传了 15 次才会通知应用程序终止该 TCP 连接,内核会根据 tcp_retries2 设置的值,计算出一个 timeout,如果重传间隔超过这个 timeout,则认为连接异常,然后断开连接。

13 拔掉网线后, 原本的 TCP 连接还存在吗√!

首先,拔掉网线只会断开物理层,并不会影响到传输层。

拔掉网线后,有数据传输

  • 在客户端拔掉网线后,服务端向客户端发送的数据报文会得不到任何的响应,在等待一定时长后,服务端就会触发超时重传机制,重传未得到响应的数据报文。
  • 如果在服务端重传报文的过程中,如果客户端刚好把网线插回去了由于拔掉网线并不会改变客户端的 TCP 连接状态(注意,不是断电!不是进程崩溃!),并且还是处于 ESTABLISHED 状态,所以这时客户端是可以正常接收服务端发来的数据报文的,然后客户端就会回 ACK 响应报文。此时,客户端和服务端的 TCP 连接依然存在的,就感觉什么事情都没有发生。
  • 但是,如果如果在服务端重传报文的过程中,客户端一直没有将网线插回去,服务端超时重传报文的次数达到一定阈值后,内核就会判定出该 TCP 有问题,直接断开连接。

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

  • 如果没有开启 TCP keepalive 机制,在客户端拔掉网线后,并且双方都没有进行数据传输,那么客户端和服务端的 TCP 连接将会一直保持存在。
  • 而如果开启了 TCP keepalive 机制,在客户端拔掉网线后,即使双方都没有进行数据传输,在持续一段时间后,TCP 就会发送探测报文:
    • 如果对端是正常工作的。当 TCP 保活的探测报文发送给对端, 对端会正常响应,这样 TCP 保活时间会被重置,等待下一个 TCP 保活时间的到来。
    • 如果对端主机宕机注意不是进程崩溃,进程崩溃后操作系统在回收进程资源的时候,会发送 FIN 报文,而主机宕机则是无法感知的,所以需要 TCP 保活机制来探测对方是不是发生了主机宕机),或对端由于其他原因导致报文不可达。当 TCP 保活的探测报文连续几次没有响应后,内核会判断连接异常,然后断开连接。

14 tcp_tw_reuse为什么默认是关闭的√!

回答这个问题其实就是要回答为什么要有TIME_WAIT状态,设计TIME_WAIT的目的有二:

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

    • 因为TCP头部中的序列号不是无限递增的,会发生绕回初始值的情况。那么客户端主动断开连接之后,如果没有TIME_WAIT或者TIME_WAIT状态时间过短,并且服务端在关闭连接之前发送的报文被网络延迟了,然后双方又立即以相同的四元组建立了连接,不巧之前被延迟的数据包正好落在客户端的接收窗口内,但这个数据包是上次连接残留下来的,因为导致了严重的数据错误。
    • 为了防止历史连接中的数据,被后面相同四元组的连接错误的接收,因此 TCP 设计了 TIME_WAIT 状态,状态会持续 2MSL`时长,这个时间足以让两个方向上的数据包都被丢弃,再出现的数据包一定都是新建立连接所产生的。
  • 原因2:保证「被动关闭连接」的一方,能被正确的关闭。

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

tcp_tw_reuse 是什么?

  • tcp_tw_reuse 的作用是让客户端快速复用处于 TIME_WAIT 状态的端口,相当于跳过了 TIME_WAIT 状态,这可能会出现这样的两个问题:就是上面TIME_WAIT的两个功能。
  • 虽然 TIME_WAIT 状态持续的时间是有一点长,显得很不友好,但是它被设计来就是用来避免发生乱七八糟的事情。
  • 所以尽量不要开启tcp_tw_reuse。

15 TCP Keepalive 和 HTTP Keep-Alive 是一个东西吗√!

TCP的Keepalive

事实上,这两个完全是两样不同东西,实现的层面也不同:

  • TCP 的 Keepalive,是由 TCP 层(内核态) 实现的,称为 TCP 保活机制;

  • HTTP 的 Keep-Alive,是由应用层(用户态) 实现的,称为 HTTP 长连接;

HTTP 的 Keep-Alive

  • HTTP 的 Keep-Alive的为了避免每次请求都要经历三握四挥所带来的开销,它的特点是,只要任意一端没有明确提出断开连接,则保持 TCP 连接状态。在 HTTP 1.0 中默认是关闭的,从 HTTP 1.1 开始, 就默认是开启了 Keep-Alive。
  • 开启了 Keep-Alive后,连接就不会中断,直到客户端或服务器端提出断开连接。
  • 但是在开启长连接的过程中,如果双方一直没有通信,也会造成资源的浪费,为了避免这种浪费,服务器还有一个keepalive_timeout参数,如果超过这个时间客户端没有发起新的请求,那么就会触发回调来释放连接。

但是服务器还是按照顺序响应,先回应 A 请求,完成后再回应 B 请求。这时就可能会产生队头阻塞问题。
在HTTP流水线中,队头阻塞问题是指由于请求和响应之间的顺序依赖关系,队列中前面的请求(队头)阻塞了后面的请求的处理。
例如,假设客户端在一个HTTP流水线中发送了三个请求(请求A、请求B和请求C),这些请求按照顺序在服务器上处理。如果请求A的处理时间很长,它会阻塞后续请求B和请求C的处理,即使请求B和请求C的处理时间很短。

队头阻塞问题可能会导致性能下降,因为后续的请求必须等待前一个请求的响应到达,即使它们本身的处理时间很短。这种情况可能会降低流水线的效率,从而减少了整体的吞吐量和响应时间的改善。

为了解决队头阻塞问题,可以采取一些策略,例如使用并行连接来发送多个独立的请求,或者在服务器端实现异步处理,以便可以同时处理多个请求而不会受到前一个请求的阻塞。此外,HTTP/2协议引入了二进制分帧和多路复用等机制,可以有效地避免队头阻塞问题。

总结

HTTP 的 Keep-Alive 也叫 HTTP 长连接,该功能是由「应用程序」实现的,可以使得用同一个 TCP 连接来发送和接收多个 HTTP 请求/应答,减少了 HTTP 短连接带来的多次 TCP 连接建立和释放的开销。

TCP 的 Keepalive 也叫 TCP 保活机制,该功能是由「内核」实现的,当客户端和服务端长达一定时间没有进行数据交互时,内核为了确保该连接是否还有效,就会发送探测报文,来检测对方是否还在线,然后来决定是否要关闭该连接。

16 TCP 协议有什么缺陷√!

  • 升级TCP的工作很困难。因为升级TCP就意味着升级内核,而升级内核是非常缓慢的,所以即使TCP有较好的更新,也很难快速推广。
  • TCP连接的建立是需要三次握手的,首先这个过程存在延迟;其次对于HTTP来说,TCP是在内核实现的握手,而TLS是在应用层实现的握手,也就是说得先完成 TCP 握手,才能进行 TLS 握手。所以TLS是无法对TCP头部进行加密的,这就意味着TCP的序列号是明文传输的,这就存在安全问题。一个典型的例子就是攻击者伪造一个RST报文来关闭一条TCP连接,只要该TCP字段里的序列号是位于接收方的接受窗口内的,该报文就是合法的。
  • TCP 存在队头阻塞问题。TCP 是字节流协议,TCP 层必须保证收到的字节数据是完整且有序的,如果序列号较低的 TCP 段在网络传输中丢失了,即使序列号较高的 TCP 段已经被接收了,应用层也无法从内核中读取到这部分数据。
  • 网络迁移需要重新建立 TCP 连接。由于一个TCP连接是通过一个四元组(源 IP、源端口、目的 IP、目的端口)确定的,所以当移动设备的网络从 4G 切换到 WIFI 时,意味着 IP 地址变化了,那么就必须要断开连接,然后重新建立 TCP 连接。同时TCP还有慢启动的减速过程,用户的直观体验就是网络卡顿一下,所以连接的迁移的成本是很高的。

18 TCP\UDP 可以同时绑定相同的端口吗√!

  • 可以的。因为传输层的「端口号」的作用,是为了区分同一个主机中的同一传输协议的不同的应用程序。
  • 而TCP 和 UDP是不同的传输协议,在内核中是完全独立的两个模块。
  • 当主机收到数据包后,可以在 IP 包头的「协议号」字段知道该数据包是 TCP还是UDP,所以可以根据这个信息就可以确定应该将消息转发给哪个模块处理,再根据端口号确定转发给哪个模块处理。

20 没有 accept,能建立 TCP 连接吗√!

  • 是可以的,因为当每一个socket执行listen()函数后,内核都自动为其创建一个半连接队列和全连接队列。第三次握手前,TCP连接会放在半连接队列中,直到第三次握手到来,才会被放到全连接队列中。**accept()方法只是为了从全连接队列中拿出一条连接,本身跟三次握手几乎毫无关系。**也就是说,连接的过程根本不需要accept()参与, 执行accept()只是为了从全连接队列里取出一条连接。
  • 半连接队列(SYN队列),服务端收到第一次握手后,会将sock加入到这个队列中,队列内的sock都处于SYN_RECV 状态。
  • 全连接队列(ACCEPT队列),在服务端收到第三次握手后,会将半连接队列的sock取出,放到全连接队列中。队列里的sock都处于 ESTABLISHED状态。这里面的连接,就等着服务端执行accept()后被取出了。

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

出于效率考虑,虽然都叫队列,但半连接队列其实被设计成了哈希表,而全连接队列本质是链表。

  • 因为当服务端执行accept()去获取某一个连接时,它是不关心获取哪一个连接的,只要是个连接就行;
  • 但是半连接队列却不太一样,因为队列中都是不完整的连接,如果某个连接的第三次握手到来,则需要去队列里把相应的ip端口的连接取出,如果采用线性结构,那么时间复杂度则是O(n),因此要采用哈希表。

全连接队列满了会怎么样√!

全连接队列如果满了的话,说明服务器负载过重,来不及去全连接队列里取连接。

如果全连接队列满了,收到第三次握手的话,默认会将这个ACK丢弃;但根据tcp_abort_on_overflow的不同还会有附加行为:

  • tcp_abort_on_overflow=0的话,那么就会重传第二次握手的SYN+ACK,如果重传超过一定次数,那么会把相应的连接从半连接队列中删掉。这里的目的就是为了等待全连接队列腾出位置。
  • tcp_abort_on_overflow=1的话,则会直接发送RST给客户端,也就是断开连接。另外一种情况,如果客户端想要连接的服务器的端口未监听,那么服务端也会回RST,也就是说客户端分不清是服务器的端口未监听,还是全连接队列满了。

半连接队列要是满了会怎么样√!

半连接队列已满,说明同时发起连接请求的客户端过多,或者受到了SYN Flood攻击。**

  • 当半连接队列已满时,客户端向服务器发送连接请求时,服务器将无法将该请求存储在半连接队列中,因此会直接发送一个 RST (reset) 响应给客户端,表示连接被拒绝
  • 为了避免客户端的连接被拒绝,我们可以应用tcp_syncookies来绕开半连接队列:
    • 当tcp_syncookies=1时,客户端发来第一次握手SYN时,服务端不会将其放入半连接队列中,而是直接生成一个cookies,这个cookies会跟着第二次握手,发回客户端。客户端在发第三次握手的时候带上这个cookies,服务端验证到它就是当初发出去的那个,就会建立连接并放入到全连接队列中。可以看出整个过程不再需要半连接队列的参与。
  • 实际上cookies是通过通信双方的IP地址端口、时间戳等信息等信息进行实时计算的,保存在服务器发给客户端TCP报头Seq里。
  • 当服务端收到客户端发来的第三次握手包时,会通过Ack还原出通信双方的IP地址端口、时间戳等信息,验证通过则建立连接。

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

  • 第一点,因为应用场景,不一样,半连接队列主要是用来限制服务器的并发量,因为半连接队列满了之后,服务器会直接拒绝客户端的连接请求。而syncookies方案主要是用来应对syn洪范攻击的;
  • 第二点,syncookies也是有缺点的,因为cookies方案虽然能避免syn洪范攻击,但是由于服务器不会保存连接信息,即使传输的数据包丢了,也不会重发第二次握手的信息。
# 全连接队列溢出次数
# netstat -s | grep overflowed
    4343 times the listen queue of a socket overflowed

# 半连接队列溢出次数
# netstat -s | grep -i "SYNs to LISTEN sockets dropped"
    109 times the listen queue of a socket overflowed 

21 用了 TCP 协议,数据一定不会丢吗√!

  • **TCP保证的可靠性,是传输层的可靠性。**也就是说,TCP只保证数据可以从发送方的传输层可靠地发送到接收方的传输层,更准确的说是到达了接收方的内核中的接收缓冲区。
  • 但是数据到了接收端的传输层之后,能不能保证到应用层,TCP并不负责。
  • 比如我们使用的微信,当我们通过聊天框发送消息时,通过TCP将消息发送到接收端的内核缓冲区中,接收端会回复一个ACK报文,发送端收到这个ACK报文后,就会将自己发送缓冲区中的报文给丢掉了,到这里TCP的任务就结束了。
  • 传输层的任务结束了,但是应用层的任务还没有结束,如果微信还没有从内核缓冲区中取出消息,手机死机了,那么这条消息就丢了。发送端以为自己发的消息已经发给对方了,但接收端却并没有收到这条消息。
  • 因此TCP只保证传输层的消息可靠性,并不保证应用层的消息可靠性。如果我们还想保证应用层的消息可靠性,就需要应用层自己去实现逻辑做保证。
  • 怎么做?通过服务器来保证。服务器会记录了最近都转发过哪些消息,每条消息都有一个id。客户端可以定期向服务器发送心跳包,通知服务器自己已发送的消息id和已收到的消息id;服务器经过对比就可以将客户端没有收到的消息重新发给它,同时也可以通知客户端重发发送失败的消息。这样就实现了消息同步。

其他问题

说说TCP\IP网络模型?

TCP/IP 网络模型共有 4 层,分别是应用层、传输层、网络层和网络接口层,每一层负责的职能如下:

应用层,负责向用户提供一组应用程序,比如 HTTP、DNS、FTP 等;
传输层,负责端到端的通信,比如 TCP、UDP 等;
网络层,负责网络包的封装、分片、路由、转发,比如 IP、ICMP 等;
网络接口层,负责网络包在物理网络中的传输,比如网络包的封帧、 MAC 寻址、差错检测,以及通过网卡传输网络帧等;

Linux 接收网络包的流程√!O

当有数据到达网卡后,网卡会先通过DMA控制器将数据拷贝到内核的RingBuffer中,然后向CPU发出一个硬中断;

为了避免频繁中断带来的性能开销,Linux采用了NAPI机制,简单来说就是采用中断+轮询的方式来接收网络包:

在硬中断处理函数中,首先会开启屏蔽硬中断,然后开启一个软中断线程,在软中断线程中采用轮询收包工作模式,不断地将RingBuffer中的数据传递给网络协议栈去处理,直到一段时间内没有消息到达为止。最后关闭软中断线程,关掉硬中断屏蔽,恢复中断收包工作模式

在软中断中线程中,

  • 首先会进入网络接口层,这一层会检测数据帧是否有差错,有差错则将数据帧丢弃,没差错的话去掉帧头帧尾,将数据包交给网络层;
  • 到了网络层之后,首先判断一下是交给上层协议处理,还是转发出去,当确认这个数据包是发送给本机后,那么就在包头的协议字段看一下上一层的协议是TCP还是UDP,进而去掉包头,再将报文段交给相应的传输层模块;
  • 到了传输层之后,去掉TCP头或者UDP头,然后对四元组进行哈希运算,找到相应的socket,将消息拷贝到socket的接收缓冲区中;
  • 至此,一个网络包的接收过程就结束了。

内核的ringBuffer满了怎么办?
如果ringBuffer满了,那么软中断线程处理不过来的话,后面再来的包网卡会直接丢弃。
因此需要适当调整ringBuffer的大小:

# ethtool -G eth1 rx 4096 tx 4096

https://zhuanlan.zhihu.com/p/625399943

Linux 发送网络包的流程√!O

  • 首先肯定是将消息拷贝到socket发送缓冲区中;
  • 之后如果传输层使用的是 TCP 协议,那么会根据接收方的窗口大小来封装一个TCP报文,然后交给网络层;
  • 网络层会将TCP报文封装成ip数据包,然后通过查询路由表确认下一跳的 IP,交给网络接口层;
  • 网络接口层会根据ARP协议查询下一跳的 MAC 地址,然后增加帧头和帧尾,放到发包队列中;
  • 最后会触发一个软中断告诉网卡驱动程序,这里有新的网络包需要发送,然后网卡驱动程序会通过 DMA从发包队列中读取网络包,然后经由网卡发送出去。

多个 TCP 服务进程可以同时绑定同一个端口吗√!1

首先,如果没有开启SO_REUSEPORT选项:

  • 当多个 TCP 服务进程时不能同时绑定到 相同的IP地址和端口的,并且执行 bind() 时候就会出错。如果两个 TCP 服务进程绑定的端口都相同,但 IP 地址不同,那么执行 bind() 不会出错。
  • 因为一个监听socket对应一个半连接队列和全连接队列嘛,所以重复建立监听socket肯定是不行的。

但是如果开启了tcp_reuseport,那么多个TCP服务进程是可以绑定到ip和端口的,因为内核会为每个监听socket都分别建立一个半连接队列和全连接队列,并以负载均衡的方式将客户端的连接请求公平地分发给他们。

如何解决服务端重启时,报错“Address already in use”的问题√!O

  • 当服务端重启时,出现“Address already in use”的问题,意味服务器重启之前主动发起了关闭连接的操作,并且TIME_WAIT状态还没结束,该四元组对应的socket仍被内核当成一个有效连接, 因此执行bind()就会报错。
  • 解决办法是要么使用’lsof -i:端口号’命令找到占用端口的进程并杀死它,要么重启服务器时开启SO_REUSEADDR选项。

客户端的端口可以重复使用吗√!O

  • 首先,一个连接是由一个四元组来确定的,只要四元组任意一个元素产生了变化,那么就是不同的连接。
  • 因此,如果客户端连接的服务器不是同一个,也就是服务端的ip或端口产生了变化,那么这时客户端的端口号就是可以复用的,因为四元组产生了变化。那么linux在接收数据包时,到达传输层后,根据对四元组进行哈希运算,就会找到相应的socket。

客户端 TCP 连接 TIME_WAIT 状态过多,会导致端口资源耗尽而无法建立新的连接吗√!O

  • 要看客户端是否都是与同一个服务器(目标地址和目标端口一样)建立连接。
  • 如果客户端都是与同一个目标地址和目标端口的服务器建立连接,那么如果客户端 TIME_WAIT 状态的连接过多,端口资源被耗尽,就无法与这个服务器再建立连接了;
  • 但是与其他服务器建立连接是可以的,因为一个socket是由一个四元组来确定的,当服务器的ip或端口变了后,客户端的端口就可以复用了。

如何解决客户端 TCP 连接 TIME_WAIT 过多,导致无法与同一个服务器建立连接的问题√!O

打开 net.ipv4.tcp_tw_reuse 这个内核参数。

因为开启了这个内核参数后,客户端调用 connect 函数时,如果选择到的端口已经被相同四元组的连接占用的时候,内核就会判断该连接是否处于 TIME_WAIT 状态, 并且持续状态是否超过了 1 秒,如果是那么就会重用这个连接,然后就可以正常使用该端口了。

  • 1
    点赞
  • 6
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值