说明: 本文涉及的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实现了零拷贝的方式, 可以高效的传输文件
- 排查问题的时候, 不要忽略任何一个信息, 比如浏览器控制台的日志等, 要跟进源码查看