Skip to content

什么叫HTTP队头阻塞?为什么HTTP/2解决了HTTP队头阻塞?什么叫TCP队头阻塞?为什么HTTP/3解决了TCP队头阻塞?

  1. HTTP队头阻塞:

HTTP队头阻塞是指在HTTP/1.x中,当客户端发送多个请求时,服务器必须按顺序处理这些请求。如果第一个请求处理时间较长,后面的请求就会被阻塞,即使后面的请求可能很快就可以被处理完毕。

  1. HTTP/2如何解决HTTP队头阻塞:

HTTP/2通过引入多路复用(Multiplexing)解决了这个问题。在一个TCP连接上,HTTP/2可以同时发送多个请求和响应,而不需要等待前面的请求完成。这样,即使一个请求被阻塞,其他请求仍然可以继续处理。

让我们用图示解释下HTTP/2的工作机制:

 [客户端]                                     [服务器]
    |                                           |
    |       One TCP Connection                  |
    |------------------------------------------>|
    |                                           |
    |       HTTP/2 Frame                        |
    |------------------------------------------>|
    |<------------------------------------------|
    |                                           |
    |  S1     S2  S3   S4        S5             |
    |  |      |   |    |         |              |
    |  v      v   v    v         v              |
    |  [=====][==][===][========][=]            |
    |------------------------------------------>|
    |                                           |
    |  [===][=====][==][====][======]           |
    |<------------------------------------------|
    |                                           |

解释:

  • 顶部:客户端和服务器之间建立单一的TCP连接。
  • 中间:HTTP/2使用二进制帧进行通信,这些帧在单一连接上进行双向传输。
  • 底部:展示了多路复用的概念。
    • 每个流代表一个独立的请求-响应对。
    • 流被分解成多个帧。
    • 不同流的帧可以交错发送。
    • 箭头表示数据传输的方向(双向)。
    • 方括号 [] 代表不同大小的帧,每个流可能包含多个帧。
  • 帧的排列显示了它们如何在单一连接上交错传输。

这个图示展示了HTTP/2的几个关键特性:

  • 单一TCP连接:所有通信都在一个连接上进行。
  • 多路复用:多个请求和响应可以同时在一个连接上传输。
  • 二进制分帧:所有通信都被分解成小的二进制帧。
  • 双向通信:客户端和服务器可以同时发送数据。

HTTP/2中的流和帧是紧密协作的,它们共同构成了HTTP/2的多路复用机制:

  • 流(Stream)的定义:
    • 流是一个逻辑上的概念,代表HTTP/2连接中的一个双向通道。
    • 每个流都有一个唯一的标识符(Stream ID)。
    • 一个流可以承载一个完整的请求-响应周期。
  • 帧(Frame)的定义:
    • 帧是HTTP/2通信的最小单位。
    • 所有的HTTP/2通信都被编码为一个个的帧。
  • 流和帧的关系:
    • 每个帧都属于一个特定的流。
    • 帧头部包含Stream ID,指明该帧属于哪个流。
  • 协同工作机制:
    • 流的创建:
      • 客户端或服务器可以初始化一个新的流。
      • 新流通过发送一个HEADERS帧来创建。
    • 数据传输:
      • HTTP头部信息在HEADERS帧中传输。
      • 实际的请求或响应体在DATA帧中传输。
      • 一个流可能包含多个HEADERS帧和DATA帧。
    • 多路复用:
      • 不同流的帧可以交错发送。
      • 接收方根据帧中的Stream ID重新组装完整的消息。
    • 流量控制:
      • 每个流都有自己的流量控制窗口。
      • WINDOW_UPDATE帧用于管理流量控制。
    • 优先级:
      • 可以给流分配优先级。
      • PRIORITY帧用于设置或调整流的优先级。
    • 流的结束:
      • 流可以通过发送带有END_STREAM标志的帧来关闭。
  • 帧类型与流的关系:
    • DATA:携带请求或响应的实际数据。
    • HEADERS:携带HTTP头部信息。
    • PRIORITY:设置流的优先级。
    • RST_STREAM:立即终止一个流。
    • SETTINGS:指定连接级别的配置参数。
    • PUSH_PROMISE:用于服务器推送(预测客户端将要请求的资源)。
    • PING:测量往返时间,检查连接是否存活。
    • GOAWAY:优雅地关闭连接。
    • WINDOW_UPDATE:实现流量控制。
    • CONTINUATION:继续传输超大的头部信息。
  • 实际应用举例: 假设浏览器请求一个HTML页面和几个相关资源:
    • 流1:发送HEADERS帧请求HTML。
    • 流2:发送HEADERS帧请求CSS文件。
    • 流3:发送HEADERS帧请求JavaScript文件。
    • 服务器可以交错发送这些流的响应帧。
    • 客户端根据Stream ID重组完整的响应。
  1. TCP队头阻塞:

