计算机网络

1. 键入网址到网页显示,期间发生了什么?

  • 解析 URL:首先浏览器要解析 URL,URL 包括协议 + web 服务器 + 目录名和文件名,继而生成发送给 web 服务器的、对该资源的请求信息

  • DNS 解析:查询服务器域名对应的 IP 地址。DNS 服务器保存了 Web 服务器域名与 IP 的对应关系。客户端发送 DNS 请求给本地 DNS 服务器,查询对应的 IP 地址后返回客户端。PS:需要查询时,浏览器、操作系统、hosts 文件依次看有没有缓存,不一定每次都要解析 IP。

  • TCP 连接:HTTP 报文是基于 TCP 传输的,涉及到三次握手。组装好 TCP 报文后交给网络层处理。

  • 发送 HTTP 请求:

    1. 加入 IP 头部,生成 IP 报文

    2. 加入 MAC 头部。(发送方 MAC 地址在网卡里,接收方的 MAC 地址靠 ARP 协议在以太网广播寻找,当然也是有缓存的)

    3. 再经过网卡、交换机、路由器,抵达服务器

  • 服务器处理请求并返回 HTTP 报文:服务器依次检查 MAC 头部、IP 头、TCP 头检查序列号和端口号,HTTP 进程收到后把这个网页封装在 HTTP 响应报文里并返回(相同步骤)。

  • 浏览器解析渲染页面:浏览器是一个边解析边渲染的过程。

  • 连接结束

3. UDP 与 TCP 的区别

  1. 连接

TCP 是面向连接的传输层协议,传输数据前先要建立连接。

UDP 是不需要连接,即刻传输数据。

  1. 服务对象

TCP 是一对一的两点服务,即一条连接只有两个端点。

UDP 支持一对一、一对多、多对多的交互通信

  1. 可靠性

TCP 是可靠交付数据的,数据可以无差错、不丢失、不重复、按序到达。

UDP 是尽最大努力交付,不保证可靠交付数据。但是我们可以基于 UDP 传输协议实现一个可靠的传输协议,比如 QUIC 协议。

  1. 拥塞控制、流量控制

TCP 有拥塞控制和流量控制机制,保证数据传输的安全性。

UDP 则没有,即使网络非常拥堵了,也不会影响 UDP 的发送速率。

  1. 首部开销

TCP 首部长度较长,会有一定的开销,首部在没有使用「选项」字段时是 20 个字节,如果使用了「选项」字段则会变长的。

UDP 首部只有 8 个字节,并且是固定不变的,开销较小。

  1. 传输方式

TCP 是流式传输,没有边界,但保证顺序和可靠。

UDP 是一个包一个包的发送,是有边界的,但可能会丢包和乱序。

  1. 分片不同

TCP 的数据大小如果大于 MSS 大小,则会在传输层进行分片,目标主机收到后,也同样在传输层组装 TCP 数据包,如果中途丢失了一个分片,只需要传输丢失的这个分片。

UDP 的数据大小如果大于 MTU 大小,则会在 IP 层进行分片,目标主机收到后,在 IP 层组装完数据,接着再传给传输层。

4. 如何唯一确定一个 TCP 连接

TCP 四元组可以唯一的确定一个连接

  • 源地址

  • 源端口

  • 目的地址

  • 目的端口

源地址和目的地址的字段(32 位)是在 IP 头部中,作用是通过 IP 协议发送报文给对方主机。

源端口和目的端口的字段(16 位)是在 TCP 头部中,作用是告诉 TCP 协议应该把报文发给哪个进程。

5. 为什么是三次握手?不是两次、四次?

  • 三次握手才可以阻止重复历史连接的初始化(主要原因)

  • 三次握手才可以同步双方的初始序列号

    • 序列号能够保证数据包不重复、不丢弃和按序传输。

  • 三次握手才可以避免资源浪费

「两次握手」:无法防止历史连接的建立,会造成双方资源的浪费,也无法可靠的同步双方序列号;

「四次握手」:三次握手就已经理论上最少可靠连接建立,所以不需要使用更多的通信次数。

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

  • 为了防止历史报文被下一个相同四元组的连接接收(主要方面);

  • 为了安全性,防止黑客伪造的相同序列号的 TCP 报文被对方接收;

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

当如果一个 IP 分片丢失,整个 IP 报文的所有分片都得重传。因为 IP 层本身没有超时重传机制,它由传输层的 TCP 来负责超时和重传。因此,可以得知由 IP 层进行分片传输,是非常没有效率的。

经过 TCP 层分片后,如果一个 TCP 分片丢失后,进行重发时也是以 MSS 为单位,而不用重传所有的分片,大大增加了重传的效率。

8. 握手丢失三部曲

第一次握手丢失

客户端迟迟收不到服务端的 SYN-ACK 报文(第二次握手),就会触发「超时重传」机制,重传 SYN 报文,而且重传的 SYN 报文的序列号都是一样的。

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

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

第二次握手丢失

  • 客户端会触发超时重传机制,重传 SYN 报文。

  • 服务端这边会触发超时重传机制,重传 SYN-ACK 报文。

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

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

第三次握手丢失

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

注意,ACK 报文是不会有重传的,当 ACK 丢失了,就由对方重传对应的报文。

9. 什么是 SYN 攻击?如何避免 SYN 攻击?

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

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

  • 调大 netdev_max_backlog;

  • 增大 TCP 半连接队列;

  • 开启 tcp_syncookies;

  • 减少 SYN+ACK 重传次数

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

  • 关闭连接时,客户端向服务端发送 FIN 时,仅仅表示客户端不再发送数据了但是还能接收数据。

  • 服务端收到客户端的 FIN 报文时,先回一个 ACK 应答报文,而服务端可能还有数据需要处理和发送,等服务端不再发送数据时,才发送 FIN 报文给客户端来表示同意现在关闭连接。

从上面过程可知,服务端通常需要等待完成数据的发送和处理,所以服务端的 ACK 和 FIN 一般都会分开发送,因此是需要四次挥手。

11. 挥手丢失四部曲

第一次挥手丢失

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

第二次挥手丢失

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

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

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

但是注意,如果主动关闭方使用 shutdown 函数关闭连接,指定了只关闭发送方向,而接收方向并没有关闭,那么意味着主动关闭方还是可以接收数据的。

此时,如果主动关闭方一直没收到第三次挥手,那么主动关闭方的连接将会一直处于 FIN_WAIT2 状态(tcp_fin_timeout 无法控制 shutdown 关闭的连接)

第三次挥手丢失

内核会发出 FIN 报文,同时连接进入 LAST_ACK 状态,等待客户端返回 ACK 来确认连接关闭。

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

客户端因为是通过 close 函数关闭连接的,处于 FIN_WAIT_2 状态是有时长限制的,如果 tcp_fin_timeout 时间内还是没能收到服务端的第三次挥手(FIN 报文),那么客户端就会断开连接。

第四次挥手丢失

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

客户端在收到第三次挥手后,就会进入 TIME_WAIT 状态,开启时长为 2MSL 的定时器,如果途中再次收到第三次挥手(FIN 报文)后,就会重置定时器,当等待 2MSL 时长后,客户端就会断开连接。

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

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

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

