2. Erlang I / O协议 | 2. The Erlang I/O Protocol
2 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设备。
2.1议定书基础
正如Robert的论文所述,I / O服务器和客户端使用io_request
/ io_reply
元组进行通信,如下所示:
{io_request, From, ReplyAs, Request}
{io_reply, ReplyAs, Reply}
客户端发送io_request
元组发送到I/O服务器,服务器最终发送相应的io_reply
元组。
From
是pid()
对于客户端,I/O服务器发送I/O回复的进程。
ReplyAs
可以是任何数据并返回相应的数据io_reply
。所述io
模块监控的I / O服务器,并使用显示器参考作为ReplyAs
基准。一个更复杂的客户端可以对同一个I / O服务器有许多优秀的I / O请求,并且可以使用不同的引用(或其他)来区分传入的I / O响应。元素ReplyAs
被I / O服务器视为不透明。
注意,pid()
在元组中未显式显示I/O服务器的io_reply
答复可以从任何进程发送,而不一定是实际的I/O服务器。
Request
和Reply
在下面进行描述。当I / O服务器接收到一个io_request
元组时,它将作用于该元件Request
并最终发送一个io_reply
元组和对应的部分Reply
。2.2输出请求要输出I / O设备上的字符,Request
存在以下几种:{put_chars, Encoding, Characters} {put_chars, Encoding, Module, Function, Args}
- Encoding是unicode或latin1,意味着这些字符(如果是二进制文件)编码为UTF-8或ISO Latin-1(纯字节)。如果列表元素Encoding设置为> 255时,表现良好的I / O服务器也会返回错误指示latin1。
请注意,这并不能告诉我如何将字符放在I / O设备上或由I / O服务器处理。不同的I / O服务器可以处理它们想要的字符,这只能告诉I / O服务器预期数据的格式。在Module
/ Function
/ Args
情况下,Encoding
讲述了指定函数产生的格式。
还请注意,面向字节的数据是使用ISO拉丁语1编码发送的最简单的数据。
Characters
是要放在I / O设备上的数据。如果Encoding
是latin1
,这是一个iolist()
。如果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
模块通常返回它“按原样”。对于后向兼容性,以下Request
s为也通过一个I / O服务器处理(它们是不被二郎/ OTP R15B后存在):{PUT_CHARS,人物} {PUT_CHARS,模块,函数,参数数量}这些是作为表现{put_chars, latin1, Characters}
和{put_chars, latin1, Module, Function, Args}
,respectively.2.3输入Request
sTo读取从I / O设备的字符,以下Request
小号存在:{get_until,编码,提示,模块,功能,ExtraArgs}
Encoding
表示如何将数据发送回客户端以及将数据发送给由Module
/Function
/ 表示的函数ExtraArgs
。如果提供的功能将数据作为列表返回,则数据将转换为此编码。如果提供的函数以其他格式返回数据,则不能进行转换,并且由客户端提供的函数以正确的方式返回数据。
如果编码为latin1,则在可能的情况下,将整数列表0..255或包含普通字节的二进制文件发送回客户端。 如果Encoding是unicode,则整个Unicode范围中的整数或以UTF-8编码的二进制文件的列表被发送到客户端。 用户提供的函数总是会看到整数列表,而不是二进制文件,但如果编码是unicode,则列表可以包含大于255的数字。
Prompt
是一个字符列表(不是混合的,没有二进制文件),或者是作为I / O设备输入提示输出的原子。Prompt
经常被I / O服务器忽略; 如果设置为''
,它总是被忽略(并导致没有写入I / O设备)。
Module
,Function
与ExtraArgs
表示一个函数和参数来确定何时写入足够的数据。该函数需要两个参数,最后一个状态和一个字符列表。该功能是返回以下之一:
{已完成,结果,RESTCHARS}{更多,继续}
Result
可以是任何Erlang术语,但如果它是a list()
,I / O服务器可以在将其binary()
返回给客户端之前将其转换为适当的格式,前提是I / O服务器设置为二进制模式(请参见下文)。
调用该函数时,将使用I/O服务器在其I/O设备上找到的数据调用该函数,并返回以下内容之一:
- `{done, Result, RestChars}` when enough data is read. In this case `Result` is sent to the client and `RestChars` is kept in the I/O server as a buffer for later input.
- `{more, Continuation}`, which indicates that more characters are needed to complete the request.
Continuation
在有更多字符可用时,在稍后调用该函数时作为状态发送。当没有更多的字符可用时,该函数必须返回{done, eof, Rest}
。初始状态是空列表。在IO设备上达到文件结尾的数据是原子eof
。
模拟get_line
请求可以使用以下功能实现%28无效%29:
-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]
)中的最后一个元素被添加到参数列表中。该功能将被apply(Module, Function, [ State, Data | ExtraArgs ])
I / O服务器调用。
使用以下方法请求一个固定数目的字符Request
*
{get_chars, Encoding, Prompt, N}
Encoding
和Prompt
作为get_until
。
N
从I/O设备读取的字符数。
单行(如前例)请求如下Request
:
{get_line, Encoding, Prompt}
- 编码和提示的get_until.Clearly,get_chars和get_line可以实现与get_until请求(事实上他们最初),但效率的要求已经使这些添加必要。I / O服务器回复给客户端一个io_reply元组 ,其中元素Reply是以下之一:Data EOF {error, Error}
Data
是以列表或二进制形式读取的字符(取决于I / O服务器模式,请参阅下一节)。
eof
在达到输入结束时返回并且没有更多数据可用于客户端进程。
Error
向客户描述错误,客户可以根据需要做任何事情。该io
模块通常按原样返回。
为了向后兼容,以下Request
s也要由I / O服务器处理(它们在Erlang / OTP R15B之后不会存在):
{get_until, Prompt, Module, Function, ExtraArgs}
{get_chars, Prompt, N}
{get_line, Prompt}
这些都表现为{get_until, latin1, Prompt, Module, Function, ExtraArgs}
,{get_chars, latin1, Prompt, N}
,和{get_line, latin1, Prompt}
。
2.4 I/O服务器模式
从I / O服务器读取数据时,要求效率不仅会增加get_line和get_chars请求,还会增加I / O服务器选项的概念。 没有必须实现的选项,但Erlang标准库中的所有I / O服务器都遵守二进制选项,这使得io_reply元组中的元素Data可以是二进制而不是列表。 如果数据是以二进制形式发送的,则Unicode数据以标准的Erlang Unicode格式发送,即UTF-8(请注意,无论I / O服务器模式如何,get_until请求的功能仍会获取列表数据)。
请注意,该get_until
请求允许将数据指定为始终为列表的函数。而且,来自这种函数的返回值数据可以是任何类型的(当io:fread/2,3
请求被发送到I / O服务器时的确如此)。客户必须为收到的数据做好准备,以便以各种形式回复这些请求。但是,I / O服务器将尽可能将结果转换为二进制文件(也就是说,提供的函数get_until
返回列表时)。这在部分的例子中完成An Annotated and Working Example I/O Server
。
二进制模式下的I / O服务器会影响发送到客户端的数据,因此它必须能够处理二进制数据。为了方便起见,可以使用以下I / O请求设置和检索I / O服务器的模式:
{setopts, Opts}
Opts
是由proplists
模块(和I / O服务器)识别格式的选项列表。例如,交互式shell(ingroup.erl
)的I / O服务器理解以下选项:{binary
,boolean()} (或二进制/列表){回波,布尔()} {expand
_fun,有趣()} {编码,统一字符编码/ LATIN1}(或Unicode / latin1的)选项binary
和encoding
是常见的用于OTP所有I / O服务器,而echo
与expand
有效仅适用于此I / O服务器。选项unicode
通知如何将字符放置在物理I / O设备上,也就是说,如果终端本身具有Unicode识别功能。它不会影响在I / O协议中如何发送字符,其中每个请求都包含所提供或返回的数据的编码信息.I / O服务器将发送以下内容之一作为Reply
:ok {error,Error}enotsup
如果I / O服务器不支持该选项,则会出现错误(最好是)(例如,如果echo
在setopts
对纯文件的请求中发送选项)。要检索选项,请执行以下操作请求被使用:getoptsThis请求要求I / O服务器支持的所有选项的完整列表以及它们的当前值.I / O服务器回复:OptList {error,Error}
OptList
是一个元组列表{Option, Value}
,其中Option
总是一个原子。
2.5 多个I / O请求
该Request
元素可以在本身含有许多Request
使用以下格式S:
{requests, Requests}
Requests
是有效的列表。io_request
协议的元组。必须按照它们出现在列表中的顺序执行它们。执行将继续进行,直到其中一个请求导致错误或列表被使用为止。最后一个请求的结果被发送回客户端。对于请求列表,I/O服务器可以根据列表中的请求在答复中发送下列任何有效结果:ok {ok, Data} {ok, Options} {error, Error}2.6可选I/O请求以下I/O请求是可选实现的,客户端将为错误返回做好准备:{get_geometry, Geometry}
Geometry
是原子rows
或原子columns
。
I / O服务器将发送Reply
作为:
{ok, N}
{error, Error}
N
如果适用于I/O服务器处理的I/O设备,则为I/O设备所具有的字符行或列数。{error, enotsup}
是个很好的答案。
2.7未实现的请求类型
如果I / O服务器遇到无法识别的请求(即io_request
元组具有预期的格式,但Request
未知),则I / O服务器将发送包含错误元组的有效答复:
{error, request}
这使得扩展带有可选请求的协议成为可能,并且客户端可以向后兼容。
2.8一个说明和工作示例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
)在一个单独的循环函数中处理,一个接一个地应用列表中的请求,并返回最后的结果。
我们需要处理向后兼容性和file
模块(它使用旧的请求,直到与R13之前的节点不再需要向后兼容)。请注意如果file:write/2
不添加以下内容,则I / O服务器无法正常工作:
request{put_chars,Chars}, State) ->
request{put_chars,latin1,Chars}, State
request{put_chars,M,F,As}, State) ->
request{put_chars,latin1,M,F,As}, State
request{get_chars,Prompt,N}, State) ->
request{get_chars,latin1,Prompt,N}, State
request{get_line,Prompt}, State) ->
request{get_line,latin1,Prompt}, State
request{get_until, Prompt,M,F,As}, State) ->
request{get_until,latin1,Prompt,M,F,As}, State
{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.
这里我们还处理可以通过请求设置的模式(binary
或list
)setopts
。默认情况下,所有OTP I / O服务器都将数据作为列表发送回客户端,但binary
如果I / O服务器以适当的方式处理它,则切换模式可以提高效率。由于get_until
提供的函数被定义为将列表作为参数,但是get_chars
并且get_line
可以针对二进制模式进行优化,所以实现难以高效。但是,这个例子并没有优化任何东西。
根据设置的选项,返回的数据是正确的类型很重要。因此,如果可能,我们会
在返回之前以正确的编码将列表转换为二进制文件。在get_until
请求元组中提供的函数可以作为其最终结果返回任何内容,所以只有返回列表的函数才能将它们转换为二进制文件。如果请求包含编码标记unicode
,则列表可以包含所有的Unicode代码点,并且二进制文件将处于UTF-8格式。如果编码标签是latin1
,客户端只能获取范围内的字符0..255
。check/2
如果编码被指定为,函数会处理不会返回列表中的任意Unicode代码点latin1
。如果函数没有返回列表,则不能执行检查,结果是所提供函数的结果不变。
为了操作表,我们实现了以下实用函数:
check(unicode, List) ->
List;
check(latin1, List) ->
try
[ throw(not_unicode) || X <- List,
X > 255 ],
List
catch
throw:_ ->
{error,{cannot_convert, unicode, latin1}}
end.
如果客户端请求,如果Unicode代码点大于255,则函数检查会提供一个错误元组latin1
。
这两个函数是辅助函数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服务器一样简单。