この記事はNikkei Advent Calendar 2021の 14日目 の記事です。
こんにちは。電子版Webチームの谷出です。
概要
開発のため、PCに立てたHTTPサーバーへ他の端末からアクセスしたいことがあります。この用途ではngrokなどの便利なツールが存在しています。
試しに自分も作ってみたのでその報告をします。
github.com/rikuTanide/p2p_tunnel
用語
便宜上、ここではこのように用語を使います。
公開したいローカルのHTTPサーバーをoriginと呼びます。
originを公開したい側を配信側/publisher、そのoriginにアクセスしたい側を購読側/subscriberと呼びます。
originへ別のLANからアクセスできるようにすることをトンネリングと呼びます。
実演
配信側PC
まず、適当なサンプルアプリを用意します。
riku@riku-MacBook-Pro example-app % npx create-react-app .
riku@riku-MacBook-Pro example-app % yarn run start &

p2p_tunnelをインストールし、publisherを起動します。
--port
と --host
でoriginのポートを指定します。
riku@riku-MacBook-Pro ~% yarn global add https://github.com/rikuTanide/p2p_tunnel.git
riku@riku-MacBook-Pro ~ % p2p_tunnel pub --port=3000 --host=localhost
bind to localhost:3000
publisherID is YmMZhwo5pjRxrtvx
購読側PC
p2p_tunnelをインストールし、subscriberを起動します。
--host
と --port
でsubscriberを建てるポートを指定します。
--id
には上記で出力されたpublisherIDを指定します。
C:\Users\riku\example_app>p2p_tunnel sub --port=3000 --host=localhost --id=YmMZhwo5pjRxrtvx
表示できました。
subscriberの--host
にローカルIPアドレスを指定すれば、LAN内の他の端末でもsubscribeできます。
こだわった点
配信側と購読側をP2Pでトンネリングしました。
多くのトンネリングツールはクラウド上のサーバーで通信を中継しています。
このメリットとして以下の点が挙げられます。
- 購読側に特殊なツールがいらない
- 簡単にhttpsで動作確認ができる
- Fastlyなどの外部ネットワークと簡単に連携できる
しかし、このようなデメリットが存在します。
- 原理上、誰かがクラウドのネットワーク利用料金を負担している
- 中継サーバー自体が中間者である
そこで、P2Pでトンネリングします。
仕組み
使うツール
P2Pを実現するためにSkyWayを使いました。
SkyWayはWebRTCのシグナリングサーバやラッパーライブラリなどを提供してくれるサービスです。
SkyWayを使う手段は複数ありますが、今回はpuppeteer経由でHeadless Chromeの中のWebRTC機能を使うことにしました。
最も環境構築や互換性で躓かないだろうという理由です。
バイナリフォーマット
HTTPリクエストとHTTPレスポンスをWebRTCで送受信する都合上、それらを一つのバイナリに固めました。
このようなフォーマットです。
リクエストID: subscriber内でPromiseを解決するためのランダムな英数字
Start Line: HTTPリクエストの最初の一行の事。HTTPメソッドとパス
リクエスト
項目 | 長さ |
---|---|
リクエストID | 36バイト |
Start Lineの長さ | 4バイト |
ヘッダーの長さ | 4バイト |
ボディの長さ | 4バイト |
Start Line | 可変長 |
ヘッダー | 可変長 |
ボディ | 可変長 |
レスポンス
項目 | 長さ |
---|---|
リクエストID | 36バイト |
ステータスコード | 4バイト |
ヘッダーの長さ | 4バイト |
ボディの長さ | 4バイト |
ヘッダー | 可変長 |
ボディ | 可変長 |
躓いたところ
node-fetchのcompress
publisherからoriginへアクセスするためにnode-fetchを使いました。
node-fetchには”compress”というオプションパラメータがあり、デフォルトは『有効』であり、gzip/deflateで圧縮されたHTTPレスポンスを自動で解凍してくれます。
つまりこれが有効になっていた場合、HTTPヘッダーのcontent-lengthで指定されたbodyサイズと、渡ってくるbodyサイズにずれが生じます。
自分はここに気づかず思わぬ不具合に遭遇しました。
解決方法としては、各中継地点で適宜content-lengthを付け替えるか、もしくはcompress: false,
にしておくのが良いと思います。
今回は後者を選択しました。
その代わりWebRTCを通す前後でbinaryの塊を丸ごとgzipしました。
Uint8Arrayのコンストラクタ
HTTPリクエストとレスポンスを一塊のbinaryにするときに躓いたのが、Uint8Arrayのコンストラクタの仕様です。
Uint8Arrayには幾つかのコンストラクタがありますが、ここではこの二つの違いについて説明します。
new Uint8Array(typedArray); typedArray typedArray 引数付きで呼び出されると、これはあらゆる型付き配列型 (例えば Int32Array) にすることができますが、 typedArray を新しい型付き配列にコピーします。
new Uint8Array(buffer [, byteOffset [, length]]); buffer, byteOffset, length buffer と、オプションで byteOffset と length 引数を指定して呼び出されると、指定された ArrayBuffer を表示する型付き配列ビューが生成されます。
引数にUint8Arrayを渡した場合は コピーされます が、ArrayBufferを渡した場合は コピーされません 。
ここを間違えると、挙動やパフォーマンスが大きく変わってしまいます。
しかし、分かり辛い事にTypeScriptではUint8ArrayはArrayBufferにキャストできます。
例えば、下記のfromArrayBuffer
関数とfromUint8Array
関数は、静的にはほとんど同じに見えます が、
前者は1GB のメモリしか使わないのに対し 後者は100GB も使います。
function multiplication(original: ArrayBuffer) {
const list: Uint8Array[] = [];
for(let i = 0; i < 100; i ++) {
const next = new Uint8Array(original);
list.push(next);
}
return list;
}
function fromArrayBuffer() {
const original = new Uint8Array(1024 * 1024 * 1024);
copy(original.buffer);
}
function fromUint8Array() {
const original = new Uint8Array(1024 * 1024 * 1024);
copy(original);
}
ここで間違えないようにするために、自分が管理できるコードの範囲内ではUint8ArrayかArrayBufferのどちらか片方しか使わないようにするといいと思います。
そして、入出力時に適宜変換します。
今回は、Uint8Arrayに統一することにしました。
ArrayBufferを引き回して必要に応じてUint8Arrayに変換するよりも、最初から最後までUint8Arrayで引き回す方が事故が少ないと思ったからです。
今後の課題
現在このような課題があります。
- 実装上Server Sent EventやWebSocketを転送することができない
- webpackのような開発ツールと連携できない
- 仕組み上、subscriber側に特別なツールをインストールしなければいけない
- 仕組み上、httpsでsubscribeできない
そこで、今後このような取り組みをしたいです。
- Server Sent EventとWebSocketに対応
- Service Workerからプロキシする仕組みに変更する
- スタンドアロンで動くようにする
- httpsでsubscribeできるようにする
まとめ
- P2Pを使って、ローカルのHTTPサーバーへ外部からアクセスできるものを作りました
- P2PのためにはHeadless ChromeでSkyWayを使うのが便利です
- Uint8Arrayのコンストラクタには気を付けましょう
終わりに
WebチームにはWebが好きなメンバーが集っています。
一緒に日経電子版のパフォーマンスやアクセシビリティ、セキュリティなどに貢献してくださるという方は、
ぜひJobsからご連絡ください!
明日は15日目、玉越さんによる『DX を支える BigQuery の安心・安全・便利・効率的な運用の実現』です。お楽しみに!