自作サーバーにここで調査した内容を組み込もうと一週間作業していたが、 諸々の罠に嵌ってやっと動くようになった。
以下嵌ったポイントに関するメモ。
なぜEdgeTriggerモードのときはNonBlockingIOを使うべきか | tori29.jp
tori29.jp
AcceptはEAGAINを返すまで繰り返す必要がある
listener用のsocketをepollに登録しておくことで、 入力があった瞬間にacceptが可能になる。
この動作がLevelTriggerでは正常に動いていたのに、EdgeTriggerにした途端低い確立で入力を見逃すという事象が発生するようになった。
ログとにらめっこしつつ、GPT4に色々教えてもらいながら数時間戦ったところ無事解決した。
原因はepollのエッジと1つの入力が1:1で対応している訳ではなく、同時に入力があった場合には1つのエッジに対して複数の入力が存在しうるという点だった。
LevelTriggerであれば一度見逃しても次のepoll_waitで拾うことができるが、EdgeTriggerの場合にはそうはいかない。
以下のように疑似コードのようにacceptがEAGAINを返すまで、複数回acceptを呼び出す必要があった。
loop {
// syscall::acceptは成功すればOk(fd)を返すResult
let accept_fd = match syscall::accept(listener_fd, &mut addr) {
Ok(fd) => fd,
Err(MyError::SyscallError(libc::EAGAIN)) => {
break;
}
Err(e) => {
panic!("Error");
}
};
// accept後の処理をここに書く
}
closeとshutdownの違い
これは嵌ったポイントというよりは、 理解せず適当に作っていたので直した点。
ログを見ているとacceptで獲得しているファイルディスクリプタの番号が単調増加していて、 過去つかったファイルディスクリプタが使い回されている気配がまったくない。
何かがおかしい気がして調査していると、 shutdownとclose両方を適切に呼び出す必要があることを知った。
- shutdown これ以上の通信を禁止する。フラグによって受信禁止、 送信禁止、 両方禁止を制御できる。 ソケットをcloseするわけではないのでファイルディスクリプタは開放されない。
- close ファイルディスクリプタを開放する。これによりファイルディスクリプタの再利用が可能になる。
shutdownとcloseで全く意味合いが違うことがわかる。TCPはコネクションを確立するプロトコルなので、 shutdownでコネクションを切断してからcloseするのが親切ということのようだ。
一方でUDPはコネクションレスなのでいきなりcloseしても特に問題はなさそう。
TIME_WAITとSO_REUSEADDR
一度テスト用にサーバーを立ち上げるとしばらくbindできない問題が発生していた。
作業効率が落ちる以外の実害が発生していなかったので無視していたが原因が明らかになったので修正した。
socketはcloseした後、CLOSEな状態になる前にTIME_WAITという状態を挟む。
これはcloseした後すぐ同じファイルディスクリプタのsocketを使ってしまうと、 その前の通信の遅延パケットが到着して混線してしまうことがあるため、一定時間の猶予を設定しているということのようだ。
作ったサーバーなどを実運用する場合には必要な機能かも知れないが、 トライアンドエラーを繰り返したい開発環境だとこの機能は無効にしたい。
作成したlistnerソケットをbindする前にsetsockoptでSO_REUSEADDRフラグを建てることによって、 サーバーを立ち下げた後すぐに同じポートで立ち上げることができる。
まとめ
GPT4は新しいことを勉強する際に必須のツールになりつつある気がする。
あとちゃんとエラーコードなどは早目に環境整えたほうがいい。