connect-es編
今回は番外編です。とある事情で簡単にconnect-esの内部実装を読みたくなったので雑に読んで1記事にまとめます。
connect-esはUniversal Handler/Clientという抽象化レイヤーを導入し、HTTPのバージョンに依存しない設計になっています。
code:md
┌─────────────────────────────────────────────────────────┐
│ プロトコル層 (Connect / gRPC / gRPC-Web) │
├─────────────────────────────────────────────────────────┤
│ Universal Handler / Client(抽象化レイヤー) │
├───────────────────────┬─────────────────────────────────┤
│ HTTP/1.1 Client │ HTTP/2 Client │
│ (node:http/https) │ (node:http2 + SessionManager) │
└───────────────────────┴─────────────────────────────────┘
Node.js標準の http / https モジュールを直接使用
URLプロトコルでHTTPS/HTTPを自動判別
Keep-Aliveソケットの再利用をサポート
トランスポート作成時に httpVersion を指定するだけで切り替え可能です。
code:ts
export type NodeTransportOptions =
| (NodeHttp1TransportOptions & { httpVersion: "1.1" })
| (NodeHttp2TransportOptions & { httpVersion: "2" });
HTTP/1.1 クライアント: Node.js 標準の http / https モジュールをラップしています。
URL のプロトコルhttp:// or https:// で自動判別
Keep-Alive ソケットの再利用をサポート
code:ts
function createNodeHttp1Client(
httpOptions:
| Omit<http.RequestOptions, "signal">
| Omit<https.RequestOptions, "signal">
| undefined,
): UniversalClientFn {
return async function request(
req: UniversalClientRequest,
): Promise<UniversalClientResponse> {
// ... sentinel とエラーハンドリング
h1Request(
sentinel,
req.url,
{
...httpOptions,
headers: webHeaderToNodeHeaders(req.header, httpOptions?.headers),
method: req.method,
},
// ...
);
};
}
Http2SessionManager を使って HTTP/2 セッションを管理します。
code:ts
function createNodeHttp2Client(
sessionProvider: (url: string) => NodeHttp2ClientSessionManager,
): UniversalClientFn {
return function request(
req: UniversalClientRequest,
): Promise<UniversalClientResponse> {
const sentinel = createSentinel(req.signal);
const sessionManager = sessionProvider(req.url);
// ... h2Request でストリームを開く
};
}
たとえばExpress等のフレームワークとConnect RPCを共存させることができ、expressConnectMiddlewareを噛ませることで、特定パスのみRPCとして処理できることができます。
これらを実現するのがUniversal Clientという仕組みであり、HTTP/HTTP2におけるエラーハンドリングやステータス管理等を一元的に行っているようです。
code:md
http.IncomingMessage Request
│ │
▼ ▼
┌────────────────────────────────────────────────┐
│ UniversalServerRequest │
│ { url, method, header, body, signal, ... } │
└────────────────────────────────────────────────┘
│
▼
┌─────────────────────────┐
│ Connect/gRPC Handler │
│ (プロトコル実装) │
└─────────────────────────┘
│
▼
┌────────────────────────────────────────────────┐
│ UniversalServerResponse │
│ { status, header, body, trailer } │
└────────────────────────────────────────────────┘
│ │
▼ ▼
http.ServerResponse Response
ちなみにweb用もありますが、こちらはUniversal Clinetが使わずに標準のfetchをwrapされて実装されています。
まとめ
connect-esの実装を簡単にみました。
各ランタイムにおける標準実装の上に構築された仕組みなので、部分的な導入や既存HTTPライブラリとの共存等もしやすそうに思いました。