使用 Mojolicious 写非阻塞的应用: Part 2
上次 , 我想你看了用 Mojolicious 实现 写非阻塞的 (web) 应用 .在那个例子中, 每个动作都只执行了单个的非阻塞的动作. 在今天这篇文章中, 我打算讲深入一点.向你介绍 Mojo::IOLoop::Delay ,今天我们的例子会使用这个 delay 对象来等待同时执行的多个非阻塞的操作.直到他们完成操作之前, 在同一个进程内服务器都不会阻断其它请求的处理.
译者注: Mojo::IOLoop::Delay 这个模块是我最喜欢 Mojo 的一个原因, 大量的用到了这种技术, 这个模块做事件同步超级方便. 使用这种技术, 非常实用,例如, 我们一个动作可能要查询多次数据库, 我们可能想都查询出来,才做下一个动作. 我也写过一个这样的文章,可以看 "Mojolicious 之类事件程序的异步和技巧"
The Application
这个应用的演示和全部代码在 这, 通过这个例子, 这个应用的目标是从其它的 websites 站点取得网站标题的列表. 这是一个简单的任务,但非常有用,也能说明更加复杂的一些情况.
#!/usr/bin/env perl useMojolicious::Lite; app->ua->max_redirects(10); my@urls= qw/mojolicio.us mojocasts.com/; helper 'render_dumper'=> sub{ my$self= shift; $self->render( text => $self->dumper( \@_) ); }; # The routes will go here! app->start;
必然,我需要导入 Mojolicious::Lite, 这会自动的导入 strict, warnings
, utf8
和 v5.10
的特性. 每个应用内部其实有默认的Mojo::UserAgent 的 ua 的 helper . 我们会在以后使用它. 接下来的行, 我们重新定义了 redirect 对每个请求最多可以 10 次. 接下来我们定义了一个 @urls 的列表, 是我们接下来要抓取的网站. 在这个例子中, 我其实建议你写更加多的网站地址, 但现在,我们只是一个演示.
我们可以通过 helper 来创建很多有意思的 rendering helper . 在这是一个简单的 web 接口的应用, 在这我们会给全部的参数通过我们自己创建的Data::Dumper 的 helper 来 dump 出来. 当然 ,如果你有兴趣, 也可以给 render_dumper 这个自己的 helper 替换为 $self->render( json =>[...] )
这样会使用原生的 Mojo::JSON 来渲染这个数据结构,也会更加容易.
其它的路由的子函数没列出来, 我们先跳过这些. 现在我们启动我们的应用程序, 注意我们这个例子中没使用任何模板.
如果你存成 titles.pl 你可以这样运行
perl titles.pl get [page]
我们通常使用下面的命令启动一个持久化的应用程序
perl titles.pl daemon
或开发时,你可能会想
morbo titles.pl
如果你改变了程序本身的代码, morbo 它会自动重新加载的应用程序.
One Non-Blocking Request
就象上一个文章中的例子, 我们在这是一个简单的 /one 的路由选择, 这是一个请求其它网站内容然后取得标题的高延时服务. 由于我们在等待这个请求的时候不想阻塞其它的客户端请求, 我们使用的是 Mojolicious 提供的非阻塞风格.
any '/one'=> sub{ my$self= shift; $self->render_later; $self->ua->get($urls[0] => sub{ my($ua, $tx) = @_; $self->render_dumper($tx->res->dom->at('title')->text); }); };
在这, 我们可以见到这个 ua 的会去请求外部的 web 资源. 通过传一个 get 的回调引用代码块, 来让服务器会处理其它接着处理其它的客户端请求, 同时等待这个回调的响应. 一旦响应从服务器上回来后会立即调用回调,并且给页面显现出来提供给客户端. 细心的看代码会发现, 我们是使用的Mojo::DOM 来解析响应的内容并提取标题文字.
如果想实现得更加灵活,可以使用 Mojo::DOM::CSS 提供的 CSS3 选择器. 它非常集成了绑定数组引用的容器类 Mojo::Collection 可以很方便的提取和转换结果. 如果常常用 JQuery 的用户会感觉没有区别, 在这, 我们提取标题用现在的方案就足够了.
通过运行试试吧
perl titles.pl get /one
Two Non-Blocking Requests
一个请求是够简单,但两个这样的请求会变成什么样子呢?
any '/two'=> sub{ my$self= shift; $self->render_later; my@titles; $self->ua->get($urls[0] => sub{ my($ua, $tx) = @_; push@titles, $tx->res->dom->at('title')->text; $self->ua->get($urls[1] => sub{ my($ua, $tx) = @_; push@titles, $tx->res->dom->at('title')->text; $self->render_dumper(@titles); }); }); };
哎呀!这是可以运行, 但是……
当我看看这段代码, 好几个问题立即显现出来.首先, 开始出现大量的箭头代码和多得象地狱一样的回调. 其次 这个不容易扩展到 2 个以上的元素, 当然你可以写一些递归回调之类来帮助你,但仍然没法解决最大的问题.
虽然这个代码不阻塞服务器,但在整个请求的范围内, 每个请求都会阻塞下一个请求! 这很容易看出来, 因为每次只调用一个回调, 直到收到响应, 然后才调用下一个. 因为下一个请求包含在当前回调中. 所以你并不能一开始就给所有的请求发送出去, 然后收集完每个结果就完成.
如果我写的教程代码就这样, 这真是一个糟糕的教程
使用 Mojo::IOLoop::Delay 来管理非阻塞流
正如我介绍中所暗示的, 这外解决方案在于使用 Mojo::IOLoop::Delay. 它可以用于管理并行和顺序的非阻塞流程和对完成采取某些动作. 用 delay 的对象, 上面的例子看起来会变成:
any '/all'=> sub{ my$self= shift; $self->render_later; my$delay= Mojo::IOLoop->delay(sub{ my$delay= shift; my@titles= map{ $_->res->dom->at('title')->text } @_; $self->render_dumper(@titles); }); $delay->steps(sub{ my$delay= shift; $self->ua->get( $_=> $delay->begin ) for@urls; }); };
乍一看, 我们就会发现路由处理的流程非常不同, 在这, 我会引导你了解这的非阻塞处理, 我们首先调用 render_later 防止自动渲染.
然后我们创建一个 delay 的对象. 如果传递一个单独的子函数的引用给这个对象, 会被当成 "on finish" 的处理, 也就是事件完成时回调. 你可以在创建对象时传递这个函数引用, 也可以使用 $delay->on( finish => sub { … } ); 的方式注册一个. 但我喜欢直接传进去.
delay 的回调的处理会给自己做为第一个参数传过去, 然后可能还有其它接下来的参数; 在接下来, 我们会讲到这个. 正如你见到的, @_ 包含着之前的客户端连接的 $tx 对象, 然后我们通过这个提取标题, 然后来显示结果. 但程序是怎么进入到这个 delay 的 on finish 的完成的回调中啦? 注意这是 "on finish" 的处理函数, 在我们看这些传进来的参数这前, 我们先看看这个处理过程.
在设置完 "on finish" 的回调后,我们调用 steps 的方法. 对于我们要完成的事情, 这个单词可能有点让大家混乱. 因为在我们这个应用只, 我们只需要一步("setp"). 每个 setps 对象中的 sub 中的子函数引用是顺序执行的. 任何非阻塞的动作在 setp 的子函数内部是并行执行的. 我们这个例子中只需要并行请求, 所以我们只需要一步 setp.
看那个 'on finish' 的处理函数, 每步 setp 的回调都会调用它, 并给 delay 对象自己做为第一个参数,然后传送其它的的参数过去.
delay 对象大量的语法糖是由 begin 方法提供. 这个方法做二个事情. 首先, 增加一个内部的计数器, 跟踪记录的并行任务数量并等待进入到下一个步骤 setp 或者调用"on finish"完成的处理函数. 其次 begin 会替换原来的回调, 用于给你的非阻塞应用提供回调和回调的结果.(Of course you may wrap that callback in your own subroutine reference if you want to do some other processing, but that is for another post).
回调本身调用时,做了两件事情。首先, 它减少内部计数器的值. 其次, 给存储的参数传给这个回调(虽然默认情况, 第一个参数是对象本身, 你可能并不需要这个). 当下一个步骤(setp)或者完成"on finish"的回调地被调用时, 会得前面步骤的结果的参数列表.
这个列表是前面一个 setp 步骤所有 begin 的替换的回调所传过来的结果, 全部的参数从第二个位置开始, 以此类推, 这个顺序是 begin 调用的顺序. 并不关心执行结果的顺序.
现在你需要知道, 你可以见到 finish 的处理怎么样工作的. 当我们使用 ua->get 的调用的时候, 我们就知道所有结果会通过 begin 回调来调用完成的处理.
所以现在你会知道, @urls 有多少网站地址都不要紧, 这会完全按预期工作, 任何数量的网站抓取都会并行调用. 你可以尝试增加网站地址的数量, 并运行:
perl titles.pl get /all
你可以见到结果.
The Proof in the Pudding
象我上个文章, 我们在对指标做一些比较. 我希望我已经说服你写无阻塞的应用,对于很多客户端的是非常有用的. 这个例子只是为了告诉你, 当只有一个客户端的时候, 当这个几个高延迟的数据需要取时, 也是非常非常有效果的.
当我运行
$ timeperl titles.pl get /two
我看到预期的结果,而花费的时间为
real 0m1.121s user 0m0.222s sys 0m0.012s
相比于同时运行所有的
$ timeperl titles.pl get /all
请求相同的信息,但只需要
real 0m0.670s user 0m0.202s sys 0m0.019s
这个时间差是很容易理解和解释. 从数据显示上来看, 每个网站大约需要 0.5-0.6s 回应. 通过并行执行这些请求, 这个时候总的花费的时候是最慢的响应决定的. 如果服务器对这些请求顺序执行的话, 时间就是加起来的.
如果你需要同时为很多客户端连接提供服务, 同时你的程序是使用的非阻塞的风格并且每个请求响应都会更加快,因为你更好的处理了高延迟的请求. 达到这样才是真正的进展.这也是这个文章希望实现的.
总结
这个例子主要是讲的网页提取, 但现实中, 这种技术对于任何高延迟和事情链都可以用这个处理, 包括数据库的读取写入, 文件系统的读取写入,或者其它的长时间运行的进程. 但你需要这些都需要这些请求有能非阻塞的机制.
在上一个示例中, 我们使用了 Mango 做非阻塞的数据库交互, 在这个例子中, 我们使用了 Mojolicious 原生的非阻塞的 web user agent 来实现 web 请求. 在这个系列以后的文档中, 我会介绍如何做到阻塞的进程如果在无阻塞的框架中的一些思想.
我这展示了如果使用 delay 来实现在运行的非阻塞调用, 现在, 我希望大家给它做为一个练习使用多个 steps 来执行顺序非阻塞并行调用.在未来我们也会讲到.
happy Perling!
P.S. YAPC::Brazil
如果碰巧你会在未来几周内到 YAPC::Brazil 我会在那讲我使用科学建模设计的方案. 不过, 我很乐意和任何人交流有关 Mojolicious 或者其它项目.希望在那里能见到你们 !
建议继续学习:
- 关于IO的同步,异步,阻塞,非阻塞 (阅读:14550)
- perl更新/修改/删除文本文件内容 (阅读:9483)
- perl大牛flw传说 (阅读:6525)
- perl模块Getopt::Std用法及实例-从命令行读取参数模块 (阅读:5940)
- [Perl] Template::Toolkit 模板技术. (阅读:5384)
- 在perl中连接和使用sqlite做数据存储 (阅读:5110)
- Perl命令行常见用法及技巧 (阅读:4868)
- perl模块之MIME::Lite发送有附件的邮件 (阅读:4460)
- perl的expect使用方法,实现非交互式登录。 (阅读:4547)
- Perl 倒行分析文件方法。perl读文本文件,从末尾往前读. (阅读:4497)
扫一扫订阅我的微信号:IT技术博客大学习
- 作者:扶 凯 来源: 扶凯
- 标签: Mojolicious perl 非阻塞
- 发布时间:2014-12-02 23:50:19
- [68] IOS安全–浅谈关于IOS加固的几种方法
- [66] Twitter/微博客的学习摘要
- [64] 如何拿下简短的域名
- [61] android 开发入门
- [60] find命令的一点注意事项
- [59] Go Reflect 性能
- [57] 流程管理与用户研究
- [56] Oracle MTS模式下 进程地址与会话信
- [56] 图书馆的世界纪录
- [55] 读书笔记-壹百度:百度十年千倍的29条法则