Netty ChunkedStream 实现文件下载的流程及踩坑记录

Posted 毕小宝

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了Netty ChunkedStream 实现文件下载的流程及踩坑记录相关的知识,希望对你有一定的参考价值。

背景

Netty 实现 http 服务很方便,几行代码、一个自定义 Handler 就能实现一个 Web 服务。本文记录使用 Netty 实现文件下载功能的流程和问题。

使用新技术,虽然可用资料一大堆,但处于接触新技术的生成理解阶段,没有细究,总会踩坑哇!

ChunkedStream 实现文件下载

使用 ChunkedStream 可以实现 Server 向 Client 传输文件流的功能,重点有三项:

  1. 必须加上 ChunkedWriteHandler
  2. 需要区分 Http 和 Https 请求,如果是 Https 请求,写入流内容后,需要手动添加一个结束标识;Http 会自动添加结束标识的;
  3. 编写回调函数,便于跟踪写入过程。

下面是一段可用的文件下载请求的代码,笔者是从 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());
    }
}

代码分为四块:

  1. 构造响应头域
  2. 发响应头
  3. 发文件正文
  4. 如果是 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 实现文件下载的流程及踩坑记录的主要内容,如果未能解决你的问题,请参考以下文章

Netty 之 FileRegion 文件传输

零拷贝在Netty中的实现(下)

Netty实现类似Http File Server文件传输功能

Netty 实现文件上传的过程及内存泄露问题排查

厉害了,Netty 轻松实现文件上传!

Netty学习(源码分析)