比如,如果被动关闭方没有收到断开连接的最后的 ACK 报文,就会触发超时重发 FIN 报文,另一方接收到 FIN 后,会重发 ACK 给被动关闭方, 一来一去正好 2 个 MSL。可以看到 2MSL 时长 这其实是相当于至少允许报文丢失一次。

为什么不是 4 或者 8 MSL 的时长呢?你可以想象一个丢包率达到百分之一的糟糕网络,连续两次丢包的概率只有万分之一,这个概率实在是太小了,忽略它比解决它更具性价比。

13. 为什么需要 TIME_WAIT 状态?

主动发起关闭连接的一方,才会有 TIME-WAIT 状态。

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

  • 防止历史连接中的数据,被后面相同四元组的连接错误的接收,这个时间足以让两个方向上的数据包都被丢弃,使得原来连接的数据包在网络中都自然消失,再出现的数据包一定都是新建立连接所产生的。

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

14. TIME_WAIT 过多有什么危害?

  • 第一是占用系统资源,比如文件描述符、内存资源、CPU 资源、线程资源等;

  • 第二是占用端口资源,端口资源也是有限的,一般可以开启的端口为 32768 ~ 61000,也可以通过 net.ipv4.ip_local_port_range 参数指定范围。

15. 如何优化 TIME_WAIT?

  • 打开 net.ipv4.tcp_tw_reuse 和 net.ipv4.tcp_timestamps 选项;

  • net.ipv4.tcp_max_tw_buckets

  • 程序中使用 SO_LINGER ,应用强制使用 RST 关闭。

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

  • 第一个场景:HTTP 没有使用长连接

  • 第二个场景:HTTP 长连接超时

    • 可以往网络问题的方向排查,比如是否是因为网络问题,导致客户端发送的数据一直没有被服务端接收到,以至于 HTTP 长连接超时。

  • 第三个场景:HTTP 长连接的请求数量达到上限

    • 调大 nginx 的 keepalive_requests 参数就行。

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

当服务端出现大量 CLOSE_WAIT 状态的连接的时候,说明服务端的程序没有调用 close 函数关闭连接。

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

TCP 有保活机制。

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

  • 第一种,对端程序是正常工作的。当 TCP 保活的探测报文发送给对端, 对端会正常响应,这样 TCP 保活时间会被重置,等待下一个 TCP 保活时间的到来。

  • 第二种,对端主机宕机并重启。当 TCP 保活的探测报文发送给对端后,对端是可以响应的,但由于没有该连接的有效信息,会产生一个 RST 报文,这样很快就会发现 TCP 连接已经被重置。

  • 第三种,是对端主机宕机(注意不是进程崩溃,进程崩溃后操作系统在回收进程资源的时候,会发送 FIN 报文,而主机宕机则是无法感知的,所以需要 TCP 保活机制来探测对方是不是发生了主机宕机),或对端由于其他原因导致报文不可达。当 TCP 保活的探测报文发送给对端后,石沉大海,没有响应,连续几次,达到保活探测次数后,TCP 会报告该 TCP 连接已经死亡。

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

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

20. 重传机制

超时重传

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

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

  • 数据包丢失

  • 确认应答丢失

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

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

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

快速重传

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

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

SACK 方法

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

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

Duplicate SACK

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

  • 可以让「发送方」知道,是发出去的包丢了,还是接收方回应的 ACK 包丢了;

  • 可以知道是不是「发送方」的数据包被网络延迟了;

  • 可以知道网络中是不是把「发送方」的数据包给复制了;

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

21. 滑动窗口

窗口大小就是指无需等待确认应答,而可以继续发送数据的最大值。

ACK 600 确认应答报文丢失,也没关系,因为可以通过下一个确认应答进行确认,只要发送方收到了 ACK 700 确认应答,就意味着 700 之前的所有数据「接收方」都收到了。这个模式就叫累计确认或者累计应答。

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

22. 流量控制

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

但是实际上,发送窗口和接收窗口中所存放的字节数,都是放在操作系统内存缓冲区中的,而操作系统的缓冲区,会被操作系统调整。

当应用进程没办法及时读取缓冲区的内容时,也会对我们的缓冲区造成影响。

如果发生了先减少缓存,再收缩窗口,就会出现丢包的现象。

为了防止这种情况发生,TCP 规定是不允许同时减少缓存又收缩窗口的,而是采用先收缩窗口,过段时间再减少缓存,这样就可以避免了丢包情况。

23. 窗口关闭

如果窗口大小为 0 时,就会阻止发送方给接收方传递数据,直到窗口变为非 0 为止,这就是窗口关闭。

发送方一直等待接收方的非 0 窗口通知,接收方也一直等待发送方的数据,如不采取措施,这种相互等待的过程,会造成了死锁的现象。

为了解决这个问题,TCP 为每个连接设有一个持续定时器,只要 TCP 连接一方收到对方的零窗口通知,就启动持续计时器。

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

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

24. 糊涂窗口综合症

如果接收方腾出几个字节并告诉发送方现在有几个字节的窗口,而发送方会义无反顾地发送这几个字节,这就是糊涂窗口综合症。

  • 接收方通常的策略如下:

当「窗口大小」小于 min( MSS,缓存空间/2 ) ,也就是小于 MSS 与 1/2 缓存大小中的最小值时,就会向发送方通告窗口为 0,也就阻止了发送方再发数据过来。

等到接收方处理了一些数据后,窗口大小 >= MSS,或者接收方缓存空间有一半可以使用,就可以把窗口打开让发送方发送数据过来。

  • 发送方通常的策略如下:

使用 Nagle 算法,该算法的思路是延时处理,只有满足下面两个条件中的任意一个条件,才可以发送数据:

条件一:要等到窗口大小 >= MSS 并且 数据大小 >= MSS; 条件二:收到之前发送数据的 ack 回包; 只要上面两个条件都不满足,发送方一直在囤积数据,直到满足上面的发送条件。

接收方得满足「不通告小窗口给发送方」+ 发送方开启 Nagle 算法,才能避免糊涂窗口综合症。

25. 拥塞控制

控制的目的就是避免「发送方」的数据填满整个网络。

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

慢启动

当发送方每收到一个 ACK,拥塞窗口 cwnd 的大小就会加 1。

发包的个数是指数性的增长。

有一个叫慢启动门限 ssthresh (slow start threshold)状态变量。

  • 当 cwnd < ssthresh 时,使用慢启动算法。

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

拥塞避免算法

进入拥塞避免算法后,它的规则是:每当收到一个 ACK 时,cwnd 增加 1/cwnd。

当 8 个 ACK 应答确认到来时,每个确认增加 1/8,8 个 ACK 确认 cwnd 一共增加 1,于是这一次能够发送 9 个 MSS 大小的数据,变成了线性增长。

就这么一直增长着后,网络就会慢慢进入了拥塞的状况了,于是就会出现丢包现象,这时就需要对丢失的数据包进行重传。

当触发了重传机制,也就进入了「拥塞发生算法」。

拥塞发生

当发生了「超时重传」,则就会使用拥塞发生算法。

这个时候,ssthresh 和 cwnd 的值会发生变化:

  • ssthresh 设为 cwnd/2

  • cwnd 重置为 1 (是恢复为 cwnd 初始化值,我这里假定 cwnd 初始化值 1)

还有更好的方式,前面我们讲过「快速重传算法」。当接收方发现丢了一个中间包的时候,发送三次前一个包的 ACK,于是发送端就会快速地重传,不必等待超时再重传。

