Servlet 3.0笔记之异步请求Facebook BigPipe简单模型实现

http://www.blogjava.net/yongboy/archive/2011/02/22/346196.html

Servlet 3.0笔记之异步请求Facebook BigPipe简单模型实现

当前的前端技术明星为Facebook,相继抛出了Quickling,BigPipe等新鲜概念,引领着前端优化的潮流。果然具有大公司的范儿,适时的回馈给整个开发社群,让全体朝前前进了一小步。超赞!
抽空学习了BigPipe,大概相当于分段刷新(flush),强制输出。虽有点旧瓶装新酒的味道,不过前端已经进入了细节为王的阶段,或许在国内已经有人在用,但缺乏分享沟通,或者还不成熟,缺乏关注,缺少重要数据用以论证等。
BigPipe:高性能的“流水线技术”网页,或许可以为需要认知的人们打开一扇窗户。
原文介绍BigPiple:
BigPipe是一个重新设计的基础动态网页服务体系。大体思路是,分解网页成叫做Pagelets的小块,然后通过Web服务器和浏览器建立管道并管理他们在不同阶段的运行。这是类似于大多数现代微处理器的流水线执行过程:多重指令管线通过不同的处理器执行单元,以达到性能的最佳。虽然BigPipe是对现有的服务网络基础过程的重新设计,但它却不需要改变现有的网络浏览器或服务器,它完全使用PHP和JavaScript来实现。
BigPipe在Facebook应用中的工作流程:
1. 请求解析:Web服务器解析和完整性检查的HTTP请求。
2. 数据获取:Web服务器从存储层获取数据。
3. 标记生成:Web服务器生成的响应的HTML标记。
4. 网络传输:响应从Web服务器传送到浏览器。
5. CSS的下载:浏览器下载网页的CSS的要求。
6. DOM树结构和CSS样式:浏览器构造的DOM文档树,然后应用它的CSS规则。
7. JavaScript中下载:浏览器下载网页中JavaScript引用的资源。
8. JavaScript执行:浏览器的网页执行JavaScript代码。
其带来效果较为明显处在于:
这种高度并行系统的最终结果是,多个Pagelets的不同执行阶段同时进行。例如,浏览器可以正在下载三个Pagelets CSS的资源,同时已经显示另一Pagelet内容,与此同时,服务器也在生成新的Pagelet。从用户的角度来看,页面是逐步呈现的。最开始的网页内容会更快的显示,这大大减少了用户的对页面延时的感知。如果您要自己亲眼看到区别,你可以尝试以下连接:  传统模式BigPipe。第一个链接是传统模式单一模式显示页面。第二个链接是BigPipe管道模式的页面。如果您的浏览器版本比较老,网速也很慢,浏览器缓存不佳,哪么两页之间的加截时间差别将更加明显。
值得一提的是BigPipe是从微处理器的流水线中得到启发。然而,他们的流水线过程之间存在一些差异。例如,虽然大多数阶段BigPipe只能操作一次Pagelet,但有时多个Pagelets的CSS和JavaScript下载却可以同时运作,这类似于超标量微处理器。BigPipe另一个重要区别是,我们实现了从并行编程引入的“障碍”概念,所有的Pagelets要完成一个特定阶段,如多个Pagelet显示区,它们都可以进行进一步JavaScript下载和执行。
启用了BigPipe后,服务器端优先输出页面整体布局,浏览器渲染布局;服务器按照串行方式,一段段按优先级顺序,输出片段内容到浏览器端,浏览器执行JS函数,同时可能会同时请求新的JS文件(尽可能不要涉及外部JS),下载特定样式文件(这个时候可以产生并发连接)。浏览器渲染页面单个组件(Pagelet),组件同时加载CSS、JS文件时,不会影响到下一个组件的渲染工作。以往的页面模型在于,浏览器接收所有数据,然后一次性渲染,显示最终结果给客户。
BigPipe涉及到的一些问题,原文没有涉及,来自淘宝的李牧在他的博客中 (Facebook让网站速度提升一倍的BigPipe技术分析)提出一些疑问:
脚本阻滞:
         我们知道直接在html中引入外部脚本会造成页面阻滞,即使使用无阻脚本下载的一系列方法引入外部js,但因为JS单线程,当这些脚本load进来之后运行时也会发生阻滞.因为Facebook页面多样,依赖脚本大小不一,这些阻滞会对页面的快速展现造成影响.
        Facebook做法是在ondomready之前只在头部输出一个很小的外部脚本,作为bigpipe的支撑.其余所有模块如果依赖外部脚本(比如某一模块需要日历控件支持),都会在domready事件之后加载.这样做即保证了所有页面所有模块都能在domready前快速形成展现,又可以保证无脚本阻滞的快速的domready时间.
        最快可交互时间:
         domready再快也至少是在页面第一个使用bigpipe输出的chunked的http请求完成之后,对于Facebook来说,这很可能是2秒之后了.那在这2s期间,如果只是页面展现了而不能交互(点击日历无反应),那方案依然不够完美.
         Facebook的做法是,在domready后所依赖脚本已被加载之前,点击行为将会生成一个额外的脚本请求,只将这个点击所依赖的脚步预先load进来.这样保证了最快的可交互时间.Facebook在另一篇文章中对这个技术进行了详细的描述.
         Bigpipe原理简单,实现不复杂,但Facebook却用了1年的时间才形成完备的解决方案.生成方案需要时间,而解决随之而来的脚本阻滞,保障最快交互时间等等问题也会消耗大量时间. 
