Elixir IO內幕(一)讀操做

玩過Elixir的人也許注意有注意到,File.open(path)返回的不是什麼文件描述子(file descriptor,簡稱fd),也不是什麼文件句柄(handle),而是{:ok, pid}node

爲毛是pid?難道說打開文件的同時打開了一個新的進程?沒錯。之因此這樣作,官方給出的解釋是網絡

By modelling IO devices with processes, the Erlang VM allows different nodes in the same network to exchange file processes in order to read/write files in between nodes.app

看不懂英文的能夠看下面個人翻譯(非直譯):框架

把IO設備建模成進程,可使Erlang虛擬機在同一個網絡內的不一樣節點(主機)上交換文件進程,從而實現節點之間的相互讀寫。編輯器

但這樣一來,給咱們本身實現IO設備帶來了不小的麻煩。當執行讀操做的時候,文件進程接收到的消息是什麼?須要回覆給主進程什麼樣的消息?碰上EOF了怎麼處理?其餘異常呢?寫操做的時候又是什麼樣的消息機制呢?全部的這些都沒有文檔可查(我事先聲明我不會Erlang,因此請別讓我去查Erlang文檔。前陣子查了erlsom的文檔,差點沒讓我哭出來)。字體

沒文檔怎麼辦?Elixir又不像Ruby那樣可讓我猴子補丁一把。突然想到有個模塊叫StringIO,貌似據說是用Elixir寫的,因而去GitHub上啃了一下它的源代碼,啃完才知道什麼叫醍醐灌頂。今天比較晚了,因此就先說說讀操做吧。編碼

當主進程須要讀取IO的內容時,它會向文件進程發送一條如下幾種消息之一atom

{:io_request, sender_pid, reference, {:get_chars, prompt, chunk_size}}
{:io_request, sender_pid, reference, {:get_chars, encoding, prompt, chunk_size}}
{:io_request, sender_pid, reference, {:get_line, prompt}}
{:io_request, sender_pid, reference, {:get_line, encoding, prompt}}
{:io_request, sender_pid, reference, {:get_until, prompt, mod, func, args}}
{:io_request, sender_pid, reference, {:get_until, encoding, prompt, mod, func, args}}
  • 第1種消息對應IO.binread(device, n),其中n爲大於0的整數。
  • 第2種消息對應IO.read(device, n)IO.getn(device, n),IO設備實現者本身決定輸出什麼字符編碼的字符。
  • 第3種消息對應IO.binread(device, :line)
  • 第4種消息對應IO.read(device, :line)IO.gets(device)
  • 最後兩種暫時不知道對應什麼。

接下來講說消息的參數。上面列出的消息中,spa

  • sender_pid 是消息發送方的pid。
  • reference 是對消息發送方的一個引用(由於消息發送方可能不是一個進程,而是一個Port神馬的)。
  • encoding 是消息發送方指望的字符編碼,是一個atom,默認:latin1
  • prompt 是給消息接收方(文件進程)的提示信息,一般沒用,可是若是消息接收方是到的標準輸入流,則能夠在控制檯上把這個信息打印出來,提示操做者(人)能夠開始輸入文本了。
  • chunk_size 是一次應該讀取多少字節。你本身實現的文件進程能夠無視它,但一般都會尊重它。

不管哪一種消息,文件進程都應向消息發送方回覆下列三種消息之一(至少我在StringIO的源代碼裏沒發現第四種):.net

{:io_reply, reference, chunk}
{:io_reply, reference, :eof}
{:io_reply, reference, {:error, reason}}

第一種是在成功讀到數據時的回覆。其中reference是發送方發過來的那個引用(就是上面提到的那個),而chunk就是獲取到的數據片斷。

第二種是在沒有讀到數據,碰到文件結尾時的回覆。

第三種固然是讀取出錯時的回覆了。其中reason能夠是任何東西。

明白了這些,咱們就能夠實現本身的IO設備了(固然使用GenServer啦,除非你想自虐)。好比,Phoenix框架的基礎Plug.Conn並無實現IO的接口(也就是不能用IO.read這樣的方法來讀取HTTP請求內容),因而咱們就能夠給Conn來個包裝,包裝成IO的樣子(僅對應IO.binread(device, n)):

defmodule ConnIOWrapper do
  use GenServer

  def wrap(conn) do
    GenServer.start_link(__MODULE__, %{conn: conn, done: false})
  end

  def init(state) do
    {:ok, state}
  end

  def handle_info({:io_request, from, reply_as, {:get_chars, _, chunk_size}}, %{conn: conn, done: false} = state) do
    state = case Plug.Conn.read_body(conn, length: chunk_size) do
      {status, data, conn} when status in [:ok, :more] ->
        send(from, {:io_reply, reply_as, to_string(data)})
        %{conn: conn, done: status == :ok}
      {:error, _} = reply ->
        send(from, {:io_reply, reply_as, reply})
        %{state | done: true}
    end
    {:noreply, state}
  end

  def handle_info({:io_request, from, reply_as, {:get_chars, _, _}}, %{conn: conn, done: true} = state) do
    send(from, {:io_reply, reply_as, :eof})
    GenServer.cast(self, :close)
    {:noreply, state}
  end

  def handle_cast(:close, state) do
    {:stop, :normal, state}
  end
end

好吧這期就寫到這兒。欲知後事如何,且聽下回分解。(話說這破Markdown編輯器能不能用monospace字體啊?害得我對縮進對了半天,尚未Stack Overflow上那種Ctrl + K)

相關文章
相關標籤/搜索