Skip to content

HTTP/2

node:http2 模块提供了 HTTP/2 协议的实现,与 http API 相比,http2 核心 API 在客户端和服务器之间更加对称。例如,大多数事件,如 'error'、'connect' 和 'stream',可以由客户端代码或服务器端代码触发。

生成证书和密钥

bash
openssl req -x509 -newkey rsa:2048 -nodes -sha256 -subj '/CN=localhost' \
  -keyout localhost-privkey.pem -out localhost-cert.pem

服务端

由于没有已知的浏览器支持 未加密的 HTTP/2,因此在与浏览器客户端通信时必须使用 http2.createSecureServer()。

js
const http2 = require('node:http2');
const fs = require('node:fs');

/** Http2Server & Http2SecureServer
 *@close([callback])
 @setTimeout([msecs][, callback])
 @timeout
 @updateSettings([settings])
 */
const server = http2.createSecureServer({
  key: fs.readFileSync('localhost-privkey.pem'),
  cert: fs.readFileSync('localhost-cert.pem'),
  origins: ['https://example.com', 'https://example.org']
});
server.on('checkContinue', () => console.log());
server.on('connection', () => console.log('当建立新的 TCP 流时会触发此事件'));
server.on('request', () => console.log('每次有请求时触发'));
server.on('stream', () => console.log(''));
server.on('timeout', () => console.log());
server.on('error', (err) => console.error(err));
server.on('session', (session) => {
  console.log('当 Http2Server 创建新的 Http2Session 时')
    // 向连接的客户端提交 ALTSVC 帧
   session.altsvc('h2=":8000"', 'https://example.org:80');

   /**向连接的客户端提交 ORIGIN 帧,以通告服务器能够提供权威响应的源集。 */
     session.origin('https://example.com', 'https://example.org');

});

// 当与服务器关联的 Http2Session 触发 'stream' 事件时
server.on('stream', (stream, headers) => {
  /**
   * ServerHttp2Stream 类是 Http2Stream 的扩展,专门用于 HTTP/2 服务器
   * @additionalHeaders(headers) 向连接的 HTTP/2 对等方发送额外的信息性 HEADERS 帧。
   * @pushStream(headers[, options], callback) 启动推送流
   * @headersSent
   * @pushAllowed
   */
  stream.respond({
    'content-type': 'text/html; charset=utf-8',
    ':status': 200,
  },{
    endStream:false,  //true 表示响应将不包含有效负载数据
    waitForTrailers:true,//传输完最后的 DATA 帧时,Http2Stream 不会自动关闭
  }
  );
  stream.pushStream({ ':path': '/' }, (err, pushStream, headers) => {
    if (err) throw err;
    pushStream.respond({ ':status': 200 });
    pushStream.end('some pushed data');
  });

  stream.on('wantTrailers', () => {
    stream.sendTrailers({ ABC: 'some value to send' });
  });

  const stat = fs.fstatSync(fd);
  const headers = {
    'content-length': stat.size,
    'last-modified': stat.mtime.toUTCString(),
    'content-type': 'text/plain; charset=utf-8',
  };
  // 启动响应,其数据从给定的文件描述符中读取。
  stream.respondWithFD(fd, headers);


 stream.respondWithFile('/some/file',
                         { 'content-type': 'text/plain; charset=utf-8' },
                         {  function statCheck(stat, headers) {
    headers['last-modified'] = stat.mtime.toUTCString();
  }, onError(error){} });
  stream.end('<h1>Hello World</h1>');
});

server.listen(8443);

客户端

以下说明了 HTTP/2 客户端:

js
const http2 = require('node:http2');
const fs = require('node:fs');

// Http2Session 类的实例表示 HTTP/2 客户端和服务器之间的活动通信会话
/** http2session 属性
 * @alpnProtocal 返回已连接的 TLSSocket 自己的 alpnProtocol 属性的值
 * @closed
 * @connecting
 * @destroyed
 * @encrypted TLSSocket 连接则为true
 * @localSettings 当前本地设置的无原型对象
 * @remoteSettings 当前远程设置的无原型对象
 * @originSet 连接到 TLSSocket,则 originSet 属性将返回 Array 的起源
 * @pendingSettingsAck 是否正在等待已发送的 SETTINGS 帧的确认
 * @socket 返回 Proxy 对象,它充当 net.Socket(或 tls.TLSSocket)
 * @state 提供有关 Http2Session 当前状态的其他信息。
 * @type 是客户端还是服务器
 * @destroyed
 * @settings([settings][, callback]) 更新此 Http2Session 的当前本地设置
 * @close([callback])
 * @destroy([error][, code]) 立即终止关联的 net.Socket 或 tls.TLSSocket。
 * @goaway([code[, lastStreamID[, opaqueData]]]) 将 GOAWAY 帧传输到连接的对等方
 * @ping([payload, ]callback) 向连接的 HTTP/2 对等方发送 PING 帧
 * @ref() 底层 net.Socket 上调用 ref()。
 * @unref() 底层 net.Socket 上调用 unref()。
 */
const session = http2.connect('https://localhost:8443', {
  ca: fs.readFileSync('localhost-cert.pem'),
});