TCP 认为这种情况不严重,因为大部分没丢,只丢了一小部分,则 ssthresh 和 cwnd 变化如下:

  • cwnd = cwnd/2 ,也就是设置为原来的一半;

  • ssthresh = cwnd;

  • 进入快速恢复算法

快速恢复

  • 拥塞窗口 cwnd = ssthresh + 3 ( 3 的意思是确认有 3 个数据包被收到了);

  • 重传丢失的数据包;

  • 如果再收到重复的 ACK,那么 cwnd 增加 1;

  • 如果收到新数据的 ACK 后,把 cwnd 设置为第一步中的 ssthresh 的值,原因是该 ACK 确认了新的数据,说明从 duplicated ACK 时的数据都已收到,该恢复过程已经结束,可以回到恢复之前的状态了,也即再次进入拥塞避免状态;

26. TCP 半连接和全连接队列

  • 半连接队列,也称 SYN 队列;

  • 全连接队列,也称 accept 队列;

全连接队列

服务端收到客户端发起的 SYN 请求后,内核会把该连接存储到半连接队列,并向客户端响应 SYN+ACK,接着客户端会返回 ACK,服务端收到第三次握手的 ACK 后,内核会把连接从半连接队列移除,然后创建新的完全的连接,并将其添加到 accept 队列,等待进程调用 accept 函数时把连接取出来。

当服务端并发处理大量请求时,如果 TCP 全连接队列过小,就容易溢出。发生 TCP 全连接队溢出的时候,后续的请求就会被丢弃,这样就会出现服务端请求数量上不去的现象。

TCP 全连接队列的最大值取决于 somaxconn 和 backlog 之间的最小值,也就是 min(somaxconn, backlog)

半连接队列

如果半连接队列满了,并且没有开启 tcp_syncookies,则会丢弃;

若全连接队列满了,且没有重传 SYN+ACK 包的连接请求多于 1 个,则会丢弃;

如果没有开启 tcp_syncookies,并且 max_syn_backlog 减去 当前半连接队列长度小于 (max_syn_backlog >> 2),则会丢弃;

开启 syncookies 功能就可以在不使用 SYN 半连接队列的情况下成功建立连接

27. 如何优化 TCP?

TCP 三次握手的性能提升

客户端优化

当客户端发起 SYN 包时,可以通过 tcp_syn_retries 控制其重传的次数。

服务端优化

当服务端 SYN 半连接队列溢出后,会导致后续连接被丢弃,可以通过 netstat -s 观察半连接队列溢出的情况,如果 SYN 半连接队列溢出情况比较严重,可以通过 tcp_max_syn_backlog somaxconn backlog 参数来调整 SYN 半连接队列的大小。

服务端回复 SYN+ACK 的重传次数由 tcp_synack_retries 参数控制。如果遭受 SYN 攻击,应把 tcp_syncookies 参数设置为 1,表示仅在 SYN 队列满后开启 syncookie 功能,可以保证正常的连接成功建立。

服务端收到客户端返回的 ACK,会把连接移入 accpet 队列,等待进行调用 accpet() 函数取出连接。

可以通过 ss -lnt 查看服务端进程的 accept 队列长度,如果 accept 队列溢出,系统默认丢弃 ACK,如果可以把 tcp_abort_on_overflow 设置为 1 ,表示用 RST 通知客户端连接建立失败。

如果 accpet 队列溢出严重,可以通过 listen 函数的 backlog 参数和 somaxconn 系统参数提高队列大小,accept 队列长度取决于 min(backlog, somaxconn)

如何绕过三次握手?

TCP Fast Open 功能可以绕过三次握手,使得 HTTP 请求减少了 1 个 RTT 的时间,Linux 下可以通过 tcp_fastopen 开启该功能,同时必须保证服务端和客户端同时支持。

TCP 四次挥手的性能提升

主动方的优化

主动发起 FIN 报文断开连接的一方,如果迟迟没收到对方的 ACK 回复,则会重传 FIN 报文,重传的次数由 tcp_orphan_retries 参数决定。

当主动方收到 ACK 报文后,连接就进入 FIN_WAIT2 状态,根据关闭的方式不同,优化的方式也不同:

如果这是 close 函数关闭的连接,那么它就是孤儿连接。如果 tcp_fin_timeout 秒内没有收到对方的 FIN 报文,连接就直接关闭。同时,为了应对孤儿连接占用太多的资源,tcp_max_orphans 定义了最大孤儿连接的数量,超过时连接就会直接释放。 反之是 shutdown 函数关闭的连接,则不受此参数限制; 当主动方接收到 FIN 报文,并返回 ACK 后,主动方的连接进入 TIME_WAIT 状态。这一状态会持续 1 分钟,为了防止 TIME_WAIT 状态占用太多的资源,tcp_max_tw_buckets 定义了最大数量,超过时连接也会直接释放。

当 TIME_WAIT 状态过多时,还可以通过设置 tcp_tw_reusetcp_timestamps 为 1 ,将 TIME_WAIT 状态的端口复用于作为客户端的新连接,注意该参数只适用于客户端。

被动方的优化

被动关闭的连接方应对非常简单,它在回复 ACK 后就进入了 CLOSE_WAIT 状态,等待进程调用 close 函数关闭连接。因此,出现大量 CLOSE_WAIT 状态的连接时,应当从应用程序中找问题。

当被动方发送 FIN 报文后,连接就进入 LAST_ACK 状态,在未等到 ACK 时,会在 tcp_orphan_retries 参数的控制下重发 FIN 报文。

TCP 传输数据的性能提升

TCP 可靠性是通过 ACK 确认报文实现的,又依赖滑动窗口提升了发送速度也兼顾了接收方的处理能力。

可是,默认的滑动窗口最大值只有 64 KB,不满足当今的高速网络的要求,要想提升发送速度必须提升滑动窗口的上限,在 Linux 下是通过设置 tcp_window_scaling 为 1 做到的,此时最大值可高达 1GB。

滑动窗口定义了网络中飞行报文的最大字节数,当它超过带宽时延积时,网络过载,就会发生丢包。而当它小于带宽时延积时,就无法充分利用网络带宽。因此,滑动窗口的设置,必须参考带宽时延积。

内核缓冲区决定了滑动窗口的上限,缓冲区可分为:发送缓冲区 tcp_wmem 和接收缓冲区 tcp_rmem

Linux 会对缓冲区动态调节,我们应该把缓冲区的上限设置为带宽时延积。发送缓冲区的调节功能是自动打开的,而接收缓冲区需要把 tcp_moderate_rcvbuf 设置为 1 来开启。其中,调节的依据是 TCP 内存范围 tcp_mem

但需要注意的是,如果程序中的 socket 设置 SO_SNDBUFSO_RCVBUF,则会关闭缓冲区的动态整功能,所以不建议在程序设置它俩,而是交给内核自动调整比较好。

有效配置这些参数后,既能够最大程度地保持并发性,也能让资源充裕时连接传输速度达到最大值。

28. 如何理解是 TCP 面向字节流协议?

UDP 是面向报文的协议

当用户消息通过 UDP 协议传输时,操作系统不会对消息进行拆分,在组装好 UDP 头部后就交给网络层来处理,所以发出去的 UDP 报文中的数据部分就是完整的用户消息,也就是每个 UDP 报文就是一个用户消息的边界,这样接收方在接收到 UDP 报文后,读一个 UDP 报文就能读取到完整的用户消息。

