解決了一個Web網頁顯示不全的BUG

一【BUG描述】

最近開發一個Web系統的過程中遇到了一個詭異的BUG,花了2天時間解決,感覺如釋重負。

這個BUG的現象是這樣的:一個很普通的JSP網頁,本來顯示很正常,後來我在這個html頁面上加了一些控件元素,並修改了下CSS, JavaScript代碼,並加入了些中文字符。本來是覺得沒什麼問題,可是在服務器上跑起來就發現瀏覽器上這個頁面顯示一片空白,同時其他頁面卻顯示正常。另外,我用的Tomcat版本是8.0.17。

那麼我第一反應就是這個頁面代碼哪裏改錯了,可是我仔細看了下覺得又沒什麼問題,然後頁面字符編碼和Content-type都是顯示設置UTF-8編碼。看來前端好像沒什麼問題,然後我就在瀏覽器上查看頁面源碼,結果發現瀏覽器上顯示的HTML源碼居然不完整,html頁面尾部的4~5行代碼全部不見了。這就是導致頁面無法正常顯示的直接原因。

這個無非就兩種可能:瀏覽器數據沒有全部接收,或者服務器數據沒有全部發送。這時,我就用WireShark抓包觀察,結果顯示tomcat服務器在發送數據時最後幾行數據根本就沒有發送過來。而且這兒非常巧合,因爲html數據以chunked的方式發送,最後那幾行數據恰好應該全部在最後的一個chunked包裏面,換句話說,就是最後一個chunked包丟了。

爲啥偏偏這個頁面的最後一個chunked包丟了,而其他頁面好好的呢?

這時沒別的辦法啦,只能一步一步調試JSP頁面代碼咯(這個耗費了我1天多的時間)。通過觀察其他正常顯示的JSP頁面與這個出問題的JSP頁面的執行流程,我發現問題出在:JSP頁面在tomcat中輸出的時候,最後一個chunked數據包在緩存中沒有被flush。

二【定位】

爲啥不flush?這個多種原因造成的:

  1. JSP頁面在通過JspWriter輸出之後,其實還留了點在緩存裏面。以前一般是在JSP執行完,銷燬PageContext的時候將緩存中的數據flush到網絡流上,可現在作者爲了提高網絡性能,只是將數據flush到了較低一層的CoyoteWriter的緩存中,等到整個請求-響應過程結束,再flush到網絡流上。如下圖:
               

  2. 作者想法是不錯,可惜少算了一步。CoyoteWriter對象是通過一個org.apache.catalina.connector.OutputBuffer來完成數據輸出的。這個OutputBuffer對象有個suspended(暫停)標誌位,用來隨時暫停這個輸出對象,處於暫停狀態的OutputBuffer對象是不會進行數據輸出的。tomcat在完成JspWriter輸出的工作後,提前將OutputBuffer的狀態設置爲suspended狀態,這一步的意思防止響應數據輸出之後又輸出其他不該輸出的數據。這個本意是好的,可是等到整個過程結束,關閉OutputBuffer對象的時候,就因爲這個suspended狀態導致close這個函數還沒來得及執行flush就直接退出了。如下圖: 
               

爲啥別的頁面沒有這個情況,偏偏這個Html頁面就出現問題了?這個情況有點複雜:
  1. 當頁面通過OutputBuffer輸出第1個chunked包的數據的時候,這個對象偷偷將這個chunked包的字符數據轉換成二進制數據緩存了起來,並沒有真正輸出到網絡流中。只有等到要輸出第二個chunked包的時候,才flush緩存,將第一個包的數據輸出到網絡中,爲第二個chunked包騰出足夠的緩存空間,並緩存第二個chunked包。
  2. 當OutputBuffer輸出第一個chunked數據包到網絡中的時候,會搶先輸出的Http 響應頭,並將response對象的commited標誌設置爲true(這就表示響應頭已經發送)。
  3. 在ErrorReportValve.invoke函數中,處理完http請求後,會判斷response對象的commited標誌是否爲true,如果不是true,則將OutputBuffer的suspended標誌設置爲false,保證響應數據能夠輸出。
  4. 好了,原因找到了:因爲其他web網頁比較小,導致jsp頁面處理結束了,所有的chunked數據還在緩衝中,完全並沒有被輸出到網絡中(一部分在OutputBuffer緩存中,一部分在JspWriter緩存中),commited標誌此時爲false,等到ErrorReportValve處理時,根據commited標誌,將suspended標誌重置爲false,這樣在最後關閉OutputBuffer的時候,就將所有這些緩存中的數據一個個都flush到了網絡上。相反,我修改的那個網頁比較大,導致OutputBuffer中途就將部分數據flush到了網絡上,是的commited標誌位true,那麼ErrorReportValve也就不會將suspended標誌重置爲false,導致OutputBuffer在關閉時,並不會flush殘留在緩存中的數據。

三【解決】

如何解決這個bug?
  1. 在JSP頁面最後加<% out.flush %>強制刷新;
  2. 升級或者回退Tomcat 服務器版本;
後面的Tomcat(8.0.23)版本是怎麼解決這個bug的?
在StandardHostValve.invoke函數中加了一行代碼,重置OutputBuffer的suspended標誌爲false,這樣就可以保證關閉OutputBuffer時,flush殘留在緩存中的數據了。如下圖:

           



發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章