技术头条 - 一个快速在微博传播文章的方式     搜索本站
您现在的位置首页 --> 编程语言 --> 使用 Mojolicious 写非阻塞的应用: Part 1

使用 Mojolicious 写非阻塞的应用: Part 1

浏览:780次  出处信息
我们常常听到一个问题 "在众多 Perl Web 框架中, 我为什么要选择 Mojolicious?", 对于这个问题,我有太多的答案可以告诉你,但我认为最主要的区别是 Mojolicious  的设计是非阻塞的. 你们中很多人可能听说 Node.js 之所有受欢迎的原因是它是设计成非阻塞的. 当你写你的 webapp 的应用使用非阻塞的框架和技术时,你可以创建一个更加快,更加精巧的应用. 只需要很少的服务器资源来处理和其它大量程序处理相同的处理量. 虽然 Perl 有很多 Web 框架. 但只有 Mojolicious 从设计开始就是为非阻塞而生的.

为了演示一个无阻塞的应用, 我打算写一个简单的应用使用 Mojolicious 和 Mango, 非阻塞的 MongoDB 的 lib 库(这个的作者也是 Mojo 的作者);

模板技术

我们在看服务的其它代码前, 我们看看模板, 它将构成应用的程序的视图部分(MVC). Mojolicious 有自己的 模板引擎技术 它只是对 Perl 语法简单的包装,所以性能很好,上手非常快.

@@ layouts/basic.html.ep
  
<!DOCTYPE html>
<html>
  <head>
    <title><%= title =%></title>
    %= stylesheet '//netdna.bootstrapcdn.com/bootstrap/3.0.0/css/bootstrap.min.css'
  </head>
  <body>
    <div class="container">
      <%= content =%>
    </div>
  </body>
</html>
  
@@ show.html.ep
  
% title $doc->{title};
% layout 'basic';
  
%= stylesheet begin
pre.prettyprint {
  background-color:inherit;
  border:none;
}
% end
%= tag h1 => $doc->{title}
%= tag div => class => 'well' => begin
  %= tag pre => class => 'prettyprint' => begin
    <%= $doc->{content} =%>
  % end
% end
%= javascript 'https://google-code-prettify.googlecode.com/svn/loader/run_prettify.js'
  
@@ submit.html.ep
  
% title 'Paste your content';
% layout 'basic';
  
%= form_for '/' => role => form => method => POST => begin
  %= tag div => class => 'form-group' => begin
    %= tag label => for => 'title' => 'Title'
    %= text_field 'title', class => 'form-control'
  % end
  %= tag div => class => 'form-group' => begin
    %= tag label => for => 'content' => 'Paste Content'
    %= text_area  'content', class => 'form-control'
  % end
  %= submit_button 'Paste' => class => 'btn btn-primary'
% end

在第一个模板 layouts/basic.html.ep 会被用任何其它模板, 在其它模板调用的时候,都会请求这个 base 的 HTML 的布局(layout). 请求模板的时候中的内容时指定的 content  字段直接替换成相应的内容. 这的 show.html.ep 和 submit.html.ep 的模板(两个加载的 base 的 layout 层) 是用于当用户想要粘贴和创建新的时候的时候显示用.

注意 show.html.ep  的模板, 可以见到一个 $doc 的变量. 这是模板变量由 controller 使用 stash 命令之后创建的. 你见到的这些功能都是由 Mojolicious 中调用的 helpers 的用法. 另外,这使用了一些  Twitter’s Bootstrap 的样式.

这些模板有二个地方可以放,一个可以插入代码文件本身的 __DATA__ 部分.也可以单独放置在一个叫 templates 的模板目录中.

阻塞的实现方式

由于大多数人都熟悉编写使用阻塞风格(默认)来写你的应用程序,我这先以这种方式来写我们的应用, 当然 Mojo 也支持这种方式并且也工作的非常好。

#!/usr/bin/env perl
  
use Mojolicious::Lite;
use Mango;
use Mango::BSON 'bson_oid';
  
helper mango  => sub { state $mango = Mango->new($ENV{PASTEDB}) };
helper pastes => sub { shift->mango->db->collection('pastes') };
  
get '/' => 'submit';
  
post '/' => sub {
  my $self = shift;
  my $title = $self->param('title') || 'Untitled';
  my $content = $self->param('content')
    or return $self->redirect_to('/');
  my $doc = {
    title   => $title,
    content => $content,
  };
  my $oid = $self->pastes->save($doc);
  $self->redirect_to( show => id => "$oid" );
};
  
