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)的许可值。javascript- const 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函数中,看到了应用该上限的具体逻辑:cpp- case 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 个并发流的绝对硬性上限,任何来自服务器的更高请求都将被此上限所限制。


 XiSenao
 XiSenao