檢視原始碼 gen_server 行為
建議您同時閱讀 STDLIB 中的 gen_server
部分。
客戶端-伺服器原則
客戶端-伺服器模型以中央伺服器和任意數量的客戶端為特徵。客戶端-伺服器模型用於資源管理操作,其中數個不同的客戶端想要共享一個通用資源。伺服器負責管理此資源。
---
title: Client Server Model
---
flowchart LR
client1((Client))
client2((Client))
client3((Client))
server((Server))
client1 --> server
server -.-> client1
client2 --> server
server -.-> client2
client3 --> server
server -.-> client3
subgraph Legend
direction LR
start1[ ] -->|Query| stop1[ ]
style start1 height:0px;
style stop1 height:0px;
start2[ ] -.->|Reply| stop2[ ]
style start2 height:0px;
style stop2 height:0px;
end
範例
在 概述 中提供了一個以純 Erlang 撰寫的簡單伺服器範例。可以使用 gen_server
重新實作伺服器,產生此回呼模組
-module(ch3).
-behaviour(gen_server).
-export([start_link/0]).
-export([alloc/0, free/1]).
-export([init/1, handle_call/3, handle_cast/2]).
start_link() ->
gen_server:start_link({local, ch3}, ch3, [], []).
alloc() ->
gen_server:call(ch3, alloc).
free(Ch) ->
gen_server:cast(ch3, {free, Ch}).
init(_Args) ->
{ok, channels()}.
handle_call(alloc, _From, Chs) ->
{Ch, Chs2} = alloc(Chs),
{reply, Ch, Chs2}.
handle_cast({free, Ch}, Chs) ->
Chs2 = free(Ch, Chs),
{noreply, Chs2}.
程式碼將在接下來的章節中說明。
啟動 Gen_Server
在前一節的範例中,gen_server
是透過呼叫 ch3:start_link()
來啟動的
start_link() ->
gen_server:start_link({local, ch3}, ch3, [], []) => {ok, Pid}
start_link/0
呼叫函式 gen_server:start_link/4
。此函式會產生並連結到一個新的行程,即 gen_server
。
第一個引數
{local, ch3}
指定名稱。然後gen_server
會在本機註冊為ch3
。如果省略名稱,則不會註冊
gen_server
。而是必須使用其 pid。名稱也可以給定為{global, Name}
,在這種情況下,會使用global:register_name/2
註冊gen_server
。第二個引數
ch3
是回呼模組的名稱,也就是回呼函式所在的模組。介面函式(
start_link/0
、alloc/0
和free/1
)與回呼函式(init/1
、handle_call/3
和handle_cast/2
)位於同一個模組中。通常良好的程式設計慣例是將對應於一個行程的程式碼包含在單一模組中。第三個引數
[]
是一個詞彙,會原封不動地傳遞給回呼函式init
。在這裡,init
不需要任何輸入資料,並忽略此引數。第四個引數
[]
是一個選項列表。如需可用選項,請參閱gen_server
。
如果名稱註冊成功,則新的 gen_server
行程會呼叫回呼函式 ch3:init([])
。init
預期會傳回 {ok, State}
,其中 State
是 gen_server
的內部狀態。在這種情況下,狀態是可用的通道。
init(_Args) ->
{ok, channels()}.
gen_server:start_link/4
是同步的。它會等到 gen_server
初始化完成並準備好接收請求後才會傳回。
如果 gen_server
是監督樹的一部分,也就是由監督者啟動的,則必須使用 gen_server:start_link/4
。還有另一個函式 gen_server:start/4
,可啟動不屬於監督樹的獨立 gen_server
。
同步請求 - 呼叫
同步請求 alloc()
是使用 gen_server:call/2
實作的
alloc() ->
gen_server:call(ch3, alloc).
ch3
是 gen_server
的名稱,必須與用於啟動它的名稱一致。alloc
是實際的請求。
請求會被轉換為訊息,並傳送給 gen_server
。收到請求後,gen_server
會呼叫 handle_call(Request, From, State)
,其預期會傳回一個元組 {reply,Reply,State1}
。Reply
是要傳回給客戶端的答覆,而 State1
是 gen_server
狀態的新值。
handle_call(alloc, _From, Chs) ->
{Ch, Chs2} = alloc(Chs),
{reply, Ch, Chs2}.
在這種情況下,答覆是配置的通道 Ch
,而新狀態是剩餘可用通道的集合 Chs2
。
因此,呼叫 ch3:alloc()
會傳回配置的通道 Ch
,然後 gen_server
會等待新的請求,現在具有更新的可用通道清單。
非同步請求 - 投射
非同步請求 free(Ch)
是使用 gen_server:cast/2
實作的
free(Ch) ->
gen_server:cast(ch3, {free, Ch}).
ch3
是 gen_server
的名稱。{free, Ch}
是實際的請求。
請求會被轉換為訊息,並傳送給 gen_server
。cast
,因此 free
,接著會傳回 ok
。
收到請求後,gen_server
會呼叫 handle_cast(Request, State)
,其預期會傳回一個元組 {noreply,State1}
。State1
是 gen_server
狀態的新值。
handle_cast({free, Ch}, Chs) ->
Chs2 = free(Ch, Chs),
{noreply, Chs2}.
在這種情況下,新狀態是更新的可用通道清單 Chs2
。gen_server
現在已準備好接受新的請求。
停止
在監督樹中
如果 gen_server
是監督樹的一部分,則不需要停止函式。gen_server
會由其監督者自動終止。具體做法由監督者中設定的關機策略定義。
如果需要在終止之前進行清理,則關機策略必須是逾時值,並且必須將 gen_server
設定為在函式 init
中捕捉結束訊號。當收到關機命令時,gen_server
接著會呼叫回呼函式 terminate(shutdown, State)
init(Args) ->
...,
process_flag(trap_exit, true),
...,
{ok, State}.
...
terminate(shutdown, State) ->
%% Code for cleaning up here
...
ok.
獨立 Gen_Servers
如果 gen_server
不屬於監督樹的一部分,則停止函式會很有用,例如
...
export([stop/0]).
...
stop() ->
gen_server:cast(ch3, stop).
...
handle_cast(stop, State) ->
{stop, normal, State};
handle_cast({free, Ch}, State) ->
...
...
terminate(normal, State) ->
ok.
處理 stop
請求的回呼函式會傳回一個元組 {stop,normal,State1}
,其中 normal
指定這是正常終止,而 State1
是 gen_server
狀態的新值。這會導致 gen_server
呼叫 terminate(normal, State1)
,然後正常終止。
處理其他訊息
如果 gen_server
要能夠接收請求以外的其他訊息,則必須實作回呼函式 handle_info(Info, State)
來處理它們。其他訊息的範例是結束訊息,如果 gen_server
已連結到監督者以外的其他行程,並且正在捕捉結束訊號。
handle_info({'EXIT', Pid, Reason}, State) ->
%% Code to handle exits here.
...
{noreply, State1}.
要實作的最後一個函式是 code_change/3
code_change(OldVsn, State, Extra) ->
%% Code to convert state (and more) during code change.
...
{ok, NewState}.