代码改变世界

【译】MochiWeb(基于Erlang的高性能WEB服务器)实用入门教程

2011-10-24 02:13  码农.KEN  阅读(6182)  评论(0编辑  收藏  举报


原文出处:http://alexmarandon.com/articles/mochiweb_tutorial/


简介

MochiWeb由Bob Ippolito 创建,其描述为:“一个创建轻量级http服务器的Erlang库”。它不是框架:不附带URL调度、模版引擎、数据持久等。尽管没有官方网站和文档,但仍然是Erlang构建web服务的热门选择。这篇随笔将带您逐步入门并构建一个支持URL调度和模版引擎的迷你型框架。(不包含数据持久)

我假设您已经具备一些Erlang语言的基础,否则,建议您先学习这本指南的前面部分章节,本教程不需要具备对并发和分布式Erlang的知识。

如果您遇到问题,可以从这里获取本教程相应的代码。每次提交对应本教程相应部分,所以你可以很容易的找到某个步骤相关的代码。

入门

首先使用Git从github获取MochiWeb源代码:

$ git clone git://github.com/mochi/mochiweb.git

接下来,我们创建一个项目,叫做 greeting:

$ cd mochiweb
$ make app PROJECT=greeting

很简单,现在我们可以编译和运行我们的app了:

$ cd ../greeting/
$ make
$ ./start-dev.sh

此时,你会看到一堆进度报告的信息,其中有类似于{port, 8080}字样,这是说我们的app将运行在8080端口。此时,可以打开浏览器,访问http://localhost:8080,你会看到一行信息:“stat running.” 标题栏显示“It Worked”,这说明运行成功了。

这时回到终端,按回车后将出现一个Erlang Shell>,你可以使它来跟你的app交互,这对app的调试非常有用。(相当于ROR的./script/console 或者 Django的manage.py shell)

文档

当我第一次在网上找关于MochiWeb的文档时,大部分搜索结果都是一些人也在找文档的信息。这也是我写本教程的原因。

实际上,MochiWeb本身包含一些参考文档,你可以这样生成它:

$ cd ../mochiweb
$ make edoc


之后,你可以在mochiweb所在目录的找到doc/index.html,用浏览器打开即可。它有助于了解模块概述、可用函数和函数的具体说明。

这里有一个很棒的视频教程展示了一种有趣的方法搭建完全基于MochiWeb的AJAX应用程序.

本篇教程中,我使用了一种更传统的方法来实现按规则将请求映射到相应的Erlang函数.(译者注:类似于Django中的url pattern 映射相应的views方法)

基本的请求处理

当我们首次请求app时,页面上显示的信息来自于greeting/priv/www/下的index.html文件,此目录将供我们放置一些静态文件,如css,图片等。现在,可能更有趣的事应该是开始创建一个请求处理程序,得到一些用户输入。

我们将在src/greeting_web.erl中插入一些代码来处理请求,该模块中包含一个函数 loop/2:

loop(Req, DocRoot) ->
"/" ++ Path = Req:get(path),
try
case Req:get(method) of
Method when Method =:= 'GET'; Method =:= 'HEAD' ->
case Path of
_ ->
Req:serve_file(Path, DocRoot)
end;
'POST' ->
case Path of
_ ->
Req:not_found()
end;
_ ->
Req:respond({501, [], []})
end
catch
%% ... exception handling code ...
end.

如果你能阅读Erlang,应该不难理解这段代码的作用。它从请求中提取路径,如果该请求的Method是GET或HEAD(默认将"/"映射至/index.html),如果是POST或者其他HTTP动作,将返回404和错误提示。任何异常将被捕获并显示在终端,此处我没有给出异常处理的代码,但并不意味着你可以不管它。

我们现在要做的是添加一些代码来处理访问路径为/hello 的请求,从QueryString中获得用户名,并显示欢迎词,在上面代码中对GET请求处理的条件分支处添加下面的字句:

"hello" ->
QueryStringData = Req:parse_qs(),
Username = proplists:get_value("username", QueryStringData, "Anonymous"),
Req:respond({200, [{"Content-Type", "text/plain"}],
"Hello " ++ Username ++ "!\n"});

首先我们使用 mochiweb_request:parse_qs/0 方法得到包含query string参数的proplist ,然后用proplist:get_value/3 方法得到username的参数值,如果不存在则默认为"Anonymous".

最后我们调用mochiweb_request:respond/1方法,它需要传递一个元组参数,其中包含:HTTP状态码、头信息proplist、主体信息. 下面是我们的新loop/2函数:

loop(Req, DocRoot) ->
"/" ++ Path = Req:get(path),
try
case Req:get(method) of
Method when Method =:= 'GET'; Method =:= 'HEAD' ->
case Path of
"hello" ->
QueryStringData = Req:parse_qs(),
Username = proplists:get_value("username", QueryStringData, "Anonymous"),
Req:respond({200, [{"Content-Type", "text/plain"}],
"Hello " ++ Username ++ "!\n"});
_ ->
Req:serve_file(Path, DocRoot)
end;
'POST' ->
case Path of
_ ->
Req:not_found()
end;
_ ->
Req:respond({501, [], []})
end
catch
%% ... exception handling code ...
end.

使用make 编译你的项目,然后你可以访问http://localhost:8080/hello?username=Mike 将会看到《Erlang: The Movie》中的名言:Hello Mike!

渲染模版

你还在为刚刚实现的问候语功能而兴奋,但那太简单了。接下来让我们使用先进的HTML来提升下用户体验,我们使用ErlyDTL ,由Evan Miller 编写的Django模版语法的Erlang版实现。如果你还不了解Django的模版引擎语法,可以看其文档 ,不过我可以告诉你一些基本的,变量看起来像这样{{my_variable}},控制语句是用这样的标签语法来实现{% tagname param %}这里是一些内容{% endtagname %}.

安装 ErlyDTL

首先添加ErlyDTL到我们的项目,MochiWeb使用rabar ,一个用于Erlang应用程序构建和打包的工具。我们可以用它来为项目添加依赖.打开rebar.config文件,你会看到一个条目指向MochiWeb的git仓库,让我们添加另外一个指向ErlyDTL的条目,此时配置文件应该是这样:

%% -*- erlang -*-
{erl_opts, [debug_info]}.
{deps, [
{erlydtl, ".*",
{git, "git://github.com/evanmiller/erlydtl.git", "master"}},
{mochiweb, ".*",
{git, "git://github.com/mochi/mochiweb.git", "master"}}]}.
{cover_enabled, true}.
{eunit_opts, [verbose, {report,{eunit_surefire,[{dir,"."}]}}]}.


你仅仅只需要输入make,即可获取和编译ErlyDTL:

$ make
==> mochiweb (get-deps)
==> greeting (get-deps)
Pulling erlydtl from {git,"git://github.com/evanmiller/erlydtl.git","master"}
Initialized empty Git repository in /home/al/dev/projects/greeting/deps/erlydtl/.git/
==> erlydtl (get-deps)
==> erlydtl (compile)
Compiled src/erlydtl_parser.yrl
[...]

由于使用了新的库,要使其生效,需重启应用程序。终端输入q(). 然后再执行./start-dev.sh。

当然,这个方法不仅仅用于ErlyDTL,你也可以用rabar,同样的方法添加其他的依赖。

模版编译

ErlyDTL会把Django模版编译为Erlang字节码,rabar恰恰完美支持在我们的代码中管理编译ErlyDTL模版,所以我们使用它。

我们将创建一个 templates 目录,它是rabar编译时默认的模版目录:

$ mkdir templates

现在创建一个模版文件 templates/greeting.dtl ,内容大概如下:

<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01//EN" "http://www.w3.org/TR/html4/strict.dtd">
<html>
<head>
<meta http-equiv="Content-Type" content="text/html; charset=utf-8">
<title>MochiWeb Tutorial</title>
<link rel="stylesheet" type="text/css" href="/style.css" media="screen">
</head>
<body>
<p>
Hello {{ username }}!
</p>
</body>
</html>

再次make, 你会看到rabar创建了一个Erlang模块 ebin/greeting_dtl.beam. 注意,rabar提供了一些选项来自定义模版源文件和编译文件的路径和名称。

现在你可以用下面的代码在处理请求时使用新的模版:

QueryStringData = Req:parse_qs(),
Username = proplists:get_value("username", QueryStringData, "Anonymous"),
{ok, HTMLOutput} = greeting_dtl:render([{username, Username}]),
Req:respond({200, [{"Content-Type", "text/html"}],
HTMLOutput});

运行make ,刷新浏览器,你会被你的设计佳作吓到。

正如您看到的,当代码改动后,你需要执行make使之生效。从现在起,我将不再重申需要make。

POST请求处理

至此,你的app已经很受欢迎了,但是很多用户会抱怨他们记不住QueryString的语法,并希望能够通过页面填写表单的形式来提交用户名。现在编辑模版,为username添加一个文本框表单:

<form method="POST">
<p>Username: <input type="text" name="username"></p>
<input type="submit">
</form>

你还需要更改一下请求处理程序,使其支持POST请求,它看起来应该像这样:

loop(Req, DocRoot) ->
"/" ++ Path = Req:get(path),
try
case Req:get(method) of
Method when Method =:= 'GET'; Method =:= 'HEAD' ->
case Path of
"hello" ->
QueryStringData = Req:parse_qs(),
Username = proplists:get_value("username", QueryStringData, "Anonymous"),
{ok, HTMLOutput} = greeting_dtl:render([{username, Username}]),
Req:respond({200, [{"Content-Type", "text/html"}],
HTMLOutput});
_ ->
Req:serve_file(Path, DocRoot)
end;
'POST' ->
case Path of
"hello" ->
PostData = Req:parse_post(),
Username = proplists:get_value("username", PostData, "Anonymous"),
{ok, HTMLOutput} = greeting_dtl:render([{username, Username}]),
Req:respond({200, [{"Content-Type", "text/html"}],
HTMLOutput});
_ ->
Req:not_found()
end;
_ ->
Req:respond({501, [], []})
end
catch
% ... exception handling code ...
end.


它看起来工作得非常好,但还是有一些问题。可见"hello"出现在了2个地方,不是个好兆头。重复代码来渲染模版和返回Response也不太好。我们注意到,当我们访问/hello/ (末尾加斜杠)的地址时会得到一个页面未找到的错误。是时候做一些重构了。

简单的URL调度器(Dispatcher)

我们将创建一个简约的URL调度器来实现对URL规则与Erlang函数的的映射,url规则配置看起来像这样:

[
{"^hello/?$", hello}
]

这是说所有以/hello 或者 /hello/ 的请求都会被路由到名为hello的函数。下面给出调度器的代码:

% Iterate recursively on our list of {Regexp, Function} tuples
dispatch(_, []) -> none;
dispatch(Req, [{Regexp, Function}|T]) ->
"/" ++ Path = Req:get(path),
Method = Req:get(method),
Match = re:run(Path, Regexp, [global, {capture, all_but_first, list}]),
case Match of
{match,[MatchList]} ->
% We found a regexp that matches the current URL path
case length(MatchList) of
0 ->
% We didn't capture any URL parameters
greeting_views:Function(Method, Req);
Length when Length > 0 ->
% We pass URL parameters we captured to the function
Args = lists:append([[Method, Req], MatchList]),
apply(greeting_views, Function, Args)
end;
_ ->
dispatch(Req, T)
end.

将调度器代码插入到 greeting_web.erl 的适当位置,并修改loop/2 函数来使用它:

loop(Req, DocRoot) ->
"/" ++ Path = Req:get(path),
try
case dispatch(Req, greeting_views:urls()) of
none ->
% No request handler found
case filelib:is_file(filename:join([DocRoot, Path])) of
true ->
% If there's a static file, serve it
Req:serve_file(Path, DocRoot);
false ->
% Otherwise the page is not found
Req:not_found()
end;
Response ->
Response
end
catch
% ... exception handling code ...
end.

现在我们来创建一个模块,其包含所需的URL规则配置和请求处理程序。新建 src/greeting_views.erl 文件,并输入以下代码:

-module(greeting_views).
-compile(export_all).
-import(greeting_shortcuts, [render_ok/3]).

urls() -> [
{"^hello/?$", hello}
].

hello('GET', Req) ->
QueryStringData = Req:parse_qs(),
Username = proplists:get_value("username", QueryStringData, "Anonymous"),
render_ok(Req, greeting_dtl, [{username, Username}]);
hello('POST', Req) ->
PostData = Req:parse_post(),
Username = proplists:get_value("username", PostData, "Anonymous"),
render_ok(Req, greeting_dtl, [{username, Username}]).

我们再使用一个函数 render_ok/3 来防止返回Response时的重复代码。让我们把这个函数放到 src/greeting_shortcuts.erl 文件中:

-module(greeting_shortcuts).
-compile(export_all).

render_ok(Req, TemplateModule, Params) ->
{ok, Output} = TemplateModule:render(Params),
% Here we use mochiweb_request:ok/1 to render a reponse
Req:ok({"text/html", Output}).

现在,你已经有了一些通用方式来处理请求了;我们删除了一些重复代码,使得看起来更加有条理了,并且也定义了专门放置请求处理程序和工具函数的地方。

一切都挺不错,但是你的朋友告诉你,他想能通过GET请求获得一个欢迎辞,但他却觉得用QueryString的方式(?username=alice)很难看。而他希望通过这样的访问地址/hello/Alice 或/hello/Alice/ 即可得到一个欢迎辞页面。幸运的是,我们的URL调度器已经能很容易的实现该新功能。

添加第二个URL配置项,现在配置看起来是这样:

urls() -> [
{"^hello/?$", hello},
{"^hello/(.+?)/?$", hello}
].

再创建一个请求处理函数 (在 greeting_views.erl 中),该函数可接收URL中的参数:

hello('GET', Req, Username) ->
render_ok(Req, greeting_dtl, [{username, Username}]);
hello('POST', Req, _) ->
% Ignore URL parameter if it's a POST
hello('POST', Req).

瞧,现在/hello/Alice 或 /hello/Alice/ 都可以正常工作了。

处理COOKIES

你收到了许许多多的反馈,有些反馈是希望可以在下次再访问/hello/ 的时候能记住之前他们的名字就更好了。我们使用cookie来实现它,编辑greeting_shortcuts.erl文件,并添加一个函数返回一个cookie值,若不存在则返回默认值。还需要创建一个新函数 render_ok/4 ,它基本上很像我们已有的 render_ok/3,除了它需要一个额外的参数 Headers用于发送Cookie头。修改render_ok/3 让其直接调用 render_ok/4 ,传递一个空的list给Headers参数。

-module(greeting_shortcuts).
-compile(export_all).

render_ok(Req, TemplateModule, Params) ->
render_ok(Req, [], TemplateModule, Params).

render_ok(Req, Headers, TemplateModule, Params) ->
{ok, Output} = TemplateModule:render(Params),
Req:ok({"text/html", Headers, Output}).

get_cookie_value(Req, Key, Default) ->
case Req:get_cookie_value(Key) of
undefined -> Default;
Value -> Value
end.

现在编辑你的视图模块,使用以上新函数,而我们还需要删除一些重复的东西:

-module(greeting_views).
-compile(export_all).
-import(greeting_shortcuts, [render_ok/3, render_ok/4, get_cookie_value/3]).

urls() -> [
{"^hello/?$", hello},
{"^hello/(.+?)/?$", hello}
].

% Return username input if present, otherwise return username cookie if
% present, otherwise return "Anonymous"
get_username(Req, InputData) ->
proplists:get_value("username", InputData,
get_cookie_value(Req, "username", "Anonymous")).

make_cookie(Username) ->
mochiweb_cookies:cookie("username", Username, [{path, "/"}]).

handle_hello(Req, InputData) ->
Username = get_username(Req, InputData),
Cookie = make_cookie(Username),
render_ok(Req, [Cookie], greeting_dtl, [{username, Username}]).

hello('GET', Req) ->
handle_hello(Req, Req:parse_qs());
hello('POST', Req) ->
handle_hello(Req, Req:parse_post()).

hello('GET', Req, Username) ->
Cookie = make_cookie(Username),
render_ok(Req, [Cookie], greeting_dtl, [{username, Username}]);
hello('POST', Req, _) ->
hello('POST', Req).

当用户设置过他们的用户名时,用户名将被存储为cookie,且下次访问/hellp/时将被显示。

结论

本教程就到这里,现在你已经知道了如何添加库到项目、获取用户输入、渲染模版和设置cookies, 你需要积累更多的功能,如用户认证、全局模版上下文、数据持久等等。还需要修改URL调度器使其能映射到指定模块的指定方法,或用其他方法,或许采用“约定大于配置”会比较好。

我希望你稍微多熟悉一下MochiWeb,那样你才可以用最合适的方法来实现你的需求。浏览API文档,了解更多MochiWeb所提供的功能,毫不犹豫的阅读其源代码。这里有一些我使用Erlang库工作的一些“真理”:源代码通常比文档讲得更多;有幸你跟我一样的话,你会发现Erlang的代码通常比其他语言更容易理解, 可能是因为它的函数式特性和简约。