Archive for the ‘Erlang’ Category

对Erlang的字符串处理很失望,寻合作者

Wednesday, May 14th, 2008

对Erlang的字符串处理很失望,寻合作者

对Erlang的字符串处理很失望,充其量也就是跟string.h打个平手,在现代这种脚本与jdk横行的年代,基本上就是虐待人。

我可以忍受字符串处理稍微弱一点,但是我不能容忍一门编程语言无法支持正则表达式。Erlang虽然有regexp这个模块,但是其正则表达式只能用于匹配,无法从字符串中提取逻辑子串,这种所谓的正则表达式形同虚设。

假如我上面的还可以忍受的话,我就不好忍受erlang对扩展的不友好了。

总之呢,我要扩展erlang,现在的想法是用lua写一个erlang扩展,提供字符串的处理功能。通信接口使用erlang的term_to_binary和binary_to_term,方便erlang一端的解析。只是对于如何使用lua实现erlang的term-binary协议还是个问题。希望有了解的人一起来做。

调试了一个通宵的Erlang服务器运行问题

Tuesday, May 13th, 2008

早上5:43,我终于找到问题所在了。

现象:一个Erlang写的TCP服务器,在通过”erl -noshell -s mod fun”启动时总是在开始进行 gen_tcp:accept时告诉我closed。于是很恼火。

原理:以”-s mod fun”运行时,每个-s选项可以理解为一次session,该函数执行完成,也就代表该session的完成,同时清除与该session有关的所有资源,包括正在监听的socket。 所以,那个初始化socket的函数在执行完成前,启动一个微进程来等待accept也无济于事,socket还是被回收了。

解决:不让那个该死的session结束,而是给他一点有意义的,却又需要阻塞的工作来做。比如我正好需要让他在那里等待一个消息,以便允许其他进程方便的通过消息来关闭socket服务器。

另一个问题:通过Erlang shell来运行时,却不可以让初始化函数阻塞,否则下一步的活就不好做了。所以,可以考虑把阻塞部分的操作独立成一个函数来做,方便组合。

我的编程知识体系基本搭建完成

Monday, May 5th, 2008

我的编程知识体系基本搭建完成

我的编程知识体系由四种语言构成:C/Python/Erlang/Lua。排序按照熟悉程度。这四种语言分别用于处理不同的用途。

C:不用太多解释,C是王道,无论是以前、现在、还是未来。

Python:用以实现大规模逻辑的高层次编程语言,而我现在也是个Python程序员。

Erlang:用以实现高可用性、高并发能力、高性能的分布式服务器。不过在我看来并不适合做所有的事情,做服务器也就够了。

lua:用以实现高可移植能力的程序,用于嵌入式脚本。

lua是我最后一门搞定的语言,尽管现在还不是很熟,但是至少干完了一个项目了。

搭建好的这个知识体系会让我至少在两年内不会再去考虑学习更多的编程语言,并主要使用这四种编程语言完成几乎所有的工作。

Programming Erlang 第12章 接口技术 (完整)

Tuesday, April 15th, 2008

接口技术

译者: gashero

目录

假设我们需要以Erlang接口运行以C/Python/Shell编写的程序。想要实现这些,我们需要在一个单独的操作系统进程中运行这些程序,而不是在Erlang运行时系统中,他们之间以面向字节的通道完成通信。Erlang端通过 Port 控制。创建端口的进程成为端口的连接进程。连接进程拥有特殊意义:让所有来自扩展程序的消息都会以连接进程的PID来标志。所有的扩展程序消息都会发送到连接进程。

我们可以看看连接进程(C)与端口(P)和扩展操作系统进程的关系。

chpt12.00.jpg从程序员的角度,端口就像是Erlang进程。你可以发送消息给它,你可以注册(register)它,等等。如果扩展程序crash了,那么连接程序就会收到退出信号,如果连接进程死掉了,扩展进程也会被kill掉。

你可能会好奇与为什么要这么做。很多编程语言允许其他语言编写的程序连接到可执行文件。而在Erlang,我们为了安全性不允许这么做。如果我们连 接一个扩展程序到Erlang可执行程序,那么扩展程序的错误将会轻易的干掉Erlang。所以,其他语言编写的程序必须以单独的操作系统进程来运行。 Erlang运行时系统和扩展进程通过字节流通信。

1   端口

创建端口使用如下命令:

Port=open_port(PortName,PortSettings)

这会返回端口,而如下消息是会被发送到端口的(这些消息中PidC是连接进程的PID):

Port ! {PidC,{command,Data}} :发送数据到端口

Port ! {PidC,{connect,Pid1}} :改变控制进程的PID,从PidC到Pid1

Port ! {PidC,close} :关闭端口

连接进程会从扩展程序收到如下消息:

receive
    {Port,{data,Data}} ->
        ... 数据处理 ...

下面的节,我们会编写Erlang与C结合的简单例子。C程序尽可能的简单以避开细节,直接谈接口技术。

注意,下面的例子对接口机制和协议做了加亮。编码和解码复杂的数据结构是个困难的问题,这里没有谈及。在本章末尾,我们会指出一些用于其他编程语言的接口库。

2   扩展C程序的接口

我们先从C程序开始:

int twice(int x) {
    return 2*x;
}

int sum(int x, int y) {
    return x+y;
}

我们最终目标是从Erlang调用这些例程,我们希望看到的是这个样子(Erlang中):

X1=example1:twice(23),
Y1=example1:sum(45,32),

与用户的设置有关,example1是一个Erlang模块,所有与C接口的细节都被隐藏在了模块example1中。

我们的接口需要一个主程序,用来解码Erlang程序发来的数据。在我们的例子,我们首先定义端口和扩展C程序的协议。我们使用一个超级简单的协议,并展示如何在Erlang和C中实现。协议定义如下:

  • 所有包都以2字节的长度代码开头,后面跟着这些字节的数据。
  • 想要调用 twice(N) ,Erlang程序必须以特定形式编码函数调用。我们假设编码是2字节序列 [1,N] ;参数1表示调用函数 twice ,后面的N代表一个1字节的参数。
  • 调用 sum(N,M) ,我们编码请求到字节序列 [2,N,M] 。
  • 假设返回值都是单一的字节长度的。

扩展C程序和Erlang程序都必须遵守这个协议。作为例子,我们通过 sum(45,32) 来看看工作流程:

  1. 端口发送字节序列 0,3,2,45,32 到扩展程序。头两个字节0,3,表示包的长度是3,代码2表示调用扩展的 sum 函数,而 45 和 32 表示调用函数的参数。
  2. 扩展程序从标准输入(stdin)读取这5个字节,调用 sum 函数,然后把字节序列 0,2,77 写到标准输出(stdout)。前两个字节表示包长度,后面的77是结果(仍然是1字节长度)。

我们现在写在两端遵守协议的接口,先以C程序开始。

2.1   C程序

扩展的C程序需要编写3个文件:

  • example1.c :这个包含了我们需要调用的原始函数。
  • example1_driver.c :实现了字节流协议和对 example1.c 的调用例程。
  • erl_comm.c :包含读写内存缓冲区的例程。

example1_driver.c

#include <stdio.h>
typedef unsigned char byte;

int read_cmd(byte* buff);
int write_cmd(byte* buff, int len);

int main() {
    int fn, arg1, arg2, result;
    byte buff[100];

    while(read_cmd(buff)>0) {
        fn=buff[0];
        if (fn==1) {
            arg1=buff[1];
            result=twice(arg1);
        } else if (fn==2) {
            arg1=buff[1],
            arg2=buff[2],
            //如果希望调试,可以写到stderr
            //fprintf(stderr,"call sum %i %i\n",arg1,arg2);
            result=sum(arg1,arg2);
        }
        buff[0]=result;
        write_cmd(buff,1);
    }
}

这段代码运行一个无限循环,从标准输入stdin读取调用命令,并且将结果写入到标准输出。

如果希望调试,可以写到stderr,上面的例子里面就有相关的输出语句。

erl_comm.c 是对stdin和stdout读写带有2字节包头的数据包的代码。这么写是允许有分片的。

#include <unistd.h>
typedef unsigned char byte;

int read_cmd(byte* buf);
int write_cmd(byte* buf, int len);
int read_exact(byte* buf, int len);
int write_exact(byte* buf, int len);

int 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);
}

int 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);
}

int 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;
}

