端口

端口提供了与外部世界通讯的基本机制。用Erlang编写的应用程序往往需要与Erlang系统之外的对象交互。还有一些现存的软件包,例如窗口系统、数据库系统,或是使用C、Modula2等其他语言的程序,在使用它们构建复杂系统时,也往往需要给它们提供Erlang接口。

从程序员的视角来看,我们希望能够以处理普通Erlang程序的方式来处理Erlang系统外的所有活动。为了创造这样的效果,我们需要将Erlang系统外的对象伪装成普通的Erlang进程。端口(Port),一种为Erlang系统和外部世界提供面向字节的通讯信道的抽象设施,就是为此而设计的。

执行open_port(PortName,PortSettings)可以创建一个端口,其行为与进程类似。执行open_port的进程称为该端口的连接进程。需要发送给端口的消息都应发送至连接进程。外部对象可以通过向与之关联的端口写入字节序列的方式向Erlang系统发送消息,端口将给连接进程发送一条包含该字节序列的消息。

系统中的任意进程都可以与一个端口建立链接,端口和Erlang进程间的EXIT信号导致的行为与普通进程的情况完全一致。端口只理解三种消息:

  1. Port ! {PidC, {command, Data}}
  2. Port ! {PidC, {connect, Data}}
  3. Port ! {PidC, close}
PidC必须是一个连接进程的Pid。这些消息的含义如下:{command,Data}
Data描述的字节序列发送给外部对象。Data可以是单个二进制对象,也可以是一个元素为0..255范围内的整数的非扁平列表[2]。没有响应。
close
关闭端口。端口将向连接进程回复一条{Port, closed}消息。
{connect,Pid1}
将端口的连接进程换位Pid1。端口将向先前的连接进程发送一条{Port, connected}消息。

此外,连接进程还可以通过以下方式接收数据消息:

  1. receive
  2. {Port, {data, Data}} ->
  3. ... an external object has sent data to Erlang ...
  4. ...
  5. end

在这一节中,我们将描述两个使用端口的程序:第一个是在Erlang工作空间内部的Erlang进程;第二个是在Erlang外部执行的C程序。

打开端口

