使用Erlang和Yaws开发REST式的服务
看过那张很出名的“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的函数,例如,某个带有一个参数的函数。这个参数应该是一个Yawsarg
记录(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的动词定义的,主要包括GET
、PUT
、POST
、DELETE
。但是,有些资源只能支持这些动词的一部分。当你在设计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。如果希望只允许GET
和POST
请求,而拒绝其他的动作,可以再为这个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}].
在此,GET
和POST
以外的方法将会匹配第三个变体,它会返回HTTP状态码405
,意味着“method not allowed”。由于Method
和Project
变量并未在方法中使用,所以在它们前面加下划线可以关闭编译器对此发出的警告。
就像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
来自:http://www.infoq.com/cn/articles/vinoski-erlang-rest