TCP队头阻塞是指在TCP协议中,如果一个数据包丢失,TCP必须等待该包被重传并按序到达后,才能将后续数据包交付给应用层。这会导致即使后续数据包已经到达,应用层也无法立即使用。

  1. HTTP/3如何解决TCP队头阻塞:

HTTP/3通过使用QUIC协议代替TCP来解决这个问题。QUIC基于UDP,实现了自己的可靠性和流量控制机制。

让我们用图示解释下HTTP/3的工作机制:

 [客户端]                                     [服务器]
    |                                           |
    |        QUIC握手 (0-RTT或1-RTT)             |
    |<----------------------------------------->|
    |                                           |
    |        HTTP/3 Frame                       |
    |------------------------------------------>|
    |<------------------------------------------|
    |                                           |
    |  S0     S1  S2   S3        S4             |
    |  |      |   |    |         |              |
    |  v      v   v    v         v              |
    |  [=====][==][===][========][=]            |
    |------------------------------------------>|
    |                                           |
    |  [===][=====][==][====][======]           |
    |<------------------------------------------|
    |                                           |
    |  Streaming Error Handler                  |
    |  [X]   [✓]   [✓]   [X]   [✓]              |
    |                                           |

解释:

  • 顶部:客户端和服务器之间建立QUIC连接,可能是0-RTT(零往返时间)或1-RTT(一次往返时间)握手。
  • 中部(上半部分):HTTP/3使用二进制帧进行通信,这些帧在QUIC连接上进行双向传输。
  • 中部(下半部分):展示了多路复用的概念。
    • 流0通常用于控制流,处理设置和优先级等。
    • 其他流用于不同的请求-响应对。
    • 每个流都是独立的,可以并行处理。
    • 方括号 [] 代表不同大小的帧,每个流可能包含多个帧。
  • 底部:展示了独立的流错误处理。
    • [X] 表示出错的流。
    • [✓] 表示正常的流。
    • 一个流的错误不会影响其他流的传输。

HTTP/3的关键特性:

  • 基于QUIC:
    • 使用UDP而不是TCP作为传输层协议。
    • 内置加密(TLS 1.3)。
    • 改进的拥塞控制。
  • 快速握手:
    • 0-RTT或1-RTT连接建立,减少延迟。
  • 多路复用:
    • 不同流的帧可以交错发送。
    • 没有TCP队头阻塞问题。
  • 独立流处理:
    • 每个流都是独立的。
    • 一个流的问题不会影响其他流。
  • 连接迁移:
    • 支持客户端IP地址变化(如从WiFi切换到移动网络)而不中断连接。
  • 改进的流量控制:
    • 基于字节而非数据包的流量控制。
  • 前向纠错:
    • 可选功能,通过冗余数据减少重传需求。

这种机制使HTTP/3能够提供更快的连接建立、更好的性能和更高的可靠性,特别是在不稳定的网络环境中。

用图示讲解TCP 3次握手的工作机制。可以通过2次握手就建立连接吗?握手阶段会有什么坑?

 [客户端]                                     [服务器]
    |                                           |
    |               SYN                         |
    |------(seq=x, SYN=1)---------------------->|
    |                                           |
    |               SYN-ACK                     |
    |<-----(seq=y, ACK=x+1, SYN=1)--------------|
    |                                           |
    |               ACK                         |
    |------(seq=x+1, ACK=y+1)------------------>|
    |                                           |
    |               连接建立完成                  |
    |<----------------------------------------->|

