Archive for the ‘不着边的翻译’ Category

ProtoBuf开发者指南 – 非官方不完整版

Monday, September 22nd, 2008

ProtoBuf开发者指南

译者: gashero

目录

1   概览

欢迎来到protocol buffer的开发者指南文档,一种语言无关、平台无关、扩展性好的用于通信协议、数据存储的结构化数据串行化方法。

本文档面向希望使用protocol buffer的Java、C++或Python开发者。这个概览介绍了protocol buffer,并告诉你如何开始,你随后可以跟随编程指导( http://code.google.com/apis/protocolbuffers/docs/tutorials.html )深入了解protocol buffer编码方式( http://code.google.com/apis/protocolbuffers/docs/encoding.html )。API参考文档( http://code.google.com/apis/protocolbuffers/docs/reference/overview.html )同样也是提供了这三种编程语言的版本,不够协议语言( http://code.google.com/apis/protocolbuffers/docs/proto.html )和样式( http://code.google.com/apis/protocolbuffers/docs/style.html )指导都是编写 .proto 文件。

1.1   什么是protocol buffer

ProtocolBuffer是用于结构化数据串行化的灵活、高效、自动的方法,有如XML,不过它更小、更快、也更简单。你可以定义自己的数据结构,然后使用代码生成器生成的代码来读写这个数据结构。你甚至可以在无需重新部署程序的情况下更新数据结构。

1.2   他们如何工作

你首先需要在一个 .proto 文件中定义你需要做串行化的数据结构信息。每个ProtocolBuffer信息是一小段逻辑记录,包含一系列的键值对。这里有个非常简单的 .proto 文件定义了个人信息:

message Person {
    required string name=1;
    required int32 id=2;
    optional string email=3;

    enum PhoneType {
        MOBILE=0;
        HOME=1;
        WORK=2;
    }

    message PhoneNumber {
        required string number=1;
        optional PhoneType type=2 [default=HOME];
    }

    repeated PhoneNumber phone=4;
}

有如你所见,消息格式很简单,每个消息类型拥有一个或多个特定的数字字段,每个字段拥有一个名字和一个值类型。值类型可以是数字(整数或浮点)、布 尔型、字符串、原始字节或者其他ProtocolBuffer类型,还允许数据结构的分级。你可以指定可选字段,必选字段和重复字段。你可以在( http://code.google.com/apis/protocolbuffers/docs/proto.html )找到更多关于如何编写 .proto 文件的信息。

一旦你定义了自己的报文格式(message),你就可以运行ProtocolBuffer编译器,将你的 .proto 文件编译成特定语言的类。这些类提供了简单的方法访问每个字段(像是 query() 和 set_query() ),像是访问类的方法一样将结构串行化或反串行化。例如你可以选择C++语言,运行编译如上的协议文件生成类叫做 Person 。随后你就可以在应用中使用这个类来串行化的读取报文信息。你可以这么写代码:

Person person;
person.set_name("John Doe");
person.set_id(1234);
person.set_email("jdoe@example.com");
fstream.output("myfile",ios::out | ios::binary);
person.SerializeToOstream(&output);

然后,你可以读取报文中的数据:

fstream input("myfile",ios::in | ios:binary);
Person person;
person.ParseFromIstream(&input);
cout << "Name: " << person.name() << endl;
cout << "E-mail: " << person.email() << endl;

你可以在不影响向后兼容的情况下随意给数据结构增加字段,旧有的数据会忽略新的字段。所以如果使用ProtocolBuffer作为通信协议,你可以无须担心破坏现有代码的情况下扩展协议。

你可以在API参考( http://code.google.com/apis/protocolbuffers/docs/reference/overview.html )中找到完整的参考,而关于ProtocolBuffer的报文格式编码则可以在( http://code.google.com/apis/protocolbuffers/docs/encoding.html )中找到。

1.3   为什么不用XML?

ProtocolBuffer拥有多项比XML更高级的串行化结构数据的特性,ProtocolBuffer:

  • 更简单
  • 小3-10倍
  • 快20-100倍
  • 更少的歧义
  • 可以方便的生成数据存取类

例如,让我们看看如何在XML中建模Person的name和email字段:

<person>
    <name>John Doe</name>
    <email>jdoe@example.com</email>
</person>

对应的ProtocolBuffer报文则如下:

#ProtocolBuffer的文本表示
#这不是正常时使用的二进制数据
person {
    name: "John Doe"
    email: "jdoe@example.com"
}

当这个报文编码到ProtocolBuffer的二进制格式( http://code.google.com/apis/protocolbuffers/docs/encoding.html )时(上面的文本仅用于调试和编辑),它只需要28字节和100-200ns的解析时间。而XML的版本需要69字节(除去空白)和5000-10000ns的解析时间。

当然,操作ProtocolBuffer也很简单:

cout << "Name: " << person.name() << endl;
cout << "E-mail: " << person.email() << endl;

而XML的你需要:

cout << "Name: "
     << person.getElementsByTagName("name")->item(0)->innerText()
     << endl;
cout << "E-mail: "
     << person.getElementsByTagName("email")->item(0)->innerText()
     << end;

当然,ProtocolBuffer并不是在任何时候都比XML更合适,例如ProtocolBuffer无法对一个基于标记文本的文档建模,因为 你根本没法方便的在文本中插入结构。另外,XML是便于人类阅读和编辑的,而ProtocolBuffer则不是。还有XML是自解释的,而 ProtocolBuffer仅在你拥有报文格式定义的 .proto 文件时才有意义。

1.4   听起来像是为我的解决方案,如何开始?

下载包( http://code.google.com/p/protobuf/downloads/ ),包含了Java、Python、C++的ProtocolBuffer编译器,用于生成你需要的IO类。构建和安装你的编译器,跟随README的指令就可以做到。

一旦你安装好了,就可以跟着编程指导( http://code.google.com/apis/protocolbuffers/docs/tutorials.html )来选择语言-随后就是使用ProtocolBuffer创建一个简单的应用了。

1.5   一点历史

ProtocolBuffer最初是在Google开发的,用以解决索引服务器的请求、响应协议。在使用ProtocolBuffer之前,有一种格式用以处理请求和响应数据的编码和解码,并且支持多种版本的协议。而这最终导致了丑陋的代码,有如:

if (version==3) {
    ...
}else if (version>4) {
    if (version==5) {
        ...
    }
    ...
}

通信协议因此变得越来越复杂,因为开发者必须确保,发出请求的人和接受请求的人必须同时兼容,并且在一方开始使用新协议时,另外一方也要可以接受。

ProtocolBuffer设计用于解决这一类问题:

  • 很方便引入新字段,而中间服务器可以忽略这些字段,直接传递过去而无需理解所有的字段。
  • 格式可以自描述,并且可以在多种语言中使用(C++、Java等)

然而用户仍然需要手写解析代码。

随着系统的演化,他需要一些其他的功能:

  • 自动生成编码和解码代码,而无需自己编写解析器。
  • 除了用于简短的RPC(Remote Procedure Call)请求,人们使用ProtocolBuffer来做数据存储格式(例如BitTable)。
  • RPC服务器接口可以作为 .proto 文件来描述,而通过ProtocolBuffer的编译器生成存根(stub)类供用户实现服务器接口。

ProtocolBuffer现在已经是Google的混合语言数据标准了,现在已经正在使用的有超过48,162种报文格式定义和超过12,183个 .proto 文件。他们用于RPC系统和持续数据存储系统。

2   语言指导

本指导描述了如何使用ProtocolBuffer语言来定义结构化数据类型,包括 .proto 文件的语法和如何生成存取类。

这是一份指导手册,一步步的例子使用文档中的多种功能,查看入门指导( http://code.google.com/apis/protocolbuffers/docs/tutorials.html )选择你的语言。

2.1   定义一个消息类型

@waiting …

2.2   值类型

@waiting …

2.3   可选字段与缺省值

@waiting …

2.4   枚举

@waiting …

2.5   使用其他消息类型

@waiting …

2.6   嵌套类型

@waiting …

2.7   更新一个数据类型

@waiting …

2.8   扩展

@waiting …

2.9   包

@waiting …

2.10   定义服务

@waiting …

2.11   选项

@waiting …

2.12   生成你的类

@waiting …

3   代码风格指导

本文档提供了 .proto 文件的代码风格指导。按照惯例,你将会,你将会生成一些便于阅读和一致的ProtocolBuffer定义文件。

3.1   消息与字段名

使用骆驼风格的大小写命名,即单词首字母大写,来做消息名。使用GNU的全部小写,使用下划线分隔的方式定义字段名:

message SongServerRequest {
    required string song_name=1;
}

使用这种命名方式得到的名字如下:

C++:
    const string& song_name() {...}
    void set_song_name(const string& x) {...}

Java:
    public String getSongName() {...}
    public Builder setSongName(String v) {...}

3.2   枚举

使用骆驼风格做枚举名,而用全部大写做值的名字:

enum Foo {
    FIRST_VALUE=1;
    SECOND_VALUE=2;
}

每个枚举值最后以分号结尾,而不是逗号。

3.3   服务

如果你的 .proto 文件定义了RPC服务,你可以使用骆驼风格:

service FooService {
    rpc GetSomething(FooRequest) returns (FooResponse);
}

4   编码

本文档描述了ProtocolBuffer的串行化二进制数据格式定义。你如果仅仅是在应用中使用ProtocolBuffer,并不需要知道这些,但是这些会对你定义高效的格式有所帮助。

4.1   一个简单的消息

@waiting …

4.2   基于128的Varints

@waiting …

4.3   消息结构

@waiting …

4.4   更多的值类型

@waiting …

4.5   内嵌消息

@waiting …

4.6   可选的和重复的元素

@waiting …

4.7   字段顺序

@waiting …

5   ProtocolBuffer基础:C++

@waiting …

6   ProtocolBuffer基础:Java

@waiting …

7   ProtocolBuffer基础:Python

本指南给Python程序员一个快速使用的ProtocolBuffer的指导。通过一些简单的例子来在应用中使用ProtocolBuffer,它向你展示了如何:

  • 定义 .proto 消息格式文件
  • 使用ProtocolBuffer编译器
  • 使用Python的ProtocolBuffer编程接口来读写消息

这并不是一个在Python中使用ProtocolBuffer的完整指导。更多细节请参考手册信息,查看语言指导( http://code.google.com/apis/protocolbuffers/docs/proto.html ),Python API( http://code.google.com/apis/protocolbuffers/docs/reference/python/index.html ),和编码手册( http://code.google.com/apis/protocolbuffers/docs/encoding.html )。

7.1   为什么使用ProtocolBuffer?

下面的例子”地址本”应用用于读写人的联系信息。每个人有name、ID、email,和联系人电话号码。

如何串行化和读取结构化数据呢?有如下几种问题:

  • 使用Python的pickle,这是语言内置的缺省方法,不过没法演化,也无法让其他语言支持。
  • 你可以发明一种数据编码方法,例如4个整数”12:3-23:67″,这是简单而灵活的方法,不过你需要自己写解析器代码,且只适用于简单的数据。
  • 串行化数据到XML。这种方法因为可读性和多种语言的兼容函数库而显得比较吸引人,不过这也不是最好的方法,因为XML浪费空间是臭名昭著的,编码解码也很浪费时间。而XML DOM树也是很复杂的。

ProtocolBuffer提供了灵活、高效、自动化的方法来解决这些问题。通过ProtocolBuffer,只需要写一个 .proto 数据结构描述文件,就可以编译到几种语言的自动编码解码类。生成的类提供了setter和getter方法来控制读写细节。最重要的是ProtocolBuffer支持后期扩展协议,而又确保旧格式可以兼容。

7.2   哪里可以找到例子代码

源码发行包中已经包含了,在”example”文件夹。

7.3   定义你的协议格式

想要创建你的地址本应用,需要开始于一个 .proto 文件。定义一个 .proto 文件很简单:添加一个消息到数据结构,然后指定一个和一个类型到每一个字段,如下是本次例子使用的 addressbook.proto

package tutorial;

message Person {
    required string name=1;
    required int32 id=2;
    optional string email=3;

    enum PhoneType {
        MOBILE=0;
        HOME=1;
        WORK=2;
    }

    message PhoneNumber {
        required string number=1;
        optional PhoneType type=2 [default=HOME];
    }

    repeated PhoneNumber phone=4;
}

message AddressBook {
    repeated Person person=1;
}

有如你所见的,语法类似于C++或Java。让我们分块理解他们。

@waiting …

7.4   编译你的ProtocolBuffer

现在已经拥有了 .proto 文件,下一步就是编译生成相关的访问类。运行编译器 protoc 编译你的 .proto 文件。

  1. 如果还没安装编译器则下载并按照README的安装。
  2. 运行编译器,指定源目录和目标目录,定位你的 .proto 文件到源目录,然后执行:
    protoc -I=$SRC_DIR --python_out=$DST_DIR addressbook.proto

因为需要使用Python类,所以 --python_out 选项指定了特定的输出语言。

这个步骤会生成 addressbook_pb2.py 到目标目录。

7.5   ProtocolBuffer API

不像生成的C++和Java代码,Python生成的类并不会直接为你生成存取数据的代码。而是(有如你在 addressbook_pb2.py 中见到的)生成消息描述、枚举、和字段,还有一些神秘的空类,每个对应一个消息类型:

class Person(message.Message):
    __metaclass__=reflection.GeneratedProtocolMessageType

    class PhoneNumber(message.Message):
        __metaclass__=reflection.GeneratedProtocolMessageType
        DESCRIPTION=_PERSON_PHONENUMBER

    DESCRIPTOR=_PERSON

class AddressBook(message.Message):
    __metaclass__=reflection.GeneratedProtocolMessageType
    DESCRIPTOR=_ADDRESSBOOK

这里每个类最重要的一行是 __metaclass__=reflection.GeneratedProtocolMessageType 。通过Python的元类机制工作,你可以把他们看做是生成类的模板。在载入时, GeneratedProtocolMessageType 元类使用特定的描述符创建Python方法。随后你就可以使用完整的功能了。

最后就是你可以使用 Person 类来操作相关字段了。例如你可以写:

import addressbook_pb2
person=addressbook_pb2.Person()
person.id=1234
person.name="John Doe"
person.email="jdoe@example.com"
phone=person.phone.add()
phone.number="555-4321"
phone.type=addressbook_pb2.Person.HOME

需要注意的是这些赋值属性并不是简单的增加新字段到Python对象,如果你尝试给一个 .proto 文件中没有定义的字段赋值,就会抛出 AttributeError 异常,如果赋值类型错误会抛出 TypeError 。在给一个字段赋值之前读取会返回缺省值:

person.no_such_field=1  #raise AttributeError
person.id="1234"        #raise TypeError

更多相关信息参考( http://code.google.com/apis/protocolbuffers/docs/reference/python-generated.html )。

7.5.1   枚举

枚举在元类中定义为一些符号常量对应的数字。例如常量 addressbook_pb2.Person.WORK 拥有值2。

7.5.2   标准消息方法

每个消息类包含一些其他方法允许你检查和控制整个消息,包括:

  • IsInitialized() :检查是否所有必须(required)字段都已经被赋值了。
  • __str__() :返回人类可读的消息表示,便于调试。
  • CopyFrom(other_msg) :使用另外一个消息的值来覆盖本消息。
  • Clear() :清除所有元素的值,回到初识状态。

这些方法是通过接口 Message 实现的,更多消息参考( http://code.google.com/apis/protocolbuffers/docs/reference/python/google.protobuf.message.Message-class.html )。

7.5.3   解析与串行化

最后,每个ProtocolBuffer类有些方法用于读写消息的二进制数据( http://code.google.com/apis/protocolbuffers/docs/encoding.html )。包括:

  • SerializeToString() :串行化,并返回字符串。注意是二进制格式而非文本。
  • ParseFromString(data) :解析数据。

他们是成对使用的,提供二进制数据的串行化和解析。另外参考消息API参考( http://code.google.com/apis/protocolbuffers/docs/reference/python/google.protobuf.message.Message-class.html )了解更多信息。

Note

ProtocolBuffer与面向对象设计

ProtocolBuffer类只是用于存取数据的,类似于C++中的结构体,他们并没有在面向对象方面做很好的设计。如果你想要给这些类添加更多的行为,最好的方法是包装(wrap)。包装同样适合于复用别人写好的 .proto 文件。这种情况下,你可以把ProtocolBuffer生成类包装的很适合于你的应用,并隐藏一些数据和方法,暴露有用的函数等等。 你不可以通过继承来给自动生成的类添加行为。 这会破坏他们的内部工作机制。

7.6   写消息

现在开始尝试使用ProtocolBuffer的类。第一件事是让地址本应用可以记录联系人的细节信息。想要做这些需要先创建联系人实例,然后写入到输出流。

这里的程序从文件读取地址本,添加新的联系人信息,然后写回新的地址本到文件。

#! /usr/bin/python
import addressbook_pb2
import sys

#这个函数使用用户输入填充联系人信息
def PromptForAddress(person):
    person.id=int(raw_input("Enter person ID number: "))
    person.name=raw_input("Enter name: ")
    email=raw_input("Enter email address (blank for none): ")
    if email!="":
        person.email=email
    while True:
        number=raw_input("Enter a phone number (or leave blank to finish): ")
        if number=="":
            break
        phone_number=person.phone.add()
        phone_number.number=number
        type=raw_input("Is this a mobile, home, or work phone? ")
        if type=="mobile":
            phone_number.type=addressbook_pb2.Person.MOBILE
        elif type=="home":
            phone_number.type=addressbook_pb2.Person.HOME
        elif type=="work":
            phone_number.type=addressbook_pb2.Person.WORK
        else:
            print "Unknown phone type; leaving as default value."

#主函数,从文件读取地址本,添加新的联系人,然后写回到文件
if len(sys.argv)!=2:
    print "Usage:",sys.argv[0],"ADDRESS_BOOK_FILE"
    sys.exit(-1)

address_book=addressbook_pb2.AddressBook()

#读取已经存在的地址本
try:
    f=open(sys.argv[1],"fb")
    address_book.ParseFromString(f.read())
    f.close()
except OSError:
    print sys.argv[1]+": Count open file. Creating a new one."

#添加地址
PromptFromAddress(address_book.person.add())

#写入到文件
f=open(sys.argv[1],"wb")
f.write(address_book.SerializeToString())
f.close()

7.7   读消息

当然,一个无法读取的地址本是没什么用处的,这个例子读取刚才创建的文件并打印所有信息:

#! /usr/bin/python

import addressbook_pb2
import sys

#遍历地址本中所有的人并打印出来
def ListPeople(address_book):
    for person in address_book.person:
        print "Person ID:",person.id
        print "  Name:",person.name
        if person.HasField("email"):
            print "  E-mail:",person.email
        for phone_number in person.phone:
            if phone_number.type==addressbook_pb2.Person.MOBILE:
                print "  Mobile phone #:",
            elif phone_number.type==addressbook_pb2.Person.HOME:
                print "  Home phone #:",
            elif phone_number.type==addressbook_pb2.Person.WORK:
                print "  Work phone #:",
            print phone_number.number

#主函数,从文件读取地址本
if len(sys.argv)!=2:
    print "Usage:",sys.argv[0],"ADDRESS_BOOK_FILE"
    sys.exit(-1)

address_book=addressbook_pb2.AddressBook()

#读取整个地址本文件
f=open(sys.argv[1],"rb")
address_book.ParseFromString(f.read())
f.close()

ListPeople(address_book)

7.8   扩展ProtocolBuffer

在你发不了代码以后,可能会想要改进ProtocolBuffer的定义。如果你想新的数据结构向后兼容,而你的旧数据可以向前兼容,那么你就找对了东西了,不过有些规则需要遵守。在新版本的ProtocolBuffer中:

  • 必须不可以改变已经存在的标签的数字。
  • 必须不可以增加或删除必须(required)字段。
  • 可以删除可选(optional)或重复(repeated)字段。
  • 可以添加新的可选或重复字段,但是必须使用新的标签数字,必须是之前的字段所没有用过的。

这些规则也有例外( http://code.google.com/apis/protocolbuffers/docs/proto.html#updating ),不过很少使用。

如果你遵从这些规则,旧代码会很容易的读取新的消息,并简单的忽略新的字段。而对旧的被删除的可选字段也会简单的使用他们的缺省值,被删除的重复字段会自动为空。新的代码也会透明的读取旧的消息。然而,需要注意的是新的可选消息不会在旧的消息中显示,所以你需要使用 has_ 严格的检查他们是否存在,或者在 .proto 文件中提供一个缺省值。如果没有缺省值,就会有一个类型相关的默认缺省值:对于字符串就是空字符串;对于布尔型则是false;对于数字类型默认为0。同时要注意的是如果你添加了新的重复字段,你的新代码不会告诉你这个字段为空(新代码)也不会,也不会(旧代码)包含 has_ 标志。

7.9   高级使用

ProtocolBuffer不仅仅提供了数据结构的存取和串行化。查看Python API参考( http://code.google.com/apis/protocolbuffers/docs/reference/python/index.html )了解更多功能。

一个核心功能是通过消息类的映射(reflection)提供的。你可以通过它遍历消息的所有字段,和管理他们的值。关于映射的一个很有用的地方是 转换到其他编码,如XML或JSON。一个使用映射的更高级的功能是寻找同类型两个消息的差异,或者开发出排序、正则表达式等功能。使用你的创造力,还可 以用ProtocolBuffer实现比你以前想象的更多的问题。

映射是通过消息接口提供的。

8   参考概览

@waiting …

9   C++代码生成

@waiting …

10   C++ API

@waiting …

11   Java代码生成

@waiting …

12   Java API

@waiting …

13   Python代码生成

本页提供了Python生成类的相关细节。你可以在阅读本文档之前查看语言指导。

Python的ProtocolBuffer实现与C++和Java的略有不同,编译器只输出构建代码的描述符来生成类,而由Python的元类来执行工作。本文档描述了元类开始生效以后的东西。

13.1   编译器的使用

ProtocolBuffer通过编译器的 --python_out= 选项来生成Python的相关类。这个参数实际上是指定输出的Python类放在哪个目录下。编译器会为每个 .proto 文件生成一个对应的 .py 文件。输出文件名与输入文件名相关,不过有两处修改:

  • 扩展名 .proto 改为 .py 。
  • 路径名的修改。

如果你按照如下调用编译器:

protoc --proto_path=src --python_out=build/gen src/foo.proto src/bar/baz.proto

编译器会自动读取两个 .proto 文件然后产生两个输出文件。在需要时编译器会自动创建目录,不过 --python_out 指定的目录不会自动创建。

需要注意的是,如果 .proto 文件名或路径包含有无法在Python中使用的模块名(如连字符),就会被自动转换为下划线。所以文件 foo-bar.proto 会变成 foo_bar_pb2.py

Note

在每个文件后缀的 _pb2.py 中的2代表ProtocolBuffer版本2。版本1仅在Google内部使用,但是你仍然可以在以前发布的一些代码中找到它。自动版本2开 始,ProtocolBuffer开始使用完全不同的接口了,从此Python也没有编译时类型检查了,我们加上这个版本号来标志Python文件名。

13.2   包

Python代码生成根本不在乎包的名字。因为Python使用目录名来做包名。

13.3   消息

先看看一个简单的消息声明:

message Foo {}

ProtocolBuffer编译器会生成类Foo,它是 google.protobuf.Message 的子类。这个实体类,不含有虚拟方法。不像C++和Java,Python生成类对优化选项不感冒;实际上Python的生成代码已经为代码大小做了优化。

你不能继承Foo的子类。生成类被设计不可以被继承,否则会被打破一些设计。另外,继承本类也是不好的设计。

Python的消息类没有特定的公共成员,而是定义接口,极其嵌套的字段、消息和枚举类型。

一个消息可以在另外一个消息中声明,例如 message Foo { message Bar {}} 。在这种情况下,Bar类定义为Foo的一个静态成员,所以你可以通过 Foo.Bar 来引用。

13.4   字段

对于消息类型中的每一个字段,都有对应的同名成员。

13.4.1   简单字段

如果你有一个简单字段(包括可选的和重复的),也就是非消息字段,你可以通过简单字段的方式来管理,例如foo字段的类型是int32,你可以:

message.foo=123
print message.foo

注意设置foo的值,如果类型错误会抛出TypeError。

如果foo在赋值之前就读取,就会使用缺省值。想要检查是否已经赋值,可以用 HasField() ,而清除该字段的值用 ClearField() 。例如:

assert not message.HasField("foo")
message.foo=123
assert message.HasField("foo")
message.ClearField("foo")
assert not message.HasField("foo")

13.4.2   简单消息字段

消息类型工作方式略有不同。你无法为一个嵌入消息字段赋值。而是直接操作这个消息的成员。因为实例化上层消息时,其包含的子消息同时也实例化了,例如定义:

message Foo {
    optional Bar bar=1;
}

message bar {
    optional int32 i=1;
}

你不可以这么做,因为不能做消息类型字段的赋值:

foo=Foo()
foo.bar=Bar()   #WRONG!

而是可以直接对消息类型字段的成员赋值:

foo=Foo()
assert not foo.HasField("bar")
foo.bar.i=1
assert foo.HasField("bar")

注意简单的读取消息类型字段的未赋值成员只不过是打印其缺省值:

foo=Foo()
assert not foo.HasField("bar")
print foo.bar.i #打印i的缺省值
assert not foo.HasField("bar")

13.4.3   重复字段

重复字段表现的像是Python的序列类型。如果是嵌入的消息,你无法为字段直接赋值,但是你可以管理。例如给定的定义:

message Foo {
    repeated int32 nums=1;
}

你就可以这么做:

foo=Foo()
foo.nums.append(15)
foo.nums.append(32)
assert len(foo.nums)==2
assert foo.nums[0]==15
assert foo.nums[1]==32
for i in foo.nums:
    print i
foo.nums[1]=56
assert foo.nums[1]==56

作为一种简单字段,清除该字段必须使用 ClearField() 。

13.4.4   重复消息字段

重复消息字段工作方式与重复字段很像,除了 add() 方法用于返回新的对象以外。例如如下定义:

message Foo {
    repeated Bar bar=1;
}

message Bar {
    optional int32 i=1;
}

你可以这么做:

foo=Foo()
bar=foo.bars.add()
bar.i=15
bar=foo.bars.add()
bar.i=32
assert len(foo.bars)==2
assert foo.bars[0].i==15
assert foo.bars[1].i==32
for bar in foo.bars:
    print bar.i
foo.bars[1].i=56
assert foo.bars[1].i==56

13.4.5   枚举类型

@waiting …

13.4.6   扩展

@waiting …

13.5   服务

13.5.1   接口

一个简单的接口定义:

service Foo {
    rpc Bar(FooRequest) returns(FooResponse);
}

ProtocolBuffer的编译器会生成类 Foo 来展示这个服务。 Foo 将会拥有每个服务定义的方法。在这种情况下 Bar 方法的定义是:

def Bar(self,rpc_controller,request,done)

参数等效于 Service.CallMethod() ,除了隐含的 method_descriptor 参数。

这些生成的方法被定义为可以被子类重载。缺省实现只是简单的调用 controller.SetFailed() 而抛出错误信息告之尚未实现。然后调用done回调。在实现你自己的服务时,你必须继承生成类,然后重载各个接口方法。

Foo继承了 Service 接口。ProtocolBuffer编译器会自动声响相关的实现方法:

  • GetDescriptor :返回服务的 ServiceDescriptor 。
  • CallMethod :检测需要调用哪个方法,并且直接调用。
  • GetRequestClass 和 GetResponseClass :返回指定方法的请求和响应类。

13.5.2   存根(Stub)

ProtocolBuffer编译器也会为每个服务接口提供一个存根实现,用于客户端发送请求到服务器。对于Foo服务,存根实现是 Foo_Stub 。

Foo_Stub 是Foo的子类,他的构造器是一个 RpcChannel 。存根会实现调用每个服务方法的 CallMethod() 。

ProtocolBuffer哭并不包含RPC实现。然而,它包含了你构造服务类的所有工具,不过选择RPC实现则随你喜欢。你只需要提供 RpcChannel 和 RpcController 的实现即可。

14   Python API

@waiting …

15   其他语言

@waiting …

使用SQLObject

Monday, June 2nd, 2008

使用SQLObject

译者: gashero

目录

1   导入模块

from sqlobject import *

2   定义MySQL使用的URI连接

mysqluri="mysql://user:password@host:port/database"

端口号为3306,一定要指定的。否则按照旧式连接方法里面,端口port属性设为None,就会抛出TypeError异常,因为要求必须是一个整数类型的端口号。如果按照新式的URI方式连接,如果不指定端口号则port默认为0,也会出现连接失败的问题。

sqlite的连接:

sqlite:///full/path/to/database
sqlite:/C|full/path/to/database
sqlite:/:memory:

postgre数据库的连接:

postgres://user@host/database?debug=&cache=
postgres://host:5432/database

3   连接

conn=connectionForURI(mysqluri)
sqlhub.processConnection=conn

4   定义一个表格类

class Person(SQLObject):
    firstName=StringCol()
    middleInitial=StringCol(length=1,default=None)
    lastName=StringCol()

如果没有定义sqlhub,则需要使用 Person._connection=conn 来指定连接。

5   创建表格

Person.createTable()

可以指定参数ifNotExists=True,仅仅在表格不存在时才创建这个表格。

6   自动索引

自动给出一个叫做id的索引,所以不需要人为指定。

在MySQL中定义是:

INT PRIMARY KEY AUTO_INCREMENT

需要使用这个字段时,使用.id属性。

7   创建一个对象

就是创建类的实例:

Person(firstName="John",lastName="Doe")

在SQLObject中的NULL/None并不是代表缺省。NULL代表完全不同的事物、正文或者是人。有时NULL也代表缺省(default),有时代表不适用,有时代表未知。如果希望缺省一个值,可以使用NULL或其他。

SQLObject的default不同于数据库的default。SQLObject从不使用数据库的default。

注意,创建一个对象时,构造方法的参数必须要指定列名,也就是映射类的属性名,否则会出现异常。

8   空值

如果在Person的实例中省略了firstName和lastName则会产生错误,因为没有赋予缺省值。如果是赋予了缺省值的如middleInitial字段,则会被设为NULL值,等同于数据库的None。

9   查询

可以使用类的.get(n)方法得到已经存在的实例。

当创建一个对象时,SQLObject会自动将其存入数据库,而不像其他系统那样,需要手动提交修改。另外,对已有对象的属性修改也会直接存入数据库。

列可以按照属性来存取。注意,对象是独一无二的(unique),如果两次获取同一ID的记录,则获得的是同一对象。这种机制可以确保多个线程存取 同一对象时的一致性。当然,多个线程之间可以不共享对象实例。但是在使用事务(transaction)时,因为事务的隔离性而不成立。

10   开启调试显示

同步显示SQL语句和调试状态。建立连接时使用debug选项:

mysql://user:passwd@host:port/database?debug=t

或:

Person._connection.debug=True

还可以选用的其他选项包括debugOutput(缺省为False),cache(True),autoCommit(True),debugThreading(False)。

在可以看到SQL语句的情况下可以清除的理解工作过程。只需在连接URI中加入”?debug=t”即可。或者设置debug属性。这样,所有的SQL语句都会打印到控制台。这是可靠的,也推荐使用。

11   set方法

用于微弱的提高性能,一次指定多个属性,如:

>>> p.set(firstName="Robert",lastName="Hope Jr.")

12   懒惰的更新

缺省时,每修改一个属性就会导致一个UPDATE的发生,或者每次调用 .set() 时。如果想要避免(avoid)这种仿佛的更新,加入 _lazyUpdate=True 到类定义。这样只有在每次调用 inst.syncUpdate() 或者 obj.sync() 时才写入更新, .sync() 也同时会取出数据库中的最新数据,而 .syncUpdate() 并不这样做。

如果一个实例含有”.sqlmeta.dirty”属性,就是用来指示含有未提交数据的属性。则插入动作会立即提交,这时是没有办法延迟提交的。

13   一对多联系

就是需要建立外键。例如一个地址本类,需要建立外键对应到Person类。

class Address(SQLObject):
    street=StringCol()
    city=StringCol()
    state=StringCol(length=2)
    zip=StringCol(length=9)
    person=ForeignKey('Person')
Address.createTable()

这样通过ForeignKey()方法建立对Person类的外键。实际上是引用了Person对象。实际是按照类的名字(字符串)来引用一个类。数据库中对应person_id列,对应指向person列。

注意:SQLObject使用字符串来引用一个类是因为很多时候也许一个类还不存在。class关键字作为一个命令,在导入一个模块时才执行,并绑定类的名字和类。所以为了确保类之间的引用正确,在所有的类都装载完成之后才建立类之间的联系。

在需要一个人Person对应多个地址时,可用如下连接:

class Person(SQLObject):
    ...
    addresses=MultipleJoin('Address')

在我们已经拥有了Person类的情况下,可以用如下方式修改:

Person.sqlmeta.addJoin(MultipleJoin('Address',joinMethodName='addresses'))

大多数时候可以在创建SQLObject类之后再修改他们。在类定义中使用*Col对象属性等同于使用类方法addColumn()。

然后我们就可以使用自动联系aPerson.addresses,这将会返回一个列表。例如:

>>> p.addresses
[]
>>> Address(street='123 W Main St',city='Smallsville',
...         state="MN",zip='55407',person=p)
<Address 1 ...>
>>> p.addresses
[<Address 1 ...>]

多连接(Multiple Join)类似于关系连接(Related Join),返回结果的列表。你可能更喜欢得到SelectResults对象,可以使用SQLMultipleJoin和SQLRelatedJoin。

14   多对多联系

这个例子中包含用户(user)和角色(role)两个对象,其间包含多对多联系,将使用RelatedJoin来实现。

class User(SQLObject):
    class sqlmeta:
        #user是一些数据库的保留字,所以用了别名
        table="user_table"
    username=StringCol(alternateID=True,length=20)
    #暂时先定义这个,其他还可以有很多字段
    role=RelatedJoin("Role")
class Role(SQLObject):
    name=StringCol(alternateID=True,length=20)
    users=RelatedJoin('User")
User.createTable()
Role.createTable()

注意:使用sqlmeta类。这个类用于存储多种元信息(metadata)。这是SQLObject 0.7中新引入的特性。查看Class sqlmeta节了解详细。

使用:

bob=User(username="bob")
tim=User(username="tim")
admin=Role(name='admin')
editor=Role(name='editor')
bob.addRole(admin)
bob.addRole(editor)
tim.addRole(editor)
>>> bob.roles
[<Role 1 name="admin">, <Role 2 name='editor'>]
>>> tim.roles
[<Role 2 name='editor'>]
>>> admin.users
[<User 1 username='bob'>]
>>> editor.users
[<User 1 username='bob'>, <User 2 username='tim'>]

这会自动生成一个中间表role_user。用来同时引用其他类。这个表不会成为一个暴露(expose)的类,这个表的行也不会等同于Python对象。这种多对多的关系完全被隐藏了。

如果想要自己创建中间表,用来增加一些字段,可以查看标准的SQLObject的add/remove方法来工作。假设你可以提供连接列和其他类的 正确连接,也无法通过这些方法插入扩展信息,而且也无法设置默认值。(Assuming that you are providing the join with the correct joinColumn and otherColumn arguments, be aware it’s not possible to insert extra data via such methods, nor will they set any default value.)。

如前述的User/Role系统,如果创建了UserRole中间表,通过创建两个外键来建立MTM(多对多Many-to-Many)联系,并且 附加了DateTimeCol类型的字段(缺省为当前时间)。那么这个列在使用addRole()方法加入role之前会保持为空。

你可能会注意到列加入了扩展保留字alternateID。可以使用alternateID=True来确保该字段的值唯一(uniquely)。 有如确保用户名必须唯一一样。这个标识符会成为主键,且可以访问的。对于这种字段,如果添加了相同值,则会抛出异常 pysqlite2.dbapi2.IntegrityError: column [列名] is not unique。

注意:SQLObject必须确保主键是唯一且不可改变的。可以通过SQLObject来改变主键,但是你要自己提供确保数据一致性的机制。正是因为这个原因,才推荐使用无意义的整数ID,这样可以确保在未来改变时比较安全。

一个alternateID类创建一个类方法,形如byUsername来对应一个叫做username的列(也可以使用alternateMethodName关键字参数来重载)。如下使用:

>>> User.byUsername('bob')
<User 1 username='bob'>
>>> Role.byName('admin')
<Role 1 name='admin'>

15   选择多个对象(查询)

查询才是真正有用的东西,比显示联系重要的多。select是一个类方法,可以按照如下方式使用:

>>> Person._connection.debug=True
>>> peeps=Person.select(Person.q.firstName=="John")
>>> list(peeps)
 1/Select : 使用的SQL语句
 1/COMMIT : auto
[<Person 1 firstName='John' ...>]

这个例子放回使用John作为firstName的所有人。一个使用表达式的复杂例子:

>>> peeps=Person.select(
...     AND(Address.q.personID==Person.q.id,
...         Address.q.zip.startswith('504')))
>>> list(peeps)
..............
[]

属性q用于给出存取特定对象的结构化查询子句。所有被q所引用的列名将会转换成SQL语句。当然也可以手工生成SQL语句:

>>> peeps=Person.select("""address.id=person.id AND
...                        address.zip LIKE '504%'""",
                        clauseTable=['address'])

注意必须使用clauseTable(子表)来指定子表。如果使用q属性,SQLObject会自动计算出(figure out)所需要使用的扩展信息类。

你也可以使用MyClass.sqlrepr来手工指定任何SQL语句,而在使用q属性时是自动指定的。

还可以使用orderBy关键字创建select语句中的”ORDER BY”。orderBy获取一个字符串,表示数据库的列名,例如Person.q.firstName。也可以使用”-colname”来反向排序。或者 调用MyClass.select().reversed()。

也可以使用类实例的_defaultOrder属性指定缺省的排序列。如果在这时需要获得未排序的结果,使用orderBy=None。

select的结果是一个生成器(generator),可以用于后续调用。所以SQL仅在列出选择结果时才执行,或者使用list()返回所有结 果时。当列举查询结果时,每次取回一个行。这种方法可以在返回结果很大时避免将所有结果放入内存。也可以使用.reversed()而不必获得所有结果实 体,取而代之的是自动修改了SQL语句来获得需要的结果。

还可以对查询结果分片。这将会修改SQL语句,所以peeps[:10]将会把”LIMIT 10″加入SQL语句中。如果切片无法反映到SQL(如peeps[:-10]),则执行查询之后,对查询结果列表进行操作。当然,这只是会出现在使用负索引时。

大多数情况会得到多个查询结果对象。如果不希望这样,可以加入关键字MyClass.select(…,distinct=True),对应SQL中的SELECT DISTINCT。

你也可以通过count得到查询结果的个数,比如MyClass.select().count()。这将会导致一个COUNT(*)查询。这时并不会从数据库中取得对象,而仅仅是获得结果数量。

在少数特别注重效率的时候,效率实际上是依赖于批处理的使用方法。提高排序和查找效率的好办法是使用索引。且缓存比切片更好。

在这种情况下缓存意味着响应所有的结果。可以使用list(MyClass.select(…))来实现。可以在规定的时间内保存查询结果,来让用户分页查看结果。这样,第一次查询会看上去花费更多的时间,但是后面的页面显示却非常快速。

更多关于查询子表的问题参见”SQLBuilder documentation”。

16   selectBy方法

除了.select之外的另一个选择是.selectBy。按照如下工作:

>>> peeps=Person.selectBy(firstName="John",lastName="Doe")

每个关键字对应一个列,且所有的键值对都是以AND进行逻辑连接。返回结果是SelectResult。所以可以切片,计数,排序等等。

17   sqlmeta类

这是在SQLObject 0.7中引入的,允许使用一种清晰的方式指定数据库的元数据,而不需要使用类的命名空间中的属性来指定。

有一些特别的属性可以用在这个类当中,可以用于改变类的行为。他们包括:

  1. table 数据库的表名,是从style派生而来的,仅用于没有指定类名时。如果没有指定名字,且没有定义可选的style,则标准方式是指定MixedCase为mixed_case。
  2. idName 指定数据库表的主键名,如未指定时继承自style。缺省为id。
  3. idType 设置ID时的强制函数。缺省为int。
  4. style 一个样式(style)对象,这个对象允许使用其他算法翻译Python属性和类名称与数据库列名和表名之间的对应关系。参考”Changing the Naming Style”了解更多。它是一个IStyle实例的接口。
  5. lazyUpdate 布尔值,缺省为False。如果为True,那么改变属性时不会自动更新查询。而等待调用inst.syncUpdates()或inst.sync()时才执行更新。
  6. defaultOrder 查询数据时的缺省排序方式。
  7. cacheValues 布尔值,缺省为True。如果为True,保存在行中的值一直缓存到inst.expire()被调用。如果设为False,属性值不会被缓存,所以每次 存取一个属性的值都会使用查询来返回结果。如果需要处理多进程并发处理时,也许需要这样。当然也可以使用事务(transactions),这不是默认 的。
  8. registry SQLObject使用字符串来连接一个类,且这些类必须避开模块名,而又是又会出现在不同的系统中的命名冲突。这个属性提供了类的命名空间。
  9. fromDatabase 布尔值,缺省为False。如果为True,创建一个类时会自动查询数据库的各个列,如果有缺失的列则自动加上。
  10. columns 形如{columnName:anSOColInstance}形式的字典。可以通过只读属性获取列的信息。
  11. columnList columns的列表,用于需要一个有序而牢固的列信息列表时使用。
  12. columnDefinitions 类似columns的字典,但是包含列的原始定义信息。并非是特定类的,且没有逻辑。
  13. joins 这个类的所有联系对象。
  14. indexes 这个类的所有索引。
  15. createSQL 创建表之后的SQL查询。createSQL可以是单行的SQL命令或者是一些SQL命令组成的列表,或者是按照数据库名(dbNames)和值组成的字典,值可以是单行SQL或者是SQL列表。这经常用于ALTER TABLE命令来修改表定义。
  16. expired 布尔值。如果为True,那么下次存取对象列属性时,将会执行查询。

在上一版本的SQLObject中这些属性是作为类的属性而直接存在的,属性名前加上一个下划线。现在推荐将代码改成新的样式(style)。这些旧的方法在SQLObject 0.8释出时将不再支持。

注意:当继承SQLObject时,sqlmeta属性不会继承。也无法通过sqlmeta.columns词典访问父类列对象。

18   使用sqlmeta

按照如下代码:

class MyClass(SQLObject):
    class sqlmeta:
        lazyUpdate=True
        cacheValues=False
    columnA=StringCol()
    columnB=IntCol()
    def _set_attr1(self,value):
        #设置值时需要做的事情
    def _get_attr1(self):
        #取得值时需要作的事情

如上定义将会创建表my_class(表名在更改style属性时会有所不同),包含两个列columnA和columnB。还有第三个可以被存取 的属性MyClass.attr1。sqlmeta改变了MyClass的行为,可以延迟更新,并告知不要使用缓存,所以每次请求信息时都会查询数据库。

19   SQLObject

除了sqlmeta和列规范之外,其他的特定属性可以设置在类中。这些属性可以用除了_connection属性之外都可以在sqlmeta中定义。如果在SQLObject 0.7中使用,可以得到不赞成使用的警告。最好及时修改代码来保持对未来的兼容。

  1. _connection 使用的连接对象,从DBConnection类实现。也可以在包定义中使用__connection__供使用,但是必须确保在类定义之前定义 __connection__。也可以在实例创建时传递connection对象,有如transaction中描述的那样。如果已经定义了 sqlhub.processConnection,则会忽略这个属性而使用sqlhub。如果只有少数几个类使用相同的连接是有好处的,除了 (besides)使用了多种类型。
  2. _table 这是旧样式(style)的属性,等同于sqlmeta类中的table属性。
  3. _joins 同上,对应于sqlmeta的joins属性。
  4. _cacheValues 同上,对应于sqlmeta的cacheValues属性。
  5. _idName 同上,对应于sqlmeta的idName属性。
  6. _style 同上,对应于sqlmeta的style属性。

20   自定义对象

自定义类往往需要一些自定义方法,但是需要注意一些细节。

初始化对象:

有两种方式实例化SQLObject对象,从数据库引出和插入数据库。相同的是都要创建Python对象。这导致了__init__的微笑差异。

一般来说,不需要更改__init__。作为替换的_init方法,可以在引出或插入之后执行。方法定义如同_init(self,id, connection=None,selectResults=None),也许你喜欢使用_init(self,*args,**kw)的形式。注意, 如果重载方法,一定要调用SQLObject._init(self,*args,**kw)。

添加魔术属性:

你可以使用任何已有的技术来定义这种新样式中的方法,包括classmethod(类方法),static(静态方法),和property(属 性),而且还可以使用捷径。比如你有一个方法名以_set_、_get_、_del_、_doc_开始,它将会被用于创建属性。所以,如果对应 Person的ID下包含图像在/var/people/images目录下,可以使用:

class Person(SQLObject):
    # ...
    def imageFilename(self):
        return 'images/person-%s.jpg'%self.id
    def _get_image(self):
        if not os.path.exists(self.imageFilename()):
            return None
        f=open(self.imageFilename())
        v=f.read()
        f.close()
        return v
    def _set_image(self,value):
        f=open(self.imageFilename(),'w')
        f.write(value)
        f.close()
    def _del_image(self,value):
        os.unlink(self.imageFilename())

然后,可以像使用普通属性(attribute)一样使用.image属性(property)。对它的修改也会直接反映到文件系统当中。这是保存无意义数据的好方法。

同样也可以传递一个image关键字参数到构造方法或者set方法,形如Person(…,image=imageText)。所有的方法 (_get_、_set_等)都是可选的,你可以使用其中的任何一个而省略其他的。这样如果只定义了_get_attr方法,那么attr属性就是只读 的。

重载列属性:

重载数据库列属性时有些复杂。例如(for instance),想要在改变一个人名字的时候执行特定代码。在大多数系统中你需要自己实现这个功能,然后再调用父类的代码。但是父类(SQLObject)并不知道子类的列。

SQLObject创建了形如_set_lastName的方法定义你的列,当时当你再次想要使用时却发现父类没有相关引用(即不可以写 SQLObject._set_lastName(…),因为SQLObject类并不知道你的类的列)。你需要自己重载_set_lastName 方法。

为了处理这种问题,SQLObjec类创建了两个方法作为getter和setter,例如:_set_lastName和_SO_set_lastName。所以可以截获所有对lastName的更改:

class Person(SQLObject):
    lastName=StringCol()
    firstName=StringCol()
    def _set_lastName(self,value):
        self.notifyLastNameChange(value)
        self._SO_set_lastName(value)

或者你可能想要包含电话号码的数字,需要限制长度,可以按照如下格式:

import re
class PhoneNumber(SQLObject):
    phoneNumber=StringCol(length=30)
    _garbageCharactersRE=re.compile(r'[\-\.\(\) ]')
    _phoneNumberRE=re.compile(r'^[0-9]+$')
    def _set_phoneNumber(self,value):
        value=self._garbageCharactersRE.sub('',value)
        if not len(value)>=10:
            raise ValueError(
                'Phone numbers must be at least 10 digits long')
        if not self._phoneNumberRE.match(value):
            raise ValueError,'Phone numbers can contain only digits'
        self._SO_set_phoneNumber(value)
    def _get_phoneNumber(self):
        value=self._SO_get_phoneNumber()
        number='(%s) %s-%s'%(value[0:3],value[3:6],value[6:10])
        if len(value) > 10:
            number+=' ext.%s'%value[10:]
        return number

在修改从属性中获得的数据时必须小心。有些时候,人们希望设置与返回的值相同。这个例子中我们在存入数据库之前去除了一些字符,并在取出的时候重新格式化了。这个方法(反对存取属性)的优点之一是程序员往往希望将这些分开。

当然,还应该注意,这些转换在存入和取出时都会发生,但是在查询的时候却不会发生。所以如果你将值从Pythonic形式转换为SQLish形式 时,你的查询(当使用.select()或者.selectBy()方法)需要使用SQL/Database形式(因为这些命令是按照SQL来在数据库上 运行的)。

取消属性定义(Undefined attributes)

还有一个有用的特性,因为你有时需要返回奇怪的结果。SQLObject在你设置一个未定义的属性时不会跑出异常;这很好解释,并且不会改变数据库。他的工作方式有如其他Python类一样,但是在SQLObject类中却没有。

这在有些时候可能会出问题,如果你已经有了一个’name’属性,而你却写了’a.namme=”Victor”‘,这时不会跑出异常,但是却是错误的。

21   参考手册(Reference)

上面的信息可以让你快速进入工作,下面的信息让你定义更加完整。

22   Col类,定义列

列的列表是Col对象的列表。这些对象本身并没有功能,用于定义列。

  1. dbName 数据库的列名,如果不指定你指定的Python名称将会从大小写混用的形式转换到小写加下划线的形式。
  2. default 列的缺省值,在创建一个新行时使用。如果指定了一个可调用对象或函数,将会调用这个函数,并且使用其返回值。所以你可以使用 DateTimeCol.now作为当前时间的缺省值。或者你可以使用sqlbuilder.func.NOW()来设置数据库使用NOW()内部函数。 如果你不指定缺省值,则在调用这个记录的new时会抛出异常。
  3. alternateID 这是一个布尔型变量,缺省为False。指定列是否作为ID属性,例如用户名,尽管并不一定是主键。如果是这样,会添加一个类方法,例如 byUsername将会返回这个对象,使用laternateMethodName,当你希望使用by*类似的名称时,例如 alternateMethodName=”username”。这个列将会被声明为UNIQUE。
  4. unique 如果为True,当SQLObject创建一个表格时,将会指定这个列为UNIQUE。
  5. notNone 如果为True,则这个列不允许使用空值,用于创建表格。
  6. sqlType 这个列的SQL类型,例如INT、BOOLEAN等。你可以使用下面的类来定义,但是有时候使用sqlType更容易一些。仅在SQLObject创建表格时有效。

23   列类型

ForeignKey类可以替换Col来使用,当列是其他表的外键时。一般使用方法如ForeignKey(‘Role’),在这个列子中创建了一 个到表Role的引用。这基本等同于Col(foreignKey=’Role’,ssqlType=’INT’)。这会创建两个属性,role,会返回 Role的实例;roleID会返回与之关联的role的整数ID。

Col还有一些其他子类,用于SQLObject创建表格时指示列的类型。

  1. BLOBCol 二进制数据列,目前只能在MySQL、PostgreSQL、SQLite中使用。

  2. BoolCol 创建一个BOOLEAN列在Postgre,或者INT在其他数据库中。而且会将”t”/”f”或者0/1转换到数据库后端。

  3. CurrencyCol 等同于DecimalCol(size=10,precision=2)。注意DecimalCol可能不会返回正确的值,这个列可能共享一些行为。注意阅读DecimalCol的注意事项。

  4. DateTimeCol 日期时间,一般返回datetime或mxDateTime对象。

  5. DateCol 一个日期对象,一般返回datetime或mxDateTime对象。

  6. TimeCol 一个日期对象,一般返回datetime或mxDateTime对象。

  7. DecimalCol 以10为基础的,正确数据,使用关键字参数size指定存储的数字位数,precision指定小数点位数。警告:偶尔会发生在DecimalCol值, 尽管正确的存入数据库,但是可能返回浮点数而不是decimals。你可以自己测试一下,也可以试着导入Decimal类型,在你的数据库适配器导入 SQLObject之前。

  8. EnumCol 枚举类型,包含有限个数的字符串值。给出列表中可能的字符串,依靠enumValues关键字参数。MySQL有内置的本地ENUM类型,但是在其他数据库中也可以工作,只不过效率上并不占优势。

  9. FloatCol 浮点数

  10. ForeignKey 其他表/类的外键,例如user=ForeignKey(‘User’)

  11. IntCol 整数

  12. PickleCol 一种扩展的BLOBCol,这个列可以存储/取出任何Python对象;实际上是使用了Python的pickle来对对象串行化的压缩和解压缩的,而最终存取的是字符串。

  13. StringCol 一个字符串列。String(character)列。扩展关键字如下:

    length:如果给定了则字段类似于VARCHAR(length)。如果未指定则使用TEXT字段类型。

    varchar:如果包含了length,则用于区别CHAR和VARCHAR,缺省为True,使用VARCHAR。

  14. UnicodeCol StringCol的子类,接受dbEncoding关键字参数,缺省为”UTF-8″。其值在存取数据库的过程中被编码和解码。在使用UnicodeCol进行查询时有些限制:

    只有简单的q-magic字段支持,不允许表达式;只支持”==”和”<>”操作符。

    如下为示例代码:

    MyTable.select(u'value'==MyTable.q.name)
    MyTable.select(MyTable.q.name<>u'value')
    MyTable.select(OR(MyTable.q.col1==u'value1',MyTable.q.col2<>u'value2'))
    MyTable.selectBy(name=u'value')
    MyTable.selectBy(col1=u'value1',col2=u'value2')
    MyTable.byCol1(u'value1') #假设col1是一个alternetID字段

    如下为错误代码:

    MyTable.select((MyTable.q.name+MyTable.q.surname)==u'value')

    如下情况,必须先转换编码:

    MyTable.select((MyTable.q.name+MyTable.q.surname)==u'value'.encode(dbEncoding))

24   两个类/表之间的关系

必须使用ForeignKey来处理表的外键,后台实际使用连接(join)。

25   多连接和SQL多连接:一对多

查看”One-to-Many关系”查看一对多连接的例子。

多连接(MultipleJoin)返回结果类表,而SQLMultipleJoin返回SelectResults对象。

少数关键字参数允许MultipleJoin构造器:

joinColumn:

列名

subprocess模块指南

Monday, June 2nd, 2008

subprocess模块指南

翻译: gashero

目录

从Python2.4开始引入。

subprocess模块用于产生一个新的进程,并且连接到其input/output/error管道,并获取返回码。这个模块准备用于替换很多其他旧模块中的函数,例如 os.system 、 os.spawn* 、 os.popen* 、 popen2.* 、 commands.* 。

详细细节见下面。

1   使用subprocess模块

模块中只是定义了一个类叫做 Popen

class Popen(args,bufsize=0,executable=None,stdin=None,stdout=None,stderr=None,preexec_fn=None,close_fds=False,shell=False,cwd=None,env=None,universal_newlines=False,startupinfo=None,creationflags=0)

参数描述如下:

@waiting…

1.1   便利函数

该模块定义了两个快捷函数:

call(*popenargs,**kwargs)

使用指定的参数运行命令,并等待命令结束,然后返回返回码。参数与Popen结构相同,例如:

retcode=call(["ls","-l"])

check_call(*popenargs,**kwargs)

使用指定参数运行命令,等待结束。如果退出码是0则返回,否则抛出 CalledProcessError 异常。而该异常对象包含了返回码,称为 returncode 属性。

参数与Popen结构相同,例如:

check_call(["ls","-l"])

从Python2.5开始引入。

1.2   异常

子进程中抛出的异常可以传递到父进程。另外,异常对象包含一个附加属性叫做 child_traceback ,是一个字符串,包含了子进程那里可以看到的错误回调信息。

最常见的异常是 OSError 。例如尝试执行一个不存在的文件。应用应该防备OSError异常。

在调用Popen而使用了无效的参数时会抛出ValueError异常。

check_call() 将会抛出 CalledProcessError ,在进程返回码不是0时。

1.3   安全

不像其他popen函数,这个实现不会隐含调用 /bin/sh 。这意味着所有的字符,甚至shell的元字符,都可以安全的传递到子进程。

2   popen对象

Popen的实例拥有如下方法:

poll()

检查子进程是否结束,并返回returncode属性。

wait()

等待子进程结束,返回returncode属性。

communicate(input=None)

与进程交互:发送数据岛stdin。从stdout和stderr读取数据,直到接收到end-of-file。等待进程结束。可选的input参数是要发送到子进程的字符串,或者为None。

communicate() 返回元组 (stdout,stderr) 。

注意:读取的数据缓存在内存里,所以在数据很大或者不受限制时不要使用这个方法。

如下属性也可用:

stdin

如果stdin参数为PIPE,这个属性提供了子进程的标准输入,否则为None。

stdout

如果stdout参数为PIPE,……

stderr

如果stderr参数为PIPE,……

pid

子进程的进程ID。

returncode

子进程的返回码,在还没有返回时则为None。一个否定值-N表示子进程是被信号N所终止的(仅Unix)。

3   使用subprocess替换旧模块

在Twisted中使用线程

Wednesday, April 16th, 2008

在Twisted中使用线程

译者: gashero

目录

1   以线程安全的模式运行代码

Twisted中的大部分代码都不是线程安全的。例如protocol向transport写入数据就不是线程安全的。因此我们需要一种方法来在主事件循环中进行调度。者可以使用函数 twisted.internet.interfaces.IReactorThreads.callFromThread 来实现:

from twisted.internet import reactor

def notThreadSafe(x):
    """做一些非线程安全的事情"""
    # ...

def threadSafeScheduler():
    """以线程安全方式运行"""
    reactor.callFromThread(notThreadSafe,3) #将会运行notThreadSafe(3)在主时间循环中

Note

译者注

callFromThread 意指从线程调用,这个方法是供线程调用的,并且使其指定的函数加入到主事件循环中执行。比如worker线程可以调用此方法将提交结果的函数加入到主事件循环中。这样就可以确保多线程的运行worker,而有可以使用线程安全的方式提交结果。

2   在线程中运行代码

有时我们希望在线程中运行代码,比如阻塞的存取API。Twisted提供了这样做的方法在 IReactorThread API 中。附加的工具在包 twisted.internet.threads 中提供。这些方法允许我们把任务排队以后在线程池中运行。

例如,在线程中运行一个函数,我们可以:

from twisted.internet import reactor

def aSillyBlockingMethod(x):
    import time
    time.sleep(2)
    print x

# 在线程中运行
reactor.callInThread(aSillyBlockingMethod,"2 secodns have passed")

Note

译者注

callInThread 意指在线程中运行,调用该方法需要在主事件循环中,而执行其传入的函数则是在线程中。可以与上一节提供的 callFromThread`结合使用,即在worker线程函数中调用 `callFromThread 提交结果。

3   工具函数

工具函数作为 twisted.internet.reactor 的一部分API提供,但是并不是在 twisted.internet.threads 中实现的。

如果我们有多个方法需要在线程中以队列方式运行,我们可以做:

from twisted.internet import threads

def aSillyBlockingMethodOne(x):
    import time
    time.sleep(2)
    print x

def aSillyBlockingMethodTwo(x):
    print x

# 排队后在线程中运行两个方法
commands=[(aSillyBlockingMethodOne,["calling first"])]
commands.append((aSillyBlockingMethodTwo,["and the second"],{}))
threads.callMultipleInThread(commands)

如果我们希望得到函数的运行结果,那么我们可以使用Deferred:

from twisted.internet import threads

def doLongCalculation():
    # ... do long calculation here ...
    return 3

def printResult(x):
    print x

# 在线程中运行,并且通过 defer.Deferred 获取结果
d=threads.deferToThread(doLongCalculation)
d.addCallback(printResult)

如果你希望在reactor线程中调用一个方法,并且获取结果,你可以使用 blockingCallFromThread

from twisted.internet import threads,reactor,defer
from twisted.web.client import getPage
from twisted.web.error import Error

def inThread():
    try:
        result=threads.blockingCallFromThread(reactor,getPage,"http://twistedmatrix.com/")
    except Error,exc:
        print exc
    else:
        print result
    reactor.callFromThread(reactor.stop)

reactor.callInThread(inThread)
reactor.run()

blockingCallFromThread 将会返回对象或者抛出异常,或者通过抛出到传递给他的函数。如果传递给它的函数返回了一个Deferred,他会返回Deferred回调的值或者抛出异常到errback。

4   管理线程池

线程池是在 twisted.python.threadpool.ThreadPool 中实现的。

我们可以修改线程池的大小,增加或者减少可用线程的数量,可以这么做:

from twisted.internet import reactor
reactor.suggestThreadPoolSize(30)

缺省的线程池大小依赖于使用的reactor,缺省的reactor使用最小为5个,最大为10个。在你改变线程池尺寸之前,确保你理解了线程和他们的资源使用方式。

在Twisted2.5.0中使用线程

译者: gashero

刚才翻译了对应版本8.0.0的Twisted的线程指南,但是我还是在用2.5.0,所以这里只记录与8.0.0的差异,不做重新翻译。

当你开始使用线程之前,确保你在启动程序的时候使用了如下:

from twisted.python import threadable
threadable.init()

这回让Twisted以线程安全的方式初始化,不过仍然注意,Twisted的大部分仍然不是线程安全的。

以线程安全的方式运行代码中初始化多了两行:

from twisted.python import threadable
threadable.init(1)

2.5.0的文档中没有 blockingCallFromThread 的例子。也许根本就没有这个方法。

实际我下载文档的版本是2.4.0,不过应该与2.5.0一样的。

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进程等等。还有一大堆的扩展功能。

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 第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数据包头部

使用Twisted创建SSL-WEB服务器

Tuesday, January 22nd, 2008

使用Twisted创建SSL-WEB服务器

翻译: gashero

Twisted web howto中并没有提及如何创建SSL加密的WEB服务器。其实这个过程很简单,只是需要一些信息而已,所以这里讲一下。

首先,创建SSL服务器需要一个私钥文件,和一个服务器证书。这里假设你是个开发者,只想要一个测试系统,所以也没必要去搞一个正式的公共证书来。OpenSSL就是你想要的。首先,我们生成一个私钥文件:

$ openssl genrsa > privkey.pem

然后生成一个自签名的SLL证书:

$ openssl req -new -x509 -key privkey.pem -out cacert.pem -days 1000

好了,第一个挑战已经搞定了。下一步就是创建服务器了。假设你已经有了一个twisted web服务器,所以你也知道该在哪个端口监听。我们导入 ssl 模块,创建一个SSL服务器上下文,然后调用 reactor 的 listenSSL 方法:

from twisted.internet import reactor,ssl
sslContext=ssl.DefaultOpenSSLContextFactory(
    '/path/to/privkey.pem',
    '/path/to/cacert.pem',
)
reactor.listenSSL(port,site,contextFactory=sslContext)

同样在 twisted.application 中也很容易创建带有上下文的站点,不过这里就懒得介绍了。

gashero附加的内容:

需要使用ssl的功能还需要另外安装一堆东西。包括openssl(0.9.8g)、pyOpenSSL(0.6.0)。安装openssl时,debian源里面没有dev版本,只能自己编译,注意配置时要修改安装路径:

$ ./config --prefix=/usr/local

这样以后就可以正确的编译安装pyOpenSSL了,否则默认情况下找不到OpenSSL。

Zenoss快速指导

Monday, November 5th, 2007

Zenoss快速指导

原文标题: Zenoss Quick Start Guide
原文日期: 2007-9-26
对应版本: Zenoss 2.0
翻译: gashero
翻译日期: 2007-10-31

目录

1 概览

本文档的目标是让你了解并可以安装和运行zenoss,并了解如下任务:

  • 安装Zenoss Core软件
  • 自定义环境
  • 探索设备
  • 监控环境
  • 管理环境

1.1 Zenoss Core简介

Zenoss Core被下载次数最多的开源企业级IT管理软件茶品-是智能监控软件,他允许IT管理员依靠单一的WEB控制台来监控网络架构的状态和健康度。Zenoss Core同时也是开源的网络与系统管理软件,知识产权归Zenoss公司所有,地址在美国马里兰州的安娜波利斯。

Zenoss Core的强大能力来自于深入的列表与配置管理数据库,以发现和管理公司IT环境的各类资产(包括服务器、网络、和其他结构设备)。Zenoss可以创建关键资产清单和对应的组件级别(接口、服务、进程,已安装的软件等)。建立好模型后,就可以监控与报告IT架构中各种资源的状态和性能容限了。Zenoss同时提供与CMDB关联的事件和错误管理系统,以协助提高各类事件和提醒的管理效率。以此提高IT管理人员的效率。

1.2 附加资源

如果需要附加帮助,或者有问题,可以查看Zenoss的网站:

如果有关于本文档的其他问题,可以发邮件询问:

feedback@zenoss.com

2 入门

Zenoss Core可以通过Zenoss的网站来下载:

http://www.zenoss.com/download

或者从Zenoss的Sourceforge项目处下载:

http://sourceforge.net/projects/zenoss

Zenoss Core可以运行于大多数Linux和Unix操作系统,不过,我们推荐运行于Red Hat Enterprise Linux(RHEL)平台或其他任何RHEL派生平台,如Centos或Fedora。

Zenoss Core同时提供虚拟机版本,可以运行于Windows和Linux平台,简单的通过VMware Player即可实现。

Zenoss Core是一个监控系统的解决方案,使用浏览器来使用。支持的WEB浏览器包括 FireFoxInternet Explorer 7 。Zenoss Core部分的支持 SafariOpera 浏览器。

2.1 以RPM方式安装Zenoss Core

已经安装完成了,略……

2.2 以源码方式安装Zenoss Core

已经安装完成了,略……

2.3 通过VM Player使用Zenoss Core

已经安装完成了,略……

2.4 登入并使用

在浏览器中输入如下地址以访问:

http://xxx.xxx.xxx.xxx:8080

IP地址要换成你安装Zenoss的机器,如果是通过VM方式,IP地址也要指到虚拟机的IP。

如果全部是默认安装,则默认的登录过程为:

用户名: admin

密码: zenoss

点击 Submit

3 定义环境

Zenoss Core是完整的企业级监控解决方案,允许你高效的管理与监控IT架构。没有两种环境是一样的,Zenoss允许你定义自己选择的环境。如下是配置的入门。

3.1 简单设置

3.1.1 配置SMTP服务器

下面是配置SMTP服务器的简单步骤:

  1. 从左侧导航菜单点击 Settings
  2. Settings 标签页中选择 “SMTP Host” 以选择需要使用的SMTP服务器
  3. 设置 “SMTP User” 和 “SMTP Password” 字段,当然,如果需要的话
  4. 点击 Save 按钮

Note

缺省的设置指本机的SMTP服务器,通过Settings页面还可以配置其他很多东西,如SNMP主机等。

3.1.2 配置SNMP组字符串

SNMP组字符串配置在 ZProperties 页面的 “zSnmpCommunity” 参数。这些可以在设备对象树中配置。也就是指定设备的 ZProperties 页面的’ZSnmpCommunity’字段。

对于自动发现设备,Zenoss提供了在你的环境中自动发现所有有效的组字符串的能力,并可以利用这些来发现网络和实体环境建模。

下面的步骤简单的讲解了配置SNMP组列表:

  1. 从左侧导航菜单中选择 Devices
  2. 选择 zProperties 页面
  3. 添加有效的SNMP字符串到 ‘ZSnmpCommunities’ 文本框

Note

修改后的’zSnmpCommunities’ 参数会被继承。

Warning

现在这个选项在Devices->zProperties页面,中下部。版本2.1.0。

3.1.3 创建Windows管理员设置

如果希望Zenoss能够访问和控制Windows WMI框架,则Zenoss服务器必须拥有Windows设备的管理员级别的权限。下面是设置Windows管理员配置的基本步骤:

  1. 从左侧导航菜单中选择 Devices
  2. 下拉选择 Server 链接,然后点击 Windows 链接以打开Windows设备类别页
  3. Windows 类别页面,点击 zProperties
  4. zProperties 页,设置 “zWinUser” 字段,缺省为 “Administrator”
  5. 同时设置 “zWinPassword” 字段

Note

修改 “zWinUser” 和 “zWinPassword” 参数可以对任何级别的设备起效,并且这个参数可以被子设备继承。

Warning

注意,这个选项现在在Devices->zProperties里面,靠下。版本2.1.0。

3.2 创建逻辑组

可以配置逻辑组,以管理不同的环境视图。创建的逻辑组的类型完全依赖喜好,不过一般是分组和子组以方便理解IT架构为好。

3.2.1 创建位置

如下展示了创建位置组和子组的简单步骤:

  1. 从左侧导航菜单选择 Locations
  2. 点击下拉表格菜单选择 Sub-Locations ,然后选择 “Add New Organizer”
  3. 填写新的位置的ID字段
  4. 点击OK以保存 Location

这些步骤可以用于创建已有 LocationSub-Location ,步骤是类似的。

3.2.2 创建系统

下面展示了建立组以后需要做的步骤:

  1. 左侧导航菜单点击 System
  2. 点击下拉表格菜单选择 Sub-Systems 表格并选择 “Add New Organizer”
  3. 填写新的位置的ID字段
  4. 点击OK以保存 Systems

这些步骤对于建立已有系统组的子组也是一样的。

3.2.3 创建自己的组

下面展示了建立位置组或子组的步骤:

  1. 左侧导航菜单中点击 Groups
  2. 点击下拉表格菜单选择 Sub-Groups 表格并选择 “Add New Organizer”
  3. 添加新的位置ID字段
  4. 点击OK以保存 Groups

这些步骤对于建立已有组的子位置也是一样的。

3.3 定义监控进程

Zenoss允许自定义通过网络要监控的进程。在读进程建模时,Zenoss会检查定义的进程列表,对比运行在目标资源上面的进程,并自动建模和监控设备上的指定进程。下面是建立进程的简单步骤:

  1. 左侧导航菜单点击 Process
  2. 点击下拉表格菜单选择 Processes 表格并选择 “Add Process”
  3. 填写新的进程ID字段
  4. 点击OK保存新的网络地址

Note

正则表达式可以应用于指定的进程标识符,并且zenoss将会使用它去匹配。

4 了解环境

4.1 添加设备

设备可以单独添加到Zenoss监控,下面的简单步骤介绍了如何通过SNMP对一个设备建模。

  1. 从左侧导航菜单选择 Add a Device
  2. Add a Device 页面的 ‘Device Name’ 字段填入需要被监控机器的IP地址或者域名
  3. 设置 ‘SNMP Community’ 字段,如果需要的话
  4. 从 ‘Device Class Path’ 下拉菜单中,选择设备类别,比如’/Server/Linux’
  5. 填入’Location Path’字段、’Systems’字段等,如果需要分组还需要’Groups’字段
  6. 点击 ‘Add device’ 添加到自动发现模型的设备

建模过程之后,就可以通过链接导航到设备。

4.2 添加网络

你可以添加一个 Network (网络)到Zenoss系统中。下面的步骤:

  1. 从左侧导航菜单选择 Networks
  2. Subnetwork 页面的 Overview 页面点击下拉表格菜单选择 ‘Add Network’
  3. 添加新的网络地址和掩码,以 CIDR 格式,例如192.168.1.0/24 到’ID’字段
  4. 点击’OK’按钮来保存新的网络地址

Note

这些步骤可以用于任意层次的网络树的子网络。

4.3 探索网络

可以使用Zenoss自带的网络探索功能自动发现网络设备,使用下面的步骤:

  1. 在导航菜单中选择 Networks
  2. 点击下拉表格菜单中的 Sub-networks 选择’Discover Devices’

Note

这些步骤可以用于自动发现各个层次的网络树。

5 监控环境

5.1 查看管理资源

通过Zenoss Core,可以通过系统来查看所有被监控的资源。最简单的方法是通过 Device 链接。而 Device List 表格提供了一个基础架构中所有设备的完整表格,并包含简单的附加信息。

点击设备名 Device Id 列,可以以系那是所选设备的状态 Status 页面。

也可以在页面右上角以 Device/IP Search 来搜索特定的设备。

5.2 查看事件

可以通过 Event Console 来查看IT环境发生的所有事件。通过左侧导航菜单的 Events Console 来打开。

事件控制台提供了IT架构中发生的所有活跃事件。你可以使用事件控制台实现:

  • 查看详细的事件信息
  • 关于事件的附加知识
  • 配置简单或高级的报警策略
  • 更新事件修正日志
  • 分类与关联事件
  • 备份事件到历史数据库

5.3 查看报告

可以通过Zenoss生成预定义或自定义的报告,通过左侧导航菜单的 Reports 。Zenoss预定义报告提供了工业标准的报告以显示网络健康状态统计和环境的SLA。

5.4 自定义监控

Zenoss可以通过自定义的 ZenCommand 来收集信息,支持SSH、XML-RPC在内的多种协议。你可以通过在 Zenoss box 中的脚本执行自定义监控远程设备,当然也有自定义的报告。Zenoss正在开发的 ZenPack 提供了几种监控方式以提供自定义监控。更多的信息请关注 Zenoss Administrator Guide 。

5.4.1 添加ZenPack

Zenoss认证的ZenPack提供了使用Zenoss Core的自定义监控。下面是使用这些自定义监控的简单步骤。在这个实例中讲解HTTPMonitor的监控。

  1. 从服务器下载HttpMonitor ZenPack: http://www.zenoss.com/download/zenpacks/HttpMonitor.zip
  2. 到服务器上找到下载的zenpack
  3. 以zenoss用户登录,执行安装zenpack的命令,注意xxx为例子的文件名:
    zenossserver ~# zenpack run -install HttpMonitor.zip
  4. 创建一些数据源以供自定义监控,详细参考 Zenoss Administartor Guide

6 管理环境

6.1 添加Zenoss用户

Zenoss允许按照角色创建用户,以访问Zenoss系统。Zenoss也支持定义事件跟踪,以提供更多的高级提示与报警功能,并且还区分角色。

6.1.1 添加一个Zenoss用户

按照下面的步骤:

  1. 左侧导航菜单,选择 Settings
  2. 点击 Users 页面显示 User Folder 列表
  3. 点击下拉表格菜单,选择 ‘Add New User’
  4. 在 ‘Username’ 字段,输入新的用户名,然后添加email地址到 ‘Email’ 字段
  5. 点击 ‘OK’ 以保存新的用户配置
  6. User Folder 表格,以 ‘UserId’ 列点击用户链接以修改更多配置
  7. Edit 页面的 ‘Password’ 字段修改密码
  8. 在 ‘Roles’ 选择框选择用户的角色

Note

用户名必须是全部小写,而且不可以有空格。

6.1.2 创建报警规则

下面步骤展示了如何创建报警规则:

  1. User Folder 表格,点击需要编辑的用户链接
  2. 点击 Alerting Rules 页面显示 Alerting Rules 列表
  3. 点击下拉表格菜单并选择 ‘Add New Rule’
  4. 在 ‘ID’ 字段输入新的规则名
  5. 点击 ‘OK’ 保存新的报警规则
  6. Alerting Rule 表格,点击刚刚建立的规则名以修改附加信息
  7. Edit 页面,改变 ‘Enabled’ 下拉菜单为True
  8. 通过 ‘Action’ 来改变报警模式,email或page
  9. 通过 ‘Where’ 这个下拉菜单中的参数来改变发送到各个用户的警报的过滤器。缺省的条件是立即发送高于系统定义级别的警报,你可以用这个来指定广播警报还是发到指定的人

6.1.3 创建警报规则定期发送

按照如下步骤:

  1. 从用户的 Alerting Rule 表格,按照 ‘Name’ 点击进入规则的修改
  2. 点击 Schedule 页面显示 Active Periods 表格(有效的时间段)
  3. 点击下拉表格菜单并选择 ‘Add Rule Window’
  4. 添加新的规则名,并填入 ‘ID’ 字段
  5. 点击 ‘OK’ 按钮保存有效时间段
  6. 在有效时间段表格,点击新的规则窗口链接,按照 ‘Name’ 列,并修改详细信息
  7. Status 页面,改变 ‘Enabled’ 下拉菜单为 True
  8. 配置开始时间 ‘Start’
  9. 配置持续时间 ‘Duration’
  10. 设置重现规则时间 ‘Repeat’

7 注释

无……