打开端口时可以进行多种设置。BIF open_port(PortName,PortSettings可用于打开端口。PortName可以是:

{spawn,Command}
启动名为Command外部程序或驱动。Erlang驱动在附录E中有所描述。若没有找到名为Command的驱动,则将在Erlang工作空间的外部运行名为Command的外部程序。
Atom
Atom将被认作是外部资源的名称。这样将在Erlang系统和由该原子式命名的资源之间建立一条透明的连接。连接的行为取决于资源的类型。如果Atom表示一个文件,则一条包含文件全部内容的消息会被发送给Erlang系统;向该端口写入发送消息便可向文件写入数据。
{fd,In,Out}
令Erlang进程得以访问任意由Erlang打开的文件描述符。文件描述符In可作为标准输入而Out可作为标准输出。该功能很少使用:只有Erlang操作系统的几种服务(shelluser)需要使用。注意该功能与仅限于UNIX系统。
PortSettings是端口设置的列表。有效的设置有:{packet,N}
消息的长度将以大端字节序附在消息内容之前的N个字节内。N的有效取值为124
stream
输出的消息不附带消息长度──Erlang进程和外部对象间必须使用某种私有协议。
use_stdio
仅对{spawn, Command}形式的端口有效。令产生的(UNIX)进程使用标准输入输出(即文件标识符01)与Erlang通讯。
nouse_stdio
与上述相反。使用文件描述符34与Erlang通讯。
in
端口仅用于输入。
out
端口仅用于输出。
binary
端口为二进制端口(后续将详述)。
eof
到达文件末尾后端口不会关闭并发送'EXIT'信号,而是保持打开状态并向端口的连接进程发送一条{Port, eof}消息,之后连接进程仍可向端口输出数据。

除了{spawn,Command}类型的端口默认使用usestdio外,所有_类型的端口默认都使用stream

Erlang进程眼中的端口

程序9.2定义了一个简单的Erlang进程,该进程打开一个端口并向该端口发送一串消息。与端口相连的外部对象会处理并回复这些消息。一段时间之后进程将关闭端口。

程序9.2

  1. -module(demo_server).
  2. -export([start/0]).
  3.  
  4. start() ->
  5. Port = open_port({spawn, demo_server}, [{packet, 2}]),
  6. Port ! {self(), {command, [1,2,3,4,5]}},
  7. Port ! {self(), {command}, [10,1,2,3,4,5]},
  8. Port ! {self(), {command, "echo"}},
  9. Port ! {self(), {command, "abc"}},
  10. read_replies(Port).
  11.  
  12. read_replies(Port) ->
  13. receive
  14. {Port, Any} ->
  15. io:format('erlang received from port:~w~n', [Any]),
  16. read_replies(Port)
  17. after 2000 ->
  18. Port ! {self(), close},
  19. receive
  20. {Port, closed} ->
  21. true
  22. end
  23. end.

程序9.2中的open_port(PortName,PortSettings启动了一个外部程序。demo_server是即将运行的程序的名字。

表达式Port!{self(),{command,[1,2,3,4,5]}}向外部程序发送了五个字节(值为1、2、3、4、5)。

为了让事情有意思一点,我们令外部程序具备一下功能:

  • 若程序收到字符串“echo”,则它会向Erlang回复“ohce”。
  • 若程序收到的数据块的第一个字节是10,则它会将除第一个字节以外的所有字节翻倍后返回。
  • 忽略其他数据。运行该程序后我们得到以下结果:
  1. > demo_server:start().
  2. erlang received from port:{data,[10,2,4,6,8,10]}
  3. erlang received from port:{data,[111,104,99,101]}
  4. true

外部进程眼中的端口

程序9.3

  1. 1 2 3 4 5 6 7 8 91011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586878889909192939495
  1. / demo_server.c /#include <stdio.h>#include <string.h>/ Message data are all unsigned bytes /typedef unsigned char byte;main(argc, argv)int argc;char *argv;{ int len; int i; char progname; byte buf[1000]; progname = argv[0]; / Save start name of program / fprintf(stderr, "demo_server in C Starting \n"); while ((len = read_cmd(buf)) > 0){ if(strncmp(buf, "echo", 4) == 0) write_cmd("ohce", 4); else if(buf[0] == 10){ for(i=1; i < len ; i++) buf[i] = 2 buf[i]; write_cmd(buf, len); } }}/ Read the 2 length bytes (MSB first), then the data. /read_cmd(buf)byte buf;{ int len; if (read_exact(buf, 2) != 2) return(-1); len = (buf[0] << 8) | buf[1]; return read_exact(buf, len);}/ Pack the 2 bytes length (MSB first) and send it /write_cmd(buf, len)byte buf;int len;{ byte str[2]; put_int16(len, str); if (write_exact(str, 2) != 2) return(-1); return write_exact(buf, len);}/ [read|write]_exact are used since they may return BEFORE all bytes have been transmitted
  2. /read_exact(buf, len)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(buf, len)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);}put_int16(i, s)byte s;{
  3. s = (i >> 8) & 0xff; s[1] = i & 0xff;}

程序9.3通过表达式len=read_cmd(buf)读取发送至Erlang端口的字节序列,并用write_cmd(buf,len)将数据发回Erlang。

文件描述符0用于从Erlang读取数据,而文件描述符1用于向Erlang写入数据。各个C函数的功能如下:

read_cmd(buf)
从Erlang读取一条命令。
write_cmd(buf,len)
向Erlang写入一个长度为len的缓冲区。
read_exact(buf,len)
读取len个字节。
write_exact(buf,len)
写入len个字节。
put_int16(i,s)
将一个16位整数打包为两个字节。

函数read_cmdwrite_cmd假设外部服务和Erlang间的协议由一个指明数据包长度的双字节包头和紧随的数据构成。如图9.1所示。

_images/9.1.png图9.1 端口通讯

之所以使用这种协议(双字节包头加数据)是由于端口是以如下方式打开的:

  1. open_port({spawn, demo_server}, [{packet, 2}])