NodeJS 速習チュートリアル

Node.js HTTP/2 モジュール

1. HTTP/2 とは?

Node.js の http2 モジュールは HTTP/2 プロトコルの実装を提供し、単一の接続上でのパフォーマンス向上、サーバープッシュ(Server Push)機能、ヘッダー圧縮、およびマルチプレクシング(Multiplexing)を実現します。

HTTP/2 は、HTTP/1.1 を以下の主要機能によって改善しています:

  • バイナリプロトコル (Binary protocol): HTTP/1.1 のテキスト形式ではなくバイナリ形式を使用してデータ転送を行うため、パースの効率が大幅に向上します。
  • マルチプレクシング (Multiplexing): 単一の接続上で複数のリクエストとレスポンスを同時に送信できます。
  • ヘッダー圧縮 (Header compression): HPACK を使用してヘッダーを圧縮し、オーバーヘッドを削減します。
  • サーバープッシュ (Server push): クライアントがリクエストする前に、サーバー側から能動的にリソースを送信できます。
  • ストリームの優先順位付け (Stream prioritization): リソースに優先度を設定して配信できます。

2. HTTP/2 モジュールの使用方法

Node.js で http2 モジュールにアクセスするには、以下のように記述します。

const http2 = require('http2');

HTTP/2 モジュールは Node.js v10.0.0 以降で安定版(Stable)となっています。ほとんどのブラウザにおいて HTTP/2 はセキュアな接続(HTTPS)を必要とするため、多くの例では TLS/SSL を使用します。

3. HTTP/2 サーバーの作成

以下は、TLS を使用した基本的な HTTP/2 サーバーの作成例です。

const http2 = require('http2');
const fs = require('fs');
const path = require('path');

// TLS 証明書とキーの読み込み
const options = {
  key: fs.readFileSync(path.join(__dirname, 'server.key')),
  cert: fs.readFileSync(path.join(__dirname, 'server.crt'))
};

// HTTP/2 サーバーの作成
const server = http2.createSecureServer(options);

// ストリームイベントのハンドリング
server.on('stream', (stream, headers) => {
  // ヘッダーからパスを取得
  const path = headers[':path'];
  
  // レスポンスの送信
  if (path === '/') {
    stream.respond({
      'content-type': 'text/html',
      ':status': 200
    });
    stream.end('<h1>Hello from HTTP/2!</h1>');
  } else {
    stream.respond({
      ':status': 404
    });
    stream.end('Not found');
  }
});

// サーバーの起動
const port = 8080;
server.listen(port, () => {
  console.log(`HTTP/2 server running at https://localhost:${port}`);
});

この例では TLS 証明書ファイルがあることを前提としています。開発用には OpenSSL を使用して自己署名証明書(Self-signed certificates)を生成できますが、本番環境では信頼された認証局(CA)の証明書を使用してください。

また、TLS なしの HTTP/2 サーバー(暗号化なしの直接接続用)を作成することも可能です。

const http2 = require('http2');

// TLS なしの HTTP/2 サーバーを作成
const server = http2.createServer();

server.on('stream', (stream, headers) => {
  stream.respond({
    'content-type': 'text/html',
    ':status': 200
  });
  stream.end('<h1>Hello from HTTP/2 without TLS!</h1>');
});

server.listen(8080);

※ほとんどのモダンブラウザは TLS 経由の HTTP/2 のみをサポートしているため、暗号化なしのサーバーは、主に明示的にサポートしている専用の HTTP/2 クライアントとの通信に使用されます。

4. HTTP/2 クライアント

HTTP/2 サーバーに接続するクライアントの作成例です。

const http2 = require('http2');

// クライアントの作成
const client = http2.connect('https://localhost:8080', {
  // 開発時の自己署名証明書を許可する場合
  rejectUnauthorized: false
});

// エラーハンドリング
client.on('error', (err) => {
  console.error('Client error:', err);
});

// リクエストの作成
const req = client.request({ ':path': '/' });

// レスポンスデータのハンドリング
req.on('response', (headers) => {
  console.log('Status:', headers[':status']);
  console.log('Headers:', headers);
});

req.on('data', (chunk) => {
  console.log('Received data:', chunk.toString());
});

req.on('end', () => {
  console.log('Request completed');
  client.close();
});

// リクエストの送信
req.end();

5. HTTP/2 ストリーム

