实战MochiWeb
Table of Contents

MochiWebmochibot.comBob Ippolito贡献的开源项目[在这里有一个介绍它的Slide]。

MochiBot.com 提供 Flash 内容的访问统计和用户跟踪服务(大致上,可以理解为针对 flash 的 google Analytics 服务),他们在 mochiweb 之上构建了一个定制化的 web server ,并通过这个 web server 获取用户的访问数据(在这一点上有点象 Erlana 项目)。可以想象,这个定制的 web server 需要很高的并发支持,精简和牢固的底层架构,以及对于 http 协议的完备支持(乃至对于 socket 的直接操控)。如果可以的话,最好还有更为精简的 API ,易于定制的 URL 扩展方式,以及易于理解的底层框架。幸运的是,这些 mochiweb 都已经提供,而且还是开源的。

需要说明的是,相比 yaws / inets httpd 而言,它的目标并不是 apache 之类的软件,它并不是一个完整的 web server (没有cache等机制,因而也不做任何加速动作),它只是一个实现 web server 的工具包(这也就意味着,它直接通过代码来扩展,你可以在它的基础上做任何事)。正因为此,在“需要定制 Web Server”的情况下,它成为一个非常不错的选择(比如,配置在 enginx 的后面,专门用于动态内容的生成)。在 erlang 的世界里,有几个项目已经开始转而使用 mochiweb 。

下面是对这个项目代码的一些粗浅实战。

首先遵循它的提示,通过svn获取代码:

svn checkout http://mochiweb.googlecode.com/svn/trunk/ mochiweb-read-only

获得的文件和目录结构如下:

deps  ebin     LICENSE   priv    scripts  support
doc   include  Makefile  README  src

注:大写字母开头的是文件,小写字母开头的是目录。这是一个相当标准的 Erlang 项目目录结构,其 Makefile (用到 support 目录的 make 包含文件)非常值得借鉴(而且也有简化这一借鉴步骤的办法,后面会提到)。

这是一个纯粹的 Erlang 项目,并不涉及其它语言写的模块,照老规矩,直接 make :

make

注:如果你和我一样,仍在 R11* 上工作,那么 make 会在 edoc 的步骤中失败,这是因为 R11* 的 edoc 工具存在 bug 无法正确处理 mochiweb 用到的 Parameterized module 语法,不用管它,并不影响后续使用。

make 完成之后,要怎么试运行呢?这就涉及我们上面提到的“借鉴”工作。因为 mochiweb 是设计用来作为一个完整项目的一个基础部分,也就是说,它只是一个骨架(或者如作者所说的toolkit),在你 make 完之后,什么也干不了,除非你对它进行定制化编码,完成这个 web server 。好在它自己已经提供了工具来简化这一步骤:

escript scripts/new_mochiweb.erl test

new_mochiweb.erl 是一个 EScript 脚本,它负责从 mochiweb 中拷贝使用 mochiweb 所必须文件和目录,形成你的新项目的“骨架”(概念上有点类似于 rails 的自动生成代码)。上面的命令生成了名为 test 的项目,会在当前目录建立名为 test 子目录(还可以使用 escript scripts/new_mochiweb.erl test testdir 将新建立的项目放在 testdir 目录中)。上面的命令生成了一些文件,我加了注释:

./test/ 项目目录
    Makefile Make文件
    start.sh 启动脚本
    start-dev.sh 开发模式下的启动脚本(开启代码重载机制)
./test/ebin/ 编译目录
./test/support/ Make支持文件目录
    include.mk Make包含脚本
./test/deps/ 依赖目录,包含mochiweb自身
./test/src/ 代码目录
    skel.app 实际名称为test.app,OTP规范的应用定义文件
    Makefile Make文件
    skel_web.erl 实际名称为test_web.erl,应用的web服务器代码
    skel_deps.erl 实际名称为test_deps.erl,负责加载deps目录的代码
    skel_sup.erl 实际名称为test_sup.erl,OTP规范的监控树
    skel.hrl 实际名称为test.hrl,应用的头文件
    skel_app.erl 实际名称为test_app.erl,OTP规范的应用启动文件
    skel.erl 实际名称为test.erl,应用的API定义文件
./test/doc/ 文档目录
./test/include/ 包含文件目录
./test/priv/ 项目附加目录
./test/priv/www/ 项目附加的www目录
    index.html 默认的项目首页

是的,什么也不用改,在新生成的项目骨架中,一个可用的web服务器已经就绪:

make
./start-dev.sh

这会打开一个 erlang shell ,输出的信息表明在 8000 端口开了一个 web 服务,此时用浏览器访问 http://localhost:8000 (或者其它正确的地址)就能看到“MochiWeb running.”,这表明 mochiweb 配置正确,运行良好。注意,我们上面是用 start-dev.sh 来启动的,它打开了 reloader 特性。

现在修改一下 test_web.erl 的代码,加点料。因为我们上面已经打开了 reloader 所以,不用关掉这个 erlang shell ,我们可以直接修改和编译,然后刷新就能看到效果(有点 PHP 编程的意思了)。把 test_web.erl 改成这样,看看会有什么情况发生:

下载: test_web.erl

%% @author author <author@example.com>
%% @copyright YYYY author.

%% @doc Web server for test.

-module(test_web).
-author('author <author@example.com>').

-export([start/1, stop/0, loop/2]).

%% External API

start(Options) ->
    {DocRoot, Options1} = get_option(docroot, Options),
    Loop = fun (Req) ->
                   ?MODULE:loop(Req, DocRoot)
           end,
    mochiweb_http:start([{name, ?MODULE}, {loop, Loop} | Options1]).

stop() ->
    mochiweb_http:stop(?MODULE).

loop(Req, DocRoot) ->
    "/" ++ Path = Req:get(path),
    case Req:get(method) of
        Method when Method =:= 'GET'; Method =:= 'HEAD' ->
            case Path of
                "timer" ->    %% 新增了 /timer 这个 URL,它是一个 HTTP Chunked 的例子
                    Response = Req:ok({"text/plain", chunked}),
                    timer(Response);
                _ ->
                    Req:serve_file(Path, DocRoot)
            end;
        'POST' ->
            case Path of
                _ ->
                    Req:not_found()
            end;
        _ ->
            Req:respond({501, [], []})
    end.

%% Internal API

get_option(Option, Options) ->
    {proplists:get_value(Option, Options), proplists:delete(Option, Options)}.

%% 打印当前时间,间隔一秒,再在已经打开的 http 连接之上,再次打印,
%% 这也就是所谓 HTTP长连接/ServerPush 的一种
timer(Response) ->
    Response:write_chunk(io_lib:format("The time is: ~p~n",
                                       [calendar:local_time()])),
    timer:sleep(1000),
    timer(Response).

编译之前,先访问一下 http://localhost:8000/timer ,是“Not found.”。此时,不要中断之前的 erlang shell 而是直接再次 make :

make

留意到之前打开的 erlang shell 上出现了这么一行:

1> Reloading test_web ... ok.

此时,再次访问 http://localhost:8000/timer (耐心些 HTTP chunked 获得的数据要积累到一定的字节浏览器才会显示),你会发现这是一个不会“下载结束”的页面,不断会有新的内容出现在下面。你也许可以利用这个特性实现传说中的“无刷新聊天室”。

值得留意的是这样的代码:

...
Req:ok({"text/plain", chunked}),
...
Req:serve_file(Path, DocRoot)
...
Response:write_chunk(io_lib:format("The time is: ~p~n",
                              [calendar:local_time()])),
...

我们这里是用 Req:ok(…) 而不是 request:ok(Req, …) 这在 Erlang 的代码中并不寻常,Req 是一个变量,通常这个变量的值是某个 atom 表明的是一个 module 的名称,但这里的 Req 显然不是这样。它是一个 “module 的实例”,这就是我们前面提到的“ Parameterized module 语法”的实际应用,它不仅意味着某个模块的名称,还意味着(初始化时)传给这个模块的一系列参数,它包装了与一个 request 相关的数据。应该说,这个语法更加简洁易懂。

问题:

1. 如果在此时,并不关闭正在不断“下载页面”的浏览器,在 test_web.erl 中将 timer 的部分注释掉,然后再次 make ,会发生什么?为什么?
2. 找出 Req 在 mochiweb 的哪个模块中被初始化?如何被初始化?它实际上是由哪个模块来实现的?
3. 解释 test_web.erl 的代码结构,各个部分都起什么作用?它是如何服务于每一个请求的?
4. 如何在 test_web.erl 中直接访问 http 连接的 socket ?

(实际上,这个例子只是一个 HTTP Chunked 的例子而已,你并不能依赖于 HTTP Chunked 来实现聊天室,这不是 HTTP Chunked 的问题,而是因为在现实的网络环境下,路由器有可能会自动断开连接时长超过某个值的连接。)

本文出处:http://erlang-china.org/start/mochiweb_intro.htmli

Unless otherwise stated, the content of this page is licensed under Creative Commons Attribution-ShareAlike 3.0 License

Subscription expired — please renew

Pro account upgrade has expired for this site and the site is now locked. If you are the master administrator for this site, please renew your subscription or delete your outstanding sites or stored files, so that your account fits in the free plan.