端口
端口提供了与外部世界通讯的基本机制。用Erlang编写的应用程序往往需要与Erlang系统之外的对象交互。还有一些现存的软件包,例如窗口系统、数据库系统,或是使用C、Modula2等其他语言的程序,在使用它们构建复杂系统时,也往往需要给它们提供Erlang接口。
从程序员的视角来看,我们希望能够以处理普通Erlang程序的方式来处理Erlang系统外的所有活动。为了创造这样的效果,我们需要将Erlang系统外的对象伪装成普通的Erlang进程。端口(Port),一种为Erlang系统和外部世界提供面向字节的通讯信道的抽象设施,就是为此而设计的。
执行open_port(PortName,PortSettings)可以创建一个端口,其行为与进程类似。执行open_port的进程称为该端口的连接进程。需要发送给端口的消息都应发送至连接进程。外部对象可以通过向与之关联的端口写入字节序列的方式向Erlang系统发送消息,端口将给连接进程发送一条包含该字节序列的消息。
系统中的任意进程都可以与一个端口建立链接,端口和Erlang进程间的EXIT信号导致的行为与普通进程的情况完全一致。端口只理解三种消息:
PidC必须是一个连接进程的Pid。这些消息的含义如下:{command,Data}
- Port ! {PidC, {command, Data}}
- Port ! {PidC, {connect, Data}}
- Port ! {PidC, close}
close将Data描述的字节序列发送给外部对象。Data可以是单个二进制对象,也可以是一个元素为0..255范围内的整数的非扁平列表[2]。没有响应。
{connect,Pid1}关闭端口。端口将向连接进程回复一条{Port, closed}消息。
将端口的连接进程换位Pid1。端口将向先前的连接进程发送一条{Port, connected}消息。
此外,连接进程还可以通过以下方式接收数据消息:
- receive
- {Port, {data, Data}} ->
- ... an external object has sent data to Erlang ...
- ...
- end
在这一节中,我们将描述两个使用端口的程序:第一个是在Erlang工作空间内部的Erlang进程;第二个是在Erlang外部执行的C程序。
打开端口
打开端口时可以进行多种设置。BIF open_port(PortName,PortSettings可用于打开端口。PortName可以是:
{spawn,Command}Atom启动名为Command的外部程序或驱动。Erlang驱动在附录E中有所描述。若没有找到名为Command的驱动,则将在Erlang工作空间的外部运行名为Command的外部程序。
{fd,In,Out}Atom将被认作是外部资源的名称。这样将在Erlang系统和由该原子式命名的资源之间建立一条透明的连接。连接的行为取决于资源的类型。如果Atom表示一个文件,则一条包含文件全部内容的消息会被发送给Erlang系统;向该端口写入发送消息便可向文件写入数据。
PortSettings是端口设置的列表。有效的设置有:{packet,N}令Erlang进程得以访问任意由Erlang打开的文件描述符。文件描述符In可作为标准输入而Out可作为标准输出。该功能很少使用:只有Erlang操作系统的几种服务(shell和user)需要使用。注意该功能与仅限于UNIX系统。
stream消息的长度将以大端字节序附在消息内容之前的N个字节内。N的有效取值为1、2或4。
use_stdio输出的消息不附带消息长度──Erlang进程和外部对象间必须使用某种私有协议。
nouse_stdio仅对{spawn, Command}形式的端口有效。令产生的(UNIX)进程使用标准输入输出(即文件标识符0和1)与Erlang通讯。
in与上述相反。使用文件描述符3、4与Erlang通讯。
out端口仅用于输入。
binary端口仅用于输出。
eof端口为二进制端口(后续将详述)。
到达文件末尾后端口不会关闭并发送'EXIT'信号,而是保持打开状态并向端口的连接进程发送一条{Port, eof}消息,之后连接进程仍可向端口输出数据。
除了{spawn,Command}类型的端口默认使用usestdio外,所有_类型的端口默认都使用stream。
Erlang进程眼中的端口
程序9.2定义了一个简单的Erlang进程,该进程打开一个端口并向该端口发送一串消息。与端口相连的外部对象会处理并回复这些消息。一段时间之后进程将关闭端口。
程序9.2
- -module(demo_server).
- -export([start/0]).
- start() ->
- Port = open_port({spawn, demo_server}, [{packet, 2}]),
- Port ! {self(), {command, [1,2,3,4,5]}},
- Port ! {self(), {command}, [10,1,2,3,4,5]},
- Port ! {self(), {command, "echo"}},
- Port ! {self(), {command, "abc"}},
- read_replies(Port).
- read_replies(Port) ->
- receive
- {Port, Any} ->
- io:format('erlang received from port:~w~n', [Any]),
- read_replies(Port)
- after 2000 ->
- Port ! {self(), close},
- receive
- {Port, closed} ->
- true
- end
- 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,则它会将除第一个字节以外的所有字节翻倍后返回。
- 忽略其他数据。运行该程序后我们得到以下结果:
- > demo_server:start().
- erlang received from port:{data,[10,2,4,6,8,10]}
- erlang received from port:{data,[111,104,99,101]}
- true
外部进程眼中的端口
程序9.3
|
|
程序9.3通过表达式len=read_cmd(buf)读取发送至Erlang端口的字节序列,并用write_cmd(buf,len)将数据发回Erlang。
文件描述符0用于从Erlang读取数据,而文件描述符1用于向Erlang写入数据。各个C函数的功能如下:
read_cmd(buf)write_cmd(buf,len)从Erlang读取一条命令。
read_exact(buf,len)向Erlang写入一个长度为len的缓冲区。
write_exact(buf,len)读取len个字节。
put_int16(i,s)写入len个字节。
将一个16位整数打包为两个字节。
函数read_cmd和write_cmd假设外部服务和Erlang间的协议由一个指明数据包长度的双字节包头和紧随的数据构成。如图9.1所示。
图9.1 端口通讯
之所以使用这种协议(双字节包头加数据)是由于端口是以如下方式打开的:
- open_port({spawn, demo_server}, [{packet, 2}])