HTTP/2 はクライアントとサーバー間の通信に「ストリーム(Stream)」を使用します。各ストリームは、クライアントとサーバー間で交換されるフレームの独立した双方向シーケンスを表します。

5.1 ストリームイベント

重要なストリームイベントは以下の通りです:

  • 'headers': ヘッダーを受信したときに発生
  • 'data': データチャンクを受信したときに発生
  • 'end': ストリームが終了したときに発生
  • 'error': エラーが発生したときに発生
const http2 = require('http2');
const fs = require('fs');
const path = require('path');

// サーバーの作成
const server = http2.createSecureServer({
  key: fs.readFileSync(path.join(__dirname, 'server.key')),
  cert: fs.readFileSync(path.join(__dirname, 'server.crt'))
});

server.on('stream', (stream, headers) => {
  // ストリームエラーのハンドリング
  stream.on('error', (error) => {
    console.error('Stream error:', error);
  });
  
  stream.on('close', () => {
    console.log('Stream closed');
  });
  
  // レスポンスのハンドリング
  stream.respond({
    'content-type': 'text/plain',
    ':status': 200
  });
  
  // 複数のチャンクでデータを送信
  stream.write('First chunk of data\n');
  
  setTimeout(() => {
    stream.write('Second chunk of data\n');
    stream.end('Final chunk of data');
  }, 1000);
});

server.listen(8080);

6. HTTP/2 サーバープッシュ (Server Push)

サーバープッシュ(Server Push)を使用すると、クライアントが明示的にリクエストする前に、サーバー側から能動的にリソースを送信できます。これによりラウンドトリップ(RTT)の遅延を解消し、パフォーマンスを向上させることができます。

const http2 = require('http2');
const fs = require('fs');
const path = require('path');

const options = {
  key: fs.readFileSync(path.join(__dirname, 'server.key')),
  cert: fs.readFileSync(path.join(__dirname, 'server.crt'))
};

const server = http2.createSecureServer(options);

server.on('stream', (stream, headers) => {
  const requestPath = headers[':path'];
  
  if (requestPath === '/') {
    // CSS と JavaScript リソースをプッシュ
    stream.pushStream({ ':path': '/style.css' }, (err, pushStream) => {
      if (err) {
        console.error('Error pushing stream:', err);
        return;
      }
      
      pushStream.respond({
        'content-type': 'text/css',
        ':status': 200
      });
      
      pushStream.end('body { color: blue; }');
    });
    
    stream.pushStream({ ':path': '/script.js' }, (err, pushStream) => {
      if (err) {
        console.error('Error pushing stream:', err);
        return;
      }
      
      pushStream.respond({
        'content-type': 'application/javascript',
        ':status': 200
      });
      
      pushStream.end('console.log("Hello from HTTP/2 server push!");');
    });
    
    // メインの HTML ドキュメントを送信
    stream.respond({
      'content-type': 'text/html',
      ':status': 200
    });
    
    stream.end(`
      <!DOCTYPE html>
      <html>
      <head>
        <title>HTTP/2 Server Push Example</title>
        <link rel="stylesheet" href="/style.css">
        <script src="/script.js"></script>
      </head>
      <body>
        <h1>HTTP/2 Server Push Demo</h1>
        <p>CSS and JavaScript were pushed by the server!</p>
      </body>
      </html>
    `);
  } else {
    // 直接リクエストされた場合にプッシュ済みリソースを提供
    if (requestPath === '/style.css') {
      stream.respond({
        'content-type': 'text/css',
        ':status': 200
      });
      stream.end('body { color: blue; }');
    } else if (requestPath === '/script.js') {
      stream.respond({
        'content-type': 'application/javascript',
        ':status': 200
      });
      stream.end('console.log("Hello from HTTP/2 server push!");');
    } else {
      stream.respond({ ':status': 404 });
      stream.end('Not found');
    }
  }
});

server.listen(8080);

7. HTTP/2 ヘッダー

HTTP/2 ではヘッダー形式が異なります。特に、すべてのヘッダー名は小文字であり、リクエスト疑似ヘッダー(Pseudo-headers)はコロン (:) で始まります。

const http2 = require('http2');

// HTTP/2 疑似ヘッダー定数
const {
  HTTP2_HEADER_METHOD,
  HTTP2_HEADER_PATH,
  HTTP2_HEADER_AUTHORITY,
  HTTP2_HEADER_SCHEME,
  HTTP2_HEADER_STATUS
} = http2.constants;

const client = http2.connect('https://localhost:8080', {
  rejectUnauthorized: false
});