解释:

  • 第一次握手(SYN):
    • 客户端发送SYN包,序列号为x,SYN标志位为1。
    • 客户端进入SYN_SENT状态。
  • 第二次握手(SYN-ACK):
    • 服务器回复SYN-ACK包,序列号为y,确认号为x+1,SYN和ACK标志位都为1。
    • 服务器进入SYN_RECEIVED状态。
  • 第三次握手(ACK):
    • 客户端发送ACK包,序列号为x+1,确认号为y+1。
    • 连接建立完成,双方进入ESTABLISHED状态。

为什么不能通过2次握手建立连接:

  • 确认双方的接收和发送能力:
    • 3次握手确保了双方都能发送和接收数据。
    • 2次握手只能确认客户端的收发能力和服务器的发送能力。
  • 防止历史连接的意外建立:
    • 如果网络中存在延迟的历史SYN包,2次握手可能会导致错误的连接建立。
    • 第3次握手可以让服务器确认客户端是在响应最新的SYN-ACK,而不是历史连接。
  • 同步序列号:
    • 3次握手允许双方都初始化并确认对方的序列号。

握手阶段可能遇到的问题(坑):

  • SYN洪泛攻击:
    • 攻击者发送大量SYN包但不完成握手,耗尽服务器资源。
    • 解决:SYN cookie技术。
  • 中间人攻击:
    • 握手过程可能被劫持。
    • 解决:使用SSL/TLS等加密协议。
  • RST攻击:
    • 攻击者可能发送RST包中断握手。
    • 解决:实施合适的包过滤和安全策略。
  • 超时问题:
    • 网络延迟可能导致握手包超时。
    • 解决:合理设置超时重传机制。
  • 序列号选择:
    • 不当的序列号选择可能导致安全问题。
    • 解决:使用随机和不可预测的初始序列号。
  • 半开连接:
    • 如果第3次握手丢失,服务器会保持在SYN_RECEIVED状态。
    • 解决:服务器端设置SYN_RECEIVED状态超时机制。
  • 同时打开:
    • 双方同时发起连接可能导致异常。
    • 解决:TCP协议有机制处理这种情况,但应尽量避免。

用图示讲解TCP 4次挥手的工作机制。挥手阶段会有什么坑?

客户端 - 主动关闭一端 服务器 - 被动关闭一端

 [客户端]                                     [服务器]
    |                                           |
    |               FIN                         |
    |------(seq=u, FIN=1)---------------------->|
    |                                           |
    |               ACK                         |
    |<-----(seq=v, ACK=u+1)---------------------|
    |                                           |
    |               FIN                         |
    |<-----(seq=w, FIN=1)-----------------------|
    |                                           |
    |               ACK                         |
    |------(seq=u+1, ACK=w+1)------------------>|
    |                                           |
    |          连接完全关闭                       |
    |                                           |

解释:

  • 第一次挥手(FIN):
    • 客户端发送FIN包,序列号为u,FIN标志位为1。
    • 客户端进入FIN_WAIT_1状态。
  • 第二次挥手(ACK):
    • 服务器回复ACK包,序列号为v,确认号为u+1。
    • 服务器进入CLOSE_WAIT状态。
    • 客户端收到后进入FIN_WAIT_2状态。
  • 第三次挥手(FIN):
    • 服务器发送FIN包,序列号为w,FIN标志位为1。
    • 服务器进入LAST_ACK状态。
  • 第四次挥手(ACK):
    • 客户端回复ACK包,序列号为u+1,确认号为w+1。
    • 客户端进入TIME_WAIT状态。
    • 服务器收到后关闭连接;客户端等待2MSL(Max-Segment-Lifetime,最大报文生存时间)后关闭连接。

