Piping Serverのコードリーディング - なぜそういう設計なのか?など
#Piping_Server #TypeScript
はじめに
Piping Serer、作り始めはとってもシンプルだったけど、
ユーザーのコネクション切れたときの処理や
ブラウザ用に伝えないといけないHTTPヘッダ
など加えて少しづつ長くなって来ているコード。
思い出しながら、コードを読んで、「なぜそういう設計にしているか」などをまとめる。
読むコミットはこちら:dc0f68ada204cb198adf5af8fad2f33e49228128
登場する用語たち
Piping Server上で決めている用語。コードとの対応関係を明確にするために、このページでもこの用語を使う。
ソースコード内の識別子の命名や
コメントや
テストコード
などで使われる。
sender: 送信者
例:echo "hello" | curl -T - ppng.io/mymsgしたりするPOST/PUTする送信者
receiver:受信者
例:curl ppng.io/mysgなどでGETして受信するもの
GET /helpとかGET /versionする人は含まれず、送信者に対して存在する
送信者に対して1つ以上の受信者が存在する
path: 送受信に使うパス
例:echo "hello" | curl -T - ppng.io/mymsgの/mymsgの部分
すべてのpathとついた識別子がこの意味というわけではないが、単にpathと読んでる。
転送: senderとreceiversの間でデータを送受信すること
unestablished:転送準備が確率されていない
確立されていない例:
receiverはいるが、senderが来ていないときや
senderやいるが、receiverが来るべき数nに到達していない
Pipe:転送可能なsenderとreceiverたち
UnestablishedPipe:転送準備が確率されていないsenderとreceiverたち
PipeとUnestablishedPipeは型で別れていて、型を見ることで判断できる
UnestablishedPipeだとまだ、転送が始まっていないと分かる
Pipeだと転送準備完了以後であることが分かる
ルーティング
以下がルーティング処理。
https://github.com/nwtgck/piping-server/blob/dc0f68ada204cb198adf5af8fad2f33e49228128/src/piping.ts#L219-L309
http.createServer(handler)やhttp2.createSecureServer(handler)のhandlerを作る関数のコード。HTTPリクエストのエントリーポイントにあたっている。処理の内容としては、GETやPOSTのメソッドを見て、receiverなのかsenderなのかを判別したりする。
なぜhandlerを返す関数なのか?
理由:useHttps: booleanを引数にとりたいから。
HTTPSかどうかでGET /helpしたときのプロトコルの表示を変えている。HTTPSで通信しているかどうかは、IncommingMessageだけでは判別できないぽいのでこうなっている。
/helpの内容は、
ユーザーにとって参考になりやすく(≒コピペしやすく)、
セキュアであるべき
だと考えている。
なぜexpressなど使わないか?
理由:
少し低レイヤーな実装をしたくなるときにも対応するため
Node.js標準の"http"モジュールも書きづらくはなく、コードの見通しもそこまで悪くならない
API系のルーティングとは少し違うことをする
senderやreceiverのハンドリングのエントリーポイント
以下がsenderのハンドリングの開始地点。
https://github.com/nwtgck/piping-server/blob/dc0f68ada204cb198adf5af8fad2f33e49228128/src/piping.ts#L241
以下がreceiverのハンドリングの開始地点。
https://github.com/nwtgck/piping-server/blob/dc0f68ada204cb198adf5af8fad2f33e49228128/src/piping.ts#L291
上記の2つともが、最初の「ルーティング」内の関数で呼ばれている。HTTPリクエストを使って処理する。詳しくは後述。
senderをハンドリング
以下のhandleSender()に関する説明。
https://github.com/nwtgck/piping-server/blob/dc0f68ada204cb198adf5af8fad2f33e49228128/src/piping.ts#L456
成功するパターンは2つだけ。それ以外は不正なリクエストにエラーを返す。
1つ以上のreceiversが存在して、senderがいなかった場合(=既存パスへsenderを追加)と、
https://github.com/nwtgck/piping-server/blob/dc0f68ada204cb198adf5af8fad2f33e49228128/src/piping.ts#L477-L496
senderもreceiverも存在せず、receiverを待つ場合(=新規パスにsenderを登録)
https://github.com/nwtgck/piping-server/blob/dc0f68ada204cb198adf5af8fad2f33e49228128/src/piping.ts#L507-L520
「既存パスへsenderを追加」の場合は、receiversの数が必要数に達していれば、senderとreceiver間での転送が開始される。
転送開始:https://github.com/nwtgck/piping-server/blob/dc0f68ada204cb198adf5af8fad2f33e49228128/src/piping.ts#L492-L495
不正なリクエストでエラーを返す場合は以下、
すでにsenderが存在している場合
https://github.com/nwtgck/piping-server/blob/dc0f68ada204cb198adf5af8fad2f33e49228128/src/piping.ts#L503-L504
senderが要求したreceiverの数とUnestablishedPipeに登録されているreceiverの数が不一致の場合
https://github.com/nwtgck/piping-server/blob/dc0f68ada204cb198adf5af8fad2f33e49228128/src/piping.ts#L497-L500
(senderがreceiverの数をリクエストに含めるのはセキュリティ的考慮)
receiverをハンドリング
以下のhandleReceiver()に関する説明。
https://github.com/nwtgck/piping-server/blob/dc0f68ada204cb198adf5af8fad2f33e49228128/src/piping.ts#L531
(WIP: senderの説明から代替予想がつくし、もっと他の部分で書きたいことがあるので、WIP)
unestablishedなsenderやreceiverの作成
ここが一番書きたかったことかもしれない。
以下のcreateSenderOrReceiver()に関する説明。
https://github.com/nwtgck/piping-server/blob/dc0f68ada204cb198adf5af8fad2f33e49228128/src/piping.ts#L600
このcreateSenderOrReceiver()はremoverType: "sender" | "receiver"によって、まだestablishedしてないsenderやreceiverを作成するメソッド。戻り値にunsubscribeCloseListener: () => voidを含むところを書いておきたかった。
unsubscribeCloseListenerがなぜ必要ななのか?それについて書きたい。
unestablishedなときに、senderやreceiverの接続が切れたときの処理が存在する。
senderやreceiverはunestablishedなときが存在する。このときsenderが接続を切ったり、receiverが接続を切ったりする可能性がある。
senderやreceiverの接続が切れたときの処理とは、
senderが接続を切ってreceiversの数が0なら、pathを破棄
receiverが接続をきって、senderとreceiverの数が0ならpathを破棄
pathを破棄というのは、もう一度pathが使えるようにするという意味である。
Piping Serverは一度使われたpathも再利用される仕様。今まで確率したpathや確率していないpathをずっとメモリ上で保存し続けたりはしない。
そのcloseしたときの処理は、あくまでもunestablishedなときだけのものため、unsubscribeCloseListenerというものが用意されている。establishできる分かったときにunsubscribeCloseListener()を呼び出し、上記の接続切れの処理をunsubscribeするようになっている()。establishしたときは、その時用のcloseしたときの処理を登録される。
論理的には、unsubscribeしなくてもまともに動いてくれる可能性はある。ただ、意味合いが変わったタイミング(unestablishからestablishへ)でもハンドラがずっと登録され続けていることを把握してプログラムを書く必要がある。その把握をし忘れて、今後予期せぬバグを作り込むはことは避けたかった。unsubscribeために少しコードは長くはなっている。だが、「ハンドラ登録の把握し続ける」と「少しコードが長くなる」のトレードオフであると思う。今の所このほうが保守しやすいと判断しているため、このコードのままにしておく。
書くの力尽きた感と、一番書きたかったこと部分をページに残せたので、とりあえずここ終わり。