使用 Erlang 和 Yaws 开发 REST 式的服务

阅读数:5430 2008 年 6 月 26 日

话题:SOAWeb框架RESTErlang语言 & 开发架构

看过那张很出名的“Apache vs. Yaws”图么?是不是在考虑你也应该使用 Yaws 了?这些图给人的第一印象是,Yaws 在可伸缩性上具有难以置信的巨大优势,它可以扩展到 80000 个并行的连接,而 Apache 只接入 4000 个连接后就无法继续支撑了。人们对这些图的反应存在着明显的分化,一种声音说“这些图不太可能是准确的”或者“他们一定没有正确地配置 Apache”;另一种声音则完全相反,“Wow,我要尝试一下 Yaws!”

无论你是否相信上面的 Yaws 对比图,Yaws的确是一个可靠的 Web Server,可以处理动态内容。Claes Wikström 使用Erlang开发了 Yaws,“另一个 Web Server(Yet Another Web Server)”。Erlang 是一种编程语言,特别用于支持长时间运行的、并发的、高可靠的分布式系统。(要学习更多关于 Erlang 的知识,可以去看那本很精彩的“Programming Erlang”,它的作者是 Erlang 语言的创建者——Joe Armstrong。)Yaws 的灵活性和 Erlang 的多种独一无二的特性相结合,使得它们成为了一个不可忽视的 REST 式的 Web 服务平台。如果你处理的是静态页面,去试试lighttpd或者nginx吧,但是如果你在写动态的、REST 式的 Web 服务,那么 Yaws 是绝对值得尝试的。在这篇文章中,我将讲述我在使用 Yaws 和 Erlang 开发 Web 服务中的一些经验。

Yaws 基础

Yaws 提供了若干种处理动态 Web 内容以及支持 REST 式的 Web 服务的方法:

  • 在静态页面中嵌入 Erlang 代码。通过这种方法,你可以将...标签内的out/1函数直接嵌入到静态页面中。该函数包含了 Erlang 代码。这样的文件要以.yaws 为扩展名,从而通知 Yaws 处理它,并将...标签替换为执行out/1函数的结果,这正是页面应该包含的。在 Erlang 的术语中,out/1是元数(arity)1 的函数,例如,某个带有一个参数的函数。这个参数应该是一个 Yaws arg记录(record),这是一种特殊的数据结构,Yaws 使用它将接收到的请求的细节传递给处理它们的代码。例如,一个arg记录可以提供请求 URI、请求头、POST 数据等信息。

  • 应用程序模块(appmod)。由于 Yaws 的 appmod,应用程序代码可以控制 URI。在前面描述的方法中,Erlang 代码被嵌入到静态文件中了,而这些文件的 URI 是由它们的路径相对于 Web Server 的文档根决定的。然而,有了 appmod 后,应用程序就会控制 URI 的含义,这些 URI 通常不会与任何文件系统上的工件有联系。Appmod 基本上都是一个导出out/1函数的 Erlang 模块。这些模块要在 Yaws 配置文件中进行配置,来关联一个 URI 路径元素。如果一个请求中包含了某个已注册的 appmod 所关联的路径元素,Yaws 会调用这个模块的out/1 arg记录。模块的out/1函数可以继续解释 URI 剩下的部分,以此来解释请求和响应目标的具体资源是什么。

  • Yaws 应用程序(yapp)。appmods 通常仅仅是单一的 Erlang 模块,Yaws yapp 与此不同,它是全功能的应用程序。每个 yapp 都有它自己的文档根,都有它自己的 appmod 集。说得明确些,yapp 就是 Erlang/OTP 应用程序。OTP 表示“Open Telecom Platform(开发电信平台)”,它是一系列历经考验的库和框架,它们为 Erlang 程序带来了强大的能力。OTP 封装了很多构建分布式、事件驱动、高可用性系统的模式和方法。Erlang/OTP 已经在现实世界中获得了证明,它们可以被用在不同的电信系统中,例如,某些系统宣称它们的宕机时间每年不过几毫秒而已。

