PumaのコードからみるソケットへのノンブロッキングI/O

### これは何？
ソケットにおけるノンブロッキングI/OについてPumaのコードを絡めて調べてみたまとめです。
PumaはRubyで書かれたOSSのWebサーバーです。
ソケットに限らずノンブロッキングI/Oという言葉をよく聞きます。ですがイマイチなにを意味しているのかわからなかったので調べてみました。

### ブロッキングI/Oとは
ユーザーモードからシステムコールをカーネルに発行し、結果が帰ってくるまでに待ちが発生するI/O処理のことです。ファイル全般に言えることであり、ソケットもファイルなので当然ブロッキング発生します。
ソケットにおけるブロッキングI/Oは以下のようなものがあります。

- `accept(2)`でlistenキューからソケットを取り出すときに接続が確立されていない場合（TCPハンドシェイク中など）は確立されるまでブロックされる。
- ソケットに対して`read(2)`や`write(2)`を実行するとソケットバッファにデータが送信し終わるまでブロックされる。


### ノンブロッキングI/Oとは
ブロッキングが起きるのはシステムコールを発行した後「ソケットの準備ができるまで待つ」ことが原因です。そこでノンブロッキングI/Oではこの「待つ」ということをせずシステムコールを発行して準備ができていなければエラーとともにプロセスに制御を直ちに返してもらいます。これにより「ソケットの準備ができるまで待つ」時間がなくなります。
とはいっても何度も成功するかわからないノンブロッキングI/Oを試すのは大変です。事前にI/Oの準備ができているかを確認しておきたいです。
このために生まれた手法が**I/Oの多重化**です。

### I/Oの多重化

複数のファイルディスクリプタ（プロセスとファイルの接続）をチェックして準備完了になったファイルディスクリプタを返すことです。ソケットもファイルなのでこの方法を使うことができます。
Rubyでは`IO.select`を用いて`select(2)`を発行することでソケットを複数チェックし、listen中のソケットなら接続が確立しているかどうか、確立されていればソケットバッファにデータが到着しているかどうかをあらかじめ確認できます。
I/Oの多重化を行うことで複数のソケットから準備ができているものだけに限定できるので、何度もノンブロッキングI/Oを試してエラーを発生させるという事をしなくてすむようになります。


### 実際のPumaのコード
抜粋して載せています。

#### ノンブロッキングで`accept(2)`しているコード

https://github.com/puma/puma/blob/6baa4d8e1c88f2e4db2918df48416a5c49feec40/lib/puma/server.rb#L381

```ruby
while @status == :run
  begin
    ios = IO.select sockets #1
    ios.first.each do |sock|
      if sock == check
        break if handle_check
      else
        begin
          if io = sock.accept_nonblock #2
            client = Client.new io, @binder.env(sock)
```
`#1`では`IO.select`を使って`select(2)`を発行しています。複数のソケットをチェックし接続の確立を確認しています。

`#2`では`IO.accept_nonblock`を使ってノンブロッキングな`accept(2)`を行っています。`#1`でI/Oの多重化を行って接続が確立しているソケットを確認しているので、ソケットがとじてしまっている他はエラーは帰ってこない…ハズ。


#### ノンブロッキングで`read(2)`しているコード
Pumaでは`Reactor`というクラスでリクエストをモニターしてselectを行っています。[^1]

https://github.com/puma/puma/blob/70e381d853f09520b198d17523676409010a9a49/lib/puma/reactor.rb#L130

```ruby
def run_internal
  monitors = @monitors
  selector = @selector

  while true
    begin
      ready = selector.select @sleep_for #3
```
`#3`での`selector`は[nio4r](https://github.com/socketry/nio4r)というライブラリの`NIO::Selector`という`select(2)`周りを請け負うクラスのインスタンスです。複数のソケットを内部でモニターしています。

https://github.com/socketry/nio4r/blob/7992293db8f069963ebce170fbf6d34e48e9230e/lib/nio/selector.rb#L92

```ruby
# NIO::Selector#selectの内部
ready_readers, ready_writers = Kernel.select(readers, writers, [], timeout)
```

実際にノンブロッキングでの`read(2)`を行っているコードは以下です。
https://github.com/puma/puma/blob/6a39d41094823c8929f4a31613a2f6a53804997f/lib/puma/client.rb#L344

```ruby
  begin
    chunk = @io.read_nonblock(want) #ノンブロッキングI/O
  rescue Errno::EAGAIN
    return false
  rescue SystemCallError, IOError
    raise ConnectionError, "Connection error detected during read"
  end
```

このように`accept(2)`や`read(2)`どちらのケースでもI/Oの多重化とノンブロッキングI/Oが行われていることがわかります。

### 参考
https://github.com/puma/puma
[nio4r Getting Started](https://github.com/socketry/nio4r/wiki/Getting-Started)
[Working With TCP Sockets](https://www.amazon.co.jp/dp/B00BPYT6PK/ref=cm_sw_r_tw_dp_U_x_Ee77Db093BP9B)
[Rustで始めるネットワークプログラミング](https://www.amazon.co.jp/dp/B07SW2GXVF/ref=cm_sw_r_tw_dp_U_x_Ef77DbNMZWXMF)
[Rubyで学ぶWebサーバーアーキテクチャ（Preforking, ThreadPool, イベント駆動モデル）
](https://qiita.com/nullnull/items/49e7c7e7dec13fafbee4#puma)

[^1]: Reactorは他にもHTTPヘッダーとボディが全て到着したかどうかのチェックも行っています。リクエストが処理可能になるとtodoとして処理を待つキューに入れられます。https://github.com/puma/puma/blob/master/docs/architecture.md