int write_exact(byte* buf, int len) {
    itn i,wrote=0;
    do {
        if ((i=write(1,buf+wrote,len-wrote)<=0)
            return i;
        wrote+=i;
    } while(wrote<len);
    return len;
}

这段代码处理带有2字节长度包头的数据包,所以用于匹配 {packet,2} 选项的端口驱动程序。

2.2   Erlang程序

Erlang端的驱动程序参考如下:

-module(example1).
-export([start/0,stop/0]).
-export([twice/1,sum/2]).

start() ->
    spawn(fun() ->
            register(example1,self()),
            process_flag(trap_exit,true),
            Port=open_port({spawn,"./example1"},[{packet,2}]),
            loop(Port)
        end).

stop() ->
    example1 ! stop.

twice(X) -> call_port({twice,X}).
sum(X,Y) -> call_port({sum,X,Y}).

call_port(Msg) ->
    example1 ! {call,self(),Msg},
    receive
        {example1,Result} ->
            Result
    end.

loop(Port) ->
    receive
        {call,Caller,Msg} ->
            Port ! {self(),{command,encode(Msg)}},
            receive
                {Port,{data,Data}} ->
                    Caller ! {example1,decode(Data)}
            end,
            loop(Port);
        stop ->
            Port ! {self(),close},
            receive
                {Port,closed} ->
                    exit(normal)
            end;
        {'EXIT',Port,Reason} ->
            exit({port_terminated,Reason})
    end.

encode({twice,X}) -> [1,X];
encode({sum,X,Y}) -> [2,X,Y];

decode([Int]) -> Int.

端口通过如下方式打开:

Port=open_port({spawn,"./example1"},[{packet,2}])

选项 {packet,2} 告诉系统自动在发送数据包时添加一个2字节的包头。所以,会在发送消息 {PidC,{command,[2,45,32]}} 自动加上两字节的长度包头,也就是发送 0,3,2,45,32 到扩展程序。

而输入端口会假设输入数据包也有2字节的长度包头,获取指定长度的数据包之后就会去掉长度包头。

完善程序还需要一个makefile来构建。命令 make example1 用于构建 open_port 调用的扩展程序。注意makefile同时也链接了本章稍后会用到的内联驱动。

makefile

.SUFFIXES: .erl .beam .yrl

.erl.beam:
    erlc -W $<

MODS=example1 example1_lid.c

all: ${MODS:%=%.beam} example1 example1_drv.so

example1: example1.c erl_comm.c example1_driver.c
    gcc -o example1 example1.c erl_comm.c example1_driver.c

example1_drv.so: example1_lid.c example.c
    gcc -o example1_drv.so -fpic -shared example1.c example1_lid.c

clean:
    rm example example1_drv.so *.beam

2.3   运行程序

现在运行程序:

1> example1:start().
<0.32.0>
2> example1:sum(45,32).
77
4> example1:twice(10).
20
...

这就完成了我们的第一个例子。

进入下个主题之前,必须注意的是:

  1. 例子程序没有尝试统一Erlang和C程序的整数。这里我们假设整数都是单字节的,并且忽略了精度和符号的问题。在现实的应用中必须注意类型和精度的问题。这是个困难的问题,因为erlang管理着不限制大小的整数,而像C一类的语言却必须管理整数的精度等等。
  2. 我们无法在运行扩展程序之前调用对应的erlang的函数(也就是运行 example1:start() 之前不能调用对应函数)。我们更希望可以自动的启动。这在18.7节会详细讲解。

3   open_port

前一节使用了 open_port 却没有介绍具体的参数。只是使用了增加了 {packet,2} 选项的 open_port 而已。 open_port 包含很多参数。

一些常用参数如下:

@spec open_port(PortName,[Opt]) -> Port

PortName是如下之一:

{spawn,Command}

启动一个扩展程序。Command是扩展程序的名字。Command会在Erlang的工作空间以外工作,除非找到了叫做Command的内联驱动。

{fd,In,Out}

允许Erlang进程存取一个已经打开的文件描述符。文件描述符 “In” 用作stdin,而文件描述符 “Out” 用作stdout。查看例子 http://www.erlang.org/examples/examples-2.0.html

Opt是如下之一:

{packet,N}

包前面加上N(1,2,4)字节长度的长度包头。

stream

消息不是按照包长度发送的。应用自己知道如何处理这些包。

{line,Max}

以行为单位传递消息。如果一行大于Max字节,则会切割为只有Max字节。

{cd,Dir}

仅用于 {spawn,Command} 选项,扩展程序的初始目录为Dir。

{env,Env}

仅用于 {spawn,Command} 选项。指定扩展程序可用的环境变量为Env。Env是一个列表的 {VarName,Value} 对。两个变量都是字符串。

这并不是完整的 open_port 参数列表。我们可以在参考手册的erlang模块里找到详细的描述。

4   内联驱动

有时候我们希望在Erlang运行时系统内运行一个外语程序。在这种情况下,程序以共享库的方式编写并动态链接到Erlang运行时系统。内联驱动对程序员来说就是一个端口程序,而且与端口程序使用相同的协议。

创建内联驱动是最有效率的工作方式,不过却很危险。任何致命错误都会干掉整个Erlang系统,并且影响到正在工作的所有进程。因为这个原因,非常不推荐内联驱动。

通过刚才的例子讲讲内联驱动。你需要三个文件:

  1. example1_lid.erl :这个是erlang的服务器。
  2. example1.c :包含我们需要调用的C函数,跟上一节的一样。
  3. example1_lid.c :调用 example1.c 中好函数的函数。

Erlang管理接口的代码如下:

-module(example1_lid).
-export([start/0,stop/0]).
-export([twice/1,sum/2]).

start() ->
    start("example1_drv").

start(SharedLib) ->
    case erl_ddl1:load_driver(".",SharedLib) of
        ok -> ok;
        {error,already_loaded} -> ok;
        _ -> exit({error,could_not_load_driver})
    end,
    spawn(fun() -> init(SharedLib) end).

init(SharedLib) ->
    register(example1_lid,self()),
    Port=open_port({spawn,SharedLib},[]),
    loop(Port).

stop() ->
    example1_lid ! stop.

twice(X) -> call_port({twice,X}).
sum(X,Y) -> call_port({sum,X,Y}).

call_port(Msg) ->
    example1_lid ! {call,self(),Msg},
    receive
        {example1_lid,Result} ->
            Result
    end.

loop(Port) ->
    receive
        {call,Caller,Msg} ->
            Port ! {self(),{command,encode(Msg)}},
            receive
                {Port,{data,Data}} ->
                    Caller ! {example1_lid,decode(Data)}
            end,
            loop(Port);
        stop ->
            Port ! {self(),close},
            receive
                {Port,closed} ->
                    exit(normal)
            end;
        {'EXIT',Port,Reason} ->
            io:format("~p~n",[Reason]),
            exit(port_terminated)
    end.

encode({twice,X}) -> [1,X];
encode({sum,X,Y}) -> [2,X,Y].

decode([Int]) -> Int.

对比该程序与较早的版本,看起来基本相同。

驱动程序由 driver 结构体组成。命令 make example1_drv.so 用于构建动态链接库。

// example1_lid.c
#include <stdio.h>
#include "erl_driver.h"

typedef struct {
    ErlDrvPort port;
} example_data;

static ErlDrvData example_drv_start(ErlDrvPort port, char* buff) {
    example_data* d=(example_data*)driver_alloc(sizeof(example_data));
    d->port=port;
    return (ErlDrvData)d;
}

static void example_drv_stop(ErlDrvData handle) {
    driver_free((char*) handle);
}

static void example_drv_output(ErlDrvData handle, char* buff, int bufflen) {
    example_data* d=(example_data*)handle;
    char fn=buff[0], arg=buff[1], res;
    if (fn==1) {
        res=twice(arg);
    } else if (fn==2) {
        res=sum(buff[1],buff[2]);
    }
    driver_output(d->port,&res,1);
}

ErlDrvEntry example_driver_entry={
    NULL,                   // F_PTR init, N/A
    example_drv_start,      // L_PTR start, 在端口打开时调用
    example_drv_stop,       // F_PTR stop, 在端口关闭时调用
    example_drv_output,     // F_PTR output, 当erlang发送数据到端口时
    NULL,                   // F_PTR ready_input
    NULL,                   // F_PTR ready_output
    "example1_drv",         // 驱动的名字
    NULL,                   // F_PTR finish, 卸载(unload)时
    NULL,                   // F_PTR control, port_command回调
    NULL,                   // F_PTR timeout, 保留
    NULL                    // F_PTR outputv, 保留
};

DRIVER_INIT(example_drv) {  //必须与driver_entry中的名字匹配
    return &example_driver_entry;
}

这里时运行程序:

1> c(example_lid).
{ok,example1_lid}
2> example1_lid:start().
<0.41.0>
3> example1_lid:twice(50).
100
4> example1_lid:sum(10,20).
30

5   注意

本章研究了如何以端口来调用扩展程序。除了使用端口协议之外,还可以使用一些其他的BIF来操作端口。这些都在erlang模块的手册中。

在这一点上,你可能会关心如何方便的发送复杂的数据结构,如字符串、元组等等到扩展程序?很不幸,答案是还没有比较简单的方法来实现。所有的端口只 是提供了很底层的机制来传输一系列字节而已。同样的问题也发生在Socket编程中。一个Socket提供了数据流与应用交互,对于数据流的解释还需要应 用自己做。

不过一些库提供了简单的交互方法,可以查看:

http://www.erlang.org/doc/pdf/erl_interface.pdf

Erl interface (ei) 是一系列的C例程和宏,用于编码和解码Erlang的扩展格式。在Erlang端,一个程序使用 term_to_binary 来将Erlang术语串行化,而在C端ei的例程可以把binary解包。ei也可以用于构造binary,而在Erlang使用 binary_to_term 来解包。

http://www.erlang.org/doc/pdf/ic.pdf

Erlang IDL Compiler (ic) 。这是一个Erlang的OMG IDL编译器实现。

http://www.erlang.org/doc/pdf/jinterface.pdf

Jinterface 是一系列Java到Erlang的接口工具。它提供了Erlang类型到Java类型的完全映射,编码和解码Erlang术语,连接Erlang进程等等。还有一大堆的扩展功能。

Erlang的语法,一个陷阱

Monday, April 14th, 2008

最近正在用Erlang写MySQL的通信协议,以前用Python实现的一个性能是非常高的,所以这次做系统也想使用这个协议做通信。

写着的时候发现个问题,这里有个bit语法的赋值语句:

BinFC=<<Number:2>>,

本想给变量BinFC赋值为一个binary类型,但是编译时一直提示出错,后来才想起来。符号”=<“在erlang中是小于等于的意思,而这样写就会先识别成一个布尔表达式,然后就出错了。

我的习惯是并不在操作符之间留空格,这次终于被教训了。不过也确实与Erlang这种古怪的风格有关。其他语言中的小于等于都是用”<=”的,偏偏erlang必须要使用”=<“,而且,又有这个binary语法。

唉……,继续写。

Programming Erlang 第6章 编译和运行(完整)

Monday, April 14th, 2008

编译和运行

译者: gashero

目录

上一章并没有详细的说明如何编译和运行程序,而只是使用了Erlang shell。这对小例子是很好的,但是会让你的程序更复杂,你将会需要一个自动处理过程来简化它。我们一般使用Makefile。

事实上有三种不同的方式可以运行程序。本章将会结合特定场合来讲解三种运行方式。

有时候也会遇到问题:Makefile失败、环境变量错误或者搜索路径不对。我们在遇到这些问题时也会帮你处理这些问题(issue)。

1   启动和停止Erlang shell

在Unix系统(包括Mac OS X),可以从命令行启动Erlang shell:

$ erl
Erlang (BEAM) emulator version 5.5.1 [source] [async-threads:0] [hipe]

Eshell V5.5.1  (abort with ^G)
1>

而在Windows系统,则可以点击erl的图标。

最简单的退出方法是按下 Ctrl+C (对Windows系统则是 Ctrl+Break ),随后按下 A 。如下:

BREAK: (a)bort (c)ontinue (p)roc info (i)nfo (l)oaded
       (v)ersion (k)ill (D)b-tables (d)istribution
a
$

另外,你也可以对 erlang:halt() 求值以退出。

erlang:halt() 是一个BIF,可以立即停止系统,也是我最常用的方法。不过这种方法也有个问题。如果正在运行一个大型数据库应用,那么系统在下次启动时就会进入错误回复过程,所以你应该以可控的方式关闭系统。

可控的关闭系统就是如果shell可用时就输入如下:

1> q().
ok
$

这会将所有打开的文件写入磁盘,停止数据库(如果在运行),并且按照顺序关闭所有OTP应用。 q() 其实只是shell上 init:stop() 的别名。

如果这些方法都不工作,可以查阅6.6节。

2   修改开发环境

当你启动程序时,一般都是把所有模块和文件都放在同一个目录当中,并且在这个目录启动Erlang。如果这样做当然没问题。不过如果你的应用变得更加复杂时,你可能需要把它们分成便于管理的块,把代码放入不同的目录。而且你从其他项目导入代码时,扩展代码也有其自己的目录结构。

2.1   设置装载代码的搜索路径

Erlang运行时系统包含了代码自动重新载入的功能。想要让这个功能正常工作,你必须设置一些搜索路径以便找到正确版本的代码。

代码装载功能是内置在Erlang中的,我们将在E.4节详细讨论。代码的装载仅在需要时才执行。

当系统尝试调用一个模块中的函数,而这个模块还没有装载时,就会发生异常,系统就会尝试这个模块的代码文件。如果需要的模块叫做 myMissingModule ,那么代码装载器将会在当前目录的所有目录中搜索一个叫做 myMissingModule.beam 的文件。寻找到的第一个匹配会停止搜索,然后就会把这个文件中的对象代码载入系统。

你可以在Erlang shell中看看当前装载路径,使用如下命令 code:get_path() 。如下是例子:

code:get_path()
[".",
"/usr/local/lib/erlang/lib/kernel-2.11.3/ebin",
"/usr/local/lib/erlang/lib/stdlib-1.14.3/ebin",
"/usr/local/lib/erlang/lib/xmerl-1.1/ebin",
"/usr/local/lib/erlang/lib/webtool-0.8.3/ebin",
"/usr/local/lib/erlang/lib/typer-0.1.0/ebin",
"/usr/local/lib/erlang/lib/tv-2.1.3/ebin",
"/usr/local/lib/erlang/lib/tools-2.5.3/ebin",
"/usr/local/lib/erlang/lib/toolbar-1.3/ebin",
"/usr/local/lib/erlang/lib/syntax_tools-1.5.2/ebin",
...]

管理装载路径两个最常用的函数如下:

@spec code:add_patha(Dir) => true | {error,bad_directory}

添加新目录Dir到装载路径的开头

@spec code:add_pathz(Dir) => true | {error,bad_directory}

添加新目录Dir到装载路径的末尾

通常并不需要自己来关注。只要注意两个函数产生的结果是不同的。如果你怀疑装载了错误的模块,你可以调用 code:all_loaded() (返回所有已经装载的模块列表),或者 code:clash() 来帮助你研究错误所在。

code 模块中还有其他一些程序可以用于管理路径,但是你可能根本没机会用到它们,除非你正在做一些系统编程。

按照惯例,经常把这些命令放入一个叫做 .erlang 的文件到你的HOME目录。另外,也可以在启动Erlang时的命令行中指定:

> erl -pa Dir1 -pa Dir2 ... -pz DirK1 -pz DirK2

其中 -pa 标志将目录加到搜索路径的开头,而 -pz 则把目录加到搜索路径的末尾。

2.2   在系统启动时执行一系列命令

我们刚才把载入路径放到了HOME目录的 .erlang 文件中。事实上,你可以把任意的Erlang代码放入这个文件,在你启动Erlang时,它就会读取和求值这个文件中的所有命令。

假设我的 .erlang 文件如下:

io:format("Running Erlang~n").
code.add_patha(".")
code.add_pathz("/home/joe/2005/erl/lib/supported").
code.add_pathz("/home/joe/bin").

当我启动系统时,我就可以看到如下输出:

$ erl
Erlang (BEAM) emulator version 5.5.1 [source] [async-threads:0] [hipe]

Running Erlang
Eshell V5.5.1  (abort with ^G)
1>

如果当前目录也有个 .erlang 文件,则会在优先于HOME目录的。这样就可以在不同启动位置定制Erlang的行为,这对特定应用非常有用。在这种情况下,推荐加入一些打印语句到启动文件;否则你可能忘记了本地的启动文件,这可能会很混乱。

某些系统很难确定HOME目录的位置,或者根本就不是你以为的位置。可以看看Erlang认为的HOME目录的位置,通过如下的:

1> init:get_argument(home).
{ok,[["/home/joe"]]}

通过这里,我们可以推断出Erlang认为的HOME目录就是 /home/joe

3   运行程序的其他方式

Erlang程序存储在模块中。一旦写好了程序,运行前需要先编译。不过,也可以以脚本的方式直接运行程序,叫做 escript 。

下一节会展示如何用多种方式编译和运行一对程序。这两个程序很不同,启动和停止的方式也不同。

第一个程序 hello.erl 只是打印 “Hello world” ,这不是启动和停止系统的可靠方式,而且他也不需要存取任何命令行参数。与之对比的第二个程序则需要存取命令行参数。

这里是一个简单的程序。它输出 “Hello world” 然后输出换行。 “~n” 在Erlang的io和io_lib模块中解释为换行。

-module(hello).
-export([start/0]).

start() ->
    io:format("Hello world~n").

让我们以3种方式编译和运行它。

3.1   在Erlang shell中编译和运行

$ erl
...
1> c(hello).
{ok,hello}
2> hello:start().
Hello world
ok

3.2   在命令行编译和运行

$ erlc hello.erl
$ erl -noshell -s hello start -s init stop
Hello World
$

Note

快速脚本:

有时我们需要在命令行执行一个函数。可以使用 -eval 参数来快速方便的实现。这里是例子:

erl -eval 'io:format("Memory: ~p~n", [erlang:memory(total)]).'\
    -noshell -s init stop

Windows用户:想要让如上工作,你需要把Erlang可执行文件目录加入到环境变量中。否则就要以引号中的全路径来启动,如下:

"C:\Program Files\erl5.5.3\bin\erlc.exe" hello.erl

第一行 erlc hello.erl 会编译文件 hello.erl ,生成叫做 hello.beam 的代码文件。第一个命令拥有三个选项:

-noshell :启动Erlang而没有交互式shell,此时不会得到Erlang的启动信息来提示欢迎

-s hello start :运行函数 hello:start() ,注意使用 -s Mod ... 选项时,相关的模块Mod必须已经编译完成了。

-s init stop :当我们调用 apply(hello,start,[]) 结束时,系统就会对函数 init:stop() 求值。

命令 erl -noshell ... 可以放入shell脚本,所以我们可以写一个shell脚本负责设置路径(使用-pa参数)和启动程序。

在我们的例子中,我们使用了两个 -s 选项,我们可以在一行拥有多个函数。每个 -s 都会使用 apply 语句来求职,而且,在一个执行完成后才会执行下一个。

如下是启动hello.erl的例子:

#! /bin/sh
erl -noshell -pa /home/joe/2006/book/JAERANG/Book/code\
    -s hello start -s init stop

Note

这个脚本需要使用绝对路径指向包含 hello.beam 。不过这个脚本是运行在我的电脑上,你使用时应该修改。

运行这个shell脚本,我们需要改变文件属性(chmod),然后运行脚本:

$ chmod u+x hello.sh
$ ./hello.sh
Hello world
$

Note

在Windows上, #! 不会起效。在Windows环境下,可以创建.bat批处理文件,并且使用全路径的Erlang来启动(假如尚未设置PATH环境变量)。

一个典型的Windows批处理文件如下:

"C:\Program Files\erl5.5.3\bin\erl.exe" -noshell -s hello start -s init stop

3.3   以Escript运行

使用escript,你可以直接运行你的程序,而不需要先编译。

Warning

escript包含在Erlang R11B-4或以后的版本,如果你的Erlang实在太老了,你需要升级到最新版本。

想要以escript方式运行hello,我们需要创建如下文件:

#! /usr/bin/env escript

main(_) ->
    io:format("Hello world\n").

Note

开发阶段的导出函数

如果你正在开发代码,可能会非常痛苦于需要不断在导出函数列表增删函数。

一个声明 -compile(export_all) ,告知编译器导出所有函数。使用这个可以让你的开发工作简化一点。

当你完成开发工作时,你需要抓实掉这一行,并添加适当的导出列表。首先,重要的函数需要导出,而其他的都要隐藏起来。隐藏方式可以按照个人喜好,提供接口也是一样的效果。第二,编译器会对需要导出的函数生成更好的代码。

在Unix系统中,我们可以立即按照如下方式运行:

$ chmod u+x hello
$ ./hello
Hello world
$

Note

这里的文件模式在Unix系统中表示可执行,这个通过chmod修改文件属性的步骤只需要执行一次,而不是每次运行程序时。

在Windows系统中可以如下方式运行:

C:\> escript hello
Hello world
C:\>

Note

在以escript方式运行时,执行速度会明显的比编译方式慢上一个数量级。

3.4   程序的命令行参数

“Hello world”没有参数。让我们重新来做一个计算阶乘的程序,可以接受一个参数。

首先是代码:

-module(fac).
-export([fac/1]).

fac(0) -> 1;
fac(N) -> N*fac(N-1).

我们可以编译 fac.erl 并且在Erlang中运行:

$ erl
1> c(fac).
{ok,fac}
2> fac:fac(25).
15511210043330985984000000

如果我们想要在命令行运行这个程序,我们需要修改一下他的命令行参数:

-module(fac1).
-export([main/1]).

main([A]) ->
    I=list_to_integer(atom_to_list(A)).
    F=fac(I),
    io:format("factorial ~w= ~w~n",[I,F]),
    init:stop().

fac(0) -> 1;
fac(N) -> N*fac(N-1).

让我们编译和运行:

$ erlc fac1.erl
$ erl -noshell -s fac1 main 25
factorial 25 = 15511210043330985984000000

Note

事实上这里的main()函数并没有特殊意义,你可以把它改成任何名字。重要的一点是命令行参数中要使用函数名。

最终,我们可以把它直接作为escript来运行:

#! /usr/bin/env escript

main([A]) ->
    I=list_to_integer(A),
    F=fac(I),
    io:format("factorial ~w = ~w~n",[I,F]).

fac(0) -> 1;
fac(N) -> N*fac(N-1).

运行时无需编译,可以直接运行:

$ ./factorial 25
factorial 25 = 15511210043330985984000000
$

4   通过makefile自动编译

当编写一个大型程序时,我希望只在需要的时候自动编译。这里有两个原因。首先,节省那些一遍遍相同的打字。第二,因为经常需要同时进行多个项目,当再次回到这个项目时,我已经忘了该如何编译这些代码。而 make 可以帮助我们解决这个问题。

make 是一个自动工具,用以编译和发布Erlang代码。大多数我的makefile都非常简单,并且我有个模板可以解决大多数问题。

我不想在这里解释makefile的意义。而我会展示如何将makefile用于Erlang程序。推荐看看本书附带的makefile,然后你就会明白他们并且构建你自己的makefile了。

4.1   一个makefile模板

如下的模板是很常用的,很多时候可以基于他们来做实际的事情:

#让这一行就这么放着
.SUFFIXES: .erl .beam .yrl

.erl.beam:
    erlc -W $<

.yrl.erl:
    erlc -W $<

ERL=erl -boot start_clean

#如下是需要编译的模块列表
#如果一行写不下可以用反斜线 \ 来折行

#修改如下行
MODS=module1 module2 \
     module3 ....

#makefile中的第一个目标是缺省目标
#也就是键入make时默认的目标
all: compile

compile: ${MODS:%=%.beam} subdirs

#指定编译需求,并且添加到这里
special1.beam: special1.erl
    ${ERL} -Dflag1 -W0 special1.erl

#从makefile运行一个应用
application1: compile
    ${ERL} -pa Dir1 -s application1 start Arg1 Arg2

#在子目录编译子目录的目标
subdirs:
    cd dir1; make
    cd dir2; make
    ...

#清除所有编译后的文件
clean:
    rm -rf *.beam erl_crash.dump
    cd dir1; make clean
    cd dir2; make clean

makefile开始于一些编译Erlang模块的规则,包括扩展名为 .yrl 的文件(是Erlang词法解析器的定义文件)(Erlang的词法解析器生成器为yecc,是yacc的Erlang版本,参考 http://www.erlang.org/contrib/parser_tutorial-1.0.tgz )。

重要的部分开始于这一行:

MODS=module1 module2

这是我们需要编译的Erlang模块的列表。

任何在MODS列表中的模块都会被Erlang命令 erlc Mod.erl 来编译。一些模块可能会需要特殊的待遇,比如模板中的special1。所以会有单独的规则用来处理这些。

makefile中有一些目标。一个目标是一个字符串,随后是冒号”:”。在makefile模板中,all,compile和special1.beam都是目标。想要运行makefile,你可以在shell中执行:

$ make [Target]

参数Target是可选的。如果Target忽略掉了,则假设被第一个目标。在前面的例子中,目标all就是缺省的。

如果我希望构建软件并运行,我们就会使用 make application1 。如果我希望这个作为缺省行为,也就是在我每次键入 make 时都会执行,我就可以把application1目标作为第一个目标。

目标clean会删除所有编译过的Erlang目标代码和 erl_crash.dump 。crashdump包含了帮助调试应用程序的信息。查看6.10了解更多。

4.2   实际修改makefile模板

我并不热衷于让自己的程序变得混乱,所以我一般以一个不包含无用行的模板开始。所以得到的makefile会更短,而且也更易于阅读。另外,你也可以使用到处都是变量的makefile,以方便定制。

一旦我遍历整个流程,就会得到一个很简单的makefile,有如如下:

.SUFFIXES: .erl .beam

.erl.beam:
    erlc -W $<

ERL = erl -boot start_clean

MODS=module1 module2 module3

all: compile
    ${ERL} -pa '/home/joe/.../this/dir' -s module1 start

compile: ${MODS:%=%.beam}

clean:
    rm -rf *.beam erl_crash.dump

5   Erlang shell中的命令编辑

Erlang shell包含了内置的行编辑器。它可以执行emacs的一部分行编辑命令,前一行可以以多种方式来调用。可用命令如下,注意 “^Key” 是指按下 “Ctrl+Key” 。

命令 描述
^A 开始一行
^E 最后一行
^F或右箭头 向前一个字符
^B或左箭头 向后一个字符
^P或上箭头 前一行
^N或下箭头 下一行
^T 调换最后两个字符的顺序
Tab 尝试补全模块名或函数名

6   解决错误

Erlang有时会发生一些问题而退出。下面是可能的错误原因:

  1. shell没有响应
  2. Ctrl+C处理器被禁用了
  3. Erlang以 -detached 标志启动,这时你甚至感觉不到他在运行
  4. Erlang以 -heart Cmd 标志启动。这会让OS监控器进程看管Erlang的OS进程。如果Erlang的OS进程死掉,那么就会求值 Cmd 。一般来说 Cmd 只是用于简单的重启Erlang系统。这个是用于生产可容错系统的一个重要技巧,用于结点-如果erlang自己死掉了(基本不可能发生),就会自己重启。这个技巧对Unix类操作系统使用 ps 命令来监控,对Windows使用任务管理器。进行心跳信息检测并且尝试kill掉erlang进程。
  5. 有且确实很无奈的错误,留下一个erlang僵尸进程。

7   当确实出错时

本节列出了一些常见错误和解决方案。

7.1   未定义(丢失)的代码

如果你尝试调用一个模块,而代码载入器却无法找到时(比如搜索路径出错),你将会遇到 undef 错误信息,如下是例子:

1> glurk:oops(1,23).
** exited: {undef,[{glurk,oops,[1,23]},
                   {erl_eval,do_apply,5},
                   {shell,exprs,6},
                   {shell,eval_loop,3}]}**

事实上,这里没有一个叫做glurk,但是这里没有关联问题,你只需要关心错误信息就可以了。错误信息告诉我们系统尝试调用glurk模块的函数oops,参数1是23,这四个事物中的一个出了问题。

  1. 有可能不存在模块glurk,找不到或者根本不存在。可能是拼写错误。
  2. 存在模块glurk,但是还没有编译。系统尝试寻找文件 glurk.beam ,但是没有找到。
  3. 有模块glurk,但是包含 glurk.beam 的目录并没有在模块搜索路径中。想要修复这个问题,你必须改变搜索路径,一会再讲。
  4. 在载入路径中有多个不同版本的 “glurk” ,而我们选择了错误的。这是个罕见的错误,但是有可能会发生。如果你担心发生,你可以运行 code:clash() 函数,这将会报告代码搜索路径中的重复模块。

Note

有人看见我的分号了么?

如果你忘记了两个子句之间的分号,或者是放了个句点,那你可就麻烦了。

如果你定义了一个函数 foo/2 在模块bar的1234行,并且在该写分号的地方用了句点,那么编译器会提示:

bar.erl:1234 function foo/2 already defined.

不要这么做,确保你的子句总是以分号分开。

7.2   我的makefile无法make

你怎么能让makefile出错?当然,尽管这本书不是讲makefile的,但是我还是会给出一些常见错误提示。如下是两个常见的错误:

  1. makefile中的空白:makefile是很严格而挑剔的。虽然你看不到他们,但是缩进行必须以一个tab字符开始。如果这里有其他空白,那么make就会出错,而你会看到一些错误。(当然折行,也就是上一行末尾有 “\” 字符的不算)

  2. 丢失的erlang文件。如果在MODS变量中定义的模块丢失了,你会得到错误信息。举例,假设MODS中包含一个模块叫做glurk,但是却没有glurk.erl文件。这种情况下,make会出错,并给出如下信息:

    $ make
    make: *** No rule to make target 'glurk.beam',
                needed by 'compile'. Stop.

另外模块名拼写错误也会导致这个问题。

7.3   shell没有响应了

如果shell对命令不做出响应了,有可能发生一系列的问题。shell进程可能会crash了,或者你可能放任一个命令永远无法终止。你可能忘了输入关闭的括号,或者忘记了 点号回车

无论哪种问题,你都可以通过按下 “Ctrl+G” 啦关闭当前shell,并且学着如下的例子:

1> receive foo -> true end.
^G
User switch command
--> h
c [nn]    - connect to job
i [nn]    - interrupt job
k [nn]    - kill job
j         - list all jobs
s         - start local shell
r [node]  - start remote shell
q         - quit erlang
? | h     - this message
--> j
1* {shell,start,[init]}
--> s
--> j
1 {shell,start,[init]}
2* {shell,start,[]}
--> c 2
Eshell V5.5.1  (abort with ^G)
1> init:stop().
ok
2> $
  1. 第一行告诉shell接收foo消息,当时不会有人发送消息到shell,shell会进入一个无限的等待,所以需要按下 “Ctrl+G” 。
  2. “–> h” ,系统进入了 “shell JCL” 模式(Job Control Language),这里我可以不知道任何命令所以键入 h 来获取帮助。
  3. “–> j” ,列出所有任务。任务的号码1标记为星号,表示为默认的shell。所有命令选项参数 [nn] 使用缺省的shell除非指定了参数。
  4. “–> s” ,键入s以启动一个新的shell,而在其后可以看到已经把2号标记为默认的shell了
  5. “–> c 2″ ,让我连接到新启动的2号shell,然后停止了系统。

有如你所见,你可以拥有多个shell,并且通过按下Ctrl+G以后来切换。你可以通过命令r在远程启动一个shell方便调试。

8   获取帮助

在Unix类系统中,如下:

$ erl -man erl
NAME
erl - The Erlang Emulator

DESCRIPTION
...

你也可以通过这种方式获取模块的帮助文档:

$ erl -man lists
MODULE
lists - List Processing Functions
...

Note

在Unix系统,man手册页并不是默认安装的,如果命令 erl -man ... 没有工作,你需要自己安装man手册页。所有的man手册页都是单独压缩存档的,可以到 http://www.erlang.org/download.html 下载。man手册页可以通过root解压并安装到Erlang的安装目录,通常为 /usr/local/lib/erlang

也可以下载HTML文档。在Windows下默认会安装HTML文档,可以通过开始菜单访问到。

9   定制环境

Erlang shell拥有一系列的内置命令。你可以通过 help() 来看到这些命令:

1> help().
** shell internal commands **
b()        -- display all variables bindings
e(N)       -- repeat the expression in query <N>
f()        -- forget all variable bindings
f(X)       -- forget the binding of variable X
h()        -- history
...

所有这些命令都是定义在模块 shell_default 中。

如果你想定义自己的命令,只需要创建一个叫做 user_default 的模块,例如:

-module(user_default).
-compile(export_all).

hello() ->
    "Hello joe how are you?".

away(Time) ->
    io:format("Joe is away and will be back in ~w minutes~n",[Time]).

一旦你编译了它并且放在了你的载入路径,那么你可以调用 user_default 中的任何函数,而不用给出模块名:

1> hello().
"Hello joe how are you?"
2> away(10).
Joe is away and will be back in 10 minutes
ok

10   crash dump

当erlang crash时,他会生成一个文件叫做 erl_crash.dump 。文件内容包含出错的地方的相关信息。想要分析crash dump这里有个基于web的分析器。启动这个分析器,你可以给出如下命令:

1> webtool:start().
WebTool is available at http://localhost:8888/
Or http://127.0.0.1:8888/
{ok,<0.34.0>}

然后让浏览器指向 http://localhost:8888/ 。你就可以看到刚才发生的问题了。

现在我们看到的只是问题回溯的原料,具体还需要你仔细的分析,并发的程序不太好调试。从这时开始,你将离开熟悉的环境,不过这时也正是历险的开始。

Programming Erlang第14章Socket编程[完整]

Thursday, April 10th, 2008

Socket编程

译者: gashero

目录

大多数我写的更有趣的程序都包含了Socket。一个Socket是一个允许机器与Internet上另一端使用IP通信的端点。本章关注Internet上两种核心网络协议:TCP和UDP。

UDP允许应用发送简短报文(叫做数据报datagram)到另一端,但是对报文没有交付的担保。并且可能在到达时有错误的顺序。而TCP则提供了可靠的字节流,并且确保在连接后传输数据的顺序也是对的。

为什么Socket编程很有趣呢?因为它允许应用于其他Internet上的机器通信,而这些比本地操作更有潜力。

有两种主要的库用于Socket编程: gen_tcp 用于TCP编程、 gen_udp 用于UDP编程。

在本章,我们看看如果使用TCP和UDP socket编写客户端和服务器。我们将会尝试多种形式的服务器(并行、串行、阻塞、非阻塞)并且看看通信接口应用如何将数据流传递给其他应用。

1   使用TCP

我们学习Socket编程的历险从一个从服务器获取TCP数据的程序开始。然后我们会写一个简单的串行TCP服务器展示如何并行的处理多个并发会话。

1.1   从服务器获取数据

我们先写一个小函数(标准库的 http:request(Url) 实现相同的功能,但是这里是演示TCP的)来看看TCP socket编程获取 http://www.google.com 的HTML页面:

nano_get_url() ->
    nano_get_url("www.google.com").

nano_get_url(Host) ->
    {ok,Socket}=gen_tcp:connect(Host,80,[binary,{packet,0}]),
    ok=gen_tcp:send(Socket,"GET / HTTP/1.0\r\n\r\n"),
    receive_data(Socket,[]).

receive_data(Socket,SoFar) ->
    receive
        {tcp,Socket,Bin} ->
            receive_data(Socket,[Bin|SoFar]);
        {tcp_closed,Socket} ->
            list_to_binary(reverse(SoFar))
    end.

它如何工作呢?

  1. 我们通过 gen_tcp:connect 在 http://www.google.com 打开TCP协议80端口。connect的参数binary告知系统以binary模式打开socket,并且以二进制方式传递数据到应用。 {packet,0} 意味着无需遵守特定格式即可将数据传递到应用。
  2. 我们调用 gen_tcp:send 并发送消息 GET / HTTP/1.0\r\n\r\n 到socket。然后我们等待响应。响应并不会一次性得到,而是分片的、分时间的。这些分片是按照一系列报文的方式接收的,并且通过打开的socket发送到进程。
  3. 我们接收一个 {tcp,Socket,Bin} 报文。第三个参数是binary。这是因为我们已经使用二进制方式打开了socket。这是从WEB服务器发到我们的一个消息报文。我们把这个报文加到分片列表并等待下一个报文。
  4. 我们接收到 {tcp_closed,Socket} 报文,这在服务器发送完所有数据时发生(这仅在HTTP/1.0时正确,现在版本的HTTP使用另外一种结束策略)。
  5. 当我们收到了所有的分片,存储顺序是错误的,所以我们重新对分片排序和连接。

我们看看他如何工作:

1> B=socket_examples:nano_get_url().
<<"HTTP/1.0 302 Found\r\nLocation: http://www.google.se/\r\n
    Cache-Control: private\r\nSet-Cookie: PREF=ID=b57a2c:TM"...>>

Note

当运行 nano_get_url 时,结果是二进制的,而你看到的则是Erlang shell以便于阅读的方式展示的。当以便于阅读方式展示时,所有控制字符都是以转义格式显示。二进制数据也会被截短,后面以省略号显示。如果想要看所有的二进制数据,你可以通过 io:format 打印或者使用 string:tokens 分片显示:

2> io:format("~p~n",[B]).
<<"HTTP/1.0 302 Found\r\nLocation: http://www.google.se/\r\n
    Cache-Control: private\r\nSet-Cookie: PREF=ID=B57a2c:TM"
    TM=176575171639526:LM=1175441639526:S=gkfTrK6AFkybT3;
    ... 还有一大堆行 ...
>>
3> string:tokens(binary_to_list(B),"\r\n").
["HTTP/1.0 302 Found",
"Location: http://www.google.se/",
"Cache-Control: private",
... 还有一大堆行 ...

这就差不多WEB客户端的工作方式(不过在浏览器中正确的显示要做更多的工作)。上面的代码只是实验的开始。你可以尝试做一些修改来下载整个网站或者读取电子邮件。可能性是无限的。

注意分片的重新组装方式如下:

receive_data(Socket,SoFar) ->
    receive
        {tcp,Socket,Bin} ->
            receive_data(Socket,[Bin|SoFar]);
        {tcp_closed,Socket} ->
            list_to_binary(reverse(SoFar))
    end.

每当我们收到分片时,就把他们加入SoFar这个列表的头部。当收到了所有分片时,Socket就关闭了,我们颠倒顺序,并且把所有分片组合起来。

你可能以为收到分片后立即合并会好些:

receive_data(Socket,SoFar) ->
    receive
        {tcp,Socket,Bin} ->
            receive_data(Socket,list_to_binary([SoFar,Bin]));
        {tcp_closed,Socket} ->
            SoFar
    end.

这段代码是正确,但是效率比较低。因为后一种版本不断的把新的二进制数据加到缓冲区后面,也就是包含了多个数据的拷贝的。一个好办法是累积所有分片,尽管顺序是相反的,然后反序整个列表并一次连接所有分片。

1.2   一个简单的TCP服务器

在前一节,我们写了一个简单的客户端。现在我们写个服务器。

服务器开启2345端口然后等待一个消息。这个消息是包含Erlang术语的二进制数据,这个术语是包含Erlang表达式的字符串。服务器对该表达式求值并且将结果通过socket发到客户端。

Note

如何编写WEB服务器

编写WEB客户端或服务器是很有趣的。当然,有些人已经写好这些了,但是如果想要真正理解他们的工作原理,研究底层实现还是很有意义的。谁知道呢,说不定我们写的WEB服务器更好。所以我们看看如何做吧?

想要构建一个WEB服务器,任何一个需要实现标准的Internet协议,我们需要使用正确的工具和了解协议实现。

在我们的例子用来抓取一个WEB页,我们如何知道已经正确打开了80端口,并且如何知道已经发送了 GET / HTTP/1.0\r\n\r\n 到服务器?答案很简单。所有主要协议都已经在RFC文档中有描述。HTTP/1.0定义于RFC1945,所有RFC的官方网站是 http://www.letf.org

另外一个非常有用的方法是抓包。通过一个数据包嗅探器,我们可以抓取和分析所有IP数据包,无论是应用程序发出的还是接收的。大多数嗅探器包含解码器和分析器可以得出数据包的内容和格式。一个著名的嗅探器是Wireshark(以前叫Ethereal),可以到 http://www.wireshark.org/ 了解更多。

使用嗅探器和RFC武装起来的我们,就可以准备编写下一个杀手级应用了。

编写这个程序(或者其他使用TCP/IP的程序),需要响应一些简单的请求:

  • 数据如何组织,知道数据如何组成请求或者响应?
  • 数据在请求和响应中如何编码(encode & marshal)和解码(decode & demarshal)

TCP socket数据只是没有格式的字节流。在传输时,数据会切成任意长度的分片,所以我们需要多少数据如何组成请求或响应。

在Erlang的例子,我们使用简单的转换,把每个逻辑请求或响应前加上N(1/2/4)字节的长度数。这就是 {packet,N} (这里的packet表示一个应用程序请求或响应报文,而不是电线里面的物理包) 参数在 gen_tcp:connect 和 gen_tcp:listen 函数的意义。注意packet附带的那个参数在客户端和服务器端必须商定好。如果服务器使用 {packet,2} 而客户端使用 {packet,4} 则会出错。

在我们以 {packet,N} 选项打开socket后,我们就不需要担心数据分片了。Erlang驱动会自动确保数据报文的所有分片都收到并且长度正确时才发到应用程序。

另一个需要注意的是数据编码和解码。最简单时,我们可以用 term_to_binary 来对Erlang术语编码,并使用 binary_to_term 来解码数据。

注意,客户端和服务器通信的包转换和编码规则是两行代码完成,分别使用 {packet,4} 来打开socket和使用 term_to_binary 和其反函数完成编码和解码数据。

我们可以简单的打包和编码Erlang术语到基于文本的协议如HTTP或XML。使用Erlang的 term_to_binary 和其反函数可以比基于XML等文本的协议性能高出一个数量级。现在我们先看看一个简单的服务器:

start_nano_server() ->
    {ok,Listen}=gen_tcp:listen(2345,[binary,{packet,4},
                                            {reuseaddr,true},
                                            {active,true}]),
    {ok,Socket}=gen_tcp:accept(Listen),
    gen_tcp:close(Listen),
    loop(Socket).

loop(Socket) ->
    receive
        {tcp,Socket,Bin} ->
            io:format("Server received binary = ~p~n",[Bin]),
            Str=binary_to_term(Bin),
            io:format("Server (unpacked) ~p~n",[Str]),
            Reply=lib_misc:string2value(Str),
            io:format("Server replying = ~p~n",[Reply]),
            gen_tcp:send(Socket,term_to_binary(Reply)),
            loop(Socket);
        {tcp_closed,Socket} ->
            io:format("Server socket closed~n")
    end.

它如何工作?

  1. 首先,我们调用 gen_tcp:listen 来监听2345端口,并且设置报文转换格式为 {packet,4} ,意味着每个包有4个字节的包头,代表长度。然后 gen_tcp:listen(..) 会返回 {ok,Socket} 或者 {error,Why} ,但是我们先看看成功的情况。所以写下如下代码 {ok,Listen}=gen_tcp:listen(...), 这在程序返回 {error,…} 时发生匹配错误。如果成功则会绑定Listen到正在监听的socket。对于正在监听的socket,我们只需要做一件事,就是使用它做参数调用 gen_tcp:accept 。
  2. 现在我们调用 gen_tcp:accept(Listen) 。在这里,程序会挂起以等待连接。当我们获得连接时,这个函数返回已经绑定的Socket,这个socket就是可以与客户端连接并且可以通信的了。
  3. 当 gen_tcp:accept 返回,我们立即调用 gen_tcp:close(Listen) 。这就关闭了监听的socket,服务器也就不会继续接受新的连接了。而这不会影响已有的连接,只是针对新连接。
  4. 解码输入数据
  5. 对字符串求值
  6. 编码返回数据并且通过socket发送

注意,这个程序只接受一个请求,程序运行完成后就不会再接受其他请求了。

这是一个非常简单的服务器展示了如何打包和编码应用数据。接收请求,计算响应,发出响应,然后结束。

想要测试这个服务器,我们需要一个对应的客户端:

nano_client_eval(Str) ->
    {ok,Socket}=get_tcp:connect("localhost",2345,[binary,{packet,4}]),
    ok=gen_tcp:send(Socket,term_to_binary(Str)),
    receive
        {tcp,Socket,Bin} ->
            io:format("Client received binary = ~p~n",[Bin]),
            Val=binary_to_term(Bin),
            io:format("Client result = ~p~n",[Val]),
            gen_tcp:close(Socket)
    end.

想要测试你的代码,我们需要在一台机器上同时启动客户端和服务器。所以在 gen_tcp:connect 中的hostname参数就可以用硬编码的 localhost 。

注意客户端和服务器端使用的 term_to_binary 和 binary_to_term 怎样编码和解码数据。

想要运行,我们需要开两个终端然后启动Erlang shell。

首先,我们启动服务器:

1> socket_examples:start_nano_server().

我们看不到任何输出,当然什么也没发生呢。然后我们在另一个终端启动客户端,输入如下命令:

1> socket_examples:nano_client_eval("list_to_tuple([2+3*4,10+20])").

在服务器端的窗口尅看到如下输出:

Server received binary = <<131,107,0,28,108,105,115,116,95,116,
                           111,95,116,117,112,108,101,40,91,50,
                           43,51,42,52,44,49,48,43,50,48,93,41>>
Server (unpacked)  "list_to_tuple([2+3*4,10+20])"
Server replying = {14,30}

在客户端窗口可以看到:

Client received binary = <<131,104,2,97,14,97,30>>
Client result = {14,30}
ok

最后,在服务器窗口看到:

Server socket closed

1.3   改进服务器

前一节我们构造了一个服务器可以接受一个请求并且终止。简单的修改代码,我们可以就可以完成另一个不同类型的服务器:

  1. 序列服务器,同一时间只接受一个请求
  2. 并行服务器,同一时间可以接受多个请求

原始启动代码如下:

start_nano_server() ->
    {ok,Listen} = gen_tcp:listen(...),
    {ok,Socket} = gen_tcp:accept(Listen),
    loop(Socket).
...

我们将会以此为基础完成另外两种服务器。

1.4   序列服务器

想要构造序列服务器,我们如下改变了代码:

start_seq_server() ->
    {ok,Listen} = gen_tcp:listen(...),
    seq_loop(Listen).

seq_loop(Listen) ->
    {ok,Socket} = gen_tcp:accept(Listen),
    loop(Socket),
    seq_loop(Listen).

loop(...) -> %%同以前的一样处理

这会让以前的例子在需要多个请求时工作的很好。我们一直让监听的socket开着而不是关闭。另一个不同是在 loop(Socket) 结束后,我们再次调用 seq_loop(Listen) ,以便接受后面的连接请求。

如果客户端在服务器忙于处理一个已经存在的连接时尝试连接服务器,连接请求会被缓存,直到服务器完成那个已经存在的请求。如果缓存的连接数大于监听backlog,连接会被拒绝。

我们只是展示了如何开启服务器。关闭服务器很简单(停止并行服务器也一样),只要kill掉启动服务器的进程即可。 gen_tcp 连接他本身到控制进程,如果控制进程死掉了,他也就会关闭socket。

1.5   并行服务器

构造并行服务器的秘诀是每次接受新的连接以后马上用 spawn 立即生成一个新的进程:

start_parallel_server() ->
    {ok,Listen} = gen_tcp:listen(...),
    spawn(fun() -> par_connect(Listen) end).

par_connect(Listen) ->
    {ok,Socket} = gen_tcp:accept(Listen),
    spawn(fun() -> par_connect(Listen) end),
    loop(Socket).

loop(...) -> %%同上

这段代码与串行服务器类似。重要的不同在于 spawn ,可以确保为每个连接创建一个进程。现在可以对比两种服务器了,可以看到他是如何将一个串行服务器转变为并行服务器的。

所有这三种服务器调用 gen_tcp:listen 和 gen_tcp:accept 不同在于如何调用函数是并行方式还是串行方式。

Note

知识点:

  • 创建socket的进程(调用 gen_tcp:accept 或者 gen_tcp:connect )叫做这个socket的 控制进程 。所有来自于socket的消息都会被发送到控制进程;如果控制进程死掉了,对应的socket就会被关闭。可以修改一个socket的控制进程为NewPid,通过 gen_tcp:controlling_process(Socket,NewPid) 。

  • 我们的并行服务器可能会随着连接而创建数千个连接。我们可能希望限制最大并发连接数。这可以通过一个活动连接计数器来实现。每次获得一个新连接时就增加1,而在一个连接完成时减少1。可以用这种机制显示并发连接数。

  • 在我们接受连接后,最好明确的设置请求socket选项,如下:

    {ok,Socket} = gen_tcp:accept(Listen),
    inet:setopts(Socket,[{packet,4},binary,{nodelay,true},{active,true}]),
    loop(Socket)
  • 在Erlang R11B-3中,几个Erlang进程可以在同一个监听中的socket调用 gen_tcp:accept/1 。这也是一种建立并发服务器的简单方式,因为你尅拥有预分配的进程池,等待 gen_tcp:accept/1 。

2   控制问题

Erlang中socket可以以3种模式打开:active、active once、passive。其设置可以通过 gen_tcp:connect(Address,Port,Options) 或 gen_tcp:listen(Port,Options) 中的Options参数来设置为 {active,true|false|once} 。

如果指定了 {active,true} ,就会创建一个主动(active)的socket; {active,false} 会创建一个被动的(passive)的socket; {active,once} 创建主动的socket,但是只接受一条消息,接收到消息后,必须手动重新开启(reenable)才能继续接受消息。

我们看看在不同地方使用的区别。

active和passive的socket的区别在于消息到来时的处理方式:

  • 一旦一个active的socket被创建了,控制进程会发送收到的数据,以 {tcp,Socket,Data} 消息的形式。而控制进程无法控制消息流。一个无赖的客户端可以发送无数的消息到系统,而这些都会被发送到控制进程。而控制进程无法停止这个消息流。
  • 如果socket在passive模式,控制进程需要调用 gen_tcp:recv(Socket,N) 来获取数据。它会尝试获取N字节的数据,如果N=0,就会尽可能的拿到所有可以取得的数据。这种情况下,服务器尅通过选择是否调用 gen_tcp:recv 来控制消息流。

被动模式的socket用于控制发送到服务器的数据流。为了举例,我们可以以3种方式编写消息接收循环:

  • 主动消息获取(非阻塞)
  • 被动消息获取(阻塞)
  • 混合方式获取(部分阻塞)

2.1   主动消息获取(非阻塞)

第一个例子是以主动模式打开socket,然后接受来自socket的数据:

{ok,Listen} = gen_tcp:listen(Port,[...,{active,true}...]),
{ok,Socket} = gen_tcp:accept(Listen),
loop(Socket).

loop(Socket) ->
    receive
        {tcp,Socket,Data} ->
            ... 输出处理 ...
        {tcp_closed,Socket} ->
            ...
    end.

这个过程无法控制发到服务器循环的消息流,如果客户端产生数据的速度大于服务器消费数据的速度,系统就会收到洪水般地消息-消息缓冲区溢出,系统将会crash并表现怪异。

这种类型的服务器叫做非阻塞服务器,因为它无法阻塞客户端。我们仅在信任客户端的情况下才会使用非阻塞服务器。

2.2   被动消息获取(阻塞)

在这一节,我们写阻塞服务器:服务器以被动模式打开socket,通过 {active,false} 选项。这个服务器不会被危险的客户端洪水袭击。

服务器循环中的代码调用 gen_tcp:recv 来接收数据。客户端在服务器调用 recv 之前会被阻塞。注意OS会对客户端发来的数据做一下缓冲,以允许客户端在服务器调用 recv 之前仍然可以继续发送一小段数据。

{ok,Listen} = gen_tcp:listen(Port,[...,{active,false}...]),
{ok,Socket} = gen_tcp:accept(Listen),
loop(Socket).

loop(Socket) ->
    case gen_tcp:recv(Socket,N) of
        {ok,B} ->
            ... 数据处理 ...
            loop(Socket);
        {error,closed}
            ...
    end.

2.3   混合消息获取(部分阻塞)

你可能认为把被动模式用到所有服务器上都合适。不幸的是,当我们在被动模式时,我们只能等待来自于一个socket的数据。这对于需要等待多个socket来源数据的服务器则不适用。

幸运的是我们可以用混合方式,既不是阻塞的也不是非阻塞的。我们以一次主动(active once)模式 {active,once} 打开socket。在这个模式中,socket是主动的,但是只能接收一条消息。在控制进程发出一条消息之后,他必须明确的调用 inet:setopts 以便让socket恢复并接收下一条消息。系统在这发生之前会一直阻塞。这是两种世界的最好结合点。如下是代码:

{ok,Listen} = gen_tcp:listen(Port,[...,{active,once}...]),
{ok,Socket} = gen_tcp:accept(Listen),
loop(Socket).

loop(Socket) ->
    receive
        {tcp,Socket,Data} ->
            ... 数据处理 ...
            %%准备好启用下一条消息时
            inet:setopts(Socket,[{active,once}]),
            loop(Socket);
        {tcp_closed,Socket} ->
            ...
    end.

使用 {active,once} 选项,用户可以实现高层次的数据流控制(有时叫交通管制),同时又防止了服务器被过多的消息洪水所淹没。

3   连接从哪里来?

假设我们建立了某种在线服务器,而却一直有人发垃圾邮件。那么我们需要做的第一件事就是确定他的来源。想要发现这件事,我们可以使用 inet:peername(Socket) 。

@spec inet:peername(Socket) -> {ok,{IP_Address,Port}} | {error,Why}

返回连接另一端的IP地址和端口号,以便服务器找到对方的地址。IP_Address是一个元组的整数形如 {N1,N2,N3,N4} ,而 {K1,K2,K3,K4,K5,K6,K7,K8} 则是IPv6的地址。这里的整数取值范围是0到255。

4   Socket的错误处理

socket的错误处理是非常简单的,基本上你不需要做任何事情。犹如前面讲过的,每个socket拥有控制进程(就是创建socket的进程)。如果控制进程死掉了,那么socket也会自动关闭。

这意味着,如果我们有,例如,一个客户端和服务器,而服务器因为编程错误死掉了。那么服务器拥有的socket会自动关闭,而客户端会收到 {tcp_closed,Socket} 消息。

我们可以通过如下的小程序测试这个机制:

error_test() ->
    spawn(fun() -> error_test_server() end),
    lib_misc:sleep(2000),
    {ok,Socket} = gen_tcp:connect("localhost",4321,[binary,{packet,2}]),
    io:format("connected to: ~p~n",[Socket]),
    gen_tcp:send(Socket,<<"123">>),
    receive
        Any ->
            io:format("Any=~p~n",[Any])
    end.

error_test_server() ->
    {ok,Listen} = gen_tcp:listen(4321,[binary,{packet,2}]),
    {ok,Socket} = gen_tcp:accept(Listen),
    error_test_server_loop(Socket).

error_test_server_loop(Socket) ->
    receive
        {tcp,Socket,Data} ->
            io:format("received:~p~n",[Data]),
            atom_to_list(Data),
            error_test_server_loop(Socket)
    end.

当我们运行它时,会看到如下输出:

1> socket_examples:error_test().
connected to:#Port<0.152>
received:<<"123">>
=ERROR REPORT==== 9-Feb-2007::15:18:15 ===
Error in process <0.77.0> with exit value:
 {badarg,[{erlang,atom_to_list,[<<3 bytes>>]},
 {socket_examples,error_test_server_loop,1}]}
Any={tcp_closed,#Port<0.152>}
ok

我们生成了一个服务器,并让它每两秒有一次启动的机会,并且发送包含二进制数据 <<"123">> 的消息。当这个消息到达服务器时,服务器会尝试计算 atom_to_list(Data) ,因为Data是一个二进制数据,所以会立即出错(系统监控器会发现并显示错误)。现在服务器端的控制进程已经死掉了,而且其socket也关闭了。客户端会收到 {tcp_closed,Socket} 消息。

5   UDP

现在我们看看UDP协议(User Datagram Protocol,用户数据报协议)。使用UDP,互联网上的机器之间可以互相发送小段的数据,叫做数据报。UDP数据报是不可靠的,这意味着如果客户端发送一系列的UDP数据报到服务器,收到的数据报顺序可能是错误的。不过收到的数据报肯定是正确的。大的数据报会被分为多个小的分片,IP协议负责重新组装这些分片,并最终交付给应用。

UDP是无连接的协议,这意味着客户端无需连接服务器即可发送消息。这也意味着程序更加适于大量客户端收发小的消息报文。

在Erlang中编写UDP客户端和服务器比TCP时更简单,因为我们无需管理连接。

5.1   简单的UDP服务器和客户端

首先,我们看看服务器,一个通用的服务器样式如下:

server(Port) ->
    {ok,Socket} = gen_udp:open(Port,[binary]),
    loop(Socket).

loop(Socket) ->
    receive
        {udp,Socket,Host,Port,Bin} ->
            BinReply = ... ,
            gen_udp:send(Socket,Host,Port,BinReply),
            loop(Socket)
    end.

这里比TCP协议的例子更简单,因为我们至少不需要关心连接关闭的消息。注意我们以二进制方式打开socket,驱动也会以二进制数据的形式将报文发送到应用。

注意客户端。这里有个简单的客户端。它仅仅打开UDP socket,发送消息到服务器,等待响应(或超时),然后关闭socket并返回从服务器接收到的值。

client(Request) ->
    {ok,Socket} = gen_udp:open(0,[binary]),
    ok = gen_udp:send(Socket,"localhost",4000,Request),
    Value = receive
                {udp,Socket,_,_,Bin} ->
                    {ok,Bin}
                after 2000 ->
                    error
                end,
    gen_udp:close(Socket),
    Value

我们必须拥有一个超时,否则UDP的不可靠会让我们永远得不到响应。

5.2   一个UDP阶乘服务器

我们可以很容易的构造一个UDP的阶乘服务器。代码模仿前一节。

-module(upd_test).
-export([start_server/0,client/1]).

start_server() ->
    spawn(fun() -> server(4000) end).

%% 服务器
server(Port) ->
    {ok,Socket}=gen_udp:open(Port,,[binary]),
    io:format("server opened socket:~p~n",[Socket]),
    loop(Socket).

loop(Socket) ->
    receive
        {udp,Socket,Host,Port,Bin} =Msg ->
            io:format("server received:~p~n",[Msg]),
            N=binary_to_term(Bin),
            Fac=fac(N),
            gen_udp:send(Socket,Host,Port,term_to_binary(Fac)),
            loop(Socket)
    end.

fac(0) -> 1;
fac(N) -> N*fac(N-1).

%% 客户端
client(N) ->
    {ok,Socket} = gen_upd:open(0,[binary]),
    io:format("client opened socket=~p~n",[Socket]),
    ok=gen_udp:send(Socket,"localhost",4000,term_to_binary(N)),
    Value=receive
        {udp,Socket,_,_,Bin}=Msg ->
            io:format("client received:~p~n",[Msg]),
            binary_to_term(Bin)
        after 2000 ->
            0
        end,
    gen_udp:close(Socket),
    Value

注意我增加了一些打印语句,所以我们可以看到程序执行的过程。我一般是开发阶段加很多打印语句,而在工作正常后就注释掉了。

现在让我们运行例子,首先启动服务器:

1> udp_test:start_server().
server opened socket:#Port<0.106>
<0.34.0>

这会在后台运行,所以我们发出一个客户端请求:

2> udp_test:client(40).
client opened socket=#Port<0.105>
server received:{udp,#Port<0.106>,{127,0,0,1},32785,<<131,97,40>>}
client received:{udp,#Port<0.105>,
                  {127,0,0,1},4000,
                  <<131,110,20,0,0,0,0,0,64,37,5,255,
                    100,222,15,8,126,242,199,132,27,
                    232,234,142>>}
815915283247897734345611269596115894272000000000

5.3   UDP的附加注释

我们必须注意的是UDP是无连接的协议,也就四海服务器无法拒绝客户端发送数据,甚至不知道客户端是谁。

大个的UDP报文会被切分成多个分片分别在网络上传输。分片发生在数据报长度大于最大传输单元(MTU)时,以确保通过路由器等网络设备以后仍然可以到达。一般的测量方法是开始于一个足够小的包(比如500字节),然后逐渐增加,直到发现MTU为止。如果在某一点发现数据报被丢弃了,那么,你就直到可以传输的最大报文长度了。

一个UDP数据报可以被传输两次,所以你必须小心的编码以防备这个事。因为他可能会对同一个请求的第二次出现而再做一次响应。想要防止,我们可以修改客户端代码来在每个请求中加一个唯一引用,并且检查响应中的这个唯一引用。想要生成一个唯一引用,我们可以用Erlang BIF的 make_ref ,就会生成一个全局唯一引用。远程过程调用现在可以这样写:

client(Request) ->
    {ok,Socket} = gen_udp:open(0,[binary]),
    Ref=make_ref(),
    B1=term_to_binary({Ref,Request}),
    ok=gen_udp:send(Socket,"localhost",4000,B1),
    wait_for_ref(Socket,Ref).

wait_for_ref(Socket,Ref) ->
    receive
        {udp,Socket,_,_,Bin} ->
            case binary_to_term(Bin) of
                {Ref,Val} ->
                    Val;
                {_SomeOtherRef,_} ->
                    wait_for_ref(Socket,Ref)
            end;
    after 1000 ->
        ...
    end.

6   向多台计算机广播

这里会展示如何构造一个广播通道。这些情况用到的很少,不过也许某天你可能会用到。

-module(broadcast).
-compile(export_all).

send(IoList) ->
    case inet:ifget("eth0",[broadaddr]) of
        {ok,[{broadaddr,Ip}]} ->
            {ok,S}=gen_udp:open(5010,[{broadcast,true}]),
            gen_udp:send(S,Ip,6000,IoList),
            gen_udp:close(S);
        _->
            io:format("Bad interface name, or\n"
                        "broadcasting not supported\n")
    end.

listen() ->
    {ok,S}=gen_udp:open(6000),
    loop(S).

loop(S) ->
    receive
        Any ->
            io:format("received:~p~n",[Any]),
            loop(S)
    end.

这里我们需要两个端口,一个用于发送广播,而另一个用来监听响应。我们选择5010用于发送请求而6000用于监听广播(这两个端口号没有任何联系,可以随便选择)。

一旦进程开启了端口5010用于发送广播,所有在网络里面的机器都可以调用 broadcast:listen() ,来打开端口6000监听广播消息。

broadcast:send(IoList) 广播 IoList 到所有本地网络的机器。

Note

为了实现这个,网络接口必须是正确的,而且必须支持广播。在我的iMac,例如,我使用了名字”en0″代替了”eth0″。同时注意,如果机器在不同的子网上面监听UDP报文,那么也无法送达,因为缺省的路由会丢弃UDP广播。

7   一个SHOUT广播服务器

作为本章的结束,我将会使用一个新的技巧来编写SHOUTcast服务器。SHOUTcast是由folks在Nullsoft开发的音频流数据协议( http://www.shoutcast.com/ )。SHOUTcast使用HTTP发送MP3-或者AAC-编码的音频数据。

想要看看他们如何工作,我们首先看看SHOUTcast协议。然后我们会看看整个服务器的结构。最后是代码。

7.1   SHOUTcast协议

SHOUTcast协议非常简单:

  1. 首先,客户端(如xmms、winamp或iTunes)发送HTTP请求到SHOUTcast服务器。这里有个xmms生成的请求:

    GET / HTTP/1.1
    Host: localhost
    User-Agent: xmms/1.2.10
    Icy-MetaData: 1
  2. 我的SHOUTcast服务器响应如下:

    ICY 200 OK
    icy-notice1: <BR>This stream requires <a href=http://www.winamp.com/>;Winamp</a><BR>
    icy-notice2: Erlang Shoutcast server<BR>
    icy-name: Erlang mix
    icy-genre: Pop Top 40 Dance Rock
    icy-url: http://localhost:3000
    content-type: audio/mpeg
    icy-pub: 1
    icy-metaint:24576
    icy-br: 96
    ... data ...
  3. 现在SHOUTcast服务器可以发送持续的数据流了,有如如下结构:

    FHFHFHF ...

F是正好是24576字节的MP3音乐数据(通过 icy-metaint 参数指定)。H是数据头块,由单一字节的K,随后加上16*k的数据组成。这样,最小的数据头块就是 <<0>> 。下一个数据头块则是:

<<1,B1,B2,...,B16>>

数据部分的内容则是字符串形式的 StreamTitle='...';StreamUrl='http://...'; ,剩余空间用0补满。

7.2   SHOUTcast服务器如何工作

构造服务器必须注意如下细节:

  1. 制作播放列表。我们的服务器使用一个文件来做,参见13.2节,读取ID3标签在232页。随即选取播放列表中的文件。
  2. 制作一个并行服务器,可以同时供应多个数据流。使用14.1节的并行服务器。
  3. 对每个音频文件,只发送音频数据而不发送内嵌的ID3标签。移除标签,使用 id3_tag_lengths ,已经在13.2节开发完成了。读取ID3标签在5.3节。寻找同步帧在92页。这些代码没有在这里。

7.3   SHOUTcast服务器伪码

看整个程序之前先看看不包含细节的工作流程:

start_parallel_server(Port) ->
    {ok,Listen}=gen_tcp:listen(Port,...),
    %%创建歌曲服务器
    PidSongServer=spawn(fun() -> songs() end),
    spawn(fun() -> par_connect(Listen,PidSongServer) end).

%%对每个连接生成一个进程
par_connect(Listen,PidSongServer) ->
    {ok,Socket}=gen_tcp:accept(Listen),
    spawn(fun() -> par_connect(Listen,PidSongServer) end),
    inet:setopts(Socket,[{packet,0},binary,{nodelay,true},
                         {active,true}]),
    get_request(Socket,PidSongServer,[]).

%%等待TCP请求
gen_request(Socket,PidSongServer,L) ->
    receive
        {tcp,Socket,Bin} ->
            ... 如果请求未完成则继续等待请求
            .... got_request(Data,Socket,PidSongServer)
        {tcp_closed,Socket} ->
            ... 在客户端中断时发生
    end.

%%获取了请求,发送响应
got_request(Data,Socket,PidSongServer) ->
    ... 分析请求数据
    gen_tcp:send(Socket,[response()]).
    play_songs(Socket,PidSongServer).

%%持续发送歌曲到客户端
play_songs(Socket,PidSongServer) ->
    ... PidSongServer维护着所有MP3文件的列表
    Song=rpc(PidSongServer,random_song),
    Header=make_header(Song),
    {ok,S}=file:open(File,[read,binary,raw]),
    send_file(1,S,Header,1,Socket),
    file:close(S),
    play_songs(Socket,PidSongServer).

send_file(K,S,Header,OffSet,Socket) ->
    ... 发送文件块到客户端
    ... 全部发送完成时返回

如果查看真实代码,你可以看到细节只有略微不同而已,机制是相同的,如下是完整代码。

-module(shout).

%% 在一个窗口中 > shout:start().
%% 另一个窗口 xmms http://localhost:3000/stream

-export([start/0]).
-import(lists,[map/2,reverse/1]).

-define(CHUNKSIZE,24576).

start() ->
    spawn(fun() ->
            start_parallel_server(3000),
            %% 现在开始睡眠,否则等待的socket会被关闭
            lib_misc:sleep(infinity)
        end).

start_parallel_server(Port) ->
    {ok,Listen}=gen_tcp:listen(Port,[binary,{packet,0},
            {reuseaddr,true},
            {active,true}]),
    PidSongServer=spawn(fun() -> songs() end),
    spawn(fun() -> par_connect(Listen,PidSongServer) end).

par_connect(Listen,PidSongServer) ->
    {ok,Socket}=gen_tcp:accept(Listen),
    spawn(fun() -> par_connect(Listen,PidSongServer) end),
    inet:setopts(Socket,[{packet,0},binary,{nodelay,true},{active,true}]),
    get_request(Socket,PidSongServer,[]).

get_request(Socket,PidSongServer,L) ->
    receive
        {tcp,Socket,Bin} ->
            L1=L ++ binary_to_list(Bin),
            %% 切割请求头检查
            case split(L1,[]) of
                more ->
                    %% 请求头未完成,需要更多数据
                    get_request(Socket,PidSongServer,L1);
                {Request,_Rest} ->
                    %% 头部完成
                    got_request_from_client(Request,Socket,PidSongServer)
            end;
        {tcp_closed,Socket} ->
            void;
        _Any ->
            %% 跳过
            get_request(Socket,PidSongServer,L)
    end.

split("\r\n\r\n" ++ T,L) -> {reverse(L),T};
split([H|T],L) -> split(T,[H|L]);
split([],_) -> more.

got_request_from_client(Request,Socket,PidSongServer) ->
    Cmds=string:tokens(Request,"\r\n"),
    Cmds1=map(fun(I) -> string:tokens(I," ") end,Cmds),
    is_request_for_stream(Cmds1),
    get_tcp:send(Socket,[response()]),
    play_songs(Socket,PidSongServer,<<>>).

play_songs(Socket,PidSongServer,SoFar) ->
    Song=rpc(PidSongServer,random_song),
    {File,PrintStr,Header}=unpack_song_descriptor(Song),
    case id3_tag_lengths:file(File) of
        error ->
            play_songs(Socket,PidSongServer,SoFar);
        {Start,Stop} ->
            io:format("Playing: ~p~n",[PrintStr]),
            {ok,S}=file:open(File,[read,binary,raw]),
            SoFar1=send_file(S,{0,Header},Start,Stop,Socket,SoFar),
            file:close(S),
            play_songs(Socket,PidSongServer,SoFar1)
    end.

send_file(S,Header,OffSet,Stop,Socket,SoFar) ->
    Need=?CHUNKSIZE - size(SoFar),
    Last=OffSet+Need,
    if
        Last>=Stop ->
            Max=Stop-OffSet,
            {ok,Bin}=file:pread(S,OffSet,Max),
            list_to_binary([SoFar,Bin]);
        true ->
            {ok,Bin}=file:pread(S,OffSet,Need),
            write_data(Socket,SoFar,Bin,Header),
            send_file(S,bump(Header),OffSet+Need,Stop,Socket,<<>>)
    end.

Warning

剩余代码主要是文件操作和socket发送数据部分,意义已经不大了。所以略掉。

7.4   运行SHOUTcast服务器

想要运行测试,我们需要做如下三步:

  1. 生成播放列表
  2. 开启服务器
  3. 用客户端连接服务器

7.5   构造播放列表

构造播放列表需要完成如下三步:

  1. 转到代码的目录

  2. 编辑函数 start1 的启动路径到文件 mp3_manager.erl 以便指向包含MP3文件的根目录。

  3. 编译 mp3_manager ,并执行 mp3_manager:start1() ,你可以看到如下输出:

    1> c(mp3_manager).
    {ok,mp3_manager}
    2> mp3_manager:start1().
    Dumping term to mp3data
    ok

如果你有兴趣,你可以看看文件 mp3data 来看分析结果。

7.6   启动SHOUTcast服务器

通过shell命令启动服务器:

1> shout:start().
...

7.7   测试服务器

  1. 到另外一个窗口启动音频播放器,定位到 http://localhost:3000
  2. 查看诊断输出。
  3. 享受音乐

8   深入挖掘

本章只是讲解了操作Socket的基本操作。你可以查阅更多关于Socket接口的文档,包括 gen_tcpgen_udpinet

erlang的regexp模块

Monday, March 31st, 2008

regexp

翻译: gashero

目录

应用于字符串的正则表达式函数。

1   导出函数

1.1   match(String,RegExp) -> MatchRes

String=RegExp=string()
MatchRes={match,Start,Length} | nomatch | {error,errordesc()}
Start=Length=integer()

在字符串String中寻找正则表达式RegExp的第一个最长的匹配。搜索最长可能匹配,如果几个结果相同则返回第一个。返回如下:

  1. {match,Start,Length} :匹配成功,返回开始和长度。
  2. nomatch :无法匹配。
  3. {error,Error} :发生错误。

1.2   first_match(String,RegExp) -> MatchRes

String=RegExp=string()
MatchRes={match,Start,Length} | nomatch | {error,errordesc()}
Start=Length=integer()

寻找第一个匹配,通常比 match 更快,并确定匹配的存在与否。返回值同 match 。

1.3   matches(String,RegExp) -> MatchRes

String=RegExp=string()
MatchRes={match,MatchRes} | {error,errordesc()}
MatchRes=list()

返回所有的不重叠匹配结果,返回如下:

  1. {match,Matches} :如果正则表达式是正确的,哪么如果没有匹配则返回空的列表。每个元素都是形如 {Start,Length} 的元组。
  2. {error,Error} :正则表达式有错。

1.4   sub(String,RegExp,New) -> SubRes

String=RegExp=New=string()
SubRes={ok,NewString,RepCount} | {error,errordesc()}
RepCount=integer

将第一个匹配成功的子字符串替换成New。字符串New中的 & 符号代表被替换掉的字符串,而 & 则代表原来的 & 符号。返回结果如:

  1. {ok,NewString,RepCount} :如果正则表达式正确,则RepCount为替换执行的次数,为0或1。
  2. {error,Error} :正则表达式有误。

1.5   gsub(String,RegExp,New) -> SubRes

基本等同于 sub ,不同在于所有的不重叠会被替换,而不仅仅是替换一次。

1.6   split(String,RegExp) -> SplitRes

String=RegExp=string()
SubRes={ok,FieldList} | {error,errordesc()}
FieldList=[string()]

通过正则表达式将字符串切割成多个字段。如果分隔字符是空格 ” ” ,则分隔字符也隐含包括TAB字符。其他分隔字符没有此效应。返回值如下:

  1. {ok,FieldList} :字符串已经被切分成各个字段了。
  2. {error,Error} :正则表达式有误。

1.7   sh_to_awk(ShRegExp) -> AwkRegExp

ShRegExp=AwkRegExp=string()
SubRes={ok,NewString,RepCount} | {error,errordesc()}
RepCount=integer()

转换sh类型的正则表达式到awk类型的正则表达式。返回转换过的字符串。sh正则表达式是给shell用于匹配文件名用的,支持如下特殊字符:

    • :匹配任何数量任何字符
  1. ? :匹配单一任意字符
  2. […] :匹配范围内的字符,字符范围用符号 – 来分隔。如果第一个字符是 ! 则是相反的匹配。

尽管sh正则表达式并不强大,但在大多数时候却很好用。

1.8   parse(RegExp) -> ParseRes

RegExp=string()
ParseRes={ok,RE} | {error,errordesc()}

转换正则表达式字符串到可供其他正则表达式函数使用的内部格式。可以在调用其他函数时替换正则表达式的位置。这对于同一个正则表达式需要使用多次时非常有效。返回值:

  1. {ok,RE} :匹配成功则返回内部表示法。
  2. {error,Error} :正则表达式有误。

1.9   format_error(ErrorDescription) -> Chars

ErrorDescriptor=errordesc()
Chars=[char() | Chars]

在匹配失败时返回匹配错误的描述信息。

2   正则表达式

这里提到的正则表达式知识 egrep 和AWK语言中的子集。他们由如下字符组成:

c 非特殊意义的字母c
\c 匹配转码序列或字面上的c
. 匹配任意字符
^ 字符串开头
$ 字符串结尾
[abc…] 字符类,即指定字符组成的集合。字符范围是两个字符用 – 连接
[^abc…] 否定字符类,不匹配集合中的字符
r1 | r2 轮流,匹配r1或r2
r1r2 串联,匹配r1并且r2
r+ 匹配一个或更多的r
r* 匹配零个或多个的r
r? 匹配零个或一个的r
分组,匹配r

转码序列允许等同于Erlang字符串:

\b 退格
\f 换页(form feed)
\n 换行(line feed)
\r 回车
\t TAB
\e escape ESC
\v 纵向TAB
\s 空格
\d 删除
\ddd 八进制值ddd
\c 任何除了上面字符以外的,如\或”

可以让这些函数工作的更方便,比如在 io:get_line 中读取新行,当然字符 $ 也会匹配 “…n” 。如下例子时Erlang一些数据类型的正则表达式:

Atoms [a-z][0-9a-zA-Z_]*
Variables [A-Z_][0-9a-zA-Z_]*
Floats (\+|-)?[0-9]+\.[0-9]+((E|e)(\+|-)?[0-9]+)?

正则表达式是以Erlang字符串来编写的。这意味着字符 \ 或 ” 必须以转码方式来书写。例如浮点数的正则表达式就是: (\\+|-)?[0-9]+\\.[0-9]+((E|e)(\\+|-)?[0-9]+)?

正则表达式并不是一定要有转义序列字符的,他们可以自动生成。除了用在不同的地方,否则与普通的Erlang字符串是一样的。

3   作者

Robert Virding – support@erlang.ericsson.se

erlang的string模块

Monday, March 31st, 2008

string

翻译: gashero

目录

字符串处理函数库。

1   导出函数

1.1   len(String) -> Length

String=string()
Length

返回字符串的字符数。

1.2   equal(String1,String2) -> bool()

String1=String2=string()

测试两个字符串是否相等,如果相等返回 true ,不相等返回 false

1.3   concat(String1,String2) -> String3

String1=String2=String3=string()

连接两个字符串成为新的字符串,返回新的字符串。

1.4   chr(String,Character) -> Index

String=string()
Character=char()
Index=integer()

返回一个字符串中某个字符第一次出现的位置,如果不存在则返回0。

函数 rchr 拥有相同参数,但是从右侧开始计算。

1.5   str(String,SubString) -> Index

String=SubString=string()
Index=integer()

返回子串匹配位置,未匹配则返回0。例如:

> string:str(" Hello Hello World World ", "Hello World").
8

函数 rstr 拥有相同参数,但是从右侧开始计算。

1.6   span(String,Chars) -> Length

String=Chars=string()
Length=integer()

返回String匹配Chars中最多字符长度,从前开始。例如:

> string:span("\t    abcdefg"," \t").
5
> string:cspan("\t    abcdefg"," \t").
0

函数 cspan 则是取从前开始第一个匹配时前面不匹配的部分。后面的Chars可以包含多个字符用于匹配。

1.7   substr(String,Start[,Length]) -> SubString

String=SubString=string()
Start=Length=integer()

取得字符串的子字符串,可以指定开始处和长度,长度可省略。例如:

> string:substr("Hello World",4,5).
"lo Wo"

1.8   tokens(String,SeparatorList) -> Tokens

String=SeparatorList=string()
Tokens=[string()]

根据分隔符号列表中的字符将字符串切割成词法符号。例如:

> string:tokens("abc defxxghix jkl","x ").
["abc","def","ghi","jkl"]

1.9   chars(Character,Number[,Tail]) -> String

Character=char()
Number=integer()
String=string()

返回包含指定数目个字符的字符串,可选的指定随后跟着的字符串Tail。

1.10   copies(String,Number) -> Copies

String=Copies=string()
Number=integer()

返回包含指定数量份复制过的字符串。

1.11   words(String[,Character]) -> Count

String=string()
Character=char()
Count=integer()

返回字符串中的单词个数,分隔符可以在第二个可选参数指定。例如:

> string:words(" Hello old boy!",$o).
4

注意分隔字符必须以美元符号开头,后面指定,如上的 $o

1.12   sub_word(String,Number[,Character]) -> Word

String=Word=string()
Character=char()
Number=integer()

返回指定位置的单词,单词间的分隔符定义同上。注意这里的位置数字是以1开始的。例如:

> string:sub_word(" Hello old boy !",3,$o).
"ld b"

1.13   strip(String[,Direction[,Character]]) -> Stripped

String=Stripped=string()
Direction=left | right | both
Character=char()

返回去掉了两端空白的字符串,可以指定方向和空白字符。 strip/1 等同于 strip(String,both) 。例如:

> string:strip("...Hello.....",both,$.).
"Hello"

1.14   left(String,Number[,Character]) -> Left

String=Left=string()
Character=char
Number=integer()

返回从左起,调整过长度为指定数字的字符串,可以指定后面跟的填充字符,默认为空格。如果字符串太长也不会被截断。例如:

> string:left("Hello",10,$.).
"Hello....."

函数 right 拥有相同的参数,只不过会将字符串右对齐。

1.15   centre(String,Number[,Character]) -> Centered

String=Centered=string()
Character=char
Number=integer()

将字符串中间对齐扩充到指定长度,不足不用用空格或指定字符填充。

1.16   sub_string(String,Start[,Stop]) -> SubString

String=SubString=string()
Start=Stop=integer()

返回字符串的子字符串,可以指定开始位置和结束位置。例如:

> string:sub_string("Hello World",4,8).
"lo Wo"

注意不同于 substr 的指定开始和长度,这个函数是指定开始和结束。

1.17   to_float(String) -> {Float,Rest} | {error,Reason}

String=string()
Float=float()
Rest=string()
Reason=no_float | not_a_list

将一个开始于浮点数的字符串转换成浮点数,剩余无法识别的会返回。例如:

> {F1,Fs}=string:to_float("1.0-1.0e-1"),
> {F2,[]}=string:to_float(Fs),
> F1+F2.
0.900000
> string:to_float("3/2=1.5").
{error,no_float}
> string:to_float("-1.5eX").
{-1.50000,"eX"}

1.18   to_integer(String) -> {Int,Rest} | {error,Reason}

String=string()
Int=integer()
Rest=string()
Reason= no_integer | not_a_list

将参数中以整数开头的字符串转换成整数和剩余部分。例如:

> {I1,Is}=string:to_integer("33+22"),
> {I2,[]}=string:to_integer(Is),
> I1-I2.
11
> string:to_integer("0.5").
{0,".5"}
> string:to_integer("x=2").
{error,no_integer}

1.19   to_lower(String) -> Result

String=Result=string()
Char=CharResult=integer()

将字符或字符串转换成大写或小写的形式。其他形式:

to_lower(Char) -> CharResult
to_upper(String) -> Result
to_upper(Char) -> CharResult

2   注意

这里面有些函数看起来有点像,这是因为 string 包是由以前的两个包合并而成,以前的函数全部都保留下来了。

正则表达式函数放在了独立的模块 regexp 中了。旧的入口点为了向后兼容也保留了,不过在未来的发行版中会取消,所以建议用户使用新的模块。

string 中没有文档化的函数不要使用。

3   作者

Robert Virding – support@erlang.ericsson.se

Torbjorn Tornkvist – support@erlang.ericsson.se

编程语言的哲学

Wednesday, March 12th, 2008

今天晚饭时,听到几个同事在谈用什么IDE,谈到了JBuilder、Eclipse、Netbean等等,提到了自动生成类模板,自动填充setter/getter等等。想想我自己,现在只用vim已经两年了。

我以前是用过一小段Java代码的,那时巨喜欢Eclipse,感觉那就是未来型的IDE,包含的功能太先进了。那个时候我也会用vim,不过除了写hello.java以外,真的没办法用。没法记忆那么多东西。

现在想来,也许我无法用vim写java代码的原因并不是vim的缺陷,毕竟有些语言是可以只用vim的,比如C、Python、Erlang。

如果一种编程语言所做的工作必须要依赖强大的IDE支持,是否从某种意义上暗示了编程语言设计的失败呢?

记不得哪本书上的牛人说过,人类可以理解的代码行数是很有限的,当然,人类可以记忆的东西也是很有限的。所以,我相信,一个糟糕的编程语言设计,加上强大的工具,其实带来的还是很低的工作效率。我在几年前放弃Delphi最直接的理由就是一个让我感到失望的编辑器,而我又没有其他选择。

最后,引用一句:世界上有三种程序员,一种用vi,一种用emacs,另一种用其他。

Programming Erlang 第16章 OTP简介(完整)

Sunday, February 24th, 2008

OTP简介

译者: gashero

目录

OTP代表Open Telecom Platform。这个名字容易让人误解,但是其实OTP比你想象的更加通用。它是一个应用操作系统和一堆函数库,用以构建大型、容错和分布式的应用。它最初是在瑞典的爱立信开发的,并且在爱立信用于构建容错系统。(爱立信已经以EPL的形式放出了Erlang,EPL是从Mozilla许可协议继承来的)

OTP包含一些有用的工具,比如一个完整的WEB服务器,一个FTP服务器,一个CORBA ORB,等等,他们都是用Erlang编写的。OTP同时包含在电信技术中H248的技术实现标准、SNMP、和ASN.1到Erlang的交叉编译器。这里先不谈这些,你可以从后面的附录C了解更多。

如果你想使用OTP来编写你的系统,你会发现OTP行为(behavior)的概念对你很有用。一个行为包装了很多动作模式-可以理解为使用参数回调的应用程序框架。

OTP的强大之处在于容错、可量度的和代码热部署等等,他们都可以通过行为来提供。在其他部分,回调作者无需担心容错的问题,因为OTP已经内置了。在Java的思想中,你可以把行为看作是J2EE容器。

简单来说,行为解决了问题的非功能性部分,而回调函数解决功能性部分。对大多数应用来说,非功能性部分基本是相似的(例如代码热部署)。而功能性部分则各个应用不同。

在本章,我们来了解这些行为, gen_server 模块更详细一些。在我们讨论 gen_server 如何工作之前,我们首先启动一个简单的服务器(我们能想象的最简单的服务器),然后一步步的改变它,直到我们得到 gen_server 模块。通过这种方法,你可以深入的了解 gen_server 的工作原理,并且了解足够多的底层细节。

这里是本章的计划:

  1. 编写一个很小的CS程序
  2. 一步步扩展程序并添加功能
  3. 放入真实的代码

1   通向通用服务器之路

Note

这将是本书中最重要的章节,所以首先阅读一遍,然后再阅读一遍,直到一百遍,确保知道了每一个细节。

我们即将开始编写我们的服务器,分别叫做server1、server2、…,每个只是与前一个略有不同。目标是从程序中提取非功能性部分。最后一句可能对你来说有点晕,不过别担心。深呼吸……

1.1   例子1:基本服务器

如下是我们的第一个尝试。这是一个小服务器,我们可以参数化我们的回调模块:

-module(server1).
-export([start/2,rpc/2]).

start(Name,Mod) ->
    register(Name,spawn(fun() -> loop(Name,Mod,Mod:init()) end)).

rpc(Name,Request) ->
    Name ! {self(), Request},
    receive
        {Name,Response} -> Response
    end.

loop(Name,Mod,State) ->
    receive
        {From,Request} ->
            {Response,State} = Mod:handle(Request,State),
            From ! {Name,Response},
            loop(Name,Mod,State)
    end.

这是一个服务器的代码精髓,我们来写server1的回调模块,如下是命名服务器回调:

-module(name_server).
-export([init/0,add/2,whereis/1,handle/2]).
-import(server1,[rpc/2]).

%% client routines
add(Name,Place) -> rpc(name_server,{add,Name,Place}).
whereis(Name) -> rpc(name_server,{whereis,Name}).

%% callback routines
init() -> dict:new().

handle({add,Name,Place},Dict) -> {ok,dict:store(Name,Place,Dict)};
handle({whereis,Name},Dict) -> {dict:find(Name,Dict),Dict}.

这段代码实际完成了两项任务。他作为一个回调模块被服务器框架代码所调用,同时它包含可以被客户端调用的接口程序。OTP的惯例是把相同功能的代码放在一个模块中。

我们想让它工作,只需要:

1> server1:start(name_server,name_server).
true
2> name_server:add(joe,"at home").
ok
3> name_server:whereis(joe).
{ok,"at home"}

现在停止思考。回调没有并发,没有spawn,没有send,没有receive,没有register。他是纯的序列代码-这意味着什么呢?这意味着我们可以可以在并不理解底层并发模块的情况下泄CS模型。

这是一个简单服务器的通用模式。一旦你理解的简单的结构,就可以让你更快的理解了。

1.2   例子2:包含事务处理的服务器

这里的服务器在遇到客户端错误查询时会挂掉:

-module(server2).
-export([start/2,rpc/2]).

start(Name,Mod) ->
    register(Name,spawn(fun() -> loop(Name,Mod,Mod:init()) end)).

rpc(Name,Request) ->
    Name ! {self(), Request},
    receive
        {Name,crash} -> exit(rpc);
        {Name,ok,Response} -> Response
    end.

loop(Name,Mod,OldState) ->
    receive
        {From,Request} ->
            try Mod:handle(Request,OldState) of
                {Response,NewState} ->
                    From ! {Name,ok,Response},
                    loop(Name,Mod,NewState)
            catch
                _:Why ->
                    log_the_error(Name,Request,Why),
                    %% 发送消息导致客户端挂掉
                    From ! {Name,crash},
                    %% 使用原始状态继续循环
                    loop(Name,Mod,OldState)
            end
    end.

log_the_error(Name,Request,Why) ->
    io:format("Server ~p request ~p ~n"
              "caused exception ~p~n",
              [Name,Request,Why]).

这个例子中使用了事务语义,在发生异常时,它使用原来的值继续循环。但是如果处理函数执行成功,则会以NewState值来继续下一次循环。

为什么要保留原来的值呢?当处理函数失败时,错误的消息来自于客户端的错误。而客户端无法处理,因为错误的请求已经让处理函数失败了。但是其他客户端希望服务器仍然可用。此外,服务器的状态在发生错误时不会改变。

注意回调模块与server1的非常像。通过改变服务器和保持回调模块不变,我们可以改变回调模块中的非功能行为。

Note

最后一个语句并不总是true。我们在从server1到server2时还是要修改一点代码,就是把 -import 语句从server1改为server2。除此之外,就没有改变了。

1.3   例子3:含有代码热交换的服务器

现在我们加上代码热交换:

-module(server3).
-export([start/2,rpc/2,swap_code/2]).

start(Name,Mod) ->
    register(Name,spawn(fun() -> loop(Name,Mod,Mod:init()) end)).

swap_code(Name,Mod) -> rpc(Name,{swap_code,Mod}).

rpc(Name,Request) ->
    Name ! {self(), Request},
    receive
        {Name,Response} -> Response
    end.

loop(Name,Mod,OldState) ->
    receive
        {From, {swap_code,NewCallBackMod}} ->
            From ! {Name,ack},
            loop(Name,NewCallBackMod,OldState);
        {From,Request} ->
            {Response,NewState} = Mod:handle(Request,OldState),
            From ! {Name,Response},
            loop(Name,Mod,NewState)
    end.

他如何工作?

我们首先交换代码消息给服务器,然后他会改变回调模块到消息中包含的新的模块。

我们可以通过启动server3来演示,然后动态交换毁掉模块。我们不能使用 name_server 作为回调模块,因为我们把服务器名硬编码到服务器中了。所以,我们需要复制一份,叫做 name_server1 ,我们还需要这么改变代码:

-module(name_server1).
-export([init/0,add/2,whereis/1,handle/2]).
-import(server3,[rpc/2]).

%%客户端程序
add(Name,Place) -> rpc(name_server,{add,Name,Place}).
whereis(Name) -> rpc(name_server,{whereis,Name}).

%%回调函数
init() -> dict:new().

handle({add,Name,Place},Dict) -> {ok,dict:store(Name,Place,Dict)};
handle({whereis,Name},Dict) -> {dict:find(Name,Dict),Dict}.

首先我们以name_server1启动server3:

1> server3:start(name_server,name_server1).
true
2> name_server:add(joe,"at home").
ok
3> name_server:add(helen,"at work").
ok

现在假设我们想要找到名字服务器中的所有名字。而没有函数提供此功能,name_server1模块只提供了增加和查询名字的功能。

所以,我们以迅雷不及掩耳盗铃之势启动了编辑器并写了新的回调模块:

-module(new_name_server).
-export([init/0,add/2,all_names/0,delete/1,whereis/1,handle/2]).
-import(server3,[rpc/2]).

%%接口
all_names() -> rpc(name_server,allNames).
add(Name,Place) -> rpc(name_server,{add,Name,Place}).
delete(Name) -> rpc(name_server,{delete,Name}).
whereis(Name) -> rpc(name_server,{whereis,Name}).

%%回调函数
init() -> dict:new().

handle({add,Name,Place},Dict) -> {ok,dict:store(Name,Place,Dict)};
handle(allNames,Dict) -> {dict:fetch_keys(Dict),Dict};
handle({delete,Name},Dict) -> {ok,dict:erase(Name,Dict)};
handle({whereis,Name},Dict) -> {dict:find(Name,Dict),Dict}.

我们编译如上模块,并且叫服务器交换回调模块:

4> c(new_name_server).
{ok,new_name_server}
5> server3:swap_code(name_server,new_name_server).
ack

现在我们可以运行服务器上的函数了:

6> new_name_server:all_names().
[joe,helen]

这次我们修改模块就是热部署的-动态代码升级,在你眼前运行的,有如魔术一般。

现在停下再想想。我们做的最后两项任务看起来很简单,但是事实上很困难。包含“事务语义”的服务器非常难写,含有代码热部署的服务器也很困难。

这个技术非常强大。传统意义上,我们认为服务器作为程序包含状态,并且在我们发送消息时改变状态。服务器代码在第一次被调用时就固定了,如果我们想要改变服务器的代码就需要重启服务器。在我们给出的例子中,服务器的代码改变起来有如我们改变状态一样容易(在软件的不停机维护升级中,我经常使用该技术)。

1.4   例子4:事务和代码热交换

在上两个服务器中,代码热升级和事务语义是分开介绍的。这里我们把它们合并到一起:

-module(server4).
-export([start/2,rpc/2,swap_code/2]).

start(Name,Mod) ->
    register(Name,spawn(fun() -> loop(Name,Mod,Mod:init()) end)).

swap_code(Name,Mod) -> rpc(Name,{swap_code,Mod}).

rpc(Name,Request) ->
    Name ! {self(), Request},
    receive
        {Name,crash} -> exit(rpc);
        {Name,ok,Response} -> Response
    end.

loop(Name,Mod,OldState) ->
    receive
        {From,{swap_code,NewCallBackMod}} ->
            From ! {Name,ok,ack},
            loop(Name,NewCallBackMod,OldState);
        {From,Request} ->
            try Mod:handle(Request,OldState) of
                {Response,NewState} ->
                    From ! {Name,ok,Response},
                    loop(Name,Mod,NewState)
            catch
                _:Why ->
                    log_the_error(Name,Request,Why),
                    From ! {Name,crash},
                    loop(Name,Mod,OldState)
            end
    end.

log_the_error(Name,Request,Why) ->
    io:format("Server ~p request ~p~n"
              "caused exception ~p~n",
              [Name,Request,Why]).

这个服务器同时提供了代码热交换和事务语义,并且很整洁。

1.5   例子5:更有趣的功能

现在我们已经对代码热交换有主意了,我们还可以找些有趣的做。下面的服务器在你告知他作为特定类型的服务器之前什么都不做:

-module(server5).
-export([start/0,rpc/2]).

start() -> spawn(fun() -> wait() end).

wait() ->
    receive
        {become,F} -> F()
    end.

rpc(Pid,Q) ->
    Pid ! {self(),Q},
    receive
        {Pid,Reply} -> Reply
    end.

如果我们启动它然后发送 {become,F} 消息,它就会变成对F()求值的服务器,我们可以这样启动:

1> Pid=server5:start().
<0.57.0>

我们的服务器在等待消息时什么都不做。

现在我们定义服务器函数。也并不复杂,只要计算斐波拉契:

-module(my_fac_server).
-export([loop/0]).

loop() ->
    receive
        {From,{fac,N}} ->
            From ! {self(),fac(N)},
            loop();
        {become,Something} ->
            Something()
    end.

fac(0) -> 1;
fac(N) -> N*fac(N-1).

Note

PlantLab中的Erlang

几年前,在我PlanetLab做研究。我有权访问PlanetLab的网络,所以我在所有的服务器上都安装了空的Erlang服务器(大约450台)。我并不知道我要这些机器干什么用,所以后来我开始研究服务器结构。

当我可以让这一层很好的运行时,发送消息到一台空服务器,以便让他成为真正的服务器就很简单了。

常见的途径是启动WEB服务器,然后安装WEB服务器插件。我的方法是再后退一步,只是安装一个空服务器,然后在空服务器中安装WEB服务器。而当我们安装好WEB服务器以后,再告诉它应该变成什么。

只要确保他们编译好了,然后我们就可以告诉进程 <0.57.0> 变成一个斐波拉契服务器:

2> c(my_fac_server).
{ok,my_fac_server}
3> Pid ! {become,fun my_fac_server:loop/0}.
{become,#Fun<my_fac_server.loop.0>}

现在,我们的进程已经变成斐波拉契服务器了,我们可以这样调用:

4> server5:rpc(Pid,{fac,30}).
2652528598121910586363084800000000

我们的还会继续保持作为斐波拉契服务器的状态,直到我们发送消息 {become,Something} 来让他改变。

有如你所看到的,我们可以在一定范围内改变服务器的类型。这种技术很强大,使用该技术可以构建小巧而美观的服务器,却有很高的灵活性。在我们编写工业规模的,拥有数百名程序员的项目时,我们并不希望有些事情变得太过于动态。我们必须在通用和功能强大方面做出取舍,因为我们要的是产品。灵活多变的代码往往造成一堆难于调试的bug。如果我们已经在程序中做了多处动态修改,然后crash了,那会很难找到问题。

本节的服务器例子并不是非常正确。他们只是用来展示棘手问题的发展,他们实在太小了,而且还有些狡猾的错误。我不会立即告诉你们这些,但是在本章末尾,我会给出一些提示。

Erlang模块 gen_server 是构建久经考验服务器的优秀方案。

他在1998年就开始应用于工业级别的产品中了。一个产品中往往包含数百个服务器。这些服务器是使用正规的顺序化代码写成。所有的错误和非功能性行为都被放在了服务器中的通用部分了。

所以现在我们可以直接跳到 gen_server 的怀抱了。

2   gen_server 入门

这里是另一个极端。注意使用 gen_server 编写回调模块的三点计划:

  1. 决定回调函数名
  2. 编写接口函数
  3. 编写六个必需的回调函数

这很简单,无需思考,只要跟着感觉走就行了!

2.1   步骤1:决定回调函数名

我们来做一个简单的支付系统,我们把模块名叫做 my_bank ,这个系统已经在应用了,只是客户封闭了代码,如果他们再次发布源码你会感觉跟这个例子很像。

2.2   步骤2:编写接口函数

我们定义五个接口例程,都在模块 my_bank 中:

start() :开启银行

stop() :关闭银行

new_account(Who) :创建账户

deposit(Who,Amount) :存款

withdraw(Who,Amount) :取款

他们最终都是需要调用 gen_server 的,如:

start() -> gen_server:start_link({local,?MODULE},?MODULE,[],[]).
stop() -> gen_server:call(?MODULE,stop).

new_account(Who) -> gen_server:call(?MODULE,{new,Who}).
deposit(Who,Amount) -> gen_server:call(?MODULE,{add,Who,Amount}).
withdraw(Who,Amount) -> gen_server:call(?MODULE,{remove,Who,Amount}).

gen_server:start_link({local,Name},Mod,...) 开启一个本地服务器(通过参数global,可以构造一个可以被Erlang集群节点所访问的服务器)。宏 ?MODULE 会被解析为模块名 my_bank 。 Mod 是回调模块名。我们暂时会忽略其他传递给 gen_server:start 的参数。

gen_server:call(?MODULE,Term) 用于对服务器的远程调用。

2.3   步骤3:编写六个必需的回调函数

我们的回调模块导出了六个回调例程: init/1 、 handle_call/3 、 handle_cast/2 、 handle_info/2 、 terminate/2 、 code_change/3 。为了快速实现,我们使用模板来构造 gen_server 。如下是最简单的例子:

-module().
%% gen_server_mini_template

-behaviour(gen_server).
-export([start_link/0]).
%% gen_server callbacks
-export([init/1,handle_call/3,handle_cast/2,handle_info/2,
        terminate/2,code_change/3]).

start_link() -> gen_server:start_link({local,?SERVER},?MODULE,[],[]).

init([]) -> {ok,State}.

handle_call(_Request,_From,State) -> {reply,Reply,State}.
handle_cast(_Msg,State) -> {noreply,State}.
handle_info(_Info,State) -> {noreply,State}.
terminate(_Reason,_State) -> ok.
code_change(_OldVsn,State,Extra) -> {ok,State}.

模板包含了最简单的需要填入服务器的框架(skeleton)。关键字 -behaviour 用于告知编译器在我们没有定义适当的回调函数时给出错误信息。

Tip

如果你正在使用emacs,你可以把 gen_server 模板放到快捷键中。可以修改erlang-mode,然后 Erlang>Skeleton菜单提供创建 gen_server 的模块。如果你不使用emacs,也别担心,我在本章末尾提供了该模板。

我们从模板开始编辑修改。我们所需要做的只是让参数与接口相匹配。

最重要的部分是 handle_call/3 函数。我们需要写出匹配3个查询术语的接口程序。也就是我们需要填充省略号处的代码:

handle_call({new,Who},From,State) ->
    Reply = ...
    State1 = ...
    {reply,Reply,State1};
handle_call({add,Who,Amount},From,State) ->
    Reply = ...
    State1 = ...
    {reply,Reply,State1};
handle_call({remove,Who,Amount},From,State) ->
    Reply = ...
    State1 = ...
    {reply,Reply,State1};

Reply的值会被发送到客户端作为远程调用的返回值。

State是服务器状态的全局变量,需要被服务器中持续传递。在我们的银行模块中,状态不会改变;他只是一个ETS表索引,而且是个常量(尽管表格的内容会改变)。

当我们填写模板并且修改后,我们得到如下代码:

init([]) -> {ok,ets:new(?MODULE,[])}.

handle_call({new,Who},_From,Tab) ->
    Reply=case ets:lookup(Tab,Who) of
                [] -> ets:insert(Tab,{Who,0}),
                    {welcome,Who};
                [_] -> {Who,you_already_are_a_customer}
            end,
    {reply,Reply,Tab};
handle_call({add,Who,X},_From,Tab) ->
    Reply=case ets:lookup(Tab,Who) of
                [] -> not_a_customer;
                [{Who,Balance}] ->
                    NewBalance=Balance+X,
                    ets:insert(Tab,{Who,NewBalance}),
                    {thanks,Who,your_balance_is,NewBalance}
            end,
    {reply,Reply,Tab};
handle_call({remove,Who,X},_From,Tab) ->
    Reply=case ets:lookup(Tab,Who) of
                [] -> not_a_customer;
                [{Who,Balance}] when X =< Balance ->
                    NewBalance=Balance -X,
                    ets:insert(Tab,{Who,NewBalance}),
                    {thanks,Who,your_balance_is,NewBalance};
                [{Who,Balance}] ->
                    {sorry,Who,you_only_have,Balance,in_the_bank}
            end,
    {reply,Reply,Tab};
handle_call(stop,_From,Tab) ->
    {stop,normal,stopped,Tab}.

handle_cast(_Msg,State) -> {noreply,State}.
handle_info(_Info,State) -> {noreply,State}.
terminate(_Reason,_State) -> ok.
code_change(_OldVsn,State,Extra) -> {ok,State}.

我们可以通过调用 gen_server:start_link(Name,CallBackMod,StartArgs,Opts) 来启动服务器,然后第一个被调用的历程是回调模块的 Mod:init(StartArgs) ,而且必须返回 {ok,State} 。State的值会在以后作为 handle_call 的第三个参数一直传递。

注意我们如何停止服务器。 handle_call(Stop,From,Tab) 返回 {stop,Normal,stopped,Tab} 会停止服务器。第二个参数normal用作调用 my_bank:terminate/2 的第一个参数。第三个参数stopped会作为 my_bank:stop() 的返回值。

就这些了,让我们看看如何使用银行:

1> my_bank:start().
{ok,<0.33.0>}
2> my_bank:deposit("joe",10).
not_a_customer
3> my_bank:new_account("joe").
{welcome,"joe"}
4> my_bank:deposit("joe",10).
{thanks,"joe",your_balance_is,10}
5> my_bank:deposit("joe",30).
{thanks,"joe",your_balance_is,40}
6> my_bank:withdraw("joe",15)
{thanks,"joe",your_balance_is,25}
7> my_bank:withdraw("joe",45).
{sorry,"joe",you_only_have,25,in_the_bank}

3   gen_server 回调结构

现在我们已经有主意了,我们多了解一下 gen_server 的回调结构。

3.1   启动服务器时发生了什么?

通过 gen_server:start_link(Name,Mod,InitArgs,Opts) 启动时,他会创建一个叫做Name的通用服务器。回调模块是Mod。Opts控制通用服务器的行为,可以指定消息日志、调试函数等等。通用服务器会启动回调 Mod:init(InitArgs) 。

模板的初始化入口如下:

%% Function: init(Args) -> {ok,State}
%%                         {ok,State,Timeout}
%%                         ignore
%%                         {stop,Reason}
init([]) ->
    {ok,#state{}}.

对于正常操作,我们只需要返回 {ok,State} 。其他参数的含义可以参考 gen_server 的手册页。

如果返回了 {ok,State} ,我们就算是成功的启动了服务器,并初始化了状态State。

3.2   我们调用服务器时发生了什么?

想要调用服务器,客户端程序调用 gen_server:call(Name,Request) 。这最终会调用回调模块的 handle_call/3 。

handle_call/3 拥有如下入口模板:

%% Function: handle_call(Request,From,State) -> {reply,Reply,State}
%%                                              {reply,Reply,State,Timeout}
%%                                              {noreply,State}
%%                                              {noreply,State,Timeout}
%%                                              {stop,Reason,Reply,State}
%%                                              {stop,Reason,State}
handle_call(_Request,_From,State) ->
    Reply=ok,
    {reply,Reply,State}.

Request参数重新出现在 handle_call/3 的第一个参数中。From是请求客户端进程的PID,而State则是当前客户端的状态。

一般来说,我们返回 {reply,Reply,NewState} 。当这发生时,Reply会发送到客户端,而它是作为 gen_server:call 的返回值的。NewState是服务器的下一个状态。

另一个返回值 {noreply,…} 和 {stop,…} 用于罕见的特殊情况。没有返回值会让服务器继续工作,而客户端会继续等待服务器的响应,所以服务器需要委托其他进程来作出响应。调用stop会适当的结束服务器。

3.3   调用与投送(Cast)

我们已经看到了 gen_server:call 与 handle_call 的相互影响了。这是用于实现RPC(Remote Procedure Call)的。 gen_server:cast(Name,Name) 实现了投送(cast),就是没有返回值的调用(实际上是消息,但是一般叫做投送,以区别RPC)。

相应的回调例程是 handle_cast ,入口模板如下:

%% Function: handle_cast(Msg,State) -> {noreply,NewState}
%%                                     {noreply,NewState,Timeout}
%%                                     {stop,Reason,NewState}
handle_cast(_Msg,State) ->
    {noreply,NewState}.

处理函数通常返回 {noreply,NewState} ,就是改变服务器的状态。而 {stop,…} 则会停止服务器。

3.4   发到服务器的自发信息

回调函数 handle_info(Info,State) 用于处理发送到服务器的自发信息。那什么是自发信息呢?如果服务器连接到的一个进程突然退出了,那么他会立即收到不期望的 {“EXIT”,Pid,What} 消息。另外,系统中任何知道通用服务器PID的进程都可以给他发消息。在服务器有如Info的值时会发出死掉的消息(Any message like this ends up at the server as the value of Info)。

handle_info 的入口模板如下:

%% Function: handle_info(Info,State) -> {noreply,State}
%%                                      {noreply,State,Timeout}
%%                                      {stop,Reason,State}
handle_info(_Info,State) ->
    {noreply,State}.

返回值与 handle_cast 相同。

3.5   来吧,宝贝

服务器可能会因为多种原因而停止。其中一个原因是 handle_Something 例程可能会返回 {stop,Reason,NewState} ,或者服务器可能会挂了并抛出 {‘EXIT’,reason} 。在这些情况中,无论发生了什么, terminate(Reason,NewState) 会被调用。

如下是模板:

%% Function: terminate(Reason,State) -> void()
terminate(_Reason,State) ->
    ok.

这些代码不会返回新的状态,因为他们已经停止了。所以,我们应该如何处理状态呢?可以做很多事情,比如打扫战场。我们可以把它存储到磁盘,发送消息通知其他进程,或者抛弃其依赖的应用程序。如果你想要服务器在未来被重启,你还需要编写被 terminate/2 触发的“我会回来”函数。

3.6   代码改变

你可以在服务器运行时动态改变你的代码。这个回调函数包含处理释放子系统并提供软件更新的功能。

相关章节请参考OTP设计原则文档(http://www.erlang.org/doc/pdf/design_principles.pdf)。

%% Function: code_change(OldVsn,State,Extra) -> {ok,NewState}
code_change(_OldVsn,State,_Extra) -> {ok,State}.

4   代码与模板

这是通过emacs-mode生成的,通用服务器模板:

%%% -----------------------------------------------
%%% File: gen_server_template.full
%%% Author: ...
%%% Description: ...
%%% Created: ...
%%% -----------------------------------------------
-module().
-behaviour(gen_server).

%% API
-export([start_link/0]).

%% gen_server callbacks
-export([init/1,handle_call/3,handle_cast/2,handle_info/2,
        terminate/2,code_change/3]).

-record(state,{}).

%%% Function start_link() -> {ok,Pid} | ignore | {error,Error}
%%% 启动服务器
start_link() ->
    gen_server:start_link({local,?SERVER},?MODULE,[],[]).

%%% Function init(Args) -> {ok,State} |
%%%                        {ok,State,Timeout} |
%%%                        ignore |
%%%                        {stop,Reason}
%%% 初始化服务器
init([]) ->
    {ok,#state{}}.

%%% Function handle_call(Request,From,State) -> {reply,Reply,State} |
%%%                                             {reply,Reply,State,Timeout} |
%%%                                             {noreply,State} |
%%%                                             {noreply,State,Timeout} |
%%%                                             {stop,Reason,Reply,State} |
%%%                                             {stop,Reason,State}
%%% 处理所有消息
handle_call(_Request,_From,State) ->
    Reply=ok,
    {reply,Reply,State}.

%%% Function handle_cast(Msg,State) -> {noreply,State} |
%%%                                    {noreply,State,Timeout} |
%%%                                    {stop,Reason,State}
%%% 处理所有投送消息
handle_cast(_Msg,State) ->
    {noreply,State}.

%%% Function handle_info(Info,State) -> {noreply,State} |
%%%                                     {noreply,State,Timeout} |
%%%                                     {stop,Reason,State}
%%% 处理所有调用或投送消息
handle_info(_Info,State) ->
    {noreply,State}.

%%% Function terminate(Reason,State) -> void()
%%% 在服务器停止时被调用,与init/1做的事情相反,释放资源。当它返回时,
%%% gen_server就会以Reason终止,返回值会被忽略
terminate(_Reason,_State) ->
    ok.

%%% Function code_change(OldVsn,State,Extra) -> {ok,NewState}
%%% 在代码改变时覆盖进程状态
code_change(_OldVsn,State,_Extra) ->
    {ok,State}.

%%% 内部函数

my_bank的代码:

-module(my_bank).
-behaviour(gen_server).
-export([start/0]).
%% gen_server回调
-export([init/1,handle_call/3,handle_cast/2,handle_info/2,
        terminate/2,code_change/3]).
-compile(export_all).

start() -> gen_server:start_link({local,?MODULE},?MODULE,[],[]).
stop() -> gen_server:call(?MODULE,stop).

new_account(Who) -> gen_server:call(?MODULE,{new,Who}).
deposit(Who,Amount) -> gen_server:call(?MODULE,{add,Who,Amount}).
withdraw(Who,Amount) -> gen_server:call(?MODULE,{remove,Who,Amount}).

init([]) -> {ok,ets:new(?MODULE,[])}.

handle_call({new,Who},_From,Tab) ->
    Reply=case ets:lookup(Tab,Who) of
            [] -> ets:insert(Tab,{Who,0}),
                    {welcome,Who};
            [_] -> {Who,you_already_are_a_customer}
          end,
        {reply,Reply,Tab};
handle_call({add,Who,X},_From,Tab) ->
    Reply=case ets:lookup(Tab,Who) of
            [] -> not_a_customer;
            [{Who,Balance}] ->
                NewBalance=Balance+X,
                ets:insert(Tab,{Who,NewBalance}),
                {thanks,Who,your_balance_is,NewBalance}
          end,
        {reply,Reply,Tab};
handle_call({remove,Who,X},_From,Tab) ->
    Reply=case ets:lookup(Tab,Who) of
            [] -> not_a_customer;
            [{Who,Balance}] when X =< Balance ->
                NewBalance=Balance-X,
                ets:insert(Tab,{Who,NewBalance}),
                {thanks,Who,you_only_have,Balance,in_the_bank}
          end,
        {reply,Reply,Tab};
handle_call(stop,_From,Tab) ->
    {stop,normal,stopped,Tab}.
handle_cast(_Msg,State) -> {noreply,State}.
handle_info(_Info,State) -> {noreply,State}.
terminate(_Reason,_State) -> ok.
code_change(_OldVsn,State,Extra) -> {ok,State}.

5   深度挖掘

gen_server 实际上很简单。不过我们并没有了解 gen_server 的所有接口,也没有必要知道所有参数的含义。当你明白基本原理之后,你只需要查阅 gen_server 的手册就够了。

在本章,我们只是讨论了使用 gen_server 的最简单情况,但是适用于大多数情况。复杂的应用经常让 gen_server 返回 noreply 而是委派真正的响应给另一个进程想要了解这些,请参与设计原则文档(http://www.erlang.org/doc/pdf/design_principles.pdf)。和模块 sysproc_lib 的手册页。

Programming Erlang 第14章笔记 Socket编程

Thursday, February 21st, 2008

Socket编程

译者: gashero

目录

大多数我写的更有趣的程序都包含了Socket。一个Socket是一个允许机器与Internet上另一端使用IP通信的端点。本章关注Internet上两种核心网络协议:TCP和UDP。

UDP允许应用发送简短报文(叫做数据报datagram)到另一端,但是对报文没有交付的担保。并且可能在到达时有错误的顺序。而TCP则提供了可靠的字节流,并且确保在连接后传输数据的顺序也是对的。

为什么Socket编程很有趣呢?因为它允许应用于其他Internet上的机器通信,而这些比本地操作更有潜力。

有两种主要的库用于Socket编程: gen_tcp 用于TCP编程、 gen_udp 用于UDP编程。

在本章,我们看看如果使用TCP和UDP socket编写客户端和服务器。我们将会尝试多种形式的服务器(并行、串行、阻塞、非阻塞)并且看看通信接口应用如何将数据流传递给其他应用。

1   使用TCP

我们学习Socket编程的历险从一个从服务器获取TCP数据的程序开始。然后我们会写一个简单的串行TCP服务器展示如何并行的处理多个并发会话。

1.1   从服务器获取数据

我们先写一个小函数(标准库的 http:request(Url) 实现相同的功能,但是这里是演示TCP的)来看看TCP socket编程获取 http://www.google.com 的HTML页面:

nano_get_url() ->
    nano_get_url("www.google.com").

nano_get_url(Host) ->
    {ok,Socket}=gen_tcp:connect(Host,80,[binary,{packet,0}]),
    ok=gen_tcp:send(Socket,"GET / HTTP/1.0\r\n\r\n"),
    receive_data(Socket,[]).

receive_data(Socket,SoFar) ->
    receive
        {tcp,Socket,Bin} ->
            receive_data(Socket,[Bin|SoFar]);
        {tcp_closed,Socket} ->
            list_to_binary(reverse(SoFar))
    end.

它如何工作呢?

  1. 我们通过 gen_tcp:connect 在 http://www.google.com 打开TCP协议80端口。connect的参数binary告知系统以binary模式打开socket,并且以二进制方式传递数据到应用。 {packet,0} 意味着无需遵守特定格式即可将数据传递到应用。
  2. 我们调用 gen_tcp:send 并发送消息 GET / HTTP/1.0\r\n\r\n 到socket。然后我们等待响应。响应并不会一次性得到,而是分片的、分时间的。这些分片是按照一系列报文的方式接收的,并且通过打开的socket发送到进程。
  3. 我们接收一个 {tcp,Socket,Bin} 报文。第三个参数是binary。这是因为我们已经使用二进制方式打开了socket。这是从WEB服务器发到我们的一个消息报文。我们把这个报文加到分片列表并等待下一个报文。
  4. 我们接收到 {tcp_closed,Socket} 报文,这在服务器发送完所有数据时发生(这仅在HTTP/1.0时正确,现在版本的HTTP使用另外一种结束策略)。
  5. 当我们收到了所有的分片,存储顺序是错误的,所以我们重新对分片排序和连接。

我们看看他如何工作:

1> B=socket_examples:nano_get_url().
<<"HTTP/1.0 302 Found\r\nLocation: http://www.google.se/\r\n
    Cache-Control: private\r\nSet-Cookie: PREF=ID=b57a2c:TM"...>>

Note

当运行 nano_get_url 时,结果是二进制的,而你看到的则是Erlang shell以便于阅读的方式展示的。当以便于阅读方式展示时,所有控制字符都是以转义格式显示。二进制数据也会被截短,后面以省略号显示。如果想要看所有的二进制数据,你可以通过 io:format 打印或者使用 string:tokens 分片显示:

2> io:format("~p~n",[B]).
<<"HTTP/1.0 302 Found\r\nLocation: http://www.google.se/\r\n
    Cache-Control: private\r\nSet-Cookie: PREF=ID=B57a2c:TM"
    TM=176575171639526:LM=1175441639526:S=gkfTrK6AFkybT3;
    ... 还有一大堆行 ...
>>
3> string:tokens(binary_to_list(B),"\r\n").
["HTTP/1.0 302 Found",
"Location: http://www.google.se/",
"Cache-Control: private",
... 还有一大堆行 ...

这就差不多WEB客户端的工作方式(不过在浏览器中正确的显示要做更多的工作)。上面的代码只是实验的开始。你可以尝试做一些修改来下载整个网站或者读取电子邮件。可能性是无限的。

注意分片的重新组装方式如下:

receive_data(Socket,SoFar) ->
    receive
        {tcp,Socket,Bin} ->
            receive_data(Socket,[Bin|SoFar]);
        {tcp_closed,Socket} ->
            list_to_binary(reverse(SoFar))
    end.

每当我们收到分片时,就把他们加入SoFar这个列表的头部。当收到了所有分片时,Socket就关闭了,我们颠倒顺序,并且把所有分片组合起来。

你可能以为收到分片后立即合并会好些:

receive_data(Socket,SoFar) ->
    receive
        {tcp,Socket,Bin} ->
            receive_data(Socket,list_to_binary([SoFar,Bin]));
        {tcp_closed,Socket} ->
            SoFar
    end.

这段代码是正确,但是效率比较低。因为后一种版本不断的把新的二进制数据加到缓冲区后面,也就是包含了多个数据的拷贝的。一个好办法是累积所有分片,尽管顺序是相反的,然后反序整个列表并一次连接所有分片。

1.2   一个简单的TCP服务器

在前一节,我们写了一个简单的客户端。现在我们写个服务器。

服务器开启2345端口然后等待一个消息。这个消息是包含Erlang术语的二进制数据,这个术语是包含Erlang表达式的字符串。服务器对该表达式求值并且将结果通过socket发到客户端。

Note

如何编写WEB服务器

编写WEB客户端或服务器是很有趣的。当然,有些人已经写好这些了,但是如果想要真正理解他们的工作原理,研究底层实现还是很有意义的。谁知道呢,说不定我们写的WEB服务器更好。所以我们看看如何做吧?

想要构建一个WEB服务器,任何一个需要实现标准的Internet协议,我们需要使用正确的工具和了解协议实现。

在我们的例子用来抓取一个WEB页,我们如何知道已经正确打开了80端口,并且如何知道已经发送了 GET / HTTP/1.0\r\n\r\n 到服务器?答案很简单。所有主要协议都已经在RFC文档中有描述。HTTP/1.0定义于RFC1945,所有RFC的官方网站是 http://www.letf.org

另外一个非常有用的方法是抓包。通过一个数据包嗅探器,我们可以抓取和分析所有IP数据包,无论是应用程序发出的还是接收的。大多数嗅探器包含解码器和分析器可以得出数据包的内容和格式。一个著名的嗅探器是Wireshark(以前叫Ethereal),可以到 http://www.wireshark.org/ 了解更多。

使用嗅探器和RFC武装起来的我们,就可以准备编写下一个杀手级应用了。

编写这个程序(或者其他使用TCP/IP的程序),需要响应一些简单的请求:

  • 数据如何组织,知道数据如何组成请求或者响应?
  • 数据在请求和响应中如何编码(encode & marshal)和解码(decode & demarshal)

TCP socket数据只是没有格式的字节流。在传输时,数据会切成任意长度的分片,所以我们需要多少数据如何组成请求或响应。

在Erlang的例子,我们使用简单的转换,把每个逻辑请求或响应前加上N(1/2/4)字节的长度数。这就是 {packet,N} (这里的packet表示一个应用程序请求或响应报文,而不是电线里面的物理包) 参数在 gen_tcp:connect 和 gen_tcp:listen 函数的意义。注意packet附带的那个参数在客户端和服务器端必须商定好。如果服务器使用 {packet,2} 而客户端使用 {packet,4} 则会出错。

在我们以 {packet,N} 选项打开socket后,我们就不需要担心数据分片了。Erlang驱动会自动确保数据报文的所有分片都收到并且长度正确时才发到应用程序。

另一个需要注意的是数据编码和解码。最简单时,我们可以用 term_to_binary 来对Erlang术语编码,并使用 binary_to_term 来解码数据。

注意,客户端和服务器通信的包转换和编码规则是两行代码完成,分别使用 {packet,4} 来打开socket和使用 term_to_binary 和其反函数完成编码和解码数据。

我们可以简单的打包和编码Erlang术语到基于文本的协议如HTTP或XML。使用Erlang的 term_to_binary 和其反函数可以比基于XML等文本的协议性能高出一个数量级。现在我们先看看一个简单的服务器:

start_nano_server() ->
    {ok,Listen}=gen_tcp:listen(2345,[binary,{packet,4},
                                            {reuseaddr,true},
                                            {active,true}]),
    {ok,Socket}=gen_tcp:accept(Listen),
    gen_tcp:close(Listen),
    loop(Socket).

loop(Socket) ->
    receive
        {tcp,Socket,Bin} ->
            io:format("Server received binary = ~p~n",[Bin]),
            Str=binary_to_term(Bin),
            io:format("Server (unpacked) ~p~n",[Str]),
            Reply=lib_misc:string2value(Str),
            io:format("Server replying = ~p~n",[Reply]),
            gen_tcp:send(Socket,term_to_binary(Reply)),
            loop(Socket);
        {tcp_closed,Socket} ->
            io:format("Server socket closed~n")
    end.

它如何工作?

  1. 首先,我们调用 gen_tcp:listen 来监听2345端口,并且设置报文转换格式为 {packet,4} ,意味着每个包有4个字节的包头,代表长度。然后 gen_tcp:listen(..) 会返回 {ok,Socket} 或者 {error,Why} ,但是我们先看看成功的情况。所以写下如下代码 {ok,Listen}=gen_tcp:listen(...), 这在程序返回 {error,…} 时发生匹配错误。如果成功则会绑定Listen到正在监听的socket。对于正在监听的socket,我们只需要做一件事,就是使用它做参数调用 gen_tcp:accept 。
  2. 现在我们调用 gen_tcp:accept(Listen) 。在这里,程序会挂起以等待连接。当我们获得连接时,这个函数返回已经绑定的Socket,这个socket就是可以与客户端连接并且可以通信的了。
  3. 当 gen_tcp:accept 返回,我们立即调用 gen_tcp:close(Listen) 。这就关闭了监听的socket,服务器也就不会继续接受新的连接了。而这不会影响已有的连接,只是针对新连接。
  4. 解码输入数据
  5. 对字符串求值
  6. 编码返回数据并且通过socket发送

注意,这个程序只接受一个请求,程序运行完成后就不会再接受其他请求了。

这是一个非常简单的服务器展示了如何打包和编码应用数据。接收请求,计算响应,发出响应,然后结束。

想要测试这个服务器,我们需要一个对应的客户端:

nano_client_eval(Str) ->
    {ok,Socket}=get_tcp:connect("localhost",2345,[binary,{packet,4}]),
    ok=gen_tcp:send(Socket,term_to_binary(Str)),
    receive
        {tcp,Socket,Bin} ->
            io:format("Client received binary = ~p~n",[Bin]),
            Val=binary_to_term(Bin),
            io:format("Client result = ~p~n",[Val]),
            gen_tcp:close(Socket)
    end.

想要测试你的代码,我们需要在一台机器上同时启动客户端和服务器。所以在 gen_tcp:connect 中的hostname参数就可以用硬编码的 localhost 。

注意客户端和服务器端使用的 term_to_binary 和 binary_to_term 怎样编码和解码数据。

想要运行,我们需要开两个终端然后启动Erlang shell。

首先,我们启动服务器:

1> socket_examples:start_nano_server().

我们看不到任何输出,当然什么也没发生呢。然后我们在另一个终端启动客户端,输入如下命令:

1> socket_examples:nano_client_eval("list_to_tuple([2+3*4,10+20])").

在服务器端的窗口尅看到如下输出:

Server received binary = <<131,107,0,28,108,105,115,116,95,116,
                           111,95,116,117,112,108,101,40,91,50,
                           43,51,42,52,44,49,48,43,50,48,93,41>>
Server (unpacked)  "list_to_tuple([2+3*4,10+20])"
Server replying = {14,30}

在客户端窗口可以看到:

Client received binary = <<131,104,2,97,14,97,30>>
Client result = {14,30}
ok

最后,在服务器窗口看到:

Server socket closed

1.3   改进服务器

前一节我们构造了一个服务器可以接受一个请求并且终止。简单的修改代码,我们可以就可以完成另一个不同类型的服务器:

  1. 序列服务器,同一时间只接受一个请求
  2. 并行服务器,同一时间可以接受多个请求

原始启动代码如下:

start_nano_server() ->
    {ok,Listen} = gen_tcp:listen(...),
    {ok,Socket} = gen_tcp:accept(Listen),
    loop(Socket).
...

我们将会以此为基础完成另外两种服务器。

1.4   序列服务器

想要构造序列服务器,我们如下改变了代码:

start_seq_server() ->
    {ok,Listen} = gen_tcp:listen(...),
    seq_loop(Listen).

seq_loop(Listen) ->
    {ok,Socket} = gen_tcp:accept(Listen),
    loop(Socket),
    seq_loop(Listen).

loop(...) -> %%同以前的一样处理

这会让以前的例子在需要多个请求时工作的很好。我们一直让监听的socket开着而不是关闭。另一个不同是在 loop(Socket) 结束后,我们再次调用 seq_loop(Listen) ,以便接受后面的连接请求。

如果客户端在服务器忙于处理一个已经存在的连接时尝试连接服务器,连接请求会被缓存,直到服务器完成那个已经存在的请求。如果缓存的连接数大于监听backlog,连接会被拒绝。

我们只是展示了如何开启服务器。关闭服务器很简单(停止并行服务器也一样),只要kill掉启动服务器的进程即可。 gen_tcp 连接他本身到控制进程,如果控制进程死掉了,他也就会关闭socket。

1.5   并行服务器

构造并行服务器的秘诀是每次接受新的连接以后马上用 spawn 立即生成一个新的进程:

start_parallel_server() ->
    {ok,Listen} = gen_tcp:listen(...),
    spawn(fun() -> par_connect(Listen) end).

par_connect(Listen) ->
    {ok,Socket} = gen_tcp:accept(Listen),
    spawn(fun() -> par_connect(Listen) end),
    loop(Socket).

loop(...) -> %%同上

这段代码与串行服务器类似。重要的不同在于 spawn ,可以确保为每个连接创建一个进程。现在可以对比两种服务器了,可以看到他是如何将一个串行服务器转变为并行服务器的。

所有这三种服务器调用 gen_tcp:listen 和 gen_tcp:accept 不同在于如何调用函数是并行方式还是串行方式。

Note

知识点:

  • 创建socket的进程(调用 gen_tcp:accept 或者 gen_tcp:connect )叫做这个socket的 控制进程 。所有来自于socket的消息都会被发送到控制进程;如果控制进程死掉了,对应的socket就会被关闭。可以修改一个socket的控制进程为NewPid,通过 gen_tcp:controlling_process(Socket,NewPid) 。

  • 我们的并行服务器可能会随着连接而创建数千个连接。我们可能希望限制最大并发连接数。这可以通过一个活动连接计数器来实现。每次获得一个新连接时就增加1,而在一个连接完成时减少1。可以用这种机制显示并发连接数。

  • 在我们接受连接后,最好明确的设置请求socket选项,如下:

    {ok,Socket} = gen_tcp:accept(Listen),
    inet:setopts(Socket,[{packet,4},binary,{nodelay,true},{active,true}]),
    loop(Socket)
  • 在Erlang R11B-3中,几个Erlang进程可以在同一个监听中的socket调用 gen_tcp:accept/1 。这也是一种建立并发服务器的简单方式,因为你尅拥有预分配的进程池,等待 gen_tcp:accept/1 。

2   控制问题

Erlang中socket可以以3种模式打开:active、active once、passive。其设置可以通过 gen_tcp:connect(Address,Port,Options) 或 gen_tcp:listen(Port,Options) 中的Options参数来设置为 {active,true|false|once} 。

如果指定了 {active,true} ,就会创建一个主动(active)的socket; {active,false} 会创建一个被动的(passive)的socket; {active,once} 创建主动的socket,但是只接受一条消息,接收到消息后,必须手动重新开启(reenable)才能继续接受消息。

我们看看在不同地方使用的区别。

active和passive的socket的区别在于消息到来时的处理方式:

  • 一旦一个active的socket被创建了,控制进程会发送收到的数据,以 {tcp,Socket,Data} 消息的形式。而控制进程无法控制消息流。一个无赖的客户端可以发送无数的消息到系统,而这些都会被发送到控制进程。而控制进程无法停止这个消息流。
  • 如果socket在passive模式,控制进程需要调用 gen_tcp:recv(Socket,N) 来获取数据。它会尝试获取N字节的数据,如果N=0,就会尽可能的拿到所有可以取得的数据。这种情况下,服务器尅通过选择是否调用 gen_tcp:recv 来控制消息流。

被动模式的socket用于控制发送到服务器的数据流。为了举例,我们可以以3种方式编写消息接收循环:

  • 主动消息获取(非阻塞)
  • 被动消息获取(阻塞)
  • 混合方式获取(部分阻塞)

2.1   主动消息获取(非阻塞)

第一个例子是以主动模式打开socket,然后接受来自socket的数据:

{ok,Listen} = gen_tcp:listen(Port,[...,{active,true}...]),
{ok,Socket} = gen_tcp:accept(Listen),
loop(Socket).

loop(Socket) ->
    receive
        {tcp,Socket,Data} ->
            ... 输出处理 ...
        {tcp_closed,Socket} ->
            ...
    end.

这个过程无法控制发到服务器循环的消息流,如果客户端产生数据的速度大于服务器消费数据的速度,系统就会收到洪水般地消息-消息缓冲区溢出,系统将会crash并表现怪异。

这种类型的服务器叫做非阻塞服务器,因为它无法阻塞客户端。我们仅在信任客户端的情况下才会使用非阻塞服务器。

2.2   被动消息获取(阻塞)

在这一节,我们写阻塞服务器:服务器以被动模式打开socket,通过 {active,false} 选项。这个服务器不会被危险的客户端洪水袭击。

服务器循环中的代码调用 gen_tcp:recv 来接收数据。客户端在服务器调用 recv 之前会被阻塞。注意OS会对客户端发来的数据做一下缓冲,以允许客户端在服务器调用 recv 之前仍然可以继续发送一小段数据。

{ok,Listen} = gen_tcp:listen(Port,[...,{active,false}...]),
{ok,Socket} = gen_tcp:accept(Listen),
loop(Socket).

loop(Socket) ->
    case gen_tcp:recv(Socket,N) of
        {ok,B} ->
            ... 数据处理 ...
            loop(Socket);
        {error,closed}
            ...
    end.

2.3   混合消息获取(部分阻塞)

你可能认为把被动模式用到所有服务器上都合适。不幸的是,当我们在被动模式时,我们只能等待来自于一个socket的数据。这对于需要等待多个socket来源数据的服务器则不适用。

幸运的是我们可以用混合方式,既不是阻塞的也不是非阻塞的。我们以一次主动(active once)模式 {active,once} 打开socket。在这个模式中,socket是主动的,但是只能接收一条消息。在控制进程发出一条消息之后,他必须明确的调用 inet:setopts 以便让socket恢复并接收下一条消息。系统在这发生之前会一直阻塞。这是两种世界的最好结合点。如下是代码:

{ok,Listen} = gen_tcp:listen(Port,[...,{active,once}...]),
{ok,Socket} = gen_tcp:accept(Listen),
loop(Socket).

loop(Socket) ->
    receive
        {tcp,Socket,Data} ->
            ... 数据处理 ...
            %%准备好启用下一条消息时
            inet:setopts(Socket,[{active,once}]),
            loop(Socket);
        {tcp_closed,Socket} ->
            ...
    end.

使用 {active,once} 选项,用户可以实现高层次的数据流控制(有时叫交通管制),同时又防止了服务器被过多的消息洪水所淹没。

3   连接从哪里来?

假设我们建立了某种在线服务器,而却一直有人发垃圾邮件。那么我们需要做的第一件事就是确定他的来源。想要发现这件事,我们可以使用 inet:peername(Socket) 。

@spec inet:peername(Socket) -> {ok,{IP_Address,Port}} | {error,Why}

返回连接另一端的IP地址和端口号,以便服务器找到对方的地址。IP_Address是一个元组的整数形如 {N1,N2,N3,N4} ,而 {K1,K2,K3,K4,K5,K6,K7,K8} 则是IPv6的地址。这里的整数取值范围是0到255。

4   Socket的错误处理

socket的错误处理是非常简单的,基本上你不需要做任何事情。犹如前面讲过的,每个socket拥有控制进程(就是创建socket的进程)。如果控制进程死掉了,那么socket也会自动关闭。

这意味着,如果我们有,例如,一个客户端和服务器,而服务器因为编程错误死掉了。那么服务器拥有的socket会自动关闭,而客户端会收到 {tcp_closed,Socket} 消息。

我们可以通过如下的小程序测试这个机制:

error_test() ->
    spawn(fun() -> error_test_server() end),
    lib_misc:sleep(2000),
    {ok,Socket} = gen_tcp:connect("localhost",4321,[binary,{packet,2}]),
    io:format("connected to: ~p~n",[Socket]),
    gen_tcp:send(Socket,<<"123">>),
    receive
        Any ->
            io:format("Any=~p~n",[Any])
    end.

error_test_server() ->
    {ok,Listen} = gen_tcp:listen(4321,[binary,{packet,2}]),
    {ok,Socket} = gen_tcp:accept(Listen),
    error_test_server_loop(Socket).

error_test_server_loop(Socket) ->
    receive
        {tcp,Socket,Data} ->
            io:format("received:~p~n",[Data]),
            atom_to_list(Data),
            error_test_server_loop(Socket)
    end.

当我们运行它时,会看到如下输出:

1> socket_examples:error_test().
connected to:#Port<0.152>
received:<<"123">>
=ERROR REPORT==== 9-Feb-2007::15:18:15 ===
Error in process <0.77.0> with exit value:
 {badarg,[{erlang,atom_to_list,[<<3 bytes>>]},
 {socket_examples,error_test_server_loop,1}]}
Any={tcp_closed,#Port<0.152>}
ok

我们生成了一个服务器,并让它每两秒有一次启动的机会,并且发送包含二进制数据 <<"123">> 的消息。当这个消息到达服务器时,服务器会尝试计算 atom_to_list(Data) ,因为Data是一个二进制数据,所以会立即出错(系统监控器会发现并显示错误)。现在服务器端的控制进程已经死掉了,而且其socket也关闭了。客户端会收到 {tcp_closed,Socket} 消息。

5   UDP

现在我们看看UDP协议(User Datagram Protocol,用户数据报协议)。使用UDP,互联网上的机器之间可以互相发送小段的数据,叫做数据报。UDP数据报是不可靠的,这意味着如果客户端发送一系列的UDP数据报到服务器,收到的数据报顺序可能是错误的。不过收到的数据报肯定是正确的。大的数据报会被分为多个小的分片,IP协议负责重新组装这些分片,并最终交付给应用。

UDP是无连接的协议,这意味着客户端无需连接服务器即可发送消息。这也意味着程序更加适于大量客户端收发小的消息报文。

在Erlang中编写UDP客户端和服务器比TCP时更简单,因为我们无需管理连接。

5.1   简单的UDP服务器和客户端

首先,我们看看服务器,一个通用的服务器样式如下:

server(Port) ->
    {ok,Socket} = gen_udp:open(Port,[binary]),
    loop(Socket).

loop(Socket) ->
    receive
        {udp,Socket,Host,Port,Bin} ->
            BinReply = ... ,
            gen_udp:send(Socket,Host,Port,BinReply),
            loop(Socket)
    end.

这里比TCP协议的例子更简单,因为我们至少不需要关心连接关闭的消息。注意我们以二进制方式打开socket,驱动也会以二进制数据的形式将报文发送到应用。

注意客户端。这里有个简单的客户端。它仅仅打开UDP socket,发送消息到服务器,等待响应(或超时),然后关闭socket并返回从服务器接收到的值。

client(Request) ->
    {ok,Socket} = gen_udp:open(0,[binary]),
    ok = gen_udp:send(Socket,"localhost",4000,Request),
    Value = receive
                {udp,Socket,_,_,Bin} ->
                    {ok,Bin}
                after 2000 ->
                    error
                end,
    gen_udp:close(Socket),
    Value

我们必须拥有一个超时,否则UDP的不可靠会让我们永远得不到响应。

5.2   一个UDP阶乘服务器

@page 261

Programming Erlang 第13章笔记 文件编程

Thursday, February 21st, 2008

文件编程

译者: gashero

目录

本章我们会看看文件管理的常见函数。Erlang标准发布版包含大量文件管理函数。我们看看程序中最常用的一部分。也会看一些文件操作的例子。另外也会提及一些罕见的文件操作,以便让你知道,他们是存在的。如果你想了解更多,去看手册吧。

1   库的组织

文件管理函数被阻止到了4个模块中:

file :用于打开、关闭、读取、写入文件、列目录等等的例程。 file 模块中的最常用函数在13.2节中有所讲解。更多细节请参考手册。

filename :管理不同平台上的文件名形式的细节,所以你可以在不同操作系统上使用相同的代码。

filelib :是 file 模块的扩展,包含一些列出文件、检查文件类型等等的函数。大部分都是使用 file 模块的函数来编写的。

io :在打开的文件上的操作函数。包含解析数据、按照格式写入数据到文件等。

2   读取文件的不同方式

让我们先看看读取文件的一些选项。首先写一个小程序来打开文件和读取数字,通过几种不同方式。

文件的内容只是字节流。他们的意义依赖于如何解释。

为了演示,我们对所有例子使用相同的文件。它事实上包含Erlang术语的序列。依赖于如何打开和读取文件,我们可以把内容解释为一列Erlang术语,作为一系列文本行,或者作为原始的二进制数据而不作任何解释。

这是原始文件:

{person, "joe", "armstrong",
    [{occupation, programmer},
     {favoriteLanguage, erlang}]}.

{cat, {name, "zorro"},
      {owner, "joe"}}.

现在我们用几种方式来读取它。

file 模块中的函数及其解释:

函数 解释
change_group 改变文件所属的组
change_owner 改变文件的拥有者
change_time 改变文件的修改时间和上次访问时间
close 关闭文件
consolt 从文件读取Erlang术语
copy 复制文件内容
del_dir 删除目录
delete 删除文件
eval 求值文件中的Erlang表达式
format_error 返回描述字符串或错误原因
get_cwd 获取当前工作目录
list_dir 列出一个目录中的所有文件
make_dir 新建目录
make_link 新建硬链接
make_symlink 新建符号链接
open 打开文件
position 设置文件指针位置
pread 在指定位置读取文件
pwrite 在指定位置写入文件
read 从文件读取
read_file 返回整个文件
read_file_info 获取文件信息
read_link 获取链接指向的位置
read_link_info 获取链接或文件的信息
rename 重命名文件
script 求值并返回文件中的Erlang表达式
set_cwd 设置当前工作目录
sync 同步内存缓冲到硬盘
truncate 截短文件
write 写入到文件
write_file 写入整个文件
write_file_info 改变文件信息

2.1   读取文件中的所有术语

Programming Erlang 第12章笔记 接口技术

Thursday, February 21st, 2008

接口技术

译者: gashero

目录

假设我们需要以Erlang接口运行以C/Python/Shell编写的程序。想要实现这些,我们需要在一个单独的操作系统进程中运行这些程序,而不是在Erlang运行时系统中,他们之间以面向字节的通道完成通信。Erlang端通过 Port 控制。创建端口的进程成为端口的连接进程。连接进程拥有特殊意义:让所有来自扩展程序的消息都会以连接进程的PID来标志。所有的扩展程序消息都会发送到连接进程。

我们可以看看连接进程(C)与端口(P)和扩展操作系统进程的关系。

chpt12.00.jpg从程序员的角度,端口就像是Erlang进程。你可以发送消息给它,你可以注册(register)它,等等。如果扩展程序crash了,那么连接程序就会收到退出信号,如果连接进程死掉了,扩展进程也会被kill掉。

你可能会好奇与为什么要这么做。很多编程语言允许其他语言编写的程序连接到可执行文件。而在Erlang,我们为了安全性不允许这么做。如果我们连接一个扩展程序到Erlang可执行程序,那么扩展程序的错误将会轻易的干掉Erlang。所以,其他语言编写的程序必须以单独的操作系统进程来运行。Erlang运行时系统和扩展进程通过字节流通信。

1   端口

创建端口使用如下命令:

Port=open_port(PortName,PortSettings)

这会返回端口,而如下消息是会被发送到端口的(这些消息中PidC是连接进程的PID):

Port ! {PidC,{command,Data}} :发送数据到端口

Port ! {PidC,{connect,Pid1}} :改变控制进程的PID,从PidC到Pid1

Port ! {PidC,close} :关闭端口

连接进程会从扩展程序收到如下消息:

receive
    {Port,{data,Data}} ->
        ... 数据处理 ...

下面的节,我们会编写Erlang与C结合的简单例子。C程序尽可能的简单以避开细节,直接谈接口技术。

注意,下面的例子对接口机制和协议做了加亮。编码和解码复杂的数据结构是个困难的问题,这里没有谈及。在本章末尾,我们会指出一些用于其他编程语言的接口库。

2   扩展C程序的接口

我们先从C程序开始:

int twice(int x) {
    return 2*x;
}

int sum(int x, int y) {
    return x+y;
}

我们最终目标是从Erlang调用这些例程,我们希望看到的是这个样子(Erlang中):

X1=example1:twice(23),
Y1=example1:sum(45,32),

与用户的设置有关,example1是一个Erlang模块,所有与C接口的细节都被隐藏在了模块example1中。

我们的接口需要一个主程序,用来解码Erlang程序发来的数据。在我们的例子,我们首先定义端口和扩展C程序的协议。我们使用一个超级简单的协议,并展示如何在Erlang和C中实现。协议定义如下:

  • 所有包都以2字节的长度代码开头,后面跟着这些字节的数据。
  • 想要调用 twice(N) ,Erlang程序必须以特定形式编码函数调用。我们假设编码是2字节序列 [1,N] ;参数1表示调用函数 twice ,后面的N代表一个1字节的参数。
  • 调用 sum(N,M) ,我们编码请求到字节序列 [2,N,M] 。
  • 假设返回值都是单一的字节长度的。

扩展C程序和Erlang程序都必须遵守这个协议。作为例子,我们通过 sum(45,32) 来看看工作流程:

  1. 端口发送字节序列 0,3,2,45,32 到扩展程序。头两个字节0,3,表示包的长度是3,代码2表示调用扩展的 sum 函数,而 45 和 32 表示调用函数的参数。
  2. 扩展程序从标准输入(stdin)读取这5个字节,调用 sum 函数,然后把字节序列 0,2,77 写到标准输出(stdout)。前两个字节表示包长度,后面的77是结果(仍然是1字节长度)。

我们现在写在两端遵守协议的接口,先以C程序开始。

2.1   C程序

@page 215

Programming Erlang 第6章笔记 编译和运行

Thursday, February 21st, 2008

编译和运行

译者: gashero

目录

上一章并没有详细的说明如何编译和运行程序,而只是使用了Erlang shell。这对小例子是很好的,但是会让你的程序更复杂,你将会需要一个自动处理过程来简化它。我们一般使用Makefile。

事实上有三种不同的方式可以运行程序。本章将会结合特定场合来讲解三种运行方式。

有时候也会遇到问题:Makefile失败、环境变量错误或者搜索路径不对。我们在遇到这些问题时也会帮你处理这些问题(issue)。

1   启动和停止Erlang shell

在Unix系统(包括Mac OS X),可以从命令行启动Erlang shell:

$ erl
Erlang (BEAM) emulator version 5.5.1 [source] [async-threads:0] [hipe]

Eshell V5.5.1  (abort with ^G)
1>

而在Windows系统,则可以点击erl的图标。

最简单的退出方法是按下 Ctrl+C (对Windows系统则是 Ctrl+Break ),随后按下 A 。如下:

BREAK: (a)bort (c)ontinue (p)roc info (i)nfo (l)oaded
       (v)ersion (k)ill (D)b-tables (d)istribution
a
$

另外,你也可以对 erlang:halt() 求值以退出。

erlang:halt() 是一个BIF,可以立即停止系统,也是我最常用的方法。不过这种方法也有个问题。如果正在运行一个大型数据库应用,那么系统在下次启动时就会进入错误回复过程,所以你应该以可控的方式关闭系统。

可控的关闭系统就是如果shell可用时就输入如下:

1> q().
ok
$

这会将所有打开的文件写入磁盘,停止数据库(如果在运行),并且按照顺序关闭所有OTP应用。 q() 其实只是shell上 init:stop() 的别名。

如果这些方法都不工作,可以查阅6.6节。

2   修改开发环境

当你启动程序时,一般都是把所有模块和文件都放在同一个目录当中,并且在这个目录启动Erlang。如果这样做当然没问题。不过如果你的应用变得更加复杂时,你可能需要把它们分成便于管理的块,把代码放入不同的目录。而且你从其他项目导入代码时,扩展代码也有其自己的目录结构。

2.1   设置装载代码的搜索路径

Erlang运行时系统包含了代码自动重新载入的功能。想要让这个功能正常工作,你必须设置一些搜索路径以便找到正确版本的代码。

代码装载功能是内置在Erlang中的,我们将在E.4节详细讨论。代码的装载仅在需要时才执行。

当系统尝试调用一个模块中的函数,而这个模块还没有装载时,就会发生异常,系统就会尝试这个模块的代码文件。如果需要的模块叫做 myMissingModule ,那么代码装载器将会在当前目录的所有目录中搜索一个叫做 myMissingModule.beam 的文件。寻找到的第一个匹配会停止搜索,然后就会把这个文件中的对象代码载入系统。

你可以在Erlang shell中看看当前装载路径,使用如下命令 code:get_path() 。如下是例子:

code:get_path()
[".",
"/usr/local/lib/erlang/lib/kernel-2.11.3/ebin",
"/usr/local/lib/erlang/lib/stdlib-1.14.3/ebin",
"/usr/local/lib/erlang/lib/xmerl-1.1/ebin",
"/usr/local/lib/erlang/lib/webtool-0.8.3/ebin",
"/usr/local/lib/erlang/lib/typer-0.1.0/ebin",
"/usr/local/lib/erlang/lib/tv-2.1.3/ebin",
"/usr/local/lib/erlang/lib/tools-2.5.3/ebin",
"/usr/local/lib/erlang/lib/toolbar-1.3/ebin",
"/usr/local/lib/erlang/lib/syntax_tools-1.5.2/ebin",
...]

管理装载路径两个最常用的函数如下:

@spec code:add_patha(Dir) => true | {error,bad_directory}

添加新目录Dir到装载路径的开头

@spec code:add_pathz(Dir) => true | {error,bad_directory}

添加新目录Dir到装载路径的末尾

通常并不需要自己来关注。只要注意两个函数产生的结果是不同的。如果你怀疑装载了错误的模块,你可以调用 code:all_loaded() (返回所有已经装载的模块列表),或者 code:clash() 来帮助你研究错误所在。

code 模块中还有其他一些程序可以用于管理路径,但是你可能根本没机会用到它们,除非你正在做一些系统编程。

按照惯例,经常把这些命令放入一个叫做 .erlang 的文件到你的HOME目录。另外,也可以在启动Erlang时的命令行中指定:

> erl -pa Dir1 -pa Dir2 ... -pz DirK1 -pz DirK2

其中 -pa 标志将目录加到搜索路径的开头,而 -pz 则把目录加到搜索路径的末尾。

2.2   在系统启动时执行一系列命令

我们刚才把载入路径放到了HOME目录的 .erlang 文件中。事实上,你可以把任意的Erlang代码放入这个文件,在你启动Erlang时,它就会读取和求值这个文件中的所有命令。

假设我的 .erlang 文件如下:

io:format("Running Erlang~n").
code.add_patha(".")
code.add_pathz("/home/joe/2005/erl/lib/supported").
code.add_pathz("/home/joe/bin").

当我启动系统时,我就可以看到如下输出:

$ erl
Erlang (BEAM) emulator version 5.5.1 [source] [async-threads:0] [hipe]

Running Erlang
Eshell V5.5.1  (abort with ^G)
1>

如果当前目录也有个 .erlang 文件,则会在优先于HOME目录的。这样就可以在不同启动位置定制Erlang的行为,这对特定应用非常有用。在这种情况下,推荐加入一些打印语句到启动文件;否则你可能忘记了本地的启动文件,这可能会很混乱。

某些系统很难确定HOME目录的位置,或者根本就不是你以为的位置。可以看看Erlang认为的HOME目录的位置,通过如下的:

1> init:get_argument(home).
{ok,[["/home/joe"]]}

通过这里,我们可以推断出Erlang认为的HOME目录就是 /home/joe

3   运行程序的其他方式

Erlang程序存储在模块中。一旦写好了程序,运行前需要先编译。不过,也可以以脚本的方式直接运行程序,叫做 escript 。

下一节会展示如何用多种方式编译和运行一对程序。这两个程序很不同,启动和停止的方式也不同。

第一个程序 hello.erl 只是打印 “Hello world” ,这不是启动和停止系统的可靠方式,而且他也不需要存取任何命令行参数。与之对比的第二个程序则需要存取命令行参数。

这里是一个简单的程序。它输出 “Hello world” 然后输出换行。 “~n” 在Erlang的io和io_lib模块中解释为换行。

-module(hello).
-export([start/0]).

start() ->
    io:format("Hello world~n").

让我们以3种方式编译和运行它。

3.1   在Erlang shell中编译和运行

$ erl
...
1> c(hello).
{ok,hello}
2> hello:start().
Hello world
ok

3.2   在命令行编译和运行

$ erlc hello.erl
$ erl -noshell -s hello start -s init stop
Hello World
$

Note

快速脚本:

有时我们需要在命令行执行一个函数。可以使用 -eval 参数来快速方便的实现。这里是例子:

erl -eval 'io:format("Memory: ~p~n", [erlang:memory(total)]).'\
    -noshell -s init stop

Windows用户:想要让如上工作,你需要把Erlang可执行文件目录加入到环境变量中。否则就要以引号中的全路径来启动,如下:

"C:\Program Files\erl5.5.3\bin\erlc.exe" hello.erl

第一行 erlc hello.erl 会编译文件 hello.erl ,生成叫做 hello.beam 的代码文件。第一个命令拥有三个选项:

-noshell :启动Erlang而没有交互式shell,此时不会得到Erlang的启动信息来提示欢迎

-s hello start :运行函数 hello:start() ,注意使用 -s Mod ... 选项时,相关的模块Mod必须已经编译完成了。

-s init stop :当我们调用 apply(hello,start,[]) 结束时,系统就会对函数 init:stop() 求值。

命令 erl -noshell ... 可以放入shell脚本,所以我们可以写一个shell脚本负责设置路径(使用-pa参数)和启动程序。

在我们的例子中,我们使用了两个 -s 选项,我们可以在一行拥有多个函数。每个 -s 都会使用 apply 语句来求职,而且,在一个执行完成后才会执行下一个。

如下是启动hello.erl的例子:

#! /bin/sh
erl -noshell -pa /home/joe/2006/book/JAERANG/Book/code\
    -s hello start -s init stop

Note

这个脚本需要使用绝对路径指向包含 hello.beam 。不过这个脚本是运行在我的电脑上,你使用时应该修改。

运行这个shell脚本,我们需要改变文件属性(chmod),然后运行脚本:

$ chmod u+x hello.sh
$ ./hello.sh
Hello world
$

Note

在Windows上, #! 不会起效。在Windows环境下,可以创建.bat批处理文件,并且使用全路径的Erlang来启动(假如尚未设置PATH环境变量)。

一个典型的Windows批处理文件如下:

"C:\Program Files\erl5.5.3\bin\erl.exe" -noshell -s hello start -s init stop

3.3   以Escript运行

使用escript,你可以直接运行你的程序,而不需要先编译。

Warning

escript包含在Erlang R11B-4或以后的版本,如果你的Erlang实在太老了,你需要升级到最新版本。

想要以escript方式运行hello,我们需要创建如下文件:

#! /usr/bin/env escript

main(_) ->
    io:format("Hello world\n").

Note

开发阶段的导出函数

如果你正在开发代码,可能会非常痛苦于需要不断在导出函数列表增删函数。

一个声明 -compile(export_all) ,告知编译器导出所有函数。使用这个可以让你的开发工作简化一点。

当你完成开发工作时,你需要抓实掉这一行,并添加适当的导出列表。首先,重要的函数需要导出,而其他的都要隐藏起来。隐藏方式可以按照个人喜好,提供接口也是一样的效果。第二,编译器会对需要导出的函数生成更好的代码。

在Unix系统中,我们可以立即按照如下方式运行:

$ chmod u+x hello
$ ./hello
Hello world
$

Note

这里的文件模式在Unix系统中表示可执行,这个通过chmod修改文件属性的步骤只需要执行一次,而不是每次运行程序时。

在Windows系统中可以如下方式运行:

C:\> escript hello
Hello world
C:\>

Note

在以escript方式运行时,执行速度会明显的比编译方式慢上一个数量级。

3.4   程序的命令行参数

@page 125

Programming Erlang 第5章笔记 高级顺序编程

Thursday, February 21st, 2008

高级顺序编程

译者: gashero

目录

现在我们已经可以很好的理解顺序编程了。本章包含如下内容:

  • BIF:是 built-in function 的缩写,是包含在Erlang语言中的一部分。他们看起来像是在Erlang中写的一样,但是实际上是Erlang虚拟机实现的原始操作。
  • binary:这是一种常用的原始数据类型,高效率的内存段。
  • bit语法:模式匹配语法,用于打包和解包binary中的字段。
  • 工具箱:包含一些小专题来完成顺序编程。

一旦你掌握了本节,你就会很了解Erlang的顺序编程了,你也可以准备深入学习并行编程了。

1 BIF

BIF就是内置在Erlang中的函数。通常用于Erlang程序无法实现的功能。例如转换list到tuple或者获取当前的时间和日期。要完成这些任务时,我们就需要调用BIF。

例如BIF的 tuple/to_list/1 转换tuple到list,而 time/0 返回当前时间:

1> tuple_to_list({12,cat,"hello"}).
[12,cat,"hello"]
2> time().
{20,0,3}

所有的BIF其实是属于 erlang 模块的虽然大多数的BIF(比如tuple_to_list)是自动导入的,所以我们可以直接使用 tuple_to_list(…) 而不是 erlang:tuple_to_list(…) 。

你可以在手册页找到BIF的完整列表,或者在 http://www.erlang.org/doc/man/erlang.html

2 Binary

Binary 数据结构用以存储大量的原始数据。二进制对象存储数据具有比list和tuple更高的空间效率,而且,运行时系统也对二进制对象的输入和输出做了优化。

二进制对象书写和打印作一系列的整数或字符串,包含在双小于号和双大于号中。例如:

1> <<5,10,20>>.
<<5,10,20>>
2> <<"hello">>.
<<"hello">>

当你在二进制对象中使用整数形式时,每个数字必须在0-255的范围内。二进制对象 <<"cat">> 其实是 <<99,97,116>> 的速记形式;也就是说二进制对象使用ASCII字符构成字符串。

同字符串一样,如果二进制对象是可打印字符串,shell就会将二进制对象当作字符串打印;否则他会按照一个序列的整数来打印。

我们可以构造一个二进制对象或者解析二进制对象的元素,通过BIF,或者我们也可以使用BIF语法(查看5.3节)。在本节,只是看看使用BIF。

Note

@spec func(Arg1,…ArgN) -> Val

@spec代表什么?

这是一种Erlang类型符号,可以被转换成描述函数的文档,包括参数和返回值类型。这是一种很好的自省方式,不过对于想要包含更多细节,请参考附录A。

2.1 管理二进制对象的BIF

如下BIF可以管理二进制对象:

@spec list_to_binary(IoList) -> binary()

list_to_binary 返回一个通过参数IoList构造的二进制对象。这里的IoList是列表,其元素是0-255的整数、二进制对象或IoList:

1> Bin1=<<1,2,3>>.
<<1,2,3>>
2> Bin2=<<4,5>>.
<<4,5>>
3> Bin3=<<6>>.
<<6>>
4> list_to_binary([Bin1,1,[2,3,Bin2],4|Bin3]).
<<1,2,3,1,2,3,4,5,4,6>>

@spec split_binary(Bin,Pos) -> {Bin1,Bin2}

这个函数按照指定位置将二进制对象切割为两部分:

1> split_binary(<<1,2,3,4,5,6,7,8,9,10>>,3).
{<<1,2,3>>,<<4,5,6,7,8,9,10>>}

@spec term_to_binary(Term) -> Bin

转换Erlang术语到二进制对象。

通过 term_to_binary 产生的二进制对象存储在叫做扩展术语格式中。转换来的术语可以存储在文件中、通过网络报文发送等等,而原始的术语还可以重建。这对于在文件中或远程机器上存储复杂数据结构非常有用。

@spec binary_to_term(Bin) -> Term

这个 term_to_binary 的反函数:

1> B=term_to_binary({binaries,"are",useful}).
<<131,104,3,100,0,8,98,105,110,97,114,105,101,115,107,
0,3,97,114,101,100,0,6,117,115,101,102,117,108>>
2> binary_to_term(B).
{binaries,"are",useful}

@spec size(Bin) -> int

获取二进制对象的字节数:

1> size(<<1,2,3,4,5>>).
5

3 比特语法

比特语法是一种扩展语法用以对二进制对象中的比特序列进行模式匹配。当你编写底层的用以解包二进制对象时,你会发现比特语法非常有用。比特语法最初设计用于协议编程(Erlang很擅长的方向)和产生高效率的打包数据。

假设我们有三个变量X/Y/Z,是我们需要从16bit的内存M中提取的字段。X占用3bit,Y占用7bit,Z占用6bit。在大多数语言中都是使用底层的位操作,包括移位和掩码。而在Erlang中,你可以这么写:

M=<<X:3,Y:7,Z:6>>

完整的比特语法会稍微复杂一点,所以我们继续下一小步。首先我们看一个简单的打包和解包RGB颜色到16bit字中的例子。然后我们会深入了解比特语法表达式。最后我们看3个实际的比特语法的例子。

3.1 打包和解包16bit颜色

我们来写一个简单的例子。假设我们想要描述一个16bit的RGB颜色。我们让5bit代表红色频道、6bit代表绿色频道、5bit代表蓝色频道。(使用更多的空间给绿色是因为,人眼对绿色更敏感)。

我们可以创建16bit的内存段Mem包含单一的RGB颜色组:

1> Red=2.
2
2> Green=61.
61
3> Blue=20.
20
4> Mem=<<Red:5,Green:6,Blue:5>>.
<<23,180>>

注意在第4行我们创建了2字节的二进制对象,包含16bit,而shell打印的则是 <<23,180>>

想要解包一个字,我们编写如下模式:

5>  <<R1:5,G1:6,B1:5>>=Mem.
<<23,180>>
6> R1.
2
7> G1.
61
8> B1.
20

3.2 比特语法表达式

比特语法表达式是如下形式的:

<<>>
<<E1,E2,...,En>>

每个元素Ei指定了二进制对象的一个字段。每个元素Ei有四种格式的可能:

Ei=Value |
    Value:Size |
    Value/TypeSpecifierList |
    Value:Size/TypeSpecifierList

无论使用哪种格式,在二进制对象中的总bit数必须可以被8整除。因为二进制对象实际上只是包含了多个字节的数据,所以没法保存不是以字节为单位的数据。

当你构造一个二进制对象时,Value必须已经是确定的了,可以是字符串、或者可以生成整数、浮点数、二进制对象的表达式。当用于模式匹配操作时,Value可以是已经绑定的或者尚未绑定的变量、整数、字符串、浮点数或二进制对象。

Size必须是一个得到整数的表达式。在模式匹配中,Size必须是整数或者值为整数的变量。Size不可以是尚未绑定的变量。

Size的值指定了数据段的单元数。缺省值依赖于类型。对整数缺省为8,浮点数缺省为64,而二进制对象则对应其长度。在模式匹配时,缺省值仅对最后一个元素有效。其他所有匹配时的二进制对象元素长度必须指定。

TypeSpecifierList是以连字符分割的一列元素,形式为End-Sign-Type-Unit。任何前述元素都可以省略,元素也可以在任何顺序。如果一个元素被省略,就使用其缺省值。

TypeSpecifierList中的项目的值可以是如下:

@type End=big | little | native

(@type是Erlang的类型符号,参阅附录A)

这是指定机器的字节序,native是运行时检测,依赖于具体的CPU。缺省是big。这个仅对从二进制对象中打包和解包整数时才有用。在从不同的字节序的机器上打包和解包二进制对象中的整数时,你必须注意正确的字节序。

有些时候,当你必须确定自己理解这些时,这里有些实验可以用。测试你所在的机器,可以尝试在shell中如下输入:

1> {<<16#12345678:32/big>>,<<16#12345678:32/little>>,
    <<16#12345678:32/native>>,<<16#12345678:32>>}.
{<<18,52,86,120>>,<<120,86,52,18>>,
 <<120,86,52,18>>,<<18,52,86,120>>}

这些输出展示了编码到二进制对象的比特语法。

如果你还是无法放心,那么可以用 term_to_binary/1 来完成转换工作,随后用 binary_to_term/1 完成解包。这样就不用担心字节序的问题了。因为在tuple中总是有正确的字节序。

@type Sign=signed | unsigned

这个参数仅用于模式匹配,缺省是unsigned。

@type Type=integer | float | binary

缺省是integer

@type Unit=1 | 2 | … 255

这个段的总单位数,这个单位数必须大于等于0,而且必须是8的整倍数。

Unit的缺省值依赖于Type,如果Type是integer则为1,如果Type是binary则为8。

如果你感觉比特语法有点复杂,不要怕。让比特语法匹配还算简单。最好的实践方法是在shell中不断的尝试,直到符合要求,然后把代码复制粘贴到程序中。我就这么干的。

3.3 高级比特语法例子

学习比特语法还是略有难度的,但是好处也是巨大的。本届包含3个实际的例子。所有代码都是从现实的程序中挖出来的。

3.3.1 寻找MPEG中的同步帧

假设我们需要一个程序管理MPEG音频数据。我们可能想要使用Erlang编写流媒体服务器而需要获得MPEG音频的tag和内容描述。想要实现这些,我们需要识别出数据流中的同步帧。

MPEG音频是从一大堆帧组成的。每个帧都有他自己的头和跟随的音频信息,不过没有文件头。原理上讲,你可以把一个MPEG文件分成很多段并且分别播放。任何相关软件都需要先读取MPEG流的头信息和同步帧。

一个MPEG头部以11bit的同步帧,就是11个连续的bit组成,后面跟真描述信息,例如:

AAAAAAAA AAABBCCD EEEEFFGH IIJJKLMM
字段 意义
AAAAAAAAAAA 同步字(11bit,全是1)
BB 2bit是MPEG音频的版本号
CC 2bit是层(layer)描述
D 1bit,保护位(bit)

其他相关细节这里不关心。基本上通过A-M的值,我们就可以计算一个MPEG帧的长度了。

想要找到同步点,我们首先假设我们已经正确的定位了MPEG帧的开始。我们使用位置找到并计算帧长度。不过也有可能定位到无效的数值。假设我们已经得到了帧长度,我们就可以跳过开始的下一帧,看看下一段是否是另外一个帧头部。

想要找到同步点,我们首先假设我们已经定位了MPEG头部。我们随后计算帧长度。然后发生如下步骤:

  • 我们的假设是正确的,所以当我们向前跳过一个帧以后,我们会找到下一个MPEG头部。
  • 我们的假设是错误的,我们定位的不是以11个1开头的帧头部标志,所以无法计算帧长度。
  • 我们的假设不正确,但是我们定位了音乐数据的两个字节,看起来像是帧头部。在这种情况下,我们计算帧长度,但是当我们向前跳这个长度时却无法找到新的头部。

为了验证,我们会尝试3个连续的头部。同步帧计算程序如下:

find_sync(Bin,N) ->
    case is_header(N,Bin) of
        {ok,Len1,_} ->
            case is_header(N+Len1,Bin) of
                {ok,Len2,_} ->
                    case is_header(N+Len1+Len2,Bin) of
                        {ok,_,_} ->
                            {ok,N};
                        error ->
                            find_sync(Bin,N+1)
                    end.
                error ->
                    find_sync(Bin,N+1)
            end.
        error ->
            find_sync(Bin,N+1)
    end.

find_sync 尝试找到3个连续的MPEG帧头部。如果字节N在Bin世帧头部的开头,随后 is_header(N,Bin) 会返回 {ok,Length,Info} 。如果 is_header 返回 error ,那么N就无法指向正确的帧开始位置。我们可以在shell中做一个快速的测试来确保它工作正常:

1> {ok,Bin} = file:read("/home/joe/music/mymusic.mp3").
{ok,<<73,68,51,3,0,0,0,0,33,22,84,73,84,50,0,0,0,28, ...>>
2> mp3_sync:find_sync(Bin,1).
{ok,4256}

这里使用 file:read_file 来读取整个文件到二进制对象。现在是函数 is_header

is_header(N,Bin) ->
    @page 94

3.3.2 解包COFF数据

3.3.3 解包IPv4数据包头部

用Erlang实现的斐波拉契数列计算

Wednesday, December 19th, 2007

用Erlang实现的斐波拉契数列计算

作者: gashero

前几日有好友发来帖子告诉我一个Ruby比Python快的实验,见了以后立马写了一个程序,不过优化效果很有限。最后倒是想起来erlang,一直吵吵着想学,但是一直学的都不紧不慢的,没效果。于是准备拿这个例子来尝试一下。

经过了好久的学习,最终写出了一个比较龌龊的例子,如下:

-module(fibmod).
-export([fib/1,calc/1,print/2]).

fib(0) ->
    0;
fib(1) ->
    1;
fib(X) ->
    fib(X-1)+fib(X-2).

calc(35) ->
    print(35,fib(35));
calc(N) ->
    print(N,fib(N)),
    calc(N+1).

print(Number,Fibnum) ->
    io:format("n=~w => ~w~n",[Number,Fibnum]).

也就是这个例子了,要在erlang控制台中运行,需要按照如下步骤:

10> c(fibmod).
{ok,fibmod}
11> fibmod:calc(1).
n=1 => 1
n=2 => 1
n=3 => 2
n=4 => 3
n=5 => 5
n=6 => 8
n=7 => 13
n=8 => 21
n=9 => 34
n=10 => 55
n=11 => 89
n=12 => 144
n=13 => 233
n=14 => 377
n=15 => 610
n=16 => 987
n=17 => 1597
n=18 => 2584
n=19 => 4181
n=20 => 6765
n=21 => 10946
n=22 => 17711
n=23 => 28657
n=24 => 46368
n=25 => 75025
n=26 => 121393
n=27 => 196418
n=28 => 317811
n=29 => 514229
n=30 => 832040
n=31 => 1346269
n=32 => 2178309
n=33 => 3524578
n=34 => 5702887
n=35 => 9227465
ok
12>

当时按照例子,Ruby1.9要十几秒,Python的要三十几秒。不过我在这台机器上没有测试。我在这台笔记本(T43 1.86GHz)上运行如上erlang程序,估算时间是4秒以内(注:不知道如何用erlang计时)。

其实例子超级简单,不过多少算是第一个erlang程序,希望是一个好的开始。现在越来越看好erlang了,也祝愿erlang发展的越来越好吧。

来自未来的erlang

Friday, November 30th, 2007

来自未来的erlang

erlang算是我认真学的第一门函数式编程语言。而他实在是带来了很多新的概念,预示着未来的发展方向。

对于函数式编程,我在使用Python时就已经开始使用了,只不过,依托Python极其垃圾的函数式编程实现,效率极低。所以除了用来摆酷,没什么实际用途。后来好友jorge也推荐过我学其他的函数式编程语言,如lisp、scheme、guile等等。终究因为没有对应的Windows版本而半途而废。我个人平时大部分时间都使用debian,只不过,我不希望我的程序难于部署。而我编程的成就感也是来自更多的人在使用。

而关于erlang,最早是我们头叫我去研究的。当时他很讨厌公司已有的软件开发和部署方式,也就是load balance+app*n的方式。而且公司常用的几种编程语言,Java、Python都无法利用多核CPU。PHP依赖Apache的多进程fork方式实现了利用多核CPU,但是无法利用数据库连接池等等问题也是很烦躁。于是,最后他发现了erlang这个东西,并且嘱咐我关注一下。

关于erlang的优点,网上有很多介绍,我就从我所关注的几个优点来讲。

1、函数式编程。函数式编程语言具有特有的简单性,尽管对于我们这类从过程式语言,并升级到面向对象语言的程序员来说,思想的转弯太大。但是如果转过来以后,却面对着桃花源一样的简单性。从效果上讲lambda算子与图灵机具有相同的作用,过程式语言可以完成的功能,用函数式语言一样可以实现。

2、并行计算。现代已有的过程式语言和面向对象语言在并行计算方面都有很恶心的复杂性。虽说简单的讲有线程、异步、协程等几种方式,但是并发控制却是大多数程序员心中的噩梦。而同时又要面对神出鬼没的死锁。

3、集群计算。这个变态的时代,我们都很无奈,当几千个并发(仅仅是短连接)向你涌来时,除了加机器还能有什么办法。但是加机器就有个艺术的问题了。load balance是不可或缺的,但是lb的价格足以让任何级别的公司心痛。而erlang内置的集群支持则可以平衡的将负载分配到各个erlang服务器上。

4、错误处理。当然,即便是连C这类的语言其实也是有错误处理的,但是错误处理有个层次的问题。如果你满足于函数调用返回错误码,那就算了。但是问题是错误的发生可能比你想象的更加离奇。也许是服务器的电源线不小心被踢掉了;也许是你新部署的应用被运维的同事当木马给杀掉了;甚至仅仅是因为你没有交机房的网费而被拔掉了网线。而在这时,你都要自己写程序来监控么,噩梦,绝对的噩梦。还好,这个世界上有erlang这个东西,确保你可以在多台计算机上分别部署应用并互相监控。并且在某台服务器出错时,不至于让整个应用挂掉。

其实每一种成功的编程语言都有其最成功的应用领域,面对不断扩大的服务器规模、多核CPU、CPU频率的制约,erlang将是未来相当一段时间的成功者。

函数式编程-考验人品

Friday, September 28th, 2007

函数式编程-考验人品

好友jorge(附加形容词:牛人,超牛,巨牛无比,变态,下流)推荐我学函数式编程语言scheme,以前倒是考虑过erlang,不过实在没空所以暂时没有动手。不过Python中对函数式编程倒是有支持,所以最近尝试了一下。

问题如下,一个字符串形式的IP地址,比如”12.24.0.9″,需要转换成32bit无符号整数形式表示的数字。这个其实可以用socket.inet_aton来实现,但是问题是,我拿到的这些IP地址都是这个形式”012.024.000.009″,来自珊瑚虫IP数据库的导出结果,而inet_aton会把这些一段段的以0开头的数字识别为八进制,所以,只能自己写转换函数。

最终呢,就是用函数式编程来解决了问题,实际代码只有一行。代码如下:

ipstr=”012.024.000.009″
numset=reduce(lambda x,y:x*256+y,map(lambda numlist:reduce(lambda x,y:x*10+y,numlist),map(lambda strlist:map(int,strlist),map(list,list(ipstr.split(‘.’,3))))))
from socket import inet_ntoa
import struct
print inet_ntoa(struct.pack(‘!L’,numset))

可以看出函数式编程的代码可读性确实河蟹的可以,我午饭前写的,现在就已经基本看不懂了。不过在TDD(测试驱动开发)的指导下,所有测试用例通过了,就算过了。

函数式编程看起来确实很酷,写这么长的语句一看就是人品很河蟹,所以有如国外的牛说的那样,他们一直希望取缔Python中的函数式编程支持。读者么如果有希望考验自己人品的,也可以多用用函数式编程。呵呵,先这样吧。

Erlang超快速入门

Monday, September 10th, 2007

Erlang超快速入门

日期: 2007-04-06

目录

1 开始使用erlang

如果你在unix系统下输入 erl ,或者在Window$系统下双击Erlang的图标,你可以看到一些提示:

os prompt > erl
Eshell V5.5.4  (abort with ^G)
1> _

其中 “>”提示符意味着系统正在等待输入。

2 使用Erlang作为计算器

1> 213183682167*12937192739173917823.
27579983733990928813319999135233
2> _

记住每个表达式以英文句号结束

3 编辑前面的表达式

可以使用简单的emacs命令获取前面的表达式。常见的几个如下:

Unix键 Win$键 说明
^P Up 获取前一行(previous)
^N Down 获取下一行(next)
^A Home 到行首
^E End 到行尾
^D Del 删除光标前字符
^F Left 向前移动一个字符
^B Right 向后移动一个字符
Return Enter 执行当前命令

注意:^X意味着Control+X 。

尝试按下Control+P来查看结果。

译者注:一位朋友提示如上的快捷键是在unix系统之下的,Window$下的快捷键附在了如上列表后的括号内。另外,在Unix系统下使用Control+G的退出方式,在Window$下使用Control+C来退出。

4 编译你的第一个程序

把如下内容输入到一个文件里:

-module(test).
-export([fac/1]).

fac(0) -> 1;
fac(N) -> N * fac(N-1).

把这些存储到文件 test.erl 中,文件名必须与模块名相同。

编译这个程序使用如下命令,并且运行:

3> c(test).
{ok,test}
30> test:fac(20).
2432902008176640000
4> test:fac(40).
815915283247897734345611269596115894272000000000
32> _

现在可以做些其他有趣的事情了。

5 深入了解Erlang

Getting Started

Warp me to the documentation – I want to read it all