上述这三种方法(它们的细节可以在Yaws 站点上找到)都可以有效地应用在 REST 式的 Web Service 中。具体情况就要依赖于 Service 本身的特征了。但是根据我的经验,yapp 和 appmod 是最好用的,因为它们提供了对 Web 应用程序的最大控制。

REST 式的设计

既然我们打算要开发 REST 式的 Web 服务,那么首先了解一下 REST 的相关细节。REST 的全称是“表象化状态转变(Representational State Transfer)”。Roy T. Fielding博士在他的论文中首次提出了“REST”这个术语,用它来描述一个适用于高可扩展性分布式系统(比如 Web)的架构风格。HTTP 本质上就是 REST 的一个实现。术语“表象化状态转换”是指 REST 式的系统通过在请求和响应之间交换资源状态的表象,来完成各种操作。例如,对于一个典型的从 HTTP GET 获得的 Web 页面来说,它就是 Web 资源的一个 HTML 表象,通过 URI 来标识,并由 GET 来触发。

开发 REST 式的 Web 服务需要注意下面几点:

  • 资源与资源标识符

  • 每种资源支持的方法

  • 数据在客户端与服务端之间交换所使用的格式

  • 状态码

  • 每个请求和响应的 HTTP 头

让我们把目光集中在 Yaws 和 Erlang 中,逐一地看看上面列出的几个问题。

资源标识符

设计 REST 式的 Web 服务时,需要你考虑组成服务的资源,比如如何最佳地标识它们、其中一个如何与另一个相关联。REST 式的资源由 URI 来标识。通常,资源都拥有一个与它们自身相关的 URI,同时共享一个公共的路径元素。例如,在一个基于 Web 的 Bug 追踪系统中,所有 “Phoenix”项目(一个虚构的项目)中的 bug 都可以在http://www.example.com/projects/Phoenix/bugs/下找到,只要指明一个 bug 号就可以了,比如 bug 12345 的 URI 可能就是http://www.example.com/projects/Phoenix/bugs/12345/。REST 式的资源还能够提供自身状态表象内部的其他资源的 URI。对于一个获得特定资源状态的用户,可以使用这个返回的 URI(包含在状态表象中)来导航到整个 Web 应用中的其他部分上。

在 Yaws 中,arg记录指定了请求的 URI,使用yaws_api模块提供的request_url方法可以很容易地获得它:

out(Arg) ->

Uri = yaws_api:request_url(Arg),