较为全面的了解了BigPipe,下面使用使用JAVA Servlet 3.0 异步特性,简单模拟BigPipe实现,没有涉及到页面组件单独请求的JS、CSS文件等,仅仅用以演示其过程(最终效果大打折扣)。
我们目标页面大致结构如下,在  The 1KB CSS Grid 生成结构上修改。  moz-screenshot-2
异步Servlet代码:
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149
          
          
/**
* 模拟Facebook BigPipe异步展现页面
* @author yongboy
* @date 2011-2-21
* @version 1.0
*/
@WebServlet ( urlPatterns = "/bigPipeDemoServlet" , asyncSupported = true )
public class BigPipeDemoServlet extends HttpServlet {
private static final long serialVersionUID = 14526556595656565L ;
private static final Log log = LogFactory . getLog ( BigPipeDemoServlet . class );
private static final Executor executor ;
static {
executor = Executors . newFixedThreadPool ( 500 , new ThreadFactory () {
public Thread newThread ( Runnable r ) {
Thread thread = new Thread ( r );
thread . setName ( "BigPipe Thread " + thread . getId ());
thread . setPriority ( Thread . MAX_PRIORITY );
 
return thread ;
}
});
}
 
protected void doGet ( HttpServletRequest request ,
HttpServletResponse response ) throws ServletException , IOException {
response . setHeader ( "Connection" , "Keep-Alive" );
response . setContentType ( "text/html;charset=UTF-8" );
response . setCharacterEncoding ( "UTF-8" );
 
PrintWriter out = response . getWriter ();
 
out . println ( docType );
out . println ( headPart );
out . println ( bodyPart );
 
out . flush ();
 
// 这里模拟把页面组件部分计算工作交由异步线程完成
executor . execute ( new AsyncRunnable ( request . startAsync ( request , response )));
}
 
/**
* 定义工作线程用于处理异步的内容输出
* @author yongboy
* @date 2011-2-21
* @version 1.0
*/
private static class AsyncRunnable implements Runnable {
private final AsyncContext asyncContext ;
private int times = 1 ;
 
public AsyncRunnable ( final AsyncContext asyncContext ) {
this . asyncContext = asyncContext ;
}
 
public void run () {
try {
// 设置超时为20s,系统默认超时时间为10s
asyncContext . setTimeout ( 20L * 1000L );
PrintWriter out = asyncContext . getResponse (). getWriter ();
// 模拟按照页面组件重要程度按照顺序计算
bussizeMethod ( out , "header" , "这里是主页LOGO区域" );
bussizeMethod ( out , "left" , genStrings ( "这里是内容文字" , 160 , false ));
bussizeMethod ( out , "right" , genStrings ( "肯德基在百事可乐杯子添加其他品牌可乐" , 41 , true ));
bussizeMethod ( out , "leftLeft" , genStrings ( "肯德基在百事可乐杯子添加其他品牌可乐" , 10 , true ));
bussizeMethod ( out , "leftRight" , genStrings ( "肯德基在百事可乐杯子添加其他品牌可乐" , 10 , true ));
 
// 补齐页面标签
outFinish ( out );
} catch ( IOException e ) {
e . printStackTrace ();
}
// 结束当前异步请求
try {
asyncContext . complete ();
} catch ( Exception e ) {
// 可能因为出现超时(大于默认的超时时间10s)异常
e . printStackTrace ();
}
}
private void bussizeMethod ( PrintWriter writer , String id , String content ){
//模拟耗时
try {
Thread . sleep ( 1000 * ( times ++));
} catch ( InterruptedException e ) {
e . printStackTrace ();
}
pagelet ( writer , id , content );
}
 
private void pagelet ( PrintWriter writer , String id , String content ) {
if ( writer . checkError ())
return ;
writer . write ( "<script>" + "show(\"" + id + "\", \"" + content
+ "\");" + "</script>\n" );
writer . flush ();
}
private void outFinish ( PrintWriter writer ){
if ( writer . checkError ())
return ;
writer . println ( "</body>" );
writer . println ( "</html>" );
writer . flush ();
writer . close ();
}
private String genStrings ( String ori , int num , boolean line ){
if ( num < 1 ) return ori ;
StringBuilder sb = new StringBuilder ();
for ( int i = 0 ; i < num ; i ++){
sb . append ( ori );
if ( line ){
sb . append ( "<br/>" );
}
}
return sb . toString ();
}
}
private static final String docType = "<!DOCTYPE html PUBLIC \"-//W3C//DTD XHTML 1.0 Transitional//EN\" \"http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd\">" ;
private static final String headPart = "<html xmlns=\"http://www.w3.org/1999/xhtml\">\n<head>\n"
+ "<meta http-equiv=\"Content-Type\" content=\"text/html; charset=UTF-8\"/>\n"
+ "<title>Bigpipe Demo</title>\n"
+ "<link href=\"css/grid.css\" type=\"text/css\" rel=\"stylesheet\" media=\"screen\"/>\n"
+ "<script type=\"text/javascript\">function show(id,text) { var b=document.getElementById(id); b.innerHTML = text; }</script>\n"
+ "</head>" ;
private static final String bodyPart = "<body>\n"
+ "<div class='row'>\n"
+ "<div class='column grid_12'><div class='container' id='header'>loading ...</div></div>\n"
+ "</div>\n"
+ "<div class='row'>\n"
+ " <div class='column grid_8'><div class='container' id='left'>loading ...</div>\n"
+ " <div class='row'>\n"
+ " <div class='column grid_4'><div class='container' id='leftLeft'>loading ...</div></div>\n"
+ " <div class='column grid_4'><div class='container' id='leftRight'>loading ...</div></div>\n"
+ " </div>\n"
+ " </div>\n"
+ " <div class='column grid_4'><div class='container' id='right'>loading ...</div></div>\n"
+ "</div>" ;
}
view raw BigPipeDemoServlet.java hosted with ❤ by  GitHub

传统MVC模式在这里貌似去除了,一切在服务器端生成,可借助Freemarker或者静态页面辅助,减轻纯手工拼接HTML代码的麻烦。
生成客户端代码:
moz-screenshot-3_thumb[1].png (800×441)
在Servlet代码中,每输出一段HTML脚本都会迫使当前线程沉睡一定时间,用户在浏览页面时,按照优先级顺序一段一段的输出,用户体验不错。下面的截图,可以略微感知一下。
image
话说若使用BigPipe,须分段刷新,则服务器无法获得最终输出内容长度,只能采用chunked压缩编码格式输出内容,能节省网络流量;但不利于SEO,按照Facebook做法就是,若搜索爬虫访问,就会按照正常方式生成页面。另一方面,BigPipe的应用场景适合计算量大、较为耗时、拥有若干相互独立模块的页面,中小页面不太适合。
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值