TCP 是面向字节流的协议

当用户消息通过 TCP 协议传输时,消息可能会被操作系统分组成多个的 TCP 报文,也就是一个完整的用户消息被拆分成多个 TCP 报文进行传输。

29. 如何解决粘包

  • 固定长度的消息;

  • 特殊字符作为边界;

  • 自定义消息结构。

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

为了防止历史报文被下一个相同四元组的连接接收。

客户端和服务端的初始化序列号都是随机生成,能很大程度上避免历史报文被下一个相同四元组的连接接收,然后又引入时间戳的机制,从而完全避免了历史报文被接收的问题。时间戳有两个好处,一个是便于精确计算 RTT ,另一个是能防止序列号回绕(PAWS)。

31. SYN 报文什么时候情况下会被丢弃?

  • 开启 tcp_tw_recycle 参数,并且在 NAT 环境下,造成 SYN 报文被丢弃

  • TCP 两个队列满了(半连接队列和全连接队列),造成 SYN 报文被丢弃

32. 已建立连接的 TCP,收到 SYN 会发生什么?

  1. 客户端的 SYN 报文里的端口号与历史连接不相同

如果客户端恢复后发送的 SYN 报文中的源端口号跟上一次连接的源端口号不一样,此时服务端会认为是新的连接要建立,于是就会通过三次握手来建立新的连接。

  1. 客户端的 SYN 报文里的端口号与历史连接相同

处于 Established 状态的服务端,如果收到了客户端的 SYN 报文(注意此时的 SYN 报文其实是乱序的,因为 SYN 报文的初始化序列号其实是一个随机数),会回复一个携带了正确序列号和确认号的 ACK 报文,这个 ACK 被称之为 Challenge ACK。

接着,客户端收到这个 Challenge ACK,发现确认号(ack num)并不是自己期望收到的,于是就会回 RST 报文,服务端收到后,就会释放掉该连接。

33. 四次挥手中收到乱序的 FIN 包会如何处理?

在 FIN_WAIT_2 状态时,如果收到乱序的 FIN 报文,那么就被会加入到「乱序队列」,并不会进入到 TIME_WAIT 状态。

等再次收到前面被网络延迟的数据包时,会判断乱序队列有没有数据,然后会检测乱序队列中是否有可用的数据,如果能在乱序队列中找到与当前报文的序列号保持的顺序的报文,就会看该报文是否有 FIN 标志,如果发现有 FIN 标志,这时才会进入 TIME_WAIT 状态。

34. 在 TIME_WAIT 状态的 TCP 连接,收到 SYN 后会发生什么?

要看 SYN 的「序列号和时间戳」是否合法,因为处于 TIME_WAIT 状态的连接收到 SYN 后,会判断 SYN 的「序列号和时间戳」是否合法,然后根据判断结果的不同做不同的处理。

非法合法根据序列号和时间戳(如果有)来进行判断。

  • 如果处于 TIME_WAIT 状态的连接收到「合法的 SYN 」后,就会重用此四元组连接,跳过 2MSL 而转变为 SYN_RECV 状态,接着就能进行建立连接过程。

  • 如果处于 TIME_WAIT 状态的连接收到「非法的 SYN 」后,就会再回复一个第四次挥手的 ACK 报文,客户端收到后,发现并不是自己期望收到确认号(ack num),就回 RST 报文给服务端。

35. 在 TIME_WAIT 状态,收到 RST 会断开连接吗?

会不会断开,关键看 net.ipv4.tcp_rfc1337 这个内核参数(默认情况是为 0):

  • 如果这个参数设置为 0, 收到 RST 报文会提前结束 TIME_WAIT 状态,释放连接。

  • 如果这个参数设置为 1, 就会丢掉 RST 报文。

36. TCP 连接,一端断电和进程崩溃有什么区别?

如果「客户端进程崩溃」,客户端的进程在发生崩溃的时候,内核会发送 FIN 报文,与服务端进行四次挥手。

但是,「客户端主机宕机」,那么是不会发生四次挥手的,具体后续会发生什么?还要看服务端会不会发送数据?

  • 如果服务端会发送数据,由于客户端已经不存在,收不到数据报文的响应报文,服务端的数据报文会超时重传,当重传总间隔时长达到一定阈值(内核会根据 tcp_retries2 设置的值计算出一个阈值)后,会断开 TCP 连接;

  • 如果服务端一直不会发送数据,再看服务端有没有开启 TCP keepalive 机制?

    • 如果有开启,服务端在一段时间没有进行数据交互时,会触发 TCP keepalive 机制,探测对方是否存在,如果探测到对方已经消亡,则会断开自身的 TCP 连接;

    • 如果没有开启,服务端的 TCP 连接会一直存在,并且一直保持在 ESTABLISHED 状态。

37. 拔掉网线后, 原本的 TCP 连接还存在吗?

客户端拔掉网线后,并不会直接影响 TCP 连接状态。所以,拔掉网线后,TCP 连接是否还会存在,关键要看拔掉网线之后,有没有进行数据传输。

有数据传输的情况:

  • 在客户端拔掉网线后,如果服务端发送了数据报文,那么在服务端重传次数没有达到最大值之前,客户端就插回了网线,那么双方原本的 TCP 连接还是能正常存在,就好像什么事情都没有发生。

  • 在客户端拔掉网线后,如果服务端发送了数据报文,在客户端插回网线之前,服务端重传次数达到了最大值时,服务端就会断开 TCP 连接。等到客户端插回网线后,向服务端发送了数据,因为服务端已经断开了与客户端相同四元组的 TCP 连接,所以就会回 RST 报文,客户端收到后就会断开 TCP 连接。至此, 双方的 TCP 连接都断开了。

没有数据传输的情况:

  • 如果双方都没有开启 TCP keepalive 机制,那么在客户端拔掉网线后,如果客户端一直不插回网线,那么客户端和服务端的 TCP 连接状态将会一直保持存在。

  • 如果双方都开启了 TCP keepalive 机制,那么在客户端拔掉网线后,如果客户端一直不插回网线,TCP keepalive 机制会探测到对方的 TCP 连接没有存活,于是就会断开 TCP 连接。而如果在 TCP 探测期间,客户端插回了网线,那么双方原本的 TCP 连接还是能正常存在。

38. tcp_tw_reuse 为什么默认是关闭的?

tcp_tw_reuse 的作用是让客户端快速复用处于 TIME_WAIT 状态的端口,相当于跳过了 TIME_WAIT 状态,这可能会出现这样的两个问题:

  • 历史 RST 报文可能会终止后面相同四元组的连接,因为 PAWS 检查到即使 RST 是过期的,也不会丢弃。

  • 如果第四次挥手的 ACK 报文丢失了,有可能被动关闭连接的一方不能被正常的关闭;

39. HTTPS 中 TLS 和 TCP 能同时握手吗?

  • 客户端和服务端都开启了 TCP Fast Open 功能,且 TLS 版本是 1.3;

  • 客户端和服务端已经完成过一次通信;

40. TCP Keepalive 和 HTTP Keep-Alive 是一个东西吗?

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

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

41. TCP 协议有什么缺陷?

  • 升级 TCP 的工作很困难;

  • TCP 建立连接的延迟;

  • TCP 存在队头阻塞问题;

  • 网络迁移需要重新建立 TCP 连接;