Path = string:tokens(Uri#url.path, "/"),

一旦你得到了请求 URI,那么可以像上面那样很方便地对请求路径切词,这要按照“/”进行分割就可以了。切词后可以得到路径元素的列表,它的起点是 appmod 的根节点。例如,假设我们将一个 appmod 绑定到“projects”路径元素上,完整的 URI 是http://www.example.com/projects/。如果一个请求 URI 的前缀是前面的 URI,那么 appmod 的out/1函数从中获取一个分离的路径元素列表,代表了请求的目标资源。例如,一个 URI 为 http://www.example.com/projects/Phoenix/bugs/的请求,在执行过上面的一段代码后,Path 变量将保存下面的路径元素的列表:

["projects", "Phoenix", "bugs"]

分离 URI 的好处在于它可以简化后面进一步的转发工作,这得益于 Erlang 的模式匹配能力。例如,我们可以写一个函数,比如是out/2,像下面这样定义它的函数头,它就可以处理这种特殊的 URI 了:

out(Arg, ["projects", Project, "bugs"]) -> 

% code to handle this URI goes here.

这个out/2函数可以处理在所有已知的项目中,所有与 bug 列表相关的请求。Project 变量会在方法体中出现,它的值被设置为正在请求的项目的名称。支持额外的 URI 同样非常简单:为out/2函数添加更多的变量。如果你不喜欢out这个函数名,可以换成任意的,因为 Yaws 框架不会直接调用它们。

注意,正确地定义资源 URI 可以产生巨大的好处。利用 appmod 和 yapp,可以非常容易地拥有一个巨大的、丰富的 URI 空间,因为无论是将不同的 appmod 绑定到不同的 URI 路径元素,还是转发请求,都相当简单。Erlang 的模式匹配降低了处理处理不同 URI 请求的难度。这与传统的非 REST 式的服务在处理这种问题时的笨拙形成鲜明的对比,它们为所有服务都提供一个相同的 URI。一般这个 URI 会指向一个脚本,它通过请求体自身的信息或者 URI 查询字符串的信息来判断将请求实际转发到哪里。这种基于传统技术的 URI 看上去似乎有永无止境的参数,与此相比,前面所示的基于 Erlang/Yaws 转发技术的 URI 要清晰的多。

资源方法

Web 客户端可以调用的 Web 资源上的方法是由 HTTP 的动词定义的,主要包括GETPUTPOSTDELETE。但是,有些资源只能支持这些动词的一部分。当你在设计 Web 服务时,需要确定每种资源都支持哪些方法,记住,RFC 2616定义了每种 HTTP 动词期望的语义。

Yaws 可以在http_request记录中找到请求方法,它可以通过arg记录很容易地获得:

Method = (Arg#arg.req)#http_request.method

它返回表示请求方法的 Erlang atom,可以将它添加到模式匹配的转发方法中去。我们可以为out函数添加一个新的参数来包含请求的方法 ,于是就有了out/3

out(Arg, 'GET', ["projects", Project, "bugs"]) ->

% code to handle GET for this URI goes here.

这个out函数的变体只能够处理对每个项目的 bug 列表的GET请求。另一个变体可以处理POST,也许通过它来在列表中添加新的 bug。如果希望只允许GETPOST请求,而拒绝其他的动作,可以再为这个 URI 编写一个统一处理的函数:

out(Arg, 'GET', ["projects", Project, "bugs"]) ->

% code to handle GET for this URI goes here;

out(Arg, 'POST', ["projects", Project, "bugs"]) ->

% code to handle POST for this URI goes here;

out(Arg, _Method, ["projects", _Project, "bugs"]) ->

[{status, 405}].

在此,GETPOST以外的方法将会匹配第三个变体,它会返回 HTTP 状态码405,意味着“method not allowed”。由于MethodProject变量并未在方法中使用,所以在它们前面加下划线可以关闭编译器对此发出的警告。

就像 URI 转发一样,Erlang 模式匹配可以让开发者很容易地将不同的 HTTP 动词转发到不同的函数上。

表现格式

在设计 REST 式的 Web 服务时,你需要考虑每个资源支持哪些表象。比如,Web 服务资源通常都支持 XML 或者 JSON 表象。Erlang 提供了xmerl library,可以创建和读取 XML,Yaws 提供了一个方便的 JSON 模块。这些都非常好用。

你可以通过请求的Accept头来判断客户端更喜欢哪种表象。这个头可以在headers记录中获得,并可以在arg记录中使用:

Accept_hdr = (Arg#arg.headers)#headers.accept

如果资源支持多种表象,你可以检查这个头,判断客户端是否指定了它希望的表象类型。如果客户端没有发送Accept头,上面代码中的Accept_hdr变量将被设置为atom undefined,你的资源可以提供任何它认为最佳的表象。如果Accept头不是空的话,服务可以解析Accept_hdr变量来判断发送哪一中资源。如果资源无法满足客户请求的表象,服务将返回 HTTP 状态码406,这意味着“not acceptable”,同时返回一个包含可接受格式列表的boby

case Accept_hdr of

undefined ->

% return default representation;

"application/xml" ->

% return XML representation;

"application/json" ->

% return JSON representation;?

_Other ->

Msg = "Accept: application/xml, application/json",

Error = "Error 406",

[{status, 406},

{header, {content_type, "text/html"}},

{ehtml,

[{head, [], [{title, [], Error}]},

{body, [],

[{h1, [], Error},

{p, [], Msg}]}]}]

end.

上面的 Erlang 代码首先检查 Accept_hdr的值,确定是否为application/xml或者application/json。如果是这两者之一,资源将返回一个适当的表象;如果不是,代码将返回 HTTP 状态码406,同时还有一个 HTML 文档,指明资源能够支持的表象类型。

处理预期表象的另一种方法是(你已经猜到了)将它做为另一个参数,添加到out函数中。利用这种方法,Erlang 模式匹配能够确保我们的请求可以被转发到正确的函数,请求中将包含URI/method/representation。这样可以避免出现像上面那样由于 case 语句导致的杂乱无章的处理程序。

顺便提一句,这个例子中也出现了 Yaws 的ehtml类型,它是一系列的 Erlang 术语之一,代表一种 HTML 的表现方式。我发现使用ehtml是相当自然的事情,因为它后面直接是一个 HTML 结构体,但是它更加紧凑,而且你在编写 HTML 语义时,避免了很多匹配标签带来的乏味和错误。

状态码

REST 式的 Web 服务必须返回一个正确的 HTTP 状态码,它们是由 RFC 2616 指定的。使用 Yaws 能够很容易地返回正确的状态:只要在out/1函数的结果中包含一个 status tuple 就可以了。如果你的代码没有显式地设置状态,Yaws 会为你设置一个 200 状态,表示成功。

HTTP 头

Yaws 也可以很容易地获得请求头和设置回复头。我们已经看到了一个从头记录中获得Accept头的示例;获取其他请求头的方法完全一样。设置回复头只需要在回复中放置一个 header tuple 就可以了,如下所示:

{header, {content_type, "text/html"}}

上面的代码会将Content-type头设置为“text/html”。类似地,在前面的例子中,我们返回 405 状态表示“method not allowed”错误,我们也应该包含下面的头:

{header, {"Allow", "GET, POST"}} 

是 Appmod,还是 Yapp?

到目前为止,我们已经看到了 Yaws 和 Erlang 是如何方便地解决 REST 式的 Web 服务中需要面对的一些关键问题的。还有一个问题,我们应该选择 appmod,还是 yapp 呢?答案依赖于你的服务要做的事情。如果你编写的服务必须与其他后端的服务交互,那么 yapp 可能是最好的选择。因为它们是彻头彻尾的 Erlang/OTP 应用程序,它们通常都有初始化和终结函数,用来创建和关闭到后端的连接。比如,如果你的 yapp 是一个 Erlang/OTP gen_server, 你的 init/1 函数可以创建gen_server框架提供给你的状态,并允许你对它进行修改。每次接收到外部到服务器的请求后都会调用init/1。另外,使用 yapp 的同时也可以使用 appmod,因此在这两者间做选择并不是非常关键。最后,yapps 可以加入到 Erlang/OTP 的监管树(supervision tree)中,这样监控进程会监控 yapp 程序,一旦失败会将它们重新启动。Erlang 系统之所以可以长时间稳定地运行,监管树在其中扮演了一个很重要的角色。

这篇文章是为基于后端,而非关系数据库的 REST 式的 Web 服务量身定做的。如果你正在编写传统的、基于关系数据库的 Web 服务,你应该试用一下专为这类 Web 服务准备的Erlyweb,它也是基于 Yaws 和 Erlang 的。

结论

编写 REST 式的 Web 服务的另一个重要方面是选择恰当的编程语言。这些年来,用不同的编程语言开发的各种服务框架令人眼花缭乱,大多数很快就从人们的视野中消逝了,因为它们不能很好的解决真正的问题。Yaws 和 Erlang 并不是专门用于提供 REST 式的服务的框架,不过它提供的功能比很多用其他语言开发专用于 REST 的框架更合适这个领域的开发。

尽管这篇文章必然无法深入 Yaws、Erlang 和 REST 式的 Web 服务的细节,不过希望它能够涉及到重要的主题,通过最少量的代码,提供一个解决这些问题的思路。根据我的经验,使用 Yaws 和 Erlang 构建 Web 应用程序非常简单,最终的代码也容易阅读、维护和扩展。

作者简介

Steve Vinoski 是 IEEE 和 ACM 的成员。在过去 20 年里,他已经独自或与他人合作编写超过了 80 篇文章,各种专栏,以及一本关于分布式计算和整合的专著,在过去的 6 年里,他负责 IEEE Internet Computing 杂志的“Toward Integration”专栏。你可以给他发送邮件vinoski@ieee.org,或者访问他的 blog:http://steve.vinoski.net/blog/

查看英文原文RESTful Services with Erlang and Yaws