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/1complex1: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