// カスタムヘッダーを含むリクエストの送信
const req = client.request({
  [HTTP2_HEADER_METHOD]: 'GET',
  [HTTP2_HEADER_PATH]: '/',
  [HTTP2_HEADER_AUTHORITY]: 'localhost:8080',
  [HTTP2_HEADER_SCHEME]: 'https',
  'user-agent': 'node-http2/client',
  'custom-header': 'custom-value'
});

req.on('response', (headers) => {
  console.log('Response status:', headers[HTTP2_HEADER_STATUS]);
  console.log('Response headers:', headers);
});

req.on('data', (chunk) => {
  console.log('Received data:', chunk.toString());
});

req.on('end', () => {
  client.close();
});

req.end();

8. HTTP/2 設定 (Settings)

HTTP/2 では、プロトコルの様々な設定をカスタマイズ可能です。

const http2 = require('http2');
const fs = require('fs');
const path = require('path');

const options = {
  key: fs.readFileSync(path.join(__dirname, 'server.key')),
  cert: fs.readFileSync(path.join(__dirname, 'server.crt')),
  
  // HTTP/2 設定
  settings: {
    // 接続あたりの最大同時ストリーム数
    maxConcurrentStreams: 100,
    
    // フロー制御の初期ウィンドウサイズ
    initialWindowSize: 1024 * 1024, // 1MB
    
    // サーバープッシュを有効化
    enablePush: true
  }
};

const server = http2.createSecureServer(options);

server.on('stream', (stream, headers) => {
  stream.respond({
    'content-type': 'text/html',
    ':status': 200
  });
  
  stream.end('<h1>HTTP/2 Server with Custom Settings</h1>');
});

server.listen(8080);

9. HTTP/1.1 との互換性

HTTP/2 サーバーは HTTP/1.1 リクエストも処理できるため、シームレスな移行が可能です。

const http2 = require('http2');
const http = require('http');
const fs = require('fs');
const path = require('path');

const options = {
  key: fs.readFileSync(path.join(__dirname, 'server.key')),
  cert: fs.readFileSync(path.join(__dirname, 'server.crt')),
  allowHTTP1: true // HTTP/1.1 接続を許可
};

const server = http2.createSecureServer(options);

// HTTP/1.1 と HTTP/2 両用のハンドラー関数
const handler = (req, res) => {
  res.writeHead(200, { 'Content-Type': 'text/plain' });
  res.end(`Hello from ${req.httpVersion} server!`);
};

// HTTP/1.1 互換リクエストハンドラー
server.on('request', handler);

// HTTP/2 固有のストリームハンドラー
server.on('stream', (stream, headers) => {
  stream.respond({
    'content-type': 'text/plain',
    ':status': 200
  });
  stream.end(`Hello from HTTP/2 stream API!`);
});

server.listen(8080, () => {
  console.log('Server running at https://localhost:8080/');
});

10. パフォーマンスに関する考慮事項

HTTP/2 はパフォーマンス向上をもたらしますが、その使用法を最適化することが重要です:

  • 接続の再利用 (Connection Reuse): HTTP/2 では、リクエストごとに新しい接続を作るのではなく、単一の接続を複数のリクエストで再利用することを目指すべきです。
  • 適切なストリーム管理: 不要になったストリームは必ず閉じ、同時ストリーム数を監視してください。
  • サーバープッシュ戦略: 必要とされる可能性が高いリソースのみをプッシュしてください。過剰なプッシュは帯域幅とリソースの無駄になります。
  • ヘッダー圧縮の活用: カスタムヘッダーの数とサイズを最小限に抑えることで、HPACK 圧縮のメリットを最大化できます。

11. HTTP/2 vs HTTP/1.1

主な違いをまとめました:

機能HTTP/1.1HTTP/2
プロトコル形式テキストベースバイナリベース
マルチプレクシングなし (複数接続が必要)あり (単一接続で複数ストリーム)
ヘッダー圧縮なしあり (HPACK)
サーバープッシュなしあり
フロー制御基本的高度(ストリームごと)
優先順位付けなしあり

12. 実践的な例:Webサイト全体の配信

HTTP/2 を使用して Web サイトを配信する完全な例です。

const http2 = require('http2');
const fs = require('fs');
const path = require('path');
const mime = require('mime-types');

const options = {
  key: fs.readFileSync(path.join(__dirname, 'server.key')),
  cert: fs.readFileSync(path.join(__dirname, 'server.crt'))
};

const server = http2.createSecureServer(options);

// public ディレクトリからファイルを提供
const publicDir = path.join(__dirname, 'public');

server.on('stream', (stream, headers) => {
  const reqPath = headers[':path'] === '/' ? '/index.html' : headers[':path'];
  const filePath = path.join(publicDir, reqPath);
  
  // パストラバーサル防止の基本的なセキュリティチェック
  if (!filePath.startsWith(publicDir)) {
    stream.respond({ ':status': 403 });
    stream.end('Forbidden');
    return;
  }
  
  fs.stat(filePath, (err, stats) => {
    if (err || !stats.isFile()) {
      stream.respond({ ':status': 404 });
      stream.end('Not found');
      return;
    }
    
    // コンテンツタイプの特定
    const contentType = mime.lookup(filePath) || 'application/octet-stream';
    
    // ファイルの提供
    stream.respond({
      'content-type': contentType,
      ':status': 200
    });
    
    const fileStream = fs.createReadStream(filePath);
    fileStream.pipe(stream);
    
    fileStream.on('error', (err) => {
      console.error('File stream error:', err);
      stream.close(http2.constants.NGHTTP2_INTERNAL_ERROR);
    });
  });
});

server.listen(8080, () => {
  console.log('HTTP/2 server running at https://localhost:8080/');
});

※この例では mime-types パッケージが必要です:npm install mime-types

13. 高度なストリーム管理

HTTP/2 のストリーム管理機能により、多数の同時リクエストを効率的に処理できます。以下はストリームの優先順位付けとフロー制御の例です。

const http2 = require('http2');
const fs = require('fs');

const server = http2.createSecureServer({
  key: fs.readFileSync('server.key'),
  cert: fs.readFileSync('server.crt'),
  settings: {
    initialWindowSize: 65535,  // 64KB 初期ウィンドウ
    maxConcurrentStreams: 100,
    enablePush: true
  }
});

server.on('stream', (stream, headers) => {
  // 優先度情報の取得
  const weight = stream.priority && stream.priority.weight || 1;
  const parent = stream.priority && stream.priority.parent ? 'with parent' : 'no parent';
  
  console.log(`New stream ${stream.id} (weight: ${weight}, ${parent})`);
  
  // 優先度レベルに応じた処理
  if (headers[':path'] === '/high-priority') {
    stream.priority({ weight: 256, exclusive: true });
    stream.respond({ ':status': 200, 'content-type': 'text/plain' });
    stream.end('High priority content');
  } else {
    stream.respond({ ':status': 200, 'content-type': 'text/plain' });
    stream.end('Standard priority content');
  }
  
  stream.on('error', (error) => {
    console.error(`Stream ${stream.id} error:`, error);
    stream.end();
  });
  
  stream.on('close', () => {
    console.log(`Stream ${stream.id} closed`);
  });
});

server.listen(8443);

14. エラーハンドリングとデバッグ

信頼性の高い HTTP/2 アプリケーションには適切なエラーハンドリングが不可欠です。

const http2 = require('http2');
const fs = require('fs');
const { promisify } = require('util');
const readFile = promisify(fs.readFile);

async function startServer() {
  try {
    const [key, cert] = await Promise.all([
      readFile('server.key'),
      readFile('server.crt')
    ]);

    const server = http2.createSecureServer({ key, cert });
    
    // グローバルエラーハンドラー
    server.on('error', (err) => {
      console.error('Server error:', err);
    });
    
    // 未捕捉の例外のハンドリング
    process.on('uncaughtException', (err) => {
      console.error('Uncaught exception:', err);
      server.close(() => process.exit(1));
    });
    
    server.on('stream', (stream, headers) => {
      try {
        setTimeout(() => {
          try {
            if (Math.random() > 0.8) {
              throw new Error('Random error for demonstration');
            }
            stream.respond({ ':status': 200 });
            stream.end('Success!');
          } catch (err) {
            handleStreamError(stream, err);
          }
        }, 100);
      } catch (err) {
        handleStreamError(stream, err);
      }
    });
    
    function handleStreamError(stream, error) {
      console.error('Stream error:', error);
      if (!stream.destroyed) {
        stream.respond({
          ':status': 500,
          'content-type': 'text/plain'
        });
        stream.end('Internal Server Error');
      }
    }
    
    server.listen(8443, () => {
      console.log('Server running on https://localhost:8443');
    });
    
  } catch (err) {
    console.error('Failed to start server:', err);
    process.exit(1);
  }
}

startServer();

15. パフォーマンスの最適化

HTTP/2 の特性を活かした最適化戦略です。

15.1 コネクションポーリング

単一の接続を効率的に管理する例:

const http2 = require('http2');
const { URL } = require('url');

class HTTP2ConnectionPool {
  constructor() {
    this.connections = new Map();
  }
  
  async getConnection(url) {
    const { origin } = new URL(url);
    
    if (!this.connections.has(origin)) {
      const client = http2.connect(origin, {
        rejectUnauthorized: false
      });
      
      client.on('error', (err) => {
        console.error('Connection error:', err);
        this.connections.delete(origin);
      });
      
      client.on('close', () => {
        this.connections.delete(origin);
      });
      
      this.connections.set(origin, {
        client,
        lastUsed: Date.now(),
        inUse: 0
      });
    }
    
    const conn = this.connections.get(origin);
    conn.lastUsed = Date.now();
    conn.inUse++;
    
    return {
      client: conn.client,
      release: () => {
        conn.inUse--;
      }
    };
  }
}

15.2 ヘッダー圧縮の最適化

  • Cookie のサイズを最小化する。
  • 短く、かつ説明的なヘッダー名を使用する。
  • 重複するヘッダーを避ける。

16. セキュリティのベストプラクティス

モダンな TLS 設定を適用したセキュアサーバーの構築例です。

const http2 = require('http2');
const fs = require('fs');

const options = {
  key: fs.readFileSync('server.key'),
  cert: fs.readFileSync('server.crt'),
  // 安全性の低いプロトコルを無効化
  secureOptions:
    require('constants').SSL_OP_NO_SSLv3 |
    require('constants').SSL_OP_NO_TLSv1 |
    require('constants').SSL_OP_NO_TLSv1_1,
  // 推奨される暗号スイート
  ciphers: [
    'TLS_AES_256_GCM_SHA384',
    'TLS_CHACHA20_POLY1305_SHA256',
    'TLS_AES_128_GCM_SHA256'
  ].join(':'),
  minVersion: 'TLSv1.3', // TLS 1.3 を推奨
  maxVersion: 'TLSv1.3',
  rejectUnauthorized: true
};

const server = http2.createSecureServer(options);

server.on('request', (req, res) => {
  // セキュリティヘッダーの設定
  res.setHeader('X-Content-Type-Options', 'nosniff');
  res.setHeader('X-Frame-Options', 'DENY');
  res.setHeader('Strict-Transport-Security', 'max-age=31536000; includeSubDomains');
  
  res.end('Secure HTTP/2 Response');
});

server.listen(8443);

17. 実戦的なユースケース

17.1 HTTP/2 による API ゲートウェイ

複数のサービスへリクエストを転送する高性能なゲートウェイ:

const http2 = require('http2');
const { URL } = require('url');

const services = {
  '/users': 'http://users-service:3000',
  '/products': 'http://products-service:3000'
};

const server = http2.createSecureServer({
  key: fs.readFileSync('server.key'),
  cert: fs.readFileSync('server.crt')
});

server.on('stream', (stream, headers) => {
  const path = headers[':path'];
  const servicePath = Object.keys(services).find(p => path.startsWith(p));
  
  if (!servicePath) {
    stream.respond({ ':status': 404 });
    return stream.end('Not Found');
  }
  
  const targetUrl = new URL(path.slice(servicePath.length), services[servicePath]);
  const client = http2.connect(targetUrl.origin);
  const req = client.request({
    ...headers,
    ':path': targetUrl.pathname + targetUrl.search,
    ':authority': targetUrl.host
  });
  
  req.pipe(stream);
  stream.pipe(req);
});

17.2 リアルタイムデータストリーミング

text/event-stream を使用した効率的なデータ配信:

const streams = new Set();

function broadcast(data) {
  const payload = JSON.stringify(data);
  for (const stream of streams) {
    try {
      stream.write(`data: ${payload}\n\n`);
    } catch (err) {
      streams.delete(stream);
    }
  }
}

server.on('stream', (stream, headers) => {
  if (headers[':method'] !== 'GET') {
    stream.respond({ ':status': 405 });
    return stream.end();
  }
  
  stream.respond({
    'content-type': 'text/event-stream',
    'cache-control': 'no-cache',
    ':status': 200
  });
  
  streams.add(stream);
  stream.on('close', () => streams.delete(stream));
});