42. QUIC 是如何实现可靠传输的?

QUIC 通过单向递增的 Packet Number,配合 Stream ID 与 Offset 字段信息,可以支持乱序确认而不影响数据包的正确组装,摆脱了 TCP 必须按顺序确认应答 ACK 的限制,解决了 TCP 因某个数据包重传而阻塞后续所有待发送数据包的问题。

43. QUIC 是如何解决 TCP 队头阻塞问题的?

QUIC 给每一个 Stream 都分配了一个独立的滑动窗口,这样使得一个连接上的多个 Stream 之间没有依赖关系,都是相互独立的,各自控制的滑动窗口。

44. QUIC 对拥塞控制改进

TCP 更改拥塞控制算法是对系统中所有应用都生效,无法根据不同应用设定不同的拥塞控制策略。但是因为 QUIC 处于应用层,所以就可以针对不同的应用设置不同的拥塞控制算法,这样灵活性就很高了。

45. QUIC 更快的连接建立

QUIC 内部包含了 TLS,它在自己的帧会携带 TLS 里的“记录”,再加上 QUIC 使用的是 TLS1.3,因此仅需 1 个 RTT 就可以「同时」完成建立连接与密钥协商,甚至在第二次连接的时候,应用数据包可以和 QUIC 握手信息(连接信息 + TLS 信息)一起发送,达到 0-RTT 的效果。

46. QUIC 是如何迁移连接的?

QUIC 协议没有用四元组的方式来“绑定”连接,而是通过连接 ID 来标记通信的两个端点,客户端和服务器可以各自选择一组 ID 来标记自己,因此即使移动设备的网络变化后,导致 IP 地址变化了,只要仍保有上下文信息(比如连接 ID、TLS 密钥等),就可以“无缝”地复用原连接,消除重连的成本,没有丝毫卡顿感,达到了连接迁移的功能。

47. TCP UDP 端口问题

TCP 和 UDP 可以同时绑定相同的端口吗?

可以的。

TCP 和 UDP 传输协议,在内核中是由两个完全独立的软件模块实现的。

因此, TCP/UDP 各自的端口号也相互独立,互不影响。

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

如果两个 TCP 服务进程同时绑定的 IP 地址和端口都相同,那么执行 bind() 时候就会出错,错误是“Address already in use”。

如果两个 TCP 服务进程绑定的端口都相同,而 IP 地址不同,那么执行 bind() 不会出错。

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

当我们重启 TCP 服务进程的时候,意味着通过服务器端发起了关闭连接操作,于是就会经过四次挥手,而对于主动关闭方,会在 TIME_WAIT 这个状态里停留一段时间,这个时间大约为 2MSL。

当 TCP 服务进程重启时,服务端会出现 TIME_WAIT 状态的连接,TIME_WAIT 状态的连接使用的 IP+PORT 仍然被认为是一个有效的 IP+PORT 组合,相同机器上不能够在该 IP+PORT 组合上进行绑定,那么执行 bind() 函数的时候,就会返回了 Address already in use 的错误。

要解决这个问题,我们可以对 socket 设置 SO_REUSEADDR 属性。

这样即使存在一个和绑定 IP+PORT 一样的 TIME_WAIT 状态的连接,依然可以正常绑定成功,因此可以正常重启成功。

客户端的端口可以重复使用吗?

在客户端执行 connect 函数的时候,只要客户端连接的服务器不是同一个,内核允许端口重复使用。

TCP 连接是由四元组(源 IP 地址,源端口,目的 IP 地址,目的端口)唯一确认的,那么只要四元组中其中一个元素发生了变化,那么就表示不同的 TCP 连接的。

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

要看客户端是否都是与同一个服务器(目标地址和目标端口一样)建立连接。

如果客户端都是与同一个服务器(目标地址和目标端口一样)建立连接,那么如果客户端 TIME_WAIT 状态的连接过多,当端口资源被耗尽,就无法与这个服务器再建立连接了。即使在这种状态下,还是可以与其他服务器建立连接的,只要客户端连接的服务器不是同一个,那么端口是重复使用的。

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

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

因为开启了这个内核参数后,客户端调用 connect 函数时,如果选择到的端口,已经被相同四元组的连接占用的时候,就会判断该连接是否处于 TIME_WAIT 状态。

如果该连接处于 TIME_WAIT 状态并且 TIME_WAIT 状态持续的时间超过了 1 秒,那么就会重用这个连接,然后就可以正常使用该端口了。

48. 服务端没有 listen,客户端发起连接建立,会发生什么?

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

49. 不使用 listen ,可以建立 TCP 连接吗?

是可以的,客户端是可以自己连自己的形成连接(TCP 自连接),也可以两个客户端同时向对方发出请求建立连接(TCP 同时打开),这两个情况都有个共同点,就是没有服务端参与,也就是没有 listen,就能建立连接。

50. 没有 accept,能建立 TCP 连接吗?

可以,accept 方法只是为了从全连接队列中拿出一条连接,本身跟三次握手几乎毫无关系。

51. 用了 TCP 协议,数据一定不会丢吗?

  • 数据从发送端到接收端,链路很长,任何一个地方都可能发生丢包,几乎可以说丢包不可避免。

  • 平时没事也不用关注丢包,大部分时候 TCP 的重传机制保证了消息可靠性。

  • 当你发现服务异常的时候,比如接口延时很高,总是失败的时候,可以用 ping 或者 mtr 命令看下是不是中间链路发生了丢包。

  • TCP 只保证传输层的消息可靠性,并不保证应用层的消息可靠性。如果我们还想保证应用层的消息可靠性,就需要应用层自己去实现逻辑做保证。

52. TCP 四次挥手,可以变成三次吗?

当被动关闭方在 TCP 挥手过程中,如果「没有数据要发送」,同时「没有开启 TCP_QUICKACK(默认情况就是没有开启,没有开启 TCP_QUICKACK,等于就是在使用 TCP 延迟确认机制)」,那么第二和第三次挥手就会合并传输,这样就出现了三次挥手。

所以,出现三次挥手现象,是因为 TCP 延迟确认机制导致的。

TCP 延迟确认的策略:

  • 当有响应数据要发送时,ACK 会随着响应数据一起立刻发送给对方

  • 当没有响应数据要发送时,ACK 将会延迟一段时间,以等待是否有响应数据可以一起发送

  • 如果在延迟等待发送 ACK 期间,对方的第二个数据报文又到达了,这时就会立刻发送 ACK

53. TCP 序列号和确认号是如何变化的?

  • 公式一:序列号 = 上一次发送的序列号 + len(数据长度)。特殊情况,如果上一次发送的报文是 SYN 报文或者 FIN 报文,则改为 上一次发送的序列号 + 1。

  • 公式二:确认号 = 上一次收到的报文中的序列号 + len(数据长度)。特殊情况,如果收到的是 SYN 报文或者 FIN 报文,则改为上一次收到的报文中的序列号 + 1。

54. HTTP 常见状态码

1xx 类状态码属于提示信息,是协议处理中的一种中间状态,实际用到的比较少。

