9.4 erlang并发编程杂项-端口

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

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

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

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

Port ! {PidC, {command, Data}}
Port ! {PidC, {connect, Data}}
Port ! {PidC, close}

PidC必须是一个连接进程的Pid。这些消息的含义如下:

{command, Data}

将Data描述的字节序列发送给外部对象。Data可以是单个二进制对象,也可以是一个元素为0..255范围内的整数的非扁平列表[2]。没有响应。

close

关闭端口。端口将向连接进程回复一条{Port, closed}消息。

{connect, Pid1}

将端口的连接进程换位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}

启动名为Command的外部程序或驱动。Erlang驱动在附录E中有所描述。若没有找到名为Command的驱动,则将在Erlang工作空间的外部运行名为Command的外部程序。

Atom

Atom将被认作是外部资源的名称。这样将在Erlang系统和由该原子式命名的资源之间建立一条透明的连接。连接的行为取决于资源的类型。如果Atom表示一个文件,则一条包含文件全部内容的消息会被发送给Erlang系统;向该端口写入发送消息便可向文件写入数据。

{fd, In, Out}

令Erlang进程得以访问任意由Erlang打开的文件描述符。文件描述符In可作为标准输入而Out可作为标准输出。该功能很少使用:只有Erlang操作系统的几种服务(shell和user)需要使用。注意该功能与仅限于UNIX系统。

PortSettings是端口设置的列表。有效的设置有:

{packet, N}

消息的长度将以大端字节序附在消息内容之前的N个字节内。N的有效取值为1、2或4。

stream

输出的消息不附带消息长度──Erlang进程和外部对象间必须使用某种私有协议。

use_stdio

仅对{spawn, Command}形式的端口有效。令产生的(UNIX)进程使用标准输入输出(即文件标识符0和1)与Erlang通讯。

nouse_stdio

与上述相反。使用文件描述符3、4与Erlang通讯。

in

端口仅用于输入。

out

端口仅用于输出。

binary

端口为二进制端口(后续将详述)。

eof

到达文件末尾后端口不会关闭并发送'EXIT'信号,而是保持打开状态并向端口的连接进程发送一条{Port, eof}消息,之后连接进程仍可向端口输出数据。

除了{spawn, Command}类型的端口默认使用use_stdio外,*所有*类型的端口默认都使用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

/* 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
 */
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;
{
    *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_cmd和write_cmd假设外部服务和Erlang间的协议由一个指明数据包长度的双字节包头和紧随的数据构成。如图9.1所示。

_images/9.1.png

图9.1 端口通讯

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

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

转载请注明:《9.4 erlang并发编程杂项-端口