Appearance
什么叫HTTP队头阻塞?为什么HTTP/2解决了HTTP队头阻塞?什么叫TCP队头阻塞?为什么HTTP/3解决了TCP队头阻塞?
- HTTP队头阻塞:
HTTP队头阻塞是指在HTTP/1.x
中,当客户端发送多个请求时,服务器必须按顺序
处理这些请求。如果第一个请求处理时间较长,后面的请求就会被阻塞,即使后面的请求可能很快就可以被处理完毕。
- 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重组完整的响应。
- TCP队头阻塞:
TCP队头阻塞是指在TCP协议中,如果一个数据包丢失,TCP必须等待该包被重传并按序到达后,才能将后续数据包交付给应用层。这会导致即使后续数据包已经到达,应用层也无法立即使用。
- 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 选项,它们的功能和用途有所不同:
- 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。
- 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 必须设置此选项。
- 主要区别
- 绑定能力:
- SO_REUSEADDR:允许绑定到不同 IP 的相同端口。
- SO_REUSEPORT:允许绑定到完全相同的 IP 和端口。
- 负载均衡:
- SO_REUSEADDR:不提供负载均衡功能。
- SO_REUSEPORT:提供内核级别的负载均衡能力。
- 安全性:
- SO_REUSEPORT:有额外的安全检查,只允许同一用户的进程共享端口。
- SO_REUSEPORT:无。
- 使用目的:
- SO_REUSEADDR:主要用于端口复用和快速重启服务。
- SO_REUSEPORT:主要用于多进程/线程服务器的负载均衡。
- 使用示例(伪代码)
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))