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

阅读数:5780 2008 年 6 月 26 日 05:13

看过那张很出名的“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

评论

发布