session.on('close', () => console.log("'close' 事件在 Http2Session 被销毁后触发。其监听器不需要任何参数。"));
session.on('connect', () => console.log('成功连接到远程对等方并且通信可以开始'));
session.on('frameError', () => console.log('在会话上发送帧时发生错误时'));
session.on('goaway', () => console.log('接收到 GOAWAY 帧时触发 'goaway' 事件。'));
session.settings({ enablePush: false });
session.on('localSettings', () => console.log('当接收到确认 SETTINGS 帧时触发 'localSettings' 事件。'));
session.on('ping', () => console.log('每当从连接的对等方接收到 PING 帧时'));
session.on('remoteSettings', (settings) => console.log('当从连接的对等方接收到新的 SETTINGS 帧时'));

  // 创建新的 Http2Stream 时会触发
session.on('stream', (stream, headers, flags) => {
  const method = headers[':method'];
  const path = headers[':path'];
  stream.respond({
    ':status': 200,
    'content-type': 'text/plain; charset=utf-8',
  });
  stream.write('hello ');
  stream.end('world');
});

session.setTimeout(2000,callback(){});
session.on('timeout', () => { /* .. */ });

session.on('error', (err) => console.error('在处理 Http2Session 期间发生错误时触发',err));


const http2 = require('node:http2');
const server = http2.createServer();
server.on('session', (session) => {

  // 设置本地端点的窗口大小。windowSize 是要设置的总窗口大小,而不是增量
  session.setLocalWindowSize(2 ** 20);
});

session.ping(Buffer.from('abcdefgh'), (err, duration, payload) => {
  if (!err) {
    console.log(`Ping acknowledged in ${duration} milliseconds`);
    console.log(`With payload '${payload.toString()}'`);
  }
});

/**
 * ClientHttp2Session属性和方法
 */

client.on('altsvc', (alt, origin, streamId) => {
  console.log(alt,'每当客户端接收到 ALTSVC 帧时');
  console.log(origin);
  console.log(streamId);
});

client.on('origin', (origins) => {
  for (let n = 0; n < origins.length; n++)
    console.log(origins[n],'每当客户端接收到 ORIGIN 帧时');
});


/** Http2Stream:代表一个通过 Http2Session 实例的双向 HTTP/2 通信流
 * Http2Stream 类是 ServerHttp2Stream 和 ClientHttp2Stream 类的基础,
 * 每个类分别由服务器端或客户端专门使用
 * @aborted
 * @bufferSize
 * @closed
 * @destroyed
 * @endAfterHeaders
 * @id
 * @pending
 * @rstCode
 * @sentHeaders 包含为此 Http2Stream 发送的出站标头的对象。
 * @sentInfoHeaders
 * @sentTrailers 为此 HttpStream 发送的出站尾随标头的对象。
 * @session 对拥有此 Http2Stream 的 Http2Session 实例的引用
 * @state
 * @sentInfoHeaders
 * @sentInfoHeaders
 * @sentInfoHeaders
 * @close(code[, callback])
 * @priority(options) 更新此 Http2Stream 实例的优先级。
 * @setTimeout(msecs, callback)
 * @sendTrailers(headers) 向连接的 HTTP/2 对等端发送尾随的 HEADERS 帧
 */

req.on('abort', () => { console.log(' Http2Stream 被销毁时触发')});
req.on('error', () => { console.log(' 处理 Http2Stream 期间发生错误时触发。')});
req.on('frameError', () => { console.log(' 发送帧时发生错误')});
req.on('ready', () => { console.log(' Http2Stream 已打开、已分配 id 且可以使用时触发。监听器不需要任何参数。')});
req.on('timeout', () => { console.log(' 设置的毫秒数内没有收到此 Http2Stream 的活动后')});
req.on('trailers', () => { console.log(' 当接收到与尾随标头字段关联的标头块')});
req.on('wantTrailers', () => { console.log('已将要在帧上发送的最后 DATA 帧排队')});

/**
 * ClientHttp2Stream属性和方法
 */
const req = session.request({ ':path': '/' },{
  endStream:true,
  exclusive:false,
  parent:1,
  weight:12,
  waitForTrailers:true,
  // signal:abortSignal.signal
});
req.respond({
  'content-type': 'text/html; charset=utf-8',
  ':status': 200,
});
req.setEncoding('utf8');
let data = '';
req.on('data', (chunk) => { data += chunk; });
req.on('continue', () => { console.log('当服务器发送 100 Continue 状态时触发')});
req.on('headers', (headers,flags) => { console.log('当接收到流的附加标头块时')});
req.on('push', (headers,flags) => { console.log('当接收到服务器推送流的响应头时')});
req.on('continue', () => { console.log('当服务器发送 100 Continue 状态时触发')});
req.on('continue', () => { console.log('当服务器发送 100 Continue 状态时触发')});
req.on('response', (headers, flags) => {
  for (const name in headers) {
    console.log(`${name}: ${headers[name]}`);
    console.log('收到此流的响应 HEADERS 帧时')
  }
});
req.on('end', () => {
  console.log(`\n${data}`);
  session.close();
});
req.end();

根据 MIT 许可证发布