get '/:id' => sub {
  my $self = shift;
  my $id = bson_oid $self->stash('id');
  my $doc = $self->pastes->find_one({ _id => $id })
    or return $self->redirect_to('/');
  $self->stash( doc => $doc );
} => 'show';
  
app->start;


导入必要的库(导入 Mojo 会打开 strict, warningsutf8 并且全部的 v5.10 特性),创建一些我们自己的 helpers. 这有个 mangohelper 用于连接到 Mongo 实例, 我会有环境变量中指定连接所需要的信息.  (是的,我可以把它在配置文件, but I had to draw the line on this example somewhere :-) ). 我还有一个 helper 用于返回实例中的  (read: table) 的集合,这是用于存储粘贴的信息. 默认的 Mango 会创建独一无二的文档 ID, 在我们这个应用中, 我们给这个做为我们的页面标识符.

helpers 创建完后, 下面创建了三个路由(route)连接到 controller 的回调的子函数(Lite 版本的 controller 方法), 其目的应该是相当自我解释, 进入这个 route 就实现这个功能.我们可以注意到一个事, 就是这个地方并没有显示的调用 render 渲染生成网页. 这个 controller 会自动的找到相同名字的模板来渲染生成网页. 这个名字是 controller 的子函数后面的字符串. 我这个地方可以使用 render 的方法调用,但是没那么简洁,也不是那么有必要.

运行这个应用(完整的程序见 here,  不要忘记数据库的环境变量!)会启动一个 web 服务器. 这个应用可以运行在 CGI 的环境或 PSGI 的服务, 也可以使用 Mojolicious 的应用服务器built-in servers.

该应用程序应该就能像您期望的那样运行, 但它有一个重大的缺点!任何单个客户端请求都会导致应用程序请求到数据库查询,然后其它的所有客户端必须等待当前这个请求做出响应后服务器才可能处理下一个客户端的请求. 同时,服务器大量其它的资源都处于闲置状态. 这样是低效的, 不是吗?

非阻塞的实现方式

现在, 我可以新写一个非常类似的应用程序, 但其中有几个小的调整,主要是防止数据库调用会阻塞应用程序.

#!/usr/bin/env perl
  
use Mojolicious::Lite;
use Mango;
use Mango::BSON 'bson_oid';
  
helper mango  => sub { state $mango = Mango->new($ENV{PASTEDB}) };
helper pastes => sub { shift->mango->db->collection('pastes') };
  
get '/' => 'submit';
  
post '/' => sub {
  my $self = shift;
  my $title = $self->param('title') || 'Untitled';
  my $content = $self->param('content')
    or return $self->redirect_to('/');
  my $doc = {
    title   => $title,
    content => $content,
  };
  $self->render_later;
  $self->pastes->save($doc, sub {
    my ($coll, $err, $oid) = @_;
    $self->redirect_to( show => id => "$oid" );
  });
};
  
get '/:id' => sub {
  my $self = shift;
  my $id = bson_oid $self->stash('id');
  $self->render_later;
  $self->pastes->find_one({ _id => $id }, sub {
    my ($coll, $err, $doc) = @_;
    return $self->redirect_to('/') if ( $err or not $doc );
    $self->render( show => doc => $doc );
  });
} => 'show';
  
app->start;


这个新的代码只相差几行, 但它们非常的重要! 首先, 在做一个非阻塞的调用之前, 我必须调用 render_later 防止上面讲到的自动渲染; 因为这时候子函数执行完立即渲染会没有数据.数据在回调中,需要显示的调用.

title   => $title,
    content => $content,
  };
-  my $oid = $self->pastes->save($doc);
-  $self->redirect_to( show => id => "$oid" );
+  $self->render_later;
+  $self->pastes->save($doc, sub {
+    my ($coll, $err, $oid) = @_;
+    $self->redirect_to( show => id => "$oid" );
+  });
};


在 POST 处理程序中, 你可以在见到 save 的方法调用, 不过, 在这个地方, 我并不只是传递文档内容, 我是给了一个包含插入数据的子函数的引用(回调). 当客户端提交新的粘贴的内容到服务器, 它会等待, 直到数据库的插入完成, 然后被重定向到查看文档的位置. 不同于上面第一个例子. 这个时候整个服务在等待这个客户端插入时, 服务器还能处理其它客户端的数据库插入的服务.因为子函数早调用完成.可以接着处理其它请求. 只是在这注册了一个回调.回调完成后自动那个用户就处理完了.其它的请求还是能正常的进来.

