GenServer(通用伺服器)是一種行為,將 Elixir 行程之間常見的用戶端與伺服器互動抽象化。
還記得我們在學行程時看過的接收迴圈嗎?GenServer行為提供了抽象機制,讓你實作這類迴圈,也讓你與執行這種迴圈的行程交換訊息。它讓保存狀態和執行非同步程式碼變得更容易。
要注意,GenServer 這個名稱承載的意義不只一種。它也用來描述_使用_ GenServer 行為的_模組_,以及從_使用_ GenServer 行為的模組啟動的_行程_。
GenServer行為定義了一個必要的回呼 init/1,以及幾個值得留意的選用回呼:handle_call/3、handle_cast/2 和 handle_info/3。使用 GenServer 的_用戶端_不應該直接呼叫這些回呼。GenServer模組提供了一些函式,讓用戶端可以用來與 GenServer 行程溝通。
通常,一個模組會同時定義_用戶端 API_ 和_伺服器回呼實作_。前者是一組函式,讓 Elixir 應用程式的其他部分能呼叫,以便與這個 GenServer 行程溝通;後者則包含這個 GenServer 的邏輯。
我們先來看一個簡單的 GenServer 範例,然後再了解每個回呼的意義吧。
這是一個範例伺服器,能回應煩人乘客在長途公路旅行中反覆的追問,更精確地說,就是「我們到了嗎?」這個問題。它會記錄這個問題被問了多少次,並回覆愈來愈不耐煩的答案。
defmodule AnnoyingPassengerAutoresponder do
use GenServer
# Client API
def start_link(init_arg) do
GenServer.start_link(__MODULE__, init_arg)
end
def are_we_there_yet?(pid) do
GenServer.call(pid, :are_we_there_yet?)
end
# Server callbacks
@impl GenServer
def init(_init_arg) do
# the initial count of questions asked is always 0
state = 0
{:ok, state}
end
@impl GenServer
def handle_call(:are_we_there_yet?, _from, state) do
reply =
cond do
state <= 3 -> "No."
state <= 10 -> "I told you #{state} times already. No."
true -> "..."
end
# increase the count of questions asked
new_state = state + 1
# reply to the caller
{:reply, reply, new_state}
end
end
init/1伺服器可以透過呼叫 GenServer.start/3 或 GenServer.start_link/3 來啟動。我們在連結這個概念中介紹過這兩個函式的差異。
這兩個函式:
GenServer 行為的模組。init_arg。正如名稱所示,這個引數會傳給 init/1 回呼。呼叫 GenServer.start/3 或 GenServer.start_link/3 來啟動伺服器時,會以阻塞的方式呼叫 init/1 回呼。init/1 的回傳值決定了伺服器是否能成功啟動。
init/1 回呼通常會回傳下列其中一個值:
{:ok, state}。伺服器會以 state 作為初始狀態,開始執行接收迴圈。state 可以是任何型別。{:stop, reason}。reason 可以是任何型別。伺服器不會開始執行接收迴圈。行程會以給定的原因結束。還有一些更進階的可能性,我們現在不會涵蓋。
如果伺服器的接收迴圈順利啟動,GenServer.start/3 和 GenServer.start_link/3 會回傳 {:ok, pid} 元組。否則會回傳 {:error, reason}
handle_call/3需要回覆的訊息可以透過 GenServer.call/2 傳送給伺服器行程。這個函式的第一個引數是執行中伺服器行程的 pid,第二個引數是訊息。訊息可以是任何型別。
handle_call/3 回呼負責處理同步訊息並做出回應。它會收到三個引數:
message:傳給 GenServer.call/2 第二個引數的值。from:呼叫 GenServer.call/2 的行程 pid。這個引數通常可以忽略。state:伺服器目前的狀態。還記得它的初始值是在 init/1 回呼中設定的。handle_call/3 回呼通常會回傳一個三元素的元組 {:reply, reply, state}。這表示元組中的第二個元素 reply(可以是任何型別)會回傳給呼叫方。元組中的第三個元素 state,是伺服器處理完這則訊息後的新狀態。
還有一些更進階的可能性,我們現在不會涵蓋。
想靠名稱記住這個回呼的功能, 就想像你打電話「呼叫」某人。
如果對方有空,你馬上就會收到回覆(同步地)。
handle_cast/2不需要回覆的訊息可以透過 GenServer.cast/2 傳送給伺服器行程。它的引數和 GenServer.call/2 相同。
handle_cast/2 回呼負責處理這些訊息。它會收到兩個引數 message 和 state,和 handle_call/3 回呼的引數相同(差別在於沒有 from)。
handle_cast/2 回呼通常會回傳一個二元素的元組 {:noreply, state}。
還有一些更進階的可能性,我們現在不會涵蓋。
想靠名稱記住這個回呼的功能, 記得「to cast」也有「丟擲」的意思。
如果你把一封瓶中信丟進海裡, 就不會期待馬上收到回覆, 或者根本永遠不會收到。
call 還是 cast?幾乎都應該使用 call,即使你的用戶端程式碼不需要伺服器的回覆。
使用 call 會等待回覆,這可作為一種背壓機制(避免用戶端一次送出太多訊息)。收到伺服器的回覆,也是唯一能確定伺服器已收到並處理用戶端訊息的方法。
handle_info/2訊息也可能透過呼叫 GenServer.call/2 或 GenServer.cast/2 以外的方式進入伺服器的信箱,例如直接呼叫 send/2 函式。
要處理這類訊息,請使用 handle_info/2 回呼。這個回呼的運作方式和 handle_cast/2 完全相同。
GenServer行為提供了 handle_info/2 的通用實作,會記錄非預期訊息的錯誤。如果你覆寫了那個預設實作,請務必自己加上能攔截所有訊息的實作。否則一旦伺服器收到非預期的訊息,就會崩潰。
上面四個回呼的回傳值,都可以再加上一個元組元素:逾時。例如,init/1 可以不回傳 {:ok, state},而是回傳 {:ok, state, timeout}。
逾時可以用來偵測信箱中在某段時間內都沒有訊息。如果伺服器從某個回呼回傳逾時,而且在指定的毫秒數內都沒有訊息送達,就會呼叫 handle_info/2,並以 :timeout 作為第一個引數。