マイペースなRailsおじさん

Ruby、Ruby on Rails、オブジェクト指向設計を主なテーマとして扱います。だんだん大きくなっていくRuby on Rails製プロダクトのメンテナンス性を損なわない方法を考えたり考えなかったりしている人のブログです。

Fiberとnio4rでサーバー

Fiberとnio4rを組み合わせてサーバーを作りました。あたまバグりそうになるのでよかったら見てってください。

Fiberとnio4rを組み合わせる

Fiberは、軽量なスレッドを作成できます。このスレッド上で、途中まで実行した処理を中断し、再開することができます。 この特徴をうまく使ってやると、nio4rのselectと組み合わせて効率的な通信処理を実現できます。

非常にややこしいので、注意してください。

クライアントとの通信

Fiber.newで、ブロックにとった処理を行うFiberを作成できます。作成した時点では処理は実行されません。Fiber#resumeを呼ぶと、Fiber.yieldまで処理を実行したあと、呼び出し元に戻ります。この時点で、Fiberは処理が「中断」された状態になります。このあと、再びFiber#resumeを呼ぶと、中断されたところから処理を再開します。

f = Fiber.new do
  puts 'Hello1'
  Fiber.yield
  puts 'Hello2'
end

f.resume # => Hello 1
f.resume # => Hello 2

ここで、nio4rのselectと組み合わせて効率的な通信を行うことを考えます。selectは複数のIOを待って、利用可能なものを選択できます。ブロッキングが起こりそうなときに処理を中断してselectし、またなくても良くなったら処理を再開するようにFiberを制御すると、Fiberはノンブロッキングに処理を実行できます。

コードにするとこういう感じです。 ioは、ノンブロッキング通信が可能な状態で渡されるソケットです。@selectorNIO::Selectorです。

Fiber.new do |io|
  message = io.read_nonblock 5000
  @selector.register io, :w
  Fiber.yield

  io.write_nonblock "response"
  io.close
end

ノンブロッキングでreadしたら、次は書き込みを行いたので、書き込みを待つようにselectorに登録します。こうなったらいったん処理を抜けます。 処理が再開されたら、それはすなわち書き込みができる状態ということなので、ソケットに書き込みます。

処理の振り分け

振り分ける側は、適切な(=ノンブロッキングで処理が可能な)fiberをresumeできるように、@fibersにioとfiberの組み合わせを持つようにします。 selectで適切なioがわかるので、ioをキーに、そのioを処理するfiberを値にもつハッシュを作ってやれば、どのfiberをresumeすべきかは簡単にわかります。

@srv = TCPServer.new 13_000
@selector.register @srv, :r

loop do
  @selector.select do |m|
    case m.io
    when @srv
      socket = m.io.accept_nonblock
      @fibers[socket] = communication_fiber
      @selector.register socket, :r
    else
      f = @fibers[m.io]
      @selector.deregister m.io
      f.resume m.io
    end
  end
end

これが非常にややこしいんだなぁ…

Fiberなサーバー

ソースコード全量がこちら。前回に引き続き、最低限のHTTPレスポンスを返すサーバーを作ってます

gistb6da8b93f525ccfdb150f160717fa786

ブラウザで表示したようす。受け取ったメッセージをそのまま返すので、WebブラウザのHTTPリクエストが書いてあります。

f:id:ytnk531:20201022010753p:plain