4.端口 | 4. Ports
4 端口
本节概述了如何previous section
通过使用端口来解决示例问题的示例。
该场景如下图所示:
图4.1:端口通信
4.1 Erlang程序
Erlang和C之间的所有通信都必须通过创建端口来建立。创建端口的Erlang进程被称为端口的连接进程
。所有来往于端口的通信都必须经过连接过程。如果连接的进程终止,端口也会终止(即使是正确编写了外部程序,外部程序也会终止)。
通过调用BIFopen_port/2
来创建端口,{spawn,ExtPrg}
作为第一个参数。该字符串ExtPrg
是外部程序的名称,包括任何命令行参数。第二个参数是一个选项列表,在本例中只有{packet,2}
。该选项表示将使用2个字节的长度指示符来简化C和Erlang之间的通信。Erlang端口会自动添加长度指示符,但这必须在外部C程序中明确完成。
该过程还设置为陷阱出口,可以检测外部程序的故障:
-module(complex1).
-export([start/1, init/1]).
start(ExtPrg) ->
spawn(?MODULE, init, [ExtPrg]).
init(ExtPrg) ->
register(complex, self()),
process_flag(trap_exit, true),
Port = open_port{spawn, ExtPrg}, [{packet, 2}]),
loop(Port).
接下来可以实现complex1:foo/1
和complex1:bar/1
函数了。这两个函数都向complex
进程发送消息并收到以下回复:
foo(X) ->
call_port{foo, X}).
bar(Y) ->
call_port{bar, Y}).
call_port(Msg) ->
complex ! {call, self(), Msg},
receive
{complex, Result} ->
Result
end.
complex
过程执行以下操作:
- 将消息编码为一系列字节。
- 将其发送到端口。
- 等待回复。
- 解码答复。
- 将其发回给调用者:
loop(Port) ->
receive
{call, Caller, Msg} ->
Port ! {self(), {command, encode(Msg)}},
receive
{Port, {data, Data}} ->
Caller ! {complex, decode(Data)}
end,
loop(Port)
end.
假设C函数的参数和结果都小于256,则采用简单的编码/解码方案。在这个方案中,foo
由字节1 bar
表示,由2表示,并且参数/结果也由单个字节表示:
encode{foo, X}) -> [1, X];
encode{bar, Y}) -> [2, Y].
decode([Int]) -> Int.
生成的Erlang程序包括停止端口和检测端口故障的功能如下:
-module(complex1).
-export([start/1, stop/0, init/1]).
-export([foo/1, bar/1]).
start(ExtPrg) ->
spawn(?MODULE, init, [ExtPrg]).
stop() ->
complex ! stop.
foo(X) ->
call_port{foo, X}).
bar(Y) ->
call_port{bar, Y}).
call_port(Msg) ->
complex ! {call, self(), Msg},
receive
{complex, Result} ->
Result
end.
init(ExtPrg) ->
register(complex, self()),
process_flag(trap_exit, true),
Port = open_port{spawn, ExtPrg}, [{packet, 2}]),
loop(Port).
loop(Port) ->
receive
{call, Caller, Msg} ->
Port ! {self(), {command, encode(Msg)}},
receive
{Port, {data, Data}} ->
Caller ! {complex, decode(Data)}
end,
loop(Port
stop ->
Port ! {self(), close},
receive
{Port, closed} ->
exit(normal)
end;
{'EXIT', Port, Reason} ->
exit(port_terminated)
end.
encode{foo, X}) -> [1, X];
encode{bar, Y}) -> [2, Y].
decode([Int]) -> Int.
4.2 C程序
在C方面,有必要编写用于接收和发送来自/来自Erlang的2个字节长度指示符的数据的函数。默认情况下,C程序将从标准输入(文件描述符0)读取并写入标准输出(文件描述符1)。这些功能的示例,read_cmd/1
并且write_cmd/2
,如下:
/* erl_comm.c */
typedef unsigned char byte;
read_cmd(byte *buf)
{
int len;
if (read_exact(buf, 2) != 2)
return(-1
len = (buf[0] << 8) | buf[1];
return read_exact(buf, len
}
write_cmd(byte *buf, int len)
{
byte li;
li = (len >> 8) & 0xff;
write_exact(&li, 1
li = len & 0xff;
write_exact(&li, 1
return write_exact(buf, len
}
read_exact(byte *buf, int len)
{
int i, got=0;
do {
if ((i = read(0, buf+got, len-got)) <= 0)
return(i
got += i;
} while (got<len
return(len
}
write_exact(byte *buf, int len)
{
int i, wrote = 0;
do {
if ((i = write(1, buf+wrote, len-wrote)) <= 0)
return (i
wrote += i;
} while (wrote<len
return (len
}
请注意,stdin
并且stdout
是用于缓冲输入/输出,并且必须不
被用于与二郎的通信。
在main
函数中,C程序将监听来自Erlang的消息,并根据所选的编码/解码方案,使用第一个字节来确定要调用哪个函数,并将第二个字节作为该函数的参数。然后调用该函数的结果将被发送回Erlang:
/* port.c */
typedef unsigned char byte;
int main() {
int fn, arg, res;
byte buf[100];
while (read_cmd(buf) > 0) {
fn = buf[0];
arg = buf[1];
if (fn == 1) {
res = foo(arg
} else if (fn == 2) {
res = bar(arg
}
buf[0] = res;
write_cmd(buf, 1
}
}
注意C程序在while
-loop中,检查返回值read_cmd/1
。这是因为C程序必须检测端口何时关闭和终止。
4.3运行示例
第一步。
编译C代码:
unix> gcc -o extprg complex.c erl_comm.c port.c
第二步。
启动Erlang并编译Erlang代码:
unix> erl
Erlang (BEAM) emulator version 4.9.1.2
Eshell V4.9.1.2 (abort with ^G)
1> c(complex1).
{ok,complex1}
第三步。
运行示例:
2> complex1:start("extprg").
<0.34.0>
3> complex1:foo(3).
4
4> complex1:bar(5).
10
5> complex1:stop().
stop