2xx 类状态码表示服务器成功处理了客户端的请求,也是我们最愿意看到的状态。

  • 「200 OK」是最常见的成功状态码,表示一切正常。如果是非 HEAD 请求,服务器返回的响应头都会有 body 数据。

  • 「204 No Content」也是常见的成功状态码,与 200 OK 基本相同,但响应头没有 body 数据。

  • 「206 Partial Content」是应用于 HTTP 分块下载或断点续传,表示响应返回的 body 数据并不是资源的全部,而是其中的一部分,也是服务器处理成功的状态。

3xx 类状态码表示客户端请求的资源发生了变动,需要客户端用新的 URL 重新发送请求获取资源,也就是重定向。

  • 「301 Moved Permanently」表示永久重定向,说明请求的资源已经不存在了,需改用新的 URL 再次访问。

  • 「302 Found」表示临时重定向,说明请求的资源还在,但暂时需要用另一个 URL 来访问。

301 和 302 都会在响应头里使用字段 Location,指明后续要跳转的 URL,浏览器会自动重定向新的 URL。

  • 「304 Not Modified」不具有跳转的含义,表示资源未修改,重定向已存在的缓冲文件,也称缓存重定向,也就是告诉客户端可以继续使用缓存资源,用于缓存控制。

4xx 类状态码表示客户端发送的报文有误,服务器无法处理,也就是错误码的含义。

  • 「400 Bad Request」表示客户端请求的报文有错误,但只是个笼统的错误。

  • 「403 Forbidden」表示服务器禁止访问资源,并不是客户端的请求出错。

  • 「404 Not Found」表示请求的资源在服务器上不存在或未找到,所以无法提供给客户端。

5xx 类状态码表示客户端请求报文正确,但是服务器处理时内部发生了错误,属于服务器端的错误码。

  • 「500 Internal Server Error」与 400 类型,是个笼统通用的错误码,服务器发生了什么错误,我们并不知道。

  • 「501 Not Implemented」表示客户端请求的功能还不支持,类似“即将开业,敬请期待”的意思。

  • 「502 Bad Gateway」通常是服务器作为网关或代理时返回的错误码,表示服务器自身工作正常,访问后端服务器发生了错误。

  • 「503 Service Unavailable」表示服务器当前很忙,暂时无法响应客户端,类似“网络服务正忙,请稍后重试”的意思。

  • 「504 Gateway Timeout」代表网关超时,指服务器作为网关或代理,但是没有及时从上游服务器收到请求。

55. GET 和 POST 有什么区别?

GET 的语义是请求获取指定的资源。GET 方法是安全、幂等、可被缓存的。

POST 的语义是根据请求负荷(报文主体)对指定的资源做出处理,具体的处理方式视资源类型而不同。POST 不安全,不幂等,(大部分实现)不可缓存。

56. 什么是强制缓存?

强缓存是利用下面这两个 HTTP 响应头部(Response Header)字段实现的,它们都用来表示资源在客户端缓存的有效期:

  • Cache-Control, 是一个相对时间;

  • Expires,是一个绝对时间;

如果 HTTP 响应头部同时有 Cache-Control 和 Expires 字段的话,Cache-Control 的优先级高于 Expires 。

57. 什么是协商缓存?

协商缓存就是与服务端协商之后,通过协商结果来判断是否使用本地缓存。

  • 第一种:请求头部中的 If-Modified-Since 字段与响应头部中的 Last-Modified 字段实现

  • 第二种:请求头部中的 If-None-Match 字段与响应头部中的 ETag 字段

协商缓存这两个字段都需要配合强制缓存中 Cache-Control 字段来使用,只有在未能命中强制缓存的时候,才能发起带有协商缓存字段的请求。

58. HTTP/1.1

优点

  • 简单

  • 灵活易扩展(例如 HTTPS 和 HTTP/3 对 TCP 层的修改)

  • 应用广泛,跨平台

缺点

  • 无状态——Cookie 解决

  • 不安全

    • 明文传输 - 被窃听

    • 不验证通信方身份 - 被冒充和伪装

    • 不校验报文完整性 - 被篡改

性能(和 HTTP1.0 相比的优势)总之一般般

  • 优点 1. 长连接——减少重复操作的开销

  • 优点 2. 管道网络传输——不等回应即可发送第二个请求,减少响应时间

  • 缺点 3. 队头阻塞 (2.没有解决的缺点)

59. HTTPS 如何解决 HTTP 的安全问题

  • 明文传输产生的窃听问题——信息加密 (混合加密)

  • 不验证身份产生的冒充问题——校验机制 (证书)

  • 不校验完整性产生的篡改问题——身份证书 (摘要算法)

60. HTTPS 一定安全可靠吗?

HTTPS 协议本身到目前为止还是没有任何漏洞的,即使你成功进行中间人攻击,本质上是利用了客户端的漏洞(用户点击继续访问或者被恶意导入伪造的根证书),并不是 HTTPS 不够安全。

61. HTTPS 的应用数据是如何保证完整性的?

TLS 在实现上分为握手协议和记录协议两层:

  • TLS 握手协议就是我们前面说的 TLS 四次握手的过程,负责协商加密算法和生成对称密钥,后续用此密钥来保护应用程序数据(即 HTTP 数据);

  • TLS 记录协议负责保护应用程序数据并验证其完整性和来源,所以对 HTTP 数据加密是使用记录协议;

62. HTTPS 是如何建立连接的?其间交互了什么?

  1. 客户端向服务器发送HTTPS请求,并发起握手过程。

  2. 服务器将公钥证书发送给客户端。公钥证书中包含了服务器的公钥、服务器的域名、证书颁发机构等信息。

  3. 客户端验证服务器的证书。客户端会检查证书的有效性,包括验证证书是否由可信任的证书颁发机构签发,以及证书中的域名是否与实际域名一致。

  4. 如果证书验证通过,客户端生成一个用于会话的对称密钥。

  5. 客户端使用服务器的公钥对对称密钥进行加密,并将加密后的密钥发送给服务器。

  6. 服务器使用私钥对客户端发送的加密密钥进行解密,得到对称密钥的值。

  7. 服务器和客户端使用对称密钥进行加密和解密数据传输。

63. HTTP 的演变

HTTP/1.1

  • 改进

  1. 长连接

  2. 管道运输:一次发送多个请求

  • 不足

  1. 头部冗长,未经压缩,浪费带宽,造成延迟

  2. 没有请求优先级,所以队头阻塞 (HTTP 层)

  3. 服务器只能被动接收客户端的请求

HTTP/2

  • 改进

  1. 头部压缩(解决 1)

  2. 二进制格式

  3. 数据流:可以乱序发送,后 stream ID 组成 HTTP 信息

  4. 多路复用,串行变成并发(解决 2)

  5. 服务器推送(解决 3)

  • 不足

  1. TCP 层队头阻塞

  2. TCP 和 TLS 握手时延

HTTP/3

  • 改进

  1. 无队头阻塞

  2. 更快的连接建立(QUIC 包含 TLS,只需 1 个 RTT)

  3. 连接迁移(没有用四元组的方式来“绑定”连接,而是通过连接 ID)

  • 不足

  • 普及慢,很多网络设备不识别 QUIC

64. HTTP/1.1 优化

三个角度具体优化方法

_ 避免发送 HTTP 请求 _

缓存 客户端第一次请求及数据保存在本地磁盘,形成<key,value> 过期?在请求的 ETag 带上第一次请求中_ 响应头部 _的摘要,服务器收到后会与本地资源的摘要作比较,如果相同则返回不含包体的 304 Not Modified

