HTTP/2 并发请求限制
TIP
- 限制最终由浏览器决定:虽然服务器可以建议并发数,但浏览器内部存在一个硬性上限,最终生效的并发数是 服务器许可值 与 浏览器硬性上限 中的较小者决定的。
chrome
的限制策略:chrome
浏览器的行为分为两个阶段:- 初始默认值:在未收到服务器明确指示时,遵循
http2
规范建议,默认采用 100 个并发流作为初始值。 - 硬性上限值:无论服务器许可多少并发流,
chrome
内部存在一个绝对的、不可逾越的上限,这个值是 256。
- 初始默认值:在未收到服务器明确指示时,遵循
背景要知
客户端 和 服务器 可以启动的流的数量不是无限的,是由每个对等方在连接开始时发送的 SETTINGS
帧的 SETTINGS_MAX_CONCURRENT_STREAMS
参数规定:section 6.5.2 of RFC 7540, 默认值为无限制,并且 RFC
有以下建议:
It is recommended that this value be no smaller than 100, so as to not unnecessarily limit parallelism.
建议这个值不小于 100,以避免不必要的并行限制。
Node.js 对并发流数量的限制
node.js
的 peerMaxConcurrentStreams
配置项描述如下:
Sets the maximum number of concurrent streams for the remote peer as if a SETTINGS frame had been received. Will be overridden if the remote peer sets its own value for maxConcurrentStreams. Default: 100.
设置远程对等方作为如果收到 SETTINGS 帧而看到的最大并发流数。如果远程对等方设置了自己的 maxConcurrentStreams 值,则将覆盖此值。默认值:100。
peerMaxConcurrentStreams
是一个启动阶段的、对对方能力的、临时的、安全的默认限制,确保了在双方完成握手和能力交换之前,通信能够有序、安全地开始。
可以通过修改 maxConcurrentStreams
配置项来限制并发流数量。
Specifies the maximum number of concurrent streams permitted on an Http2Session. There is no default value which implies, at least theoretically, 232-1 streams may be open concurrently at any given time in an Http2Session. The minimum value is 0. The maximum allowed value is 232-1. Default: 4294967295.
指定在 Http2Session 上允许的最大并发流数。没有默认值,这意味着在任何给定时间理论上最多可以打开 2^32-1 个并发流。最小值为 0。最大允许值为 2^32-1。默认值:4294967295。
接下来探索下 chrome
浏览器在 http/2
连接中,对并发流数量的限制策略。
Chrome 对并发流数量的限制
测试环境
版本限制
chrome
:137.0.7151.104 (Official Build) (arm64)
node.js
:22.14.0
测试场景
- 客户端:创建一个
html
页面 (index.html
),通过javascript
发起大量对上述延迟接口的请求。 - 服务器端:使用
node.js
创建了一个本地http/2
服务器 (server.js
),该服务器能提供一个对请求进行短暂延迟(1000ms
)响应的接口 (/slow-resource
)。
- 客户端:创建一个
测试源码
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>HTTP/2 并发请求限制测试</title>
<style>
body {
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto,
'Helvetica Neue', Arial, sans-serif;
line-height: 1.6;
color: #333;
max-width: 800px;
margin: 20px auto;
padding: 0 20px;
background-color: #f9f9f9;
}
h1 {
color: #111;
text-align: center;
}
.container {
background-color: #fff;
padding: 30px;
border-radius: 8px;
box-shadow: 0 2px 10px rgba(0, 0, 0, 0.05);
}
button {
display: block;
width: 100%;
padding: 15px;
font-size: 1.2em;
font-weight: bold;
color: #fff;
background-color: #007bff;
border: none;
border-radius: 5px;
cursor: pointer;
transition: background-color 0.3s;
}
button:hover {
background-color: #0056b3;
}
button:disabled {
background-color: #cccccc;
cursor: not-allowed;
}
#status {
margin-top: 20px;
padding: 15px;
border-radius: 5px;
text-align: center;
font-weight: bold;
}
.status-info {
background-color: #e9f7ef;
color: #1d643b;
}
.status-done {
background-color: #d4edda;
color: #155724;
}
ol {
padding-left: 20px;
}
li {
margin-bottom: 10px;
}
</style>
</head>
<body>
<div class="container">
<h1>HTTP/2 并发请求限制测试</h1>
<p>
此页面用于验证浏览器对单个HTTP/2连接的并发请求数量限制(通常为100)。
</p>
<h3>操作步骤:</h3>
<ol>
<li>
<b>打开开发者工具</b> (按 <code>F12</code> 或
<code>Ctrl+Shift+I</code>)。
</li>
<li>切换到 <b>网络 (Network)</b> 面板。</li>
<li>点击下方的"开始测试"按钮。</li>
<li>
观察网络瀑布图。你会看到大约前100个请求被立即发送,
而后续的请求会显示为"已停止(Stalled)"或"排队中(Queued)",直到前面的请求完成。
</li>
</ol>
<button id="test-btn">开始测试</button>
<div id="status"></div>
</div>
<script>
const testButton = document.getElementById('test-btn');
const statusDiv = document.getElementById('status');
const TOTAL_REQUESTS = 300;
testButton.addEventListener('click', async () => {
testButton.disabled = true;
statusDiv.textContent = `准备发送 ${TOTAL_REQUESTS} 个请求...`;
statusDiv.className = 'status-info';
console.clear();
console.log(`--- 测试开始,将发送 ${TOTAL_REQUESTS} 个请求 ---`);
const requests = [];
for (let i = 1; i <= TOTAL_REQUESTS; i++) {
const requestPromise = fetch(`/slow-resource?id=${i}`)
.then(response => {
if (!response.ok) {
console.error(`请求 #${i} 失败:`, response.statusText);
}
return response.text();
})
.catch(err => {
console.error(`请求 #${i} 发生网络错误:`, err);
});
requests.push(requestPromise);
}
await Promise.all(requests);
console.log(`--- 所有 ${TOTAL_REQUESTS} 个请求已完成 ---`);
statusDiv.textContent = `测试完成!所有 ${TOTAL_REQUESTS} 个请求均已发送并收到响应。请检查网络瀑布图。`;
statusDiv.className = 'status-done';
testButton.disabled = false;
});
</script>
</body>
</html>
const http2 = require('http2');
const fs = require('fs');
const path = require('path');
const PORT = 8443;
const SLOW_RESPONSE_DELAY_MS = 1000;
let options;
try {
options = {
key: fs.readFileSync('localhost-privkey.pem'),
cert: fs.readFileSync('localhost-cert.pem'),
};
} catch (e) {
console.error(
'Please ensure you have run the following command in the project root:'
);
console.error(
"openssl req -x509 -newkey rsa:2048 -nodes -sha256 -subj '/CN=localhost' -keyout localhost-privkey.pem -out localhost-cert.pem"
);
process.exit(1);
}
console.log('Starting HTTP/2 server...');
const server = http2.createSecureServer(options, (req, res) => {
const protocol = req.stream.session.alpnProtocol;
console.log(`收到请求: ${req.url} (协议: ${protocol})`);
if (req.url === '/') {
res.writeHead(200, { 'Content-Type': 'text/html; charset=utf-8' });
fs.createReadStream(path.join(__dirname, 'index.html')).pipe(res);
} else if (req.url.startsWith('/slow-resource')) {
setTimeout(() => {
res.writeHead(200, { 'Content-Type': 'text/plain' });
res.end('OK');
}, SLOW_RESPONSE_DELAY_MS);
} else {
res.writeHead(404);
res.end('Not Found');
}
});
server.on('error', err => console.error(err));
server.listen(PORT, () => {
console.log(`Listening on https://localhost:${PORT}`);
});
基线测试
配置:服务器端未对并发流数量做任何特殊配置,采用
node.js
的默认值。观察:在浏览器开发者工具的网络面板中,观测到大约在第
100
个请求之后,后续请求开始出现明显的 排队(Stalled/Queued)现象。分析:此阶段结果存在模糊性。因为
node.js
和chrome
的默认并发流限制 恰好都是 100,无法判断瓶颈究竟在哪一方。
解除服务器限制,隔离浏览器
配置:修改
server.js
,在创建http/2
服务器时,明确地将最大并发流数设置为一个远超默认值(如300)的许可值。javascriptconst options = { // ... key 和 cert ... settings: { maxConcurrentStreams: 300 } };
排除了服务器端成为性能瓶颈的可能性。如果此时观察到的并发数仍低于
300
,则可确认限制来自于浏览器。观察:在浏览器开发者工具的网络面板中,请求在达到 第 256 个 之后,第
257
个请求开始出现显著的 排队 现象。分析:限制源于浏览器,且其硬性上限为
256
。
Chromium 源代码分析
为了给实验结论提供最权威的支撑,对 Chromium
核心 网络模块 源代码进行了分析。
初始默认值 (
spdy_session.h
) 在该头文件中,找到了初始并发数的定义:cpp// Maximum number of concurrent streams we will create, unless the server // sends a SETTINGS frame with a different value. const size_t kInitialMaxConcurrentStreams = 100;
这证实了
chrome
在连接建立之初,遵循规范,以 100 作为默认值。硬性上限 (
spdy_session.cc
) 在实现文件中,找到了硬性上限的定义:cpp// The maximum number of concurrent streams we will ever create. Even if // the server permits more, we will never exceed this limit. const size_t kMaxConcurrentStreamLimit = 256;
注释明确指出,即使服务器允许更多,浏览器也绝不会超过
256
这个限制。决策逻辑 (
spdy_session.cc
) 在处理服务器SETTINGS
帧的HandleSetting
函数中,看到了应用该上限的具体逻辑:cppcase spdy::SETTINGS_MAX_CONCURRENT_STREAMS: max_concurrent_streams_ = std::min(static_cast<size_t>(value), kMaxConcurrentStreamLimit); ProcessPendingStreamRequests(); break;
最终生效的并发数 (
max_concurrent_streams_
) 是服务器提供的值 (value
) 和硬性上限 (kMaxConcurrentStreamLimit
) 中的 较小者。这与我们的实验结果min(300, 256) = 256
是吻合的。
结论
chrome
浏览器对于单个 HTTP/2
连接的并发请求数限制,采用的是一种 礼貌协商,底线坚守 的策略。以 100
个并发流作为 初始值 与服务器协商,并愿意听从服务器通过 SETTINGS
帧提出的更高建议,但其自身内置了一个 256
个并发流的绝对硬性上限,任何来自服务器的更高请求都将被此上限所限制。