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>";
}

傳統MVC模式在這裏貌似去除了,一切在服務器端生成,可藉助Freemarker或者靜態頁面輔助,減輕純手工拼接HTML代碼的麻煩。
生成客戶端代碼:
moz-screenshot-3_thumb[1].png (800×441)
在Servlet代碼中,每輸出一段HTML腳本都會迫使當前線程沉睡一定時間,用戶在瀏覽頁面時,按照優先級順序一段一段的輸出,用戶體驗不錯。下面的截圖,可以略微感知一下。
image
話說若使用BigPipe,須分段刷新,則服務器無法獲得最終輸出內容長度,只能採用chunked壓縮編碼格式輸出內容,能節省網絡流量;但不利於SEO,按照Facebook做法就是,若搜索爬蟲訪問,就會按照正常方式生成頁面。另一方面,BigPipe的應用場景適合計算量大、較爲耗時、擁有若干相互獨立模塊的頁面,中小頁面不太適合。
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章