說明: 本文涉及的netty源碼都是netty4.1
問題描述
我在將netty整合到我自己依賴注入框架Yao中來作爲web服務提供類似springmvc的能力的時候。先從官方demo開始。 整合Netty官方Demo HttpStaticFileServer
的時候因爲在channel中加入了HttpContentCompressor
導致下載文件不成功。
表現的現象是瀏覽器下載狀態碼是200. 但是文件下載的內容就會失敗,同時瀏覽器控制檯顯示ERR_CONTENT_DECODING_FAILED
, 但是使用https協議的時候竟然可以下載。 並且服務端不會打印任何錯誤信息。
思考
因爲控制檯不打印錯誤,我又沒有注意到瀏覽器控制檯的ERR_CONTENT_DECODING_FAILED
信息,但是官方的demo是正常的,我整合的就會有異常。所以我懷疑是我哪裏加入了錯誤的handler。於是我一個個註釋handler. 先從我自己的handler開始,最後終於確定了元兇HttpContentCompressor
。 我去掉HttpContentCompressor就一切正常。
由此確定是Http協議的壓縮編碼問題,查看請求頭髮現請求頭中Accept-Encoding字段正常傳入值了,但是下載文件的時候壓縮編碼異常了。
此時我搜百度,發現這篇 https://www.sohu.com/a/190735418_684743文章。 終於找到原因了。
原因
HttpContentCompressor繼承了HttpContentEncoder類。
從圖中可以看到這個類只是處理HttpObject和HttpRequest類型的數據的壓縮編碼,而我們的demo中HttpStaticFileServerHandler中io.netty.example.http.file.HttpStaticFileServerHandler#channelRead0
方法,
從圖中可以看到, 它對於http和https協議使用了不同的方式下載文件,
- 當使用https協議的時候往ctx中write的是HttpChunkedInput對象。
- 使用http協議的時候往ctx中write中是DefaultFileRegion對象
正式由於這兩種不同的方式導致http的方式下載文件失敗。在下載文件的時候,response是分段發送的,
- 首先HttpHeader(實現了HttpObject)會先寫入ctx, 到達HttpContentCompressor的時候, 會在響應頭中加上
content-encoding: gzip
。 源碼在: io/netty/example/http/file/HttpStaticFileServerHandler.java:191 - 再次寫入響應體的時候是DefaultFileRegion對象, DefaultFileRegion並沒有繼承HttpObject, 所以經過HttpContentCompressor的時候直接跳過,並不會進行壓縮.
- 最後是LastHttpContent, 這個是空白的內容
收到的響應請求頭中content-encoding: gzip
告訴瀏覽器響應是gzip壓縮的,所以瀏覽器就會以gzip的方式去解壓響應體, 解壓失敗瀏覽器控制檯輸出ERR_CONTENT_DECODING_FAILED
而HttpChunkedInput實現了httpObject接口,所以他結果HttpContentCompressor的時候正常壓縮了,所以沒有問題。
解決辦法
1. 全部使用HttpChunkedInput下載文件
由於gzip壓縮可以有效節約寬帶, 所以我全部使用HttpChunkedInput的方式下載文件。
2. 去掉HttpContentCompressor壓縮編碼
去掉編碼是最快捷的解決方式, 所有的http請求都不進行壓縮
總結
- 通過HttpChunkedInput + HttpContentCompressor,可以實現壓縮文件傳輸。當然,也可以自定義一個FileRegionCompressorHandler,根據客戶端請求的Accept-Encoding,實現對文件內容的壓縮,壓縮之後,調用HttpServerResponseEncoder,對響應內容進行編碼
- DefaultFileRegion實現了零拷貝的方式, 可以高效的傳輸文件
- 排查問題的時候, 不要忽略任何一個信息, 比如瀏覽器控制檯的日誌等, 要跟進源碼查看