檢視原始碼 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/0alloc/0free/1)與回呼函式(init/1handle_call/3handle_cast/2)位於同一個模組中。通常良好的程式設計慣例是將對應於一個行程的程式碼包含在單一模組中。

  • 第三個引數 [] 是一個詞彙,會原封不動地傳遞給回呼函式 init。在這裡,init 不需要任何輸入資料,並忽略此引數。

  • 第四個引數 [] 是一個選項列表。如需可用選項,請參閱 gen_server

如果名稱註冊成功,則新的 gen_server 行程會呼叫回呼函式 ch3:init([])init 預期會傳回 {ok, State},其中 Stategen_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).

ch3gen_server 的名稱,必須與用於啟動它的名稱一致。alloc 是實際的請求。

請求會被轉換為訊息,並傳送給 gen_server。收到請求後,gen_server 會呼叫 handle_call(Request, From, State),其預期會傳回一個元組 {reply,Reply,State1}Reply 是要傳回給客戶端的答覆,而 State1gen_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}).

ch3gen_server 的名稱。{free, Ch} 是實際的請求。

請求會被轉換為訊息,並傳送給 gen_servercast,因此 free,接著會傳回 ok

收到請求後,gen_server 會呼叫 handle_cast(Request, State),其預期會傳回一個元組 {noreply,State1}State1gen_server 狀態的新值。

handle_cast({free, Ch}, Chs) ->
    Chs2 = free(Ch, Chs),
    {noreply, Chs2}.

在這種情況下,新狀態是更新的可用通道清單 Chs2gen_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 指定這是正常終止,而 State1gen_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}.