_ 减少请求次数 _

1. _ 减少重定向请求次数 _:利用中间的代理服务器知晓规则 2. _ 合并请求 _:合并资源,如 CSS、webpack 3. _ 延迟发送请求 _:按需获取,滑动页面的时候再获取资源

_ 减少服务器响应数据大小 _

无损压缩:gzip。请求 Accepy-Encoding,响应 Content-Encoding(文本、程序代码) 有损压缩:舍弃一些数据(质量)。请求 Accept 中的 q 质量因子(音视频、图片)

65. HTTPS 如何优化

硬件优化、软件优化:HTTPS 协议是计算密集型,而不是 I/O 密集型,所以不能把钱花在网卡、硬盘等地方,应该花在 CPU 上

  1. 协议优化

    • (1)用 ECDHE 替换 RSA,往返 1RTT,

    • (2)TLS 1.2->TLS 1.3,往返 1RTT;在 Hello 时就发送椭圆曲线,且废除 RSA 和 DH

  2. 证书优化

    • (1)证书选择:椭圆曲线证书比 RSA 密钥长度短

    • (2)证书验证优化:OCSP(Online Certificate Status Protocal)、OCSP Stapling

  3. 密钥缓存(无前向安全,且易被重放攻击)

    • (1)Session ID:双方缓存密钥,Session ID 和密钥相当于 key-value。但是也有缺点,首先是每一个客户端都要保存密钥,其次是现在网站一般多服务器,不一定命中上次的服务器

    • (2)Session Ticket:客户端负责缓存。

    • (3)Pre-shared Key:TLS 1.3 重连只需要 0 RTT。重连时 Ticket 和 HTTP 一起发给服务端

解决重放攻击,应给密钥设定过期时间。

66. HTTP2

第一点,对于常见的 HTTP 头部通过静态表和 Huffman 编码的方式,将体积压缩了近一半,而且针对后续的请求头部,还可以建立动态表,将体积压缩近 90%,大大提高了编码效率,同时节约了带宽资源。

不过,动态表并非可以无限增大, 因为动态表是会占用内存的,动态表越大,内存也越大,容易影响服务器总体的并发能力,因此服务器需要限制 HTTP/2 连接时长或者请求次数。

第二点,HTTP/2 实现了 Stream 并发,多个 Stream 只需复用 1 个 TCP 连接,节约了 TCP 和 TLS 握手时间,以及减少了 TCP 慢启动阶段对流量的影响。不同的 Stream ID 可以并发,即使乱序发送帧也没问题,比如发送 A 请求帧 1 -> B 请求帧 1 -> A 请求帧 2 -> B 请求帧 2,但是同一个 Stream 里的帧必须严格有序。

另外,可以根据资源的渲染顺序来设置 Stream 的优先级,从而提高用户体验。

第三点,服务器支持主动推送资源,大大提升了消息的传输性能,服务器推送资源时,会先发送 PUSH_PROMISE 帧,告诉客户端接下来在哪个 Stream 发送资源,然后用偶数号 Stream 发送资源给客户端。

HTTP/2 通过 Stream 的并发能力,解决了 HTTP/1 队头阻塞的问题,看似很完美了,但是 HTTP/2 还是存在“队头阻塞”的问题,只不过问题不是在 HTTP 这一层面,而是在 TCP 这一层。

HTTP/2 是基于 TCP 协议来传输数据的,TCP 是字节流协议,TCP 层必须保证收到的字节数据是完整且连续的,这样内核才会将缓冲区里的数据返回给 HTTP 应用,那么当「前 1 个字节数据」没有到达时,后收到的字节数据只能存放在内核缓冲区里,只有等到这 1 个字节数据到达时,HTTP/2 应用层才能从内核中拿到数据,这就是 HTTP/2 队头阻塞问题。

67. HTTP3

HTTP/3 就将传输层从 TCP 替换成了 UDP,并在 UDP 协议上开发了 QUIC 协议,来保证数据的可靠传输。

QUIC 协议的特点:

  • 无队头阻塞,QUIC 连接上的多个 Stream 之间并没有依赖,都是独立的,也不会有底层协议限制,某个流发生丢包了,只会影响该流,其他流不受影响;

  • 建立连接速度快,因为 QUIC 内部包含 TLS 1.3,因此仅需 1 个 RTT 就可以「同时」完成建立连接与 TLS 密钥协商,甚至在第二次连接的时候,应用数据包可以和 QUIC 握手信息(连接信息 + TLS 信息)一起发送,达到 0-RTT 的效果。

  • 连接迁移,QUIC 协议没有用四元组的方式来“绑定”连接,而是通过「连接 ID 」来标记通信的两个端点,客户端和服务器可以各自选择一组 ID 来标记自己,因此即使移动设备的网络变化后,导致 IP 地址变化了,只要仍保有上下文信息(比如连接 ID、TLS 密钥等),就可以“无缝”地复用原连接,消除重连的成本;

另外 HTTP/3 的 QPACK 通过两个特殊的单向流来同步双方的动态表,解决了 HTTP/2 的 HPACK 队头阻塞问题。

68. RPC

  • 纯裸 TCP 是能收发数据,但它是个无边界的数据流,上层需要定义消息格式用于定义消息边界。于是就有了各种协议,HTTP 和各类 RPC 协议就是在 TCP 之上定义的应用层协议。

  • RPC 本质上不算是协议,而是一种调用方式,而像 gRPC 和 Thrift 这样的具体实现,才是协议,它们是实现了 RPC 调用的协议。目的是希望程序员能像调用本地方法那样去调用远端的服务方法。同时 RPC 有很多种实现方式,不一定非得基于 TCP 协议。

  • 从发展历史来说,HTTP 主要用于 B/S 架构,而 RPC 更多用于 C/S 架构。但现在其实已经没分那么清了,B/S 和 C/S 在慢慢融合。很多软件同时支持多端,所以对外一般用 HTTP 协议,而内部集群的微服务之间则采用 RPC 协议进行通讯。

  • RPC 其实比 HTTP 出现的要早,且比目前主流的 HTTP/1.1 性能要更好,所以大部分公司内部都还在使用 RPC。

  • HTTP/2.0 在 HTTP/1.1 的基础上做了优化,性能可能比很多 RPC 协议都要好,但由于是这几年才出来的,所以也不太可能取代掉 RPC。

69. WebSocket

  • TCP 协议本身是全双工的,但我们最常用的 HTTP/1.1,虽然是基于 TCP 的协议,但它是半双工的,对于大部分需要服务器主动推送数据到客户端的场景,都不太友好,因此我们需要使用支持全双工的 WebSocket 协议。

  • 在 HTTP/1.1 里,只要客户端不问,服务端就不答。基于这样的特点,对于登录页面这样的简单场景,可以使用定时轮询或者长轮询的方式实现服务器推送 (comet) 的效果。

  • 对于客户端和服务端之间需要频繁交互的复杂场景,比如网页游戏,都可以考虑使用 WebSocket 协议。

  • WebSocket 和 socket 几乎没有任何关系,只是叫法相似。

  • 正因为各个浏览器都支持 HTTP 协议,所以 WebSocket 会先利用 HTTP 协议加上一些特殊的 header 头进行握手升级操作,升级成功后就跟 HTTP 没有任何关系了,之后就用 WebSocket 的数据格式进行收发数据。

