InfoQ

文章

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

作者 Steve Vinoski译者 韩锴 发布于 2008年6月26日 上午5时13分

社区
SOA
主题
Web服务,
Web框架,
REST
标签
Erlang

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

5 条回复

回复

很好很强大。并行时代我们应该关注此类技术语言 发表人 deshi xiao 发表于 2008年6月27日 上午12时29分
Re: 很好很强大。并行时代我们应该关注此类技术语言 发表人 Shell Wang 发表于 2008年7月25日 下午10时6分
不知道这样的技术在国外是否已经有成功的案例。 发表人 haojun yu 发表于 2008年6月30日 上午1时45分
Re: 不知道这样的技术在国外是否已经有成功的案例。 发表人 Quiet Zoom 发表于 2008年7月7日 下午10时57分
Yaws很难用 发表人 Shell Wang 发表于 2008年7月25日 下午10时8分
  1. 返回顶部

    很好很强大。并行时代我们应该关注此类技术语言

    2008年6月27日 上午12时29分 发表人 deshi xiao

    回复为什么需要标题,呵呵。有点不失所以然

  2. 返回顶部

    不知道这样的技术在国外是否已经有成功的案例。

    2008年6月30日 上午1时45分 发表人 haojun yu

    不知道这样的技术在国外是否已经有成功的案例。但是在国内还只是技术人员讨论的前沿技术,不知道有没有成功案例。

  3. 返回顶部

    Re: 不知道这样的技术在国外是否已经有成功的案例。

    2008年7月7日 下午10时57分 发表人 Quiet Zoom

    OTP !

  4. 返回顶部

    Re: 很好很强大。并行时代我们应该关注此类技术语言

    2008年7月25日 下午10时6分 发表人 Shell Wang

    我觉得真得很不错。

  5. 返回顶部

    Yaws很难用

    2008年7月25日 下午10时8分 发表人 Shell Wang

    不知道该怎么配置。

独家内容

从卓越工程角度看微软中国开发团队的成长

开发团队的成长离不开优秀的人才,简捷有效的流程和高效率工具这三个卓越工程系统中的重要因素。本文作者从这三个因素分析了微软中国开发团队是如何“从优秀到卓越”的。

利用Ruby简化你的Java测试

本文是Productive Java with Ruby系列文章的第一篇,我将从单元测试这个话题开始,让Java的开发人员能够在实际工作中利用Ruby提高工作效率。

与赵进聊SaaS

InfoQ中文站有幸与阿里软件的首席架构师赵进在一起探讨了SaaS的相关话题,包括SOA和ASP与SaaS的异同、云计算、SaaS的前景、它的关键技术、技术瓶颈等等。

在ESB中选择路由还是编配?

在这篇文章中,Adrien Louis和Marc Dutoo在一个典型的ESB场景中讨论了编配和路由的区别和优缺点。他们讨论了几种连接服务的方法,从使用如自定义路由这样的低级别方法,到使用如工作流和编配这样面向业务的高级别方式,并总结说不存在“一边倒”的解决方案。

分布式系统中的一致性和可用性

本文是根据7月26日InfoQ中文站在杭州举行的QClub活动(第三期)后半程小组讨论总结而成。主要内容包括如何在SOA系统中实现服务编排,如何保证分布式系统中的一致性和可用性,以及如何在实施SOA的过程中控制接口的粒度等。

虚拟化导论

人们很容易想当然的以为虚拟化技术仅仅应用于服务器。而在现实中,虚拟化这一苏醒的概念正被运用于各个层面,其中包括网络,存储以及应用基础架构。在这篇导论中,InfoQ将深入每个方面,详尽向您描述虚拟化技术的运用以及其优点与不足。

用户故事估算技巧

作为开发者,同时也是ThoughtWorks的咨询师,Jay Fields总结了自己估算用户故事的有效技巧。

InfoQ案例研究:纳斯达克市场回放

在这篇案例研究中,InfoQ对Adobe AIR和Amazon的简单存储服务(Simple Storage Service ,S3)在NASDAQ市场回放程序(NASDAQ Market Replay)中的应用进行了详细的分析。