实战MochiWeb
MochiWeb是mochibot.com的Bob 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获取代码:
获得的文件和目录结构如下:
doc include Makefile README src
注:大写字母开头的是文件,小写字母开头的是目录。这是一个相当标准的 Erlang 项目目录结构,其 Makefile (用到 support 目录的 make 包含文件)非常值得借鉴(而且也有简化这一借鉴步骤的办法,后面会提到)。
这是一个纯粹的 Erlang 项目,并不涉及其它语言写的模块,照老规矩,直接 make :
注:如果你和我一样,仍在 R11* 上工作,那么 make 会在 edoc 的步骤中失败,这是因为 R11* 的 edoc 工具存在 bug 无法正确处理 mochiweb 用到的 Parameterized module 语法,不用管它,并不影响后续使用。
make 完成之后,要怎么试运行呢?这就涉及我们上面提到的“借鉴”工作。因为 mochiweb 是设计用来作为一个完整项目的一个基础部分,也就是说,它只是一个骨架(或者如作者所说的toolkit),在你 make 完之后,什么也干不了,除非你对它进行定制化编码,完成这个 web server 。好在它自己已经提供了工具来简化这一步骤:
new_mochiweb.erl 是一个 EScript 脚本,它负责从 mochiweb 中拷贝使用 mochiweb 所必须文件和目录,形成你的新项目的“骨架”(概念上有点类似于 rails 的自动生成代码)。上面的命令生成了名为 test 的项目,会在当前目录建立名为 test 子目录(还可以使用 escript scripts/new_mochiweb.erl test testdir 将新建立的项目放在 testdir 目录中)。上面的命令生成了一些文件,我加了注释:
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服务器已经就绪:
./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 改成这样,看看会有什么情况发生:
- %% @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 :
留意到之前打开的 erlang shell 上出现了这么一行:
此时,再次访问 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 的问题,而是因为在现实的网络环境下,路由器有可能会自动断开连接时长超过某个值的连接。)