get '/:id' => sub {
  my $self = shift;
  my $id = bson_oid $self->stash('id');
-  my $doc = $self->pastes->find_one({ _id => $id })
-    or return $self->redirect_to('/');
-  $self->stash( doc => $doc );
+  $self->render_later;
+  $self->pastes->find_one({ _id => $id }, sub {
+    my ($coll, $err, $doc) = @_;
+    return $self->redirect_to('/') if ( $err or not $doc );
+    $self->render( show => doc => $doc );
+  });
} => 'show';


相似的我们也要更改 show 的 controller 的子函数. 我再次, 调用 render_later 进入一个回调,然后将调用查询数据库的逻辑. 这个时候, 服务器将请求的数据库, 用来获得有关文件, 但然后继续为其他客户提供其它请求的服务, 直到数据库响应. 在当响应返回时, 服务器调用回调, 让客户端看到的请求的页面.

有个小缺点, 你必须使用 Mojolicious 原生的 Web 服务器来支持事件. 你没必要担心这个问题, 它们很强大.

Ok this is cool but …

是的, 看起来差别并不大, 但是回报还是不错. 如果你执行你的数据库在同一台主机, 你可能只见到很少或者没有性能增加. 当然, 如果你使用的异地数据库主机象 MongoHQ 之类,还有查询需要时间很长的应用, 你可以通过 wrk 清楚地看到重写成非阻塞应用程序的好处:


$ PASTEDB=mongodb://demo:pass@linus.mongohq.com:10025/MangoTest hypnotoad blocking_paste.pl
$ wrk -t 10 -c 10 -d 1m http://localhost:8080/0
Running 1m test @ http://localhost:8080/0
  10 threads and 10 connections
  Thread Stats   Avg      Stdev     Max   +/- Stdev
    Latency   286.87ms  331.49ms   1.45s    90.25%
    Req/Sec     5.91      4.32    22.00     81.12%
  3745 requests in 1.00m, 2.83MB read
Requests/sec:     62.41
Transfer/sec:     48.32KB
$ PASTEDB=mongodb://demo:pass@linus.mongohq.com:10025/MangoTest hypnotoad -s blocking_paste.pl

… 非阻塞应用:

$ PASTEDB=mongodb://demo:pass@linus.mongohq.com:10025/MangoTest hypnotoad nonblocking_paste.pl
$ wrk -t 10 -c 10 -d 1m http://localhost:8080/0
Running 1m test @ http://localhost:8080/0
  10 threads and 10 connections
  Thread Stats   Avg      Stdev     Max   +/- Stdev
    Latency    59.41ms   18.69ms 365.66ms   97.45%
    Req/Sec    16.73      2.19    21.00     75.56%
  10290 requests in 1.00m, 7.78MB read
Requests/sec:    171.49
Transfer/sec:    132.77KB
$ PASTEDB=mongodb://demo:pass@linus.mongohq.com:10025/MangoTest hypnotoad -s nonblocking_paste.pl


注意, 这个应用只是跑在一个旧电脑和免费数据库中的玩具. 上面表明, 在传输和请求的处理, 有非常明显的改善. 你可以自己试下. 二个程序可以在这找到 here

结论

Mojolicious 是一个非常强大的 Web 开发框架, 使难的事情变得更加容易, 尤其是非阻塞代码. 我这篇文章是使用 Mojolicious 系列的第一个无阻塞的 webapps , 希望有多一般的应用程序来使用 Mojolicious. Happy Perling!

P.S. 看看 Mojo::IOLoop::Delay 有关的 Mojolicious 中关于非阻塞的 tool suite 可以使你写 non-blocking 更加容易.

建议继续学习:

  1. 关于IO的同步,异步,阻塞,非阻塞    (阅读:14300)
  2. perl更新/修改/删除文本文件内容    (阅读:9317)
  3. perl大牛flw传说    (阅读:6445)
  4. perl模块Getopt::Std用法及实例-从命令行读取参数模块    (阅读:5748)
  5. [Perl] Template::Toolkit 模板技术.    (阅读:5313)
  6. 在perl中连接和使用sqlite做数据存储    (阅读:4916)
  7. Perl命令行常见用法及技巧    (阅读:4658)
  8. perl的expect使用方法,实现非交互式登录。    (阅读:4357)
  9. perl模块之MIME::Lite发送有附件的邮件    (阅读:4346)
  10. Perl 倒行分析文件方法。perl读文本文件,从末尾往前读.    (阅读:4310)
QQ技术交流群:445447336,欢迎加入!
扫一扫订阅我的微信号:IT技术博客大学习
© 2009 - 2024 by blogread.cn 微博:@IT技术博客大学习

京ICP备15002552号-1