Changing Netty 4 HTTP File Server example to use C

2019-05-29 05:11发布

问题:

I'm trying to wrap my head around Netty 4 way of implementing a HTTP Server that serve HttpResponses bodies using chunked transfer-encoding when total data size is unknown.

As a starting point, I simply changed the HttpStaticFileServerHandler (found in https://github.com/netty/netty/tree/netty-4.0.0.CR1/example/src/main/java/io/netty/example/http/file) to use a ChunkedStream instead of a ChunkedFile (they both are ChunkedByteInputs).

I understand that it is not ideal in the original example use case to use a FileInputStream but I think it makes a good example reusing already known code.

So, here is the diff against the HttpStaticFileServerHandler class from the io.netty.example.http.file package (vs. 4.0.0.CR1):

diff --git a/example/src/main/java/io/netty/example/http/file/HttpStaticFileServerHandler.java b/example/src/main/java/io/netty/example/http/file/HttpStaticFileServerHandler.java
index 904579b..0d3592f 100644
--- a/example/src/main/java/io/netty/example/http/file/HttpStaticFileServerHandler.java
+++ b/example/src/main/java/io/netty/example/http/file/HttpStaticFileServerHandler.java
@@ -27,13 +27,14 @@ import io.netty.handler.codec.http.FullHttpResponse;
 import io.netty.handler.codec.http.HttpHeaders;
 import io.netty.handler.codec.http.HttpResponse;
 import io.netty.handler.codec.http.HttpResponseStatus;
-import io.netty.handler.stream.ChunkedFile;
+import io.netty.handler.stream.ChunkedStream;
 import io.netty.util.CharsetUtil;

 import javax.activation.MimetypesFileTypeMap;
 import java.io.File;
+import java.io.FileInputStream;
 import java.io.FileNotFoundException;
-import java.io.RandomAccessFile;
+import java.io.InputStream;
 import java.io.UnsupportedEncodingException;
 import java.net.URLDecoder;
 import java.text.SimpleDateFormat;
@@ -159,17 +160,15 @@ public class HttpStaticFileServerHandler extends ChannelInboundMessageHandlerAda
             }
         }

-        RandomAccessFile raf;
+        InputStream raf; // Use an InputStream instead of a RandomAccessFile
         try {
-            raf = new RandomAccessFile(file, "r");
+            raf = new FileInputStream(file);
         } catch (FileNotFoundException fnfe) {
             sendError(ctx, NOT_FOUND);
             return;
         }
-        long fileLength = raf.length();

         HttpResponse response = new DefaultHttpResponse(HTTP_1_1, OK);
-        setContentLength(response, fileLength);
         setContentTypeHeader(response, file);
         setDateAndCacheHeaders(response, file);
         if (isKeepAlive(request)) {
@@ -180,7 +179,7 @@ public class HttpStaticFileServerHandler extends ChannelInboundMessageHandlerAda
         ctx.write(response);

         // Write the content.
-        ChannelFuture writeFuture = ctx.write(new ChunkedFile(raf, 0, fileLength, 8192));
+        ChannelFuture writeFuture = ctx.write(new ChunkedStream(raf)); // Use a ChunkedStream instead of a ChunkedFile

         // Decide whether to close the connection or not.
         if (!isKeepAlive(request)) {

And here the complete changed file: https://gist.github.com/eskatos/5311587

Changes are minimal: use a FileInputStream instead of RandomAccessFile and ChunkedStream instead of ChunkedFile. The pipeline is untouched.

To reproduce, simply apply the changes to the Netty example, run it and try do download any file.

After this change, directory listing obviously works because reponses are not chunked but file downloads don't. The client download the file but never finish the download, hold the connection and wait forever. I've tried several from browsers to curl, wget etc.. I've also tried to add a ByteLoggingHandler to the pipeline and I can see an empty trailing chunk so I don't understand why the browser still wait for data.

Any clue?

回答1:

In order to terminate a chunked transfer of unkown size (Content-Length not known and thus not specified) you simply send an empty chunk as last chunk. This permits to keep the connection open in order to support keepalives:

        ctx.write(new ChunkedInputAdapter(new ChunkedStream(raf, 8192)));
        ChannelFuture writeFuture = ctx.writeAndFlush(LastHttpContent.EMPTY_LAST_CONTENT);

Here is a complete example: https://github.com/scireum/sirius/blob/develop/web/src/sirius/web/http/Response.java#L649



回答2:

If you do not specify Content-Length header, the client has no idea about the length of the content you are sending, and thus it waits until the server closes the connection, and everything received until the disconnection is considered as the content.

Therefore, you must do one of the following:

  1. Add Content-Length header
  2. Close the connection after sending the content
  3. Send the content using chunked encoding with the Transfer-Encoding: chunked header