挥手阶段可能遇到的问题(坑):

  • TIME_WAIT状态积累:
    • 问题:客户端在TIME_WAIT状态会占用资源,大量连接可能导致资源耗尽。
    • 解决:调整TIME_WAIT超时时间;使用SO_REUSEADDR选项。
     [客户端]
        |
        |  TIME_WAIT (2MSL)
        |  |---------------|
        |                  |
        |                  V
        |              连接关闭
  • 2MSL等待带来的端口占用:
    • 问题:TIME_WAIT状态会占用端口一段时间。
    • 解决:合理规划端口使用,必要时考虑端口复用。
  • FIN_WAIT_2状态卡住:
    • 问题:如果服务器一直不发送FIN包,客户端可能长时间停留在FIN_WAIT_2状态。
    • 解决:调整FIN_WAIT_2超时时间。
  • CLOSE_WAIT累积:
    • 问题:如果应用程序没有正确关闭连接,可能导致服务器端CLOSE_WAIT状态积累。
    • 解决:确保应用程序正确处理连接关闭。
    [服务器]
        |
        |  CLOSE_WAIT (未正确处理)
        |  |----------------------
        |
        |  资源泄露
  • 半关闭状态:
    • 问题:一方关闭连接后,另一方还在发送数据。
    • 解决:正确处理半关闭状态,及时关闭不需要的连接。
  • 报文丢失:
    • 问题:任何一个挥手包丢失都可能导致连接异常。
    • 解决:实现可靠的重传机制。
    [客户端]                                    [服务器]
        |               FIN (Lost)                 |
        |------X---------------------------------->|
        |                                          |
        |               FIN (Re-Send)              |
        |----------------------------------------->|
  • 同时关闭:
    • 问题:双方同时发起关闭可能导致状态混乱。
    • 解决:TCP协议有机制处理这种情况,但应尽量避免。
  • RST包关闭:
    • 问题:使用RST包强制关闭连接可能导致数据丢失。
    • 解决:尽量使用正常的四次挥手,只在必要时使用RST。

SO_REUSEADDR TCP选项和SO_REUSEPORT TCP选项有什么区别?

SO_REUSEADDR 和 SO_REUSEPORT 是两个不同的 TCP socket 选项,它们的功能和用途有所不同:

  1. SO_REUSEADDR

主要功能:

  • 允许绑定到一个还在 TIME_WAIT 状态的地址和端口。
  • 允许在同一端口上启动同一程序的多个实例,只要每个实例绑定的 IP 地址不同。

使用场景:

  • 服务器重启时快速复用地址和端口。
  • 允许单个进程绑定到同一端口的多个地址。
[服务A]
IP: 192.168.1.1:8080
    |
    |  Use SO_REUSEADDR
    |
[服务B]
IP: 192.168.1.2:8080

注意事项:

  • 主要用于解决 TIME_WAIT 问题和多网卡服务器的场景。
  • 不允许在完全相同的地址和端口上绑定多个 socket。
  1. SO_REUSEPORT

主要功能:

  • 允许多个 socket 绑定到完全相同的 IP 地址和端口组合。
  • 内核会自动在这些 socket 之间分配传入的连接。

使用场景:

  • 多核系统上实现负载均衡。
  • 允许多个进程或线程绑定到同一「IP:PORT」上接受连接。
[进程 1]   [进程 2]   [进程 3]
    |         |         |
    |  Use SO_REUSEPORT |
    |         |         |
    V         V         V
   IP: 192.168.1.1:8080

注意事项:

  • Linux 3.9 内核版本后引入。
  • 提供了更好的负载均衡能力,特别是在多核系统上。
  • 所有绑定到相同「IP:PORT」的 socket 必须设置此选项。
  1. 主要区别
  • 绑定能力:
    • SO_REUSEADDR:允许绑定到不同 IP 的相同端口。
    • SO_REUSEPORT:允许绑定到完全相同的 IP 和端口。
  • 负载均衡:
    • SO_REUSEADDR:不提供负载均衡功能。
    • SO_REUSEPORT:提供内核级别的负载均衡能力。
  • 安全性:
    • SO_REUSEPORT:有额外的安全检查,只允许同一用户的进程共享端口。
    • SO_REUSEPORT:无。
  • 使用目的:
    • SO_REUSEADDR:主要用于端口复用和快速重启服务。
    • SO_REUSEPORT:主要用于多进程/线程服务器的负载均衡。
  1. 使用示例(伪代码)
python
# SO_REUSEADDR
sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
sock.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
sock.bind(('192.168.1.1', 8080))

# SO_REUSEPORT
sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
sock.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEPORT, 1)
sock.bind(('192.168.1.1', 8080))