70. ping 是用什么协议完成的?在哪一层?

ping 命令使用的是 ICMP 协议,全称 Internet Control Message Protocol,即 Internet 控制消息协议。 ICMP 协议是 TCP/IP 协议集中的一个子协议,属于网络层协议。因此,ping 命令属于网络层协议,即第三层协议。

71. OSI 七层模型和各层有什么协议

  • 应用层:HTTP、FTP、SMTP、POP3 等。

  • 表示层:JPEG、MPEG 等。

  • 会话层:RPC 等。

  • 传输层:TCP、UDP 等。

  • 网络层:IP、ICMP 等。

  • 数据链路层:ARP(IP 地址转换为 MAC 地址) 等。

  • 物理层:IEEE802.3 等。

72. 不同序列化方式各自的优势

  • JSON:JSON 是一种轻量级的数据交换格式,易于阅读和编写,支持几乎所有编程语言和平台,因此广泛用于 Web 应用程序和移动应用程序中。它的缺点是序列化和反序列化过程较慢,尤其是在处理大型数据集时。

  • XML:XML 是一种与 JSON 类似的文本格式,也被广泛用于 Web 和移动应用程序中。然而,与 JSON 相比,XML 更为冗长,解析速度较慢。

  • Thrift:Thrift 是 Facebook 开发的一种高效的跨语言序列化框架,支持多种数据类型、压缩和 RPC(远程过程调用)。这使得它在大型分布式系统中得到广泛应用。Thrift 的缺点是需要通过代码生成器来生成序列化和反序列化代码,这使得开发过程略微复杂。

  • Protobuf:相比较其他序列化方式,Protobuf 序列化后的数据更为紧凑,解析速度更快,在性能方面表现出色。Protobuf 支持架构演进,可以在不影响现有代码的情况下进行升级和扩展,这使得它非常适合在大型分布式系统中使用。

73. dns 查询时什么时候用 TCP 什么时候用 UDP

DNS 查询时,一般情况下使用 UDP 协议,但当 DNS 查询超过 512 字节时,协议的 TC 标志出现删除标志,这时则使用 TCP 发送。UDP 消息不大于 512 字节,如果大于此大小,则会被截断。DNS 将 TCP 用于区域传输和 UDP 作为名称,并定期(主)或反向查询。UDP 可用于交换小信息,而 TCP 必须用于交换大于 512 字节的信息。

74. TCP 三次握手的时候可以带数据吗?为什么前两次不能带?

前两次握手只是为了协商双方的初始序列号,不需要携带数据。只有在第三次握手时,客户端已经处于 ESTABLISHED 状态,并且已经能够确认服务器的接收、发送能力正常,这个时候相对安全了,可以携带数据。

75. 路由转发时目的地址和源地址的变化

路由转发时,源地址和目的地址的变化取决于数据包经过的设备类型。在数据包传递过程中,源 IP 地址和目的 IP 地址一直不变。每次经过交换机,源 MAC 地址和目的 MAC 地址不变。每次经过路由器,源 MAC 地址为本路由器接口 MAC 地址,目的 MAC 地址为该目的 IP 地址下一条对应 IP 地址的 MAC 地址。

76. 两次握手但是我一直发数据,和三次握手一样吗

不完全相同。在 TCP 协议中,两次握手只是建立了一个简单的连接,但是并没有进行双向通信确认。也就是说,在两次握手的情况下,客户端可以发送数据,但是服务器端无法确认是否已经接收到这些数据,因此可能会导致数据包丢失的情况。

相反,三次握手确保了双方的通信能力,并且在客户端和服务器端之间建立了可靠的连接。在三次握手过程中,双方都有机会发送和接收数据,从而确保了连接的可靠性和稳定性。

因此,尽管两次握手和三次握手都可以建立连接,但是使用两次握手来发送数据可能会导致不可靠的情况,建议还是使用三次握手来确保通信的可靠性。

77. gRPC 和 HTTP 区别

  1. 数据格式:HTTP 使用文本格式(通常是 JSON 或 XML)进行数据交换,而 gRPC 使用二进制格式进行数据序列化(通常是 Protocol Buffers)。二进制格式比文本格式更紧凑,可以减少网络传输的数据量。

  2. 传输方式:HTTP 使用请求 - 响应模型,客户端发送请求,服务器返回响应。gRPC 使用双向流模型,客户端和服务器可以同时发送和接收消息。这种模型可以提供更高的并发性和效率,特别适用于大规模的实时应用。

  3. 支持的语言:HTTP 是一种通用的协议,几乎所有的编程语言都有 HTTP 库和框架可以使用。gRPC 最初是为 Google 开发的,并支持多种编程语言,如 C++, Java, Python, Go, Ruby 等。由于 gRPC 使用基于 IDL(Interface Definition Language)的方式定义服务接口,可以自动生成客户端和服务器端的代码,使得不同语言之间的通信更加简便。

  4. 传输效率:由于 gRPC 使用二进制格式和高效的序列化机制,它通常比 HTTP 更高效。gRPC 使用 HTTP/2 作为底层协议,提供了多路复用、头部压缩、流控制等特性,可以减少网络延迟和带宽消耗。

  5. 应用场景:HTTP 适用于多种场景,包括传统的 Web 应用、API 调用、浏览器和服务器之间的通信等。gRPC 更适合于分布式系统、微服务架构和高性能的实时通信场景,如实时数据传输、流式处理等。

78. TCP 报文头

  1. 源端口号:16 位字段,用于标识发送端口的端口号。

  2. 目的端口号:16 位字段,用于标识接收端口的端口号。

  3. 序号:32 位字段,用于标识 TCP 连接中传输的数据流中每个字节的编号。

  4. 确认号:32 位字段,用于确认接收方期望收到的下一个数据字节的序号。

  5. 数据偏移:4 位字段,表示 TCP 报文头的长度,以 4 字节为单位。

  6. 保留字段:6 位字段,保留供将来使用,目前置为 0。

  7. 控制位:6 位字段,包括 URG、ACK、PSH、RST、SYN 和 FIN 等标志位,用于控制 TCP 连接的建立、终止和数据传输等操作。

  8. 窗口大小:16 位字段,用于告知发送方本端的 TCP 接收缓冲区还能容纳多少字节的数据。

  9. 校验和:16 位字段,用于检验 TCP 报文段在传输过程中是否损坏。

  10. 紧急指针:16 位字段,用于指示 TCP 报文段中的紧急数据的最后一个字节的序号。

  11. 选项字段:可变长度的字段,用于传递一些额外的信息,如最大报文段长度等。

78. HTTP2.0 的 Stream

HTTP/2.0 中的 "stream" 指的是多路复用中的一个逻辑概念,它是由多个帧组成的数据流。每个帧都有一个流标识符。通过流标识符,可以将帧组装成一个完整的数据流。一个 HTTP/2 连接可以同时存在多个流,这些流可以并发地在同一个 TCP 连接上发送和接收数据。

HTTP/1.x 中,每个请求都需要建立一个独立的连接,导致请求在传输过程中会被阻塞,只有前一个请求完成后才能发送下一个请求。而在 HTTP/2 中,可以在同一个 TCP 连接上同时发送多个请求和接收多个响应,这样可以避免队头阻塞的问题。

Last updated