ソケットとFiberを使ったChatプログラムの解説 with Ruby

## 目的

理解が足りていない`IO、プロセス、ソケット、スレッド`と、`Ruby`で使われる`Fiber`について調べているうちに、チャットプログラムのソースに出会いました。

コードが読めるようになり、解説もできるようになれば
理解が進み、かつ理解の証明にもなるはず。

- 理解するため
- 理解の証明とするため

以上が目的となります。

## 概要

どなたか知りませんが、今回、誠に勝手ながら、教材として頂きます。
[Github](https://gist.github.com/pfleidi/835268)


## 解説開始

プログラムを上から順に読んでいってもわかりづらいので、
実際に処理される順番で解説していきます。


`require 'rubygems'`
これはRubyにバンドルされているパッケージ管理ツール。


`include Socket::Constants`
ソケット操作の指定のための定数を定義したモジュール

---
```ruby
def initialize
    @reading = Array.new
    @writing = Array.new
    @clients = Hash.new
end
```
`ChatServerインスタンス`を作成時に、インスタンス変数を作成します。

---
```ruby
def start
    @server_socket = TCPServer.new('localhost', 4242)
    @reading.push(@server_socket)
    run_acceptor
  end
```
`localhost`のポート4242番を利用して`TCPServer`を作成し、`@server_socket`に`TCPServerインスタンス`を格納しています。
そして、作成した`TCPServerインスタンス`を`@reading`の配列に追加(`push`)

ここまではまあいいんですよ。
この後のソケットの入出力待ちとか、`Fiber`の挙動とかを把握するのに結構時間がかかりました。
メソッド少ないくせにようわからんかったなあ。
`IO`の挙動と`Fiber`の挙動が分かっていればすんなりいきそう。

---

ということで、`run_acceptor`中身を見ていきましょう。

```ruby
def run_acceptor
    puts "accepting on shared socket (localhost:4242)"
    loop do
      puts "current clients: #{@clients.length}"
      # 何かしらの入出力があるまではここで処理が止まります。
      readable, writable = IO.select(@reading, @writing)

      # 何かしらの入出力があったとき、配列で返ってくるのでeachしてます
      readable.each do |socket|
        if socket == @server_socket
          # 初めてアクセス要求があった時、socketにはTCPServerクラスのインスタンスが入っている
　　　　　　# なので、ここでクライアントが追加されることになります。
          add_client
        else
          # ① 一旦省略。後ほど解説。
        end 
      end
    end
  end
```

最初、`@reading`には`TCPServerクラスのインスタンス`だけが入っている。
`@writing`は便宜上設定しているだけ。ここで引数設定をしていないと、返り値が崩れる。
具体的には・・・ちょっと待って今考え中だから。

`IO.select`は入力or出力or例外の準備ができたもの（`TCPServerクラスのインスタンス`か、
もしくは`TCPSocketクラスのインスタンス`）を配列にして、配列の配列として返す。
初期起動時（`ruby /path/to/fiberchat.rb`をした時）は、`TCPServerクラスのインスタンス`が入ります。
クライアント側から接続要求があった時は`TCPServerクラスのインスタンス`が入ります。
データ送信がクライアント側から行われた時は`TCPSocketクラスのインスタンス`が入ります。
ここで気をつけるのは、`何かしらの入出力があるまではここで処理が止まる`ということ。

---
さて、ここから飛び飛びになっていきます。
まずは`add_client`から

```ruby
def add_client
    socket = @server_socket.accept_nonblock
    @reading.push(socket)

    @clients[socket] = Fiber.new do |message|
      loop {
        if message.nil?
        	# ② 後ほど
        else
        	# ③ 後ほど
        end
      }
    end
    puts "client #{socket} connected"
    return @clients[socket]
  end
```
---

```ruby
socket = @server_socket.accept_nonblock
@reading.push(socket)
```

`accept_nonblock`でソケットをノンブロッキングモード（複数のアクセスを受け付ける）にします。そして、クライアントからの接続要求を受け付け、接続した`TCPSocket`のインスタンスを返します。`TCPSocket`のインスタンスは`@reading`に追加されています。

---

```ruby
@clients[socket] = Fiber.new do |message| 
  ...
end
```
`@clients`（ハッシュ）の`key`を
接続した`TCPSocketのインスタンス`にして、その`value`には`Fiberインスタンス`を格納している。
格納時はブロックの中身は実行されません。
`Fiber`は`Threadより軽くて柔軟なクラス`って感じ。
ここで説明するには長くなるので別途調べて頂きたい！
覚えておいて欲しいのは、
- `Fiberクラスのインスタンス`のブロック内で`Fiber.yield`が行われたら途中で処理が止まるということ。返り値は`Fiber.yield`の引数か
- 再度呼び出した時に(`Fiber#resume`)処理が止まったところから処理が再開されるということ。

---

`add_client`がひとまずここで終了し、
`readable.each do |socket| ...`の処理が終わります。
そうしたらまた`run_acceptor`の`loop do`の処理が再開します。
つまり、`readable, writable = IO.select(@reading, @writing)`にまた返ってくるわけですね。

---
そして、接続されているクライアントからデータ通信が行われた時にどうなるか。

`readable, writable = IO.select(@reading, @writing)`が処理され、
`readable.each do |socket|`に移り、
`if socket == @server_socket`ではない`else`内の処理（①）が始まります。

---

```ruby
client = @clients[socket]
message = client.resume
```

まず`client`変数に`@clients`（ハッシュ）の`key`が`socket`（ここでは`TCPSocketクラスのインスタンス`）の`value`である`Fiberインスタンス`が格納されます。

④`message = client.resume`
この時引数は渡されていないので
if `message.nil?`内の処理（②）がスタート。

---
```ruby
chat = socket.gets
socket.flush
message = Fiber.yield(chat.strip) #⑤
```

[socket.gets](https://docs.ruby-lang.org/ja/latest/method/IO/i/gets.html)で送信されたデータを取得しています。
[socket.flush](https://docs.ruby-lang.org/ja/latest/method/IO/i/flush.html)で更新内容を反映しています。

`message = Fiber.yield(chat.strip)`ですが、
この時点では`message`に`Fiber.yield(chat.strip)`は格納されていません。
`Fiber.yield(chat.strip)`が評価された時点で一旦処理はストップします。（この処理が止まったタイミングを⑤とします）この`Fiber`クラスのインスタンスのブロックの処理、すなわち`client.resume`の返り値は`chat.strip`になります。
処理はストップされ、④に戻ります。

---

```ruby
puts "client #{socket} sent: #{message}"
broadcast(message)
```

`message = client.resume`の`message`には
先ほど処理された`chat.strip`が格納されています。
その`message`が`broadcast`メソッドに引数として渡されて処理が実行されます。

---

```ruby
def broadcast(message)
  @clients.each_pair do |key, value|
    puts "invoking client #{key}"
    value.resume(message) #⑥
  end
end
```
`each_pair`は`each`と同じです。
`@clients`には`TCPSocket`クラスのインスタンスを`key`としたハッシュが入っています。そして、`value`は`Fiber`クラスのインスタンスになります。
`value.resume(message)`を実行する（この時点を⑥とします。）ことで、引数が渡された状態で`Fiber`クラスのインスタンスのブロック処理が行われます。
ということで⑤に戻ります。

---
```ruby
loop {
  if message.nil?
    chat = socket.gets
    socket.flush
    message = Fiber.yield(chat.strip) #⑤
  else
    socket.puts("chat: #{message.strip}")
    socket.flush
    message = Fiber.yield
  end
}
```

⑤の処理が再開します。
`message`に`Fiber.yield(chat.strip)`が代入されます。
処理が終了し、また`loop`が走ります。
`message`には値が入っているので`else`内の処理が走ります。


`socket.puts("chat: #{message.strip}")`
ここでソケットを通してクライアントにデータが送られています。
[socket.puts](https://docs.ruby-lang.org/ja/latest/method/IO/i/puts.html)

`message = Fiber.yield`の時点でまた、代入処理は行われずにストップします。⑥に戻りましょう。

---
まあ⑥に戻っても特に何もないのですが。

```ruby
def broadcast(message)
  @clients.each_pair do |key, value|
    puts "invoking client #{key}"
    value.resume(message) #⑥
  end
end
```

`broadcast`の処理が終了し、
`readable.each do |socket|`の処理も終了し、
また`run_acceptor`内の`loop`の処理がスタートします。


---
これまでの流れを踏襲しつつも、ここだけは一応理解しておくべきなのですが、次に`message = client.resume`をした時、`Fiber`インスタンスの`loop`内の処理が走るのですが、始まるのは`else`内の`message = Fiber.yield`の代入処理からです。
これが終了し、また`loop`が走ります。
この時`message`には値が入っていないので・・・と、先ほど説明した処理がまた繰り返されます。


んー、`Fiber`の処理の把握が難しいですね。



[WIP]細かい情報。`Fiber`の返り値とか