Netty ChunkedStream 实现文件下载的流程及踩坑记录
Posted 毕小宝
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了Netty ChunkedStream 实现文件下载的流程及踩坑记录相关的知识,希望对你有一定的参考价值。
背景
Netty 实现 http 服务很方便,几行代码、一个自定义 Handler 就能实现一个 Web 服务。本文记录使用 Netty 实现文件下载功能的流程和问题。
使用新技术,虽然可用资料一大堆,但处于接触新技术的生成理解阶段,没有细究,总会踩坑哇!
ChunkedStream 实现文件下载
使用 ChunkedStream
可以实现 Server 向 Client 传输文件流的功能,重点有三项:
- 必须加上
ChunkedWriteHandler
; - 需要区分 Http 和 Https 请求,如果是 Https 请求,写入流内容后,需要手动添加一个结束标识;Http 会自动添加结束标识的;
- 编写回调函数,便于跟踪写入过程。
下面是一段可用的文件下载请求的代码,笔者是从 Netty In Action 里面找到并改造的可以用代码:
public void downloadFile(ChannelHandlerContext ctx) {
HttpResponse response = new DefaultHttpResponse(HttpVersion.HTTP_1_1, HttpResponseStatus.OK);
try {
//设置请求头部
InputStream in = new ByteArrayInputStream("Hello world".getBytes());
long fileLength = in.available();
Calendar time = new GregorianCalendar();
SimpleDateFormat dateFormatter = new SimpleDateFormat("yyyy-MM-dd HH:MM:ss");
response.headers().set(HttpHeaderNames.DATE, dateFormatter.format(time.getTime()));
response.headers().set(HttpHeaderNames.CONTENT_LENGTH, fileLength);
response.headers().set(HttpHeaderNames.ACCEPT_ENCODING, "gzip, deflate, br");
response.headers().set(HttpHeaderNames.CONTENT_TYPE, "application/octet-stream; charset=UTF-8");
response.headers().add(HttpHeaderNames.CONTENT_DISPOSITION,
"attachment; filename=\\"" + URLEncoder.encode("TestFile", "UTF-8") + "\\";");
// 先发送头部
ctx.channel().write(response);
// 发送文件内容
ChannelFuture sendFileFuture = null;
ChannelFuture lastContentFuture;
if (ctx.pipeline().get(SslHandler.class) != null) {
sendFileFuture =
ctx.channel().writeAndFlush(ctx.channel().writeAndFlush(new ChunkedStream(in)), ctx.channel().newProgressivePromise());
// Write the end marker.
lastContentFuture = ctx.writeAndFlush(LastHttpContent.EMPTY_LAST_CONTENT);
} else {
sendFileFuture =
ctx.channel().writeAndFlush(ctx.channel().writeAndFlush(new ChunkedStream(in)), ctx.channel().newProgressivePromise());
// HttpChunkedInput will write the end marker (LastHttpContent) for us.
lastContentFuture = sendFileFuture;
}
sendFileFuture.addListener(new ChannelProgressiveFutureListener() {
@Override
public void operationProgressed(ChannelProgressiveFuture future, long progress, long total) {
if (total < 0) { // total unknown
System.err.println(future.channel() + " Transfer progress: " + progress);
} else {
System.err.println(future.channel() + " Transfer progress: " + progress + " / " + total);
}
}
@Override
public void operationComplete(ChannelProgressiveFuture future) {
System.err.println(future.channel() + " Transfer complete.");
}
});
} catch (UnsupportedEncodingException e) {
System.err.println(e.getCause());
} catch (IOException e) {
System.err.println(e.getCause());
}
}
代码分为四块:
- 构造响应头域
- 发响应头
- 发文件正文
- 如果是 Https 服务,手动发结束标识
关键代码在 Write the end marker
这段注释后面的内容,当初没搞明白代码,最后测试的时候知道了这段代码的重要性了。
在我们的业务场景中,通过一个 Channel 长连接进行通信,采用 Https 协议提供服务。测试的时候发现:如果不发生最后一个空数据块,那么这个文件下载请求过后,客户端后续发送的请求,再也没办法收到服务端的响应了。
最后一个空数据体很重要!
最后一个空数据体的作用
如果没有这个空数据体,发送完文件下载响应内容后,如果继续通过同一个 Channel,再发普通响应时,就会报异常:
详细信息:
io.netty.handler.codec.EncoderException: java.lang.IllegalStateException: unexpected message type: DefaultFullHttpResponse, state: 1
at io.netty.handler.codec.MessageToMessageEncoder.write(MessageToMessageEncoder.java:107)
at io.netty.channel.AbstractChannelHandlerContext.invokeWrite0(AbstractChannelHandlerContext.java:716)
at io.netty.channel.AbstractChannelHandlerContext.invokeWrite(AbstractChannelHandlerContext.java:708)
at io.netty.channel.AbstractChannelHandlerContext.write(AbstractChannelHandlerContext.java:791)
at io.netty.channel.AbstractChannelHandlerContext.write(AbstractChannelHandlerContext.java:701)
at io.netty.handler.stream.ChunkedWriteHandler.doFlush(ChunkedWriteHandler.java:334)
at io.netty.handler.stream.ChunkedWriteHandler.flush(ChunkedWriteHandler.java:135)
从现象来推测这个空响应体的作用:
最后一个空响应体是通知 Netty 跳出 ChunkedWriteHandler
的,没有它,后续的写入操作依然是在这个处理器中进行的。
以上是关于Netty ChunkedStream 实现文件下载的流程及踩坑记录的主要内容,如果未能解决你的问题,请参考以下文章