background

Netty makes it easy to implement HTTP services. A few lines of code and a custom Handler can implement a Web service. This article records the process and problems of using Netty to realize the file download function.

Use new technology, although a lot of available information, but in contact with the generation of new technology understanding stage, no study, always trample pit wow!

ChunkedStream implements file download

ChunkedStream can be used to transfer file streams from Server to Client. There are three key points:

  1. Have to addChunkedWriteHandler;
  2. You need to distinguish Http requests from Https requests. If Https requests are written into the stream, you need to manually add an end identifier. Http automatically adds the end flag;
  3. Write callback functions to make it easy to track the write process.

Here is the code for the file download request, which I found and modified from Netty In Action:

public void downloadFile(ChannelHandlerContext ctx) { HttpResponse response = new DefaultHttpResponse(HttpVersion.HTTP_1_1, HttpResponseStatus.OK); Try {// Set the request header 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()); }}Copy the code

The code is divided into four pieces:

  1. Construct the response header field
  2. Send response headers
  3. Send file body
  4. If the Https service is used, manually send the end flag

The key code is the content after the comment of Write the End Marker. I didn’t understand the code at the beginning, but I knew the importance of this code in the final test.

In our business scenario, a Channel long connection is used to communicate and Https protocol is used to provide services. If the last empty block does not occur, subsequent requests sent by the client after the file download request will never receive a response from the server.

The last empty data body is important!

The role of the last empty data body

If there is no empty data body and the response content is downloaded after sending the file, an exception will be reported if the response continues through the same Channel:Details:

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)
Copy the code

The function of the empty response body can be inferred from the phenomenon:

The last empty response body notifies Netty to jump out of the ChunkedWriteHandler, without which subsequent writes will still be performed.