技术开发 频道

实战MochiWeb

  【IT168 技术文档】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 都已经提供,而且还是开源的。

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

  首先遵循它的提示,通过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),需要你自己来编代码完成这个 web server 。也就是说,在你 make 完之后,什么也干不了,除非你对它进行定制化。好在它自己已经提供了工具来简化这一步骤:

  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 改成这样,看看会有什么情况发生:

  %% @author author   %% @copyright YYYY author.   %% @doc Web server for test.   -module(test_web).   -author('author ').   -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 来实现聊天室,因为在现实的网络环境下,路由器会自动断开连接时长超过某个值的连接。)

0
相关文章