蒼時弦也
蒼時弦也
資深軟體工程師
發表於

淺談 Ruby 的 Fiber(八)

到這篇為止,我們已經完成了將 Fiber 應用在程式中的基本雛型,現在只需要將上週未完成的錯誤處理,我們就能獲得一個可以正常發送訊息到伺服器的伺服器。

觀察

經過幾次的測試,我們會發現原本預期沒有正常運作的 @fibers.delete(io) 實際上是有在執行的,但是在使用者離開時,我們還在等待「讀取」所以就會觸發 end of file reached (EOFError) 這個錯誤,也就是使用者離開的瞬間,我們可以讀取。但是讀取到的是終止的訊息。

針對這個問題,我們只需要增加 rescue EOFError 讓他不要產生錯誤,就可以順利解決。

不過以邏輯上來說,我們更這種由我們掌控的機制,不應該是一種例外,所以採取 exception: false 的做法也許是一個不錯的選擇。

重構

既然我們已經了解完整的運作原理,但是程式碼依舊還是處於混亂的狀態。因此我們最好先進行一次重構會比較適合。

 1class Selector
 2  def initialize
 3    @readers = {}
 4  end
 5
 6  def wait_readable(io)
 7    Fiber.new do
 8      @readers[io] = Fiber.current
 9      Fiber.yield
10      yield
11    end.resume
12  end
13
14  def resume
15    readable, = IO.select(@readers.keys)
16    readable.each do |io|
17      io = @readers.delete(io)
18      io.resume
19    end
20  end
21end

Selector 的部分我們沒有做太多的修改,為了配合後面會有寫入的行為,我們先把原本的 @fibers 修改為 @readers 來對應。

 1class Client
 2  def initialize(selector, server, socket)
 3    @socket = socket
 4    @selector = selector
 5    @server = server
 6    @buffer = ''
 7  end
 8
 9  def listen
10    buffer = @socket.read_nonblock(1, exception: false)
11    case buffer
12    when :wait_readable then wait
13    when nil then close
14    else
15      @buffer << buffer
16      show_message if @buffer.include?("\n")
17      listen
18    end
19  end
20
21  def wait
22    @selector.wait_readable(@socket) do
23      listen
24    end
25  end
26
27  def show_message
28    puts last_message while @buffer.include?("\n")
29  end
30
31  def last_message
32    (_, @buffer = @buffer.to_s.split("\n", 2)).first
33  end
34
35  def close
36    @server.close(self)
37  end
38end

這次增加了 Client 來針對客戶端的部分處理,當我們收到訊息時會不斷重試直到有 \n 符號出現,並且將它顯示出來。

 1class Server < TCPServer
 2  def initialize(port)
 3    super port
 4    @selector = Selector.new
 5    @clients = []
 6    async_accept
 7  end
 8
 9  def async_accept
10    socket = accept_nonblock(exception: false)
11    case socket
12    when :wait_readable then wait_accept
13    else
14      client = (@clients.push Client.new(@selector, self, socket)).last
15      client.listen
16      async_accept
17    end
18  end
19
20  def wait_accept
21    @selector.wait_readable(self) do
22      async_accept
23    end
24  end
25
26  def close(client)
27    @clients.delete(client)
28  end
29
30  def start
31    loop do
32      @selector.resume
33    end
34  end
35end
36
37server = Server.new 3000
38server.start

最後伺服器部分跟客戶端的部分採取類似的邏輯,不過我們將大部分的行為封裝進去,統一進行處理。

解析

這次比較特別的有兩個部分,第一個是伺服器的 @clients 陣列,會用來記錄在線上上的使用者,這會讓我們之後在實作發送聊天訊息的功能上方便不少。

另一個則是我們利用 Ruby 的 Block 特性,將產生 Fiber 的工作交給 Selector 來負責,如此一來其他部分的程式碼除了需要利用一些遞迴的特性之外,就不會看到 Fiber 也更崇義理解。

小結

Fiber 在思考上跟我們以往習慣的方式不太一樣,不過隨著將程式碼整理之後,實際上會發現並沒有那麼複雜,但是需要特別注意程式的執行時機點可能會被稍微的改變。

下一篇文章我們可以嘗試加入廣播訊息的機制,讓這個 TCP 聊天室完成他的實作。