檢視原始碼 Erlang I/O 協定
Erlang 中的 I/O 協定使得客戶端與伺服器之間能夠進行雙向通訊。
- I/O 伺服器是一個處理請求並在例如 I/O 裝置上執行請求任務的程序。
- 客戶端是任何希望從 I/O 裝置讀取或寫入資料的 Erlang 程序。
常見的 I/O 協定自 OTP 開始就存在,但一直沒有文件記錄,並且多年來不斷演進。在 Robert Virding 的基本原理的附錄中,描述了原始的 I/O 協定。本節描述了目前的 I/O 協定。
原始的 I/O 協定簡單且彈性。對記憶體效率和執行時間效率的需求,多年來觸發了對協定的擴展,使得協定比原始的協定更大且更難以實作。當然可以說目前的協定太複雜了,但本節描述的是它今天的樣子,而不是它應該是什麼樣子。
原始協定的基本概念仍然成立。I/O 伺服器和客戶端使用一個相當簡單的協定進行通訊,並且客戶端中永遠不會存在伺服器狀態。任何 I/O 伺服器都可以與任何客戶端程式碼一起使用,並且客戶端程式碼不需要知道 I/O 伺服器與之通訊的 I/O 裝置。
協定基礎
如 Robert 的論文中所述,I/O 伺服器和客戶端使用 io_request
/io_reply
元組進行通訊,如下所示:
{io_request, From, ReplyAs, Request}
{io_reply, ReplyAs, Reply}
客戶端向 I/O 伺服器發送一個 io_request
元組,伺服器最終發送一個相應的 io_reply
元組。
From
是客戶端的pid/0
,也就是 I/O 伺服器向其發送 I/O 回覆的程序。ReplyAs
可以是任何資料,並在相應的io_reply
中返回。io
模組監控 I/O 伺服器,並使用監控參考作為ReplyAs
資料。更複雜的客戶端可以向同一個 I/O 伺服器發出許多未完成的 I/O 請求,並且可以使用不同的參考(或其他東西)來區分傳入的 I/O 回覆。I/O 伺服器應將元素ReplyAs
視為不透明。請注意,I/O 伺服器的
pid/0
並未明確出現在元組io_reply
中。回覆可以從任何程序發送,而不一定是實際的 I/O 伺服器。Request
和Reply
描述如下。
當 I/O 伺服器收到 io_request
元組時,它會根據 Request
部分執行操作,並最終發送具有相應 Reply
部分的 io_reply
元組。
輸出請求
要在 I/O 裝置上輸出字元,存在以下 Request
:
{put_chars, Encoding, Characters}
{put_chars, Encoding, Module, Function, Args}
Encoding
是unicode
或latin1
,表示字元(如果是二進制資料)編碼為 UTF-8 或 ISO Latin-1(純位元組)。如果列表元素包含大於 255 的整數,且Encoding
設定為latin1
,一個運作正常的 I/O 伺服器也應返回錯誤指示。請注意,這並未以任何方式說明如何在 I/O 裝置上放置字元或由 I/O 伺服器處理字元。不同的 I/O 伺服器可以以任何想要的方式處理字元,這僅告訴 I/O 伺服器預期資料的格式。在
Module
/Function
/Args
的情況下,Encoding
表示指定函式產生的格式。另請注意,面向位元組的資料使用 ISO Latin-1 編碼發送最簡單。
Characters
是要放置在 I/O 裝置上的資料。如果Encoding
是latin1
,則這是一個iolist/0
。如果Encoding
是unicode
,則這是 Erlang 標準混合 Unicode 列表(每個字元在列表中為一個整數,二進制資料中的字元表示為 UTF-8)。Module
、Function
和Args
表示呼叫以產生資料的函式(例如io_lib:format/2
)。Args
是該函式的參數列表。該函式將以指定的Encoding
產生資料。I/O 伺服器應呼叫該函式,如apply(Mod, Func, Args)
,並將返回的資料放置在 I/O 裝置上,就像它是在{put_chars, Encoding, Characters}
請求中傳送的一樣。如果該函式返回二進制資料或列表以外的任何內容,或拋出異常,則應將錯誤發送回客戶端。
I/O 伺服器使用 io_reply
元組回覆客戶端,其中元素 Reply
是以下其中之一:
ok
{error, Error}
Error
向客戶端描述錯誤,客戶端可以隨意處理它。io
模組通常「按原樣」返回它。
輸入請求
要從 I/O 裝置讀取字元,存在以下 Request
:
{get_until, Encoding, Prompt, Module, Function, ExtraArgs}
Encoding
表示如何將資料發回客戶端,以及將哪些資料發送到由Module
/Function
/ExtraArgs
表示的函式。如果提供的函式返回的資料為列表,則資料將轉換為此編碼。如果提供的函式以其他格式返回資料,則無法進行轉換,並且由客戶端提供的函式以正確的方式返回資料。如果
Encoding
是latin1
,則盡可能將整數0..255
的列表或包含純位元組的二進制資料發回客戶端。如果Encoding
是unicode
,則將包含整個 Unicode 範圍內整數的列表或以 UTF-8 編碼的二進制資料發送給客戶端。使用者提供的函式始終看到整數列表,而不是二進制資料,但如果Encoding
是unicode
,則列表可以包含大於 255 的數字。Prompt
是字元列表(非混合,沒有二進制資料)或一個原子,將作為 I/O 裝置上輸入的提示輸出。Prompt
通常被 I/O 伺服器忽略;如果設定為''
,則始終將其忽略(並且不會寫入任何內容到 I/O 裝置)。Module
、Function
和ExtraArgs
表示一個函式和參數,以確定何時寫入了足夠的資料。該函式應採用兩個參數,最後的狀態和字元列表。該函式應返回以下其中之一:{done, Result, RestChars} {more, Continuation}
Result
可以是任何 Erlang 術語,但如果它是list/0
,則如果 I/O 伺服器設定為二進制模式(請參閱下文),則 I/O 伺服器可以在將其返回給客戶端之前將其轉換為適當格式的binary/0
。該函式會使用 I/O 伺服器在其 I/O 裝置上找到的資料進行呼叫,並返回以下其中之一:
{done, Result, RestChars}
當讀取足夠的資料時。在這種情況下,Result
會被發送到客戶端,並且RestChars
會保留在 I/O 伺服器中作為稍後輸入的緩衝區。{more, Continuation}
,表示需要更多字元才能完成請求。
Continuation
會在有更多字元可用時作為稍後呼叫該函式的狀態發送。當沒有更多字元可用時,該函式必須返回{done, eof, Rest}
。初始狀態為空列表。當 I/O 裝置達到檔案結尾時的資料為原子eof
。使用以下函式可以(低效地)實作
get_line
請求的模擬:-module(demo). -export([until_newline/3, get_line/1]). until_newline(_ThisFar,eof,_MyStopCharacter) -> {done,eof,[]}; until_newline(ThisFar,CharList,MyStopCharacter) -> case lists:splitwith(fun(X) -> X =/= MyStopCharacter end, CharList) of {L,[]} -> {more,ThisFar++L}; {L2,[MyStopCharacter|Rest]} -> {done,ThisFar++L2++[MyStopCharacter],Rest} end. get_line(IoServer) -> IoServer ! {io_request, self(), IoServer, {get_until, unicode, '', ?MODULE, until_newline, [$\n]}}, receive {io_reply, IoServer, Data} -> Data end.
請注意,呼叫函式時,
Request
元組中的最後一個元素([$\n]
)會附加到參數列表中。該函式將由 I/O 伺服器呼叫,如apply(Module, Function, [ State, Data | ExtraArgs ])
。
使用以下 Request
請求固定數量的字元:
{get_chars, Encoding, Prompt, N}
Encoding
和Prompt
與get_until
相同。N
是要從 I/O 裝置讀取的字元數。
使用以下 Request
請求單行(如先前範例中所示):
{get_line, Encoding, Prompt}
Encoding
和Prompt
與get_until
相同。
顯然,get_chars
和 get_line
可以使用 get_until
請求實作(實際上它們最初就是這樣實作的),但是對效率的需求使得這些新增功能成為必要。
I/O 伺服器使用 io_reply
元組回覆客戶端,其中元素 Reply
是以下其中之一:
Data
eof
{error, Error}
Data
是讀取的字元,以列表或二進制資料形式(取決於 I/O 伺服器模式,請參閱下一節)。- 當達到輸入結束且沒有更多資料可供客戶端程序使用時,將返回
eof
。 Error
向客戶端描述錯誤,客戶端可以隨意處理它。io
模組通常按原樣返回它。
I/O 伺服器模式
從 I/O 伺服器讀取資料時,對效率的需求不僅導致了 get_line
和 get_chars
請求的增加,還增加了 I/O 伺服器選項的概念。沒有強制要求實作任何選項,但 Erlang 標準函式庫中的所有 I/O 伺服器都支援 binary
選項,這允許 io_reply
元組的元素 Data
在可能的情況下為二進位制,而不是列表。如果資料以二進位制形式傳送,Unicode 資料將以標準 Erlang Unicode 格式(即 UTF-8)傳送(請注意,無論 I/O 伺服器模式如何,get_until
請求的功能仍然會取得列表資料)。
請注意,get_until
請求允許使用一個函式,其中指定的資料始終為列表。此外,此類函式的傳回值資料可以是任何類型(當向 I/O 伺服器傳送 io:fread/2,3
請求時確實是這種情況)。客戶端必須準備好接收以各種形式作為這些請求的答案的資料。但是,I/O 伺服器應在可能的情況下將結果轉換為二進位制(也就是說,當提供給 get_until
的函式傳回列表時)。這在帶註解和可運作的 I/O 伺服器範例一節的範例中說明。
二進位制模式下的 I/O 伺服器會影響傳送給客戶端的資料,因此客戶端必須能夠處理二進位制資料。為了方便起見,可以使用以下 I/O 請求設定和檢索 I/O 伺服器的模式
{setopts, Opts}
Opts
是proplists
模組(以及 I/O 伺服器)識別的格式的選項列表。
例如,互動式 shell 的 I/O 伺服器(在 group.erl
中)理解以下選項
{binary, boolean()} (or binary/list)
{echo, boolean()}
{expand_fun, fun()}
{encoding, unicode/latin1} (or unicode/latin1)
選項 binary
和 encoding
對於 OTP 中的所有 I/O 伺服器都是通用的,而 echo
和 expand
僅對此 I/O 伺服器有效。選項 unicode
通知字元如何在實體 I/O 裝置上放置,也就是說,終端本身是否支援 Unicode。它不會影響字元在 I/O 協定中的傳送方式,其中每個請求都包含所提供或傳回資料的編碼資訊。
I/O 伺服器將以下其中之一作為 Reply
傳送
ok
{error, Error}
如果 I/O 伺服器不支援該選項(例如,如果向純文字檔案的 setopts
請求中傳送了 echo
選項),則預期會出現錯誤(最好是 enotsup
)。
要檢索選項,請使用以下請求
getopts
此請求會要求 I/O 伺服器支援的所有選項的完整列表以及其目前值。
I/O 伺服器會回覆
OptList
{error, Error}
OptList
是元組{Option, Value}
的列表,其中Option
始終是原子。
多個 I/O 請求
Request
元素本身可以使用以下格式包含多個 Request
{requests, Requests}
Requests
是協定的有效io_request
元組的列表。它們必須按照列表中出現的順序執行。執行將繼續,直到其中一個請求導致錯誤或列表被消耗完。最後一個請求的結果將傳回給客戶端。
對於請求列表,I/O 伺服器可以根據列表中的請求,在回覆中傳送以下任何有效結果
ok
{ok, Data}
{ok, Options}
{error, Error}
選用的 I/O 請求
以下 I/O 請求是選用的,客戶端應準備好接收錯誤傳回
{get_geometry, Geometry}
Geometry
是原子rows
或原子columns
。
I/O 伺服器將以下其中之一作為 Reply
傳送
N
{error, Error}
N
是 I/O 裝置具有的字元行數或列數,如果適用於 I/O 伺服器處理的 I/O 裝置,否則{error, enotsup}
是一個好的答案。
未實作的請求類型
如果 I/O 伺服器遇到無法識別的請求(也就是說,io_request
元組具有預期的格式,但 Request
未知),I/O 伺服器應傳送帶有錯誤元組的有效回覆
{error, request}
這樣可以擴充使用選用請求的協定,並讓客戶端在某種程度上向後相容。
帶註解和可運作的 I/O 伺服器範例
I/O 伺服器是任何能夠處理 I/O 協定的程序。沒有通用的 I/O 伺服器行為,但可能會有。該框架很簡單,一個處理傳入請求的程序,通常是 I/O 請求和其他 I/O 裝置特定的請求(定位、關閉等)。
範例 I/O 伺服器將字元儲存在 ETS 表格中,組成一個相當粗略的 RAM 檔案。
該模組以常用的指令開始,一個啟動 I/O 伺服器的函式和一個處理請求的主迴圈
-module(ets_io_server).
-export([start_link/0, init/0, loop/1, until_newline/3, until_enough/3]).
-define(CHARS_PER_REC, 10).
-record(state, {
table,
position, % absolute
mode % binary | list
}).
start_link() ->
spawn_link(?MODULE,init,[]).
init() ->
Table = ets:new(noname,[ordered_set]),
?MODULE:loop(#state{table = Table, position = 0, mode=list}).
loop(State) ->
receive
{io_request, From, ReplyAs, Request} ->
case request(Request,State) of
{Tag, Reply, NewState} when Tag =:= ok; Tag =:= error ->
reply(From, ReplyAs, Reply),
?MODULE:loop(NewState);
{stop, Reply, _NewState} ->
reply(From, ReplyAs, Reply),
exit(Reply)
end;
%% Private message
{From, rewind} ->
From ! {self(), ok},
?MODULE:loop(State#state{position = 0});
_Unknown ->
?MODULE:loop(State)
end.
主迴圈接收來自客戶端的消息(客戶端可以使用 io
模組傳送請求)。對於每個請求,都會呼叫函式 request/2
,並最終使用函式 reply/3
傳送回覆。
"私有"消息 {From, rewind}
會將偽檔案中的目前位置重設為 0
("檔案"的開頭)。這是一個典型的 I/O 裝置特定消息,不屬於 I/O 協定的範例。通常將此類私有消息嵌入 io_request
元組中是一個壞主意,因為這會讓讀者感到困惑。
首先,我們檢查回覆函式
reply(From, ReplyAs, Reply) ->
From ! {io_reply, ReplyAs, Reply}.
它會將 io_reply
元組傳回給客戶端,並提供請求中收到的元素 ReplyAs
以及請求的結果,如前所述。
我們需要處理一些請求。首先是寫入字元的請求
request({put_chars, Encoding, Chars}, State) ->
put_chars(unicode:characters_to_list(Chars,Encoding),State);
request({put_chars, Encoding, Module, Function, Args}, State) ->
try
request({put_chars, Encoding, apply(Module, Function, Args)}, State)
catch
_:_ ->
{error, {error,Function}, State}
end;
Encoding
說明請求中的字元如何表示。我們希望將字元以列表形式儲存在 ETS 表格中,因此我們使用函式 unicode:characters_to_list/2
將它們轉換為列表。轉換函式可以方便地接受編碼類型 unicode
和 latin1
,因此我們可以直接使用 Encoding
。
當提供 Module
、Function
和 Arguments
時,我們會套用它,並對結果執行相同的操作,就像直接提供資料一樣。
我們處理檢索資料的請求
request({get_until, Encoding, _Prompt, M, F, As}, State) ->
get_until(Encoding, M, F, As, State);
request({get_chars, Encoding, _Prompt, N}, State) ->
%% To simplify the code, get_chars is implemented using get_until
get_until(Encoding, ?MODULE, until_enough, [N], State);
request({get_line, Encoding, _Prompt}, State) ->
%% To simplify the code, get_line is implemented using get_until
get_until(Encoding, ?MODULE, until_newline, [$\n], State);
在這裡,我們作弊了一下,或多或少只實作了 get_until
,並使用內部輔助程式來實作 get_chars
和 get_line
。在生產程式碼中,這可能效率不高,但這取決於不同請求的頻率。在我們開始實作函式 put_chars/2
和 get_until/5
之前,我們先檢查剩下的幾個請求
request({get_geometry,_}, State) ->
{error, {error,enotsup}, State};
request({setopts, Opts}, State) ->
setopts(Opts, State);
request(getopts, State) ->
getopts(State);
request({requests, Reqs}, State) ->
multi_request(Reqs, {ok, ok, State});
請求 get_geometry
對於此 I/O 伺服器沒有意義,因此回覆為 {error, enotsup}
。我們處理的唯一選項是 binary
/list
,這是在單獨的函式中完成的。
多重請求標籤 (requests
) 在單獨的迴圈函式中處理,該函式會依序套用列表中的請求,並傳回最後一個結果。
如果無法識別請求,則必須傳回 {error, request}
request(_Other, State) ->
{error, {error, request}, State}.
接下來,我們處理不同的請求,首先是相當通用的多重請求類型
multi_request([R|Rs], {ok, _Res, State}) ->
multi_request(Rs, request(R, State));
multi_request([_|_], Error) ->
Error;
multi_request([], Result) ->
Result.
我們一次迴圈遍歷一個請求,當我們遇到錯誤或列表耗盡時停止。最後一個傳回值會傳回給客戶端(它首先傳回到主迴圈,然後由函式 io_reply
傳回)。
請求 getopts
和 setopts
也很容易處理。我們只會變更或讀取狀態記錄
setopts(Opts0,State) ->
Opts = proplists:unfold(
proplists:substitute_negations(
[{list,binary}],
Opts0)),
case check_valid_opts(Opts) of
true ->
case proplists:get_value(binary, Opts) of
true ->
{ok,ok,State#state{mode=binary}};
false ->
{ok,ok,State#state{mode=binary}};
_ ->
{ok,ok,State}
end;
false ->
{error,{error,enotsup},State}
end.
check_valid_opts([]) ->
true;
check_valid_opts([{binary,Bool}|T]) when is_boolean(Bool) ->
check_valid_opts(T);
check_valid_opts(_) ->
false.
getopts(#state{mode=M} = S) ->
{ok,[{binary, case M of
binary ->
true;
_ ->
false
end}],S}.
按照慣例,所有 I/O 伺服器都處理 {setopts, [binary]}
、{setopts, [list]}
和 {setopts,[{binary, boolean()}]}
,因此使用了 proplists:substitute_negations/2
和 proplists:unfold/1
的技巧。如果傳送給我們的選項無效,我們會將 {error, enotsup}
傳回給客戶端。
請求 getopts
應傳回 {Option, Value}
元組的列表。這具有雙重功能,既提供此 I/O 伺服器的目前值,又提供可用選項。我們只有一個選項,因此傳回該選項。
到目前為止,此 I/O 伺服器相當通用(除了主迴圈中處理的請求 rewind
和建立 ETS 表格之外)。大多數 I/O 伺服器都包含類似於此伺服器的程式碼。
為了讓範例可執行,我們開始實作將資料讀取和寫入 ETS 表格的功能。首先是函式 put_chars/3
put_chars(Chars, #state{table = T, position = P} = State) ->
R = P div ?CHARS_PER_REC,
C = P rem ?CHARS_PER_REC,
[ apply_update(T,U) || U <- split_data(Chars, R, C) ],
{ok, ok, State#state{position = (P + length(Chars))}}.
我們已經將資料作為(Unicode)列表,因此只需將列表分割成預定義大小的執行次數,並將每次執行放在表格中的目前位置(並向前)。函式 split_data/3
和 apply_update/2
在下面實作。
現在我們要從表格中讀取資料。函式 get_until/5
會讀取資料並套用函式,直到函式表示已完成。結果會傳回給客戶端
get_until(Encoding, Mod, Func, As,
#state{position = P, mode = M, table = T} = State) ->
case get_loop(Mod,Func,As,T,P,[]) of
{done,Data,_,NewP} when is_binary(Data); is_list(Data) ->
if
M =:= binary ->
{ok,
unicode:characters_to_binary(Data, unicode, Encoding),
State#state{position = NewP}};
true ->
case check(Encoding,
unicode:characters_to_list(Data, unicode))
of
{error, _} = E ->
{error, E, State};
List ->
{ok, List,
State#state{position = NewP}}
end
end;
{done,Data,_,NewP} ->
{ok, Data, State#state{position = NewP}};
Error ->
{error, Error, State}
end.
get_loop(M,F,A,T,P,C) ->
{NewP,L} = get(P,T),
case catch apply(M,F,[C,L|A]) of
{done, List, Rest} ->
{done, List, [], NewP - length(Rest)};
{more, NewC} ->
get_loop(M,F,A,T,NewP,NewC);
_ ->
{error,F}
end.
在這裡,我們還處理可以使用請求 setopts
設定的模式(binary
或 list
)。預設情況下,所有 OTP I/O 伺服器都會將資料以列表形式傳回給客戶端,但如果 I/O 伺服器以適當的方式處理,則將模式切換為 binary
可以提高效率。get_until
的實作很難提高效率,因為提供的函式定義為將列表作為引數,但 get_chars
和 get_line
可以針對二進位制模式進行最佳化。但是,此範例不會最佳化任何內容。
重要的是,回傳的資料類型必須根據設定的選項而正確。因此,我們會在回傳前,盡可能將列表轉換為具有正確編碼的二進制資料。在 get_until
請求元組中提供的函數,其最終結果可以回傳任何內容,因此只有回傳列表的函數才能將其轉換為二進制資料。如果請求包含編碼標籤 unicode
,則列表可以包含所有 Unicode 碼位,而二進制資料應為 UTF-8 編碼。如果編碼標籤為 latin1
,則用戶端只會收到範圍在 0..255
的字元。如果編碼指定為 latin1
,函數 check/2
會負責不回傳列表中任意的 Unicode 碼位。如果函數不回傳列表,則無法執行檢查,且結果將會是所提供函數的原始輸出,不做任何變更。
為了操作表格,我們實作了以下實用函數
check(unicode, List) ->
List;
check(latin1, List) ->
try
[ throw(not_unicode) || X <- List,
X > 255 ],
List
catch
throw:_ ->
{error,{cannot_convert, unicode, latin1}}
end.
如果用戶端請求 latin1
,但函數回傳了 Unicode 碼位 > 255 的字元,則函數 check 會提供錯誤元組。
函數 until_newline/3
和 until_enough/3
是輔助函數,與函數 get_until/5
一起使用,以實現 get_chars
和 get_line
(效率較低)。
until_newline([],eof,_MyStopCharacter) ->
{done,eof,[]};
until_newline(ThisFar,eof,_MyStopCharacter) ->
{done,ThisFar,[]};
until_newline(ThisFar,CharList,MyStopCharacter) ->
case
lists:splitwith(fun(X) -> X =/= MyStopCharacter end, CharList)
of
{L,[]} ->
{more,ThisFar++L};
{L2,[MyStopCharacter|Rest]} ->
{done,ThisFar++L2++[MyStopCharacter],Rest}
end.
until_enough([],eof,_N) ->
{done,eof,[]};
until_enough(ThisFar,eof,_N) ->
{done,ThisFar,[]};
until_enough(ThisFar,CharList,N)
when length(ThisFar) + length(CharList) >= N ->
{Res,Rest} = my_split(N,ThisFar ++ CharList, []),
{done,Res,Rest};
until_enough(ThisFar,CharList,_N) ->
{more,ThisFar++CharList}.
如您所見,上述函數正是要在 get_until
請求中提供的函數類型。
要完成 I/O 伺服器,我們只需要以適當的方式讀寫表格
get(P,Tab) ->
R = P div ?CHARS_PER_REC,
C = P rem ?CHARS_PER_REC,
case ets:lookup(Tab,R) of
[] ->
{P,eof};
[{R,List}] ->
case my_split(C,List,[]) of
{_,[]} ->
{P+length(List),eof};
{_,Data} ->
{P+length(Data),Data}
end
end.
my_split(0,Left,Acc) ->
{lists:reverse(Acc),Left};
my_split(_,[],Acc) ->
{lists:reverse(Acc),[]};
my_split(N,[H|T],Acc) ->
my_split(N-1,T,[H|Acc]).
split_data([],_,_) ->
[];
split_data(Chars, Row, Col) ->
{This,Left} = my_split(?CHARS_PER_REC - Col, Chars, []),
[ {Row, Col, This} | split_data(Left, Row + 1, 0) ].
apply_update(Table, {Row, Col, List}) ->
case ets:lookup(Table,Row) of
[] ->
ets:insert(Table,{Row, lists:duplicate(Col,0) ++ List});
[{Row, OldData}] ->
{Part1,_} = my_split(Col,OldData,[]),
{_,Part2} = my_split(Col+length(List),OldData,[]),
ets:insert(Table,{Row, Part1 ++ List ++ Part2})
end.
表格是以 ?CHARS_PER_REC
大小的區塊讀寫,必要時會覆寫。實作顯然效率不高,但它能正常運作。
本範例到此結束。它完全可執行,您可以使用例如 io
模組或甚至 file
模組來讀寫 I/O 伺服器。在 Erlang 中實作一個功能完善的 I/O 伺服器就是這麼簡單。