概要

HTTP/2は、ウェブ通信の速度、効率、およびパフォーマンスを向上させるために設計されたHTTPの主要な改訂版である。HTTP/1.1を基盤としながら、以下のような重要な改善点を導入している。

  1. マルチプレクシング (Multiplexing): 複数のリクエストとレスポンスを1つの接続で同時に送信できるようにし、レイテンシを削減し、ページの読み込み速度を向上させる。
  2. ヘッダー圧縮: HPACK圧縮を使用してHTTPヘッダーのサイズを縮小し、データ伝送を高速化する。
  3. サーバプッシュ: サーバが必要なリソースを事前にクライアントに送信できるようにし、ページの読み込みをさらに迅速にする。

file

Ref: https://coolicehost.com/http2-protocol.html

httpプロトコルの各バージョンの歴史

バージョン リリース年 主な特徴
HTTP/1.0 1996年 - 初期のHTTPバージョン
- 各リクエストごとに新しいTCP接続を確立
HTTP/1.1 1997年 - パーシステント接続を導入(複数のリクエストで同じTCP接続を使用)
- ホストヘッダーの必須化
- チャンク転送エンコーディングのサポート
HTTP/2 2015年 - マルチプレクシングによる複数リクエストの同時処理
- ヘッダー圧縮(HPACK)
- サーバプッシュ機能の追加

http/2のサーバ間通信の検証

以下のような構成で検証したい。

file

検証したいこと

  • Clientからhttp/2のプロトコルで、webServer.jsに通信する
  • webServer.jsはボディとヘッダーをそのままapiServer.jsに転送
  • apiServer.jsは受信したヘッダーとボティを出力し、簡単なメッセージをClientに返す
  • webServer.jsはapiServer.jsから受信したレスポンスをClientに返す

今回はnode.jsを利用して検証する。

手順

webServer

最初はSSL/TLSを使わずにサーバを立てて検証したが、Client側でcurlにてリクエストを送信したところ

curl: (1) Received HTTP/0.9 when not allowed

という訳の分からないエラーメッセージが表示された。いろいろ調べた所、どうやらSSL/TLSを使えば解決できそうということで、自己証明書を作ることにした。

$ openssl req -x509 -newkey rsa:2048 -nodes -keyout server-key.pem -out server-cert.pem -days 365

各オプションとその意味を以下に説明する

  • -x509: X.509形式の証明書を生成するオプション。X.509は証明書の標準フォーマット。
  • -newkey rsa:2048: 新しいRSA鍵ペアを生成し、鍵の長さを2048ビットに設定。
  • -nodes: 秘密鍵をパスフレーズなしで生成。
  • -keyout server-key.pem: 秘密鍵を server-key.pem というファイルに保存。
  • -out server-cert.pem: 証明書を server-cert.pem というファイルに保存。
  • -days 365: 証明書の有効期限を365日間に設定オプション。

以下はwebServer.jsのソース

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

// サーバ用の証明書とキーを読み込み
const server = http2.createSecureServer({
	key: fs.readFileSync('server-key.pem'),
	cert: fs.readFileSync('server-cert.pem')
});

server.on('stream', (stream, headers) => {
	// ローカルのAPIサーバへの接続を確立
	const client = http2.connect('http://localhost:7001');

	// クライアントからのリクエストヘッダーをAPIサーバに転送
	const req = client.request(headers);
	req.on('response', (headers, flags) => {
		// APIサーバからのレスポンスヘッダーをクライアントに転送
		stream.respond(headers);
	});

	req.setEncoding('utf8');
	req.on('data', (chunk) => {
		// APIサーバからのレスポンスボディをクライアントに転送
		stream.write(chunk);
	});

	req.on('end', () => {
		// レスポンスが完了したら、クライントとの接続を終了
		stream.end();
		client.close();
	});

	// クライアントからのリクエストボティがあれば、APIサーバに転送
	stream.on('data', (chunk) => {
		req.write(chunk);
	});

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

server.listen(7000, () => {
	console.log('Secure HTTP/2 server listening on port 7000');
});

webServerSSL/TLSの終端になるので、apiServerへは普通のhttpプロトコルで行ける。

Webサーバを立ち上げる。

$ node webServer.js
Secure HTTP/2 server listening on port 7000

apiServer

const http2 = require('http2');

// HTTP/2サーバを作成
const apiServer = http2.createServer();

apiServer.on('stream', (stream, headers) => {
	// HTTP/2プロトコルであることを前提に、リクエストの詳細をログに表示
	console.log('Stream ID:', stream.id); // ストリームID

	// ヘッダーの情報を直接確認
	console.log('Stream Headers:', headers);

	// リクエストボディを受け取るための変数
	let body = '';

	// クライアントから送られてくるデータを受け取る
	stream.on('data', (chunk) => {
		body += chunk; // データを連結
	});

    // データ受け取りが完了した時にボディをログに出力
    stream.on('end', () => {
		console.log('Request Body:', body); // リクエストボディの出力

		// 応答を送信
		stream.respond({
			':status': 200,
			'content-type': 'text/plain'
		});
		stream.end('Hello from HTTP/2 API server!\n');
	});
});

apiServer.listen(7001, () => console.log('API server listening on port 7001'));

APIサーバを立ち上げる。

$ node apiServer.js
API server listening on port 7001

Client

curlコマンドのバージョンがhttp/2に対応しているかを確認しておく

$ curl --version
curl 8.8.0 (x86_64-apple-darwin19.6.0) libcurl/8.8.0 (SecureTransport) OpenSSL/3.3.0 zlib/1.2.11 brotli/1.1.0 zstd/1.5.6 libidn2/2.3.7 libssh2/1.11.0 nghttp2/1.61.0 librtmp/2.3 OpenLDAP/2.6.8
Release-Date: 2024-05-22
Protocols: dict file ftp ftps gopher gophers http https imap imaps ipfs ipns ldap ldaps mqtt pop3 pop3s rtmp rtsp scp sftp smb smbs smtp smtps telnet tftp
Features: alt-svc AsynchDNS brotli GSS-API HSTS HTTP2 HTTPS-proxy IDN IPv6 Kerberos Largefile libz MultiSSL NTLM SPNEGO SSL threadsafe TLS-SRP UnixSockets zstd

Features:HTTP2が含まれているので問題ない。

以下のコマンドでwebServerにヘッダーとボディを送信する

$ curl --http2 -k "https://localhost:7000" -H "Client-Set-Header: Header Value" -d "This is the body data"
Hello from HTTP/2 API server!

Clientからは正しくapiServerからのレスポンスを受信できた。

この時、apiServerを立てたターミナルには以下のような情報が出力された

Stream ID: 1
Stream Headers: [Object: null prototype] {
  ':method': 'POST',
  ':scheme': 'https',
  ':authority': 'localhost:7000',
  ':path': '/',
  'user-agent': 'curl/8.8.0',
  accept: '*/*',
  'client-set-header': 'Header Value',
  'content-length': '21',
  'content-type': 'application/x-www-form-urlencoded',
  [Symbol(nodejs.http2.sensitiveHeaders)]: []
}
Request Body: This is the body data

Clientから受信してヘッダーとボディが、webServer経由(プロキシ)し、apiServerに転送されることが検証できた。また、それがhttp2のプロトコルで、ストリームIDが1であることも分かった。

まとめ

http/2について触り程度で、streamイベントにてマルチプレクシングでの処理を検証した。http/1.1のような単純なreqresの扱い方ほど直感的ではない感覚もあるが、リクエスト間の待機時間が短縮でき、通信の効率が向上することに繋がる。 サーバプッシュの検証はしなかったが、今後時間があれば試したい。