Stream a file back
There are times REST server needs to return large content back to client. To avoid to load the full content in memory before returning it back as a whole, you can create a stream object from anonymous class against the JAX RS interface StreamingOut and then register it to Response instance. After that, server will callback the StreamingOut’s write method with the server OutputStream passed in. With the handle of the OutputStream, you can then write content in chunk to the output stream. See the code below that stream a big log file back to the client.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 |
... @GET @Path("{logFile}") @Produces("text/plain") public Response getLogFile(@PathParam("logFile") String logFile) throws URISyntaxException { String logDirPath = System.getProperty(LOG_PATH); try { File f = new File(logDirPath + "/" + logFile); final FileInputStream fStream = new FileInputStream(f); //register stream to Response and it will callback with server OutputStream StreamingOutput stream = new StreamingOutput() { @Override public void write(OutputStream output) throws IOException, WebApplicationException { try { pipe(fStream, output); } catch (Exception e) { throw new WebApplicationException(e); } } }; return Response.ok(stream).build(); } catch (Exception e) { return Response.status(Response.Status.CONFLICT).build(); } } public void pipe(InputStream is, OutputStream os) throws IOException { int n; byte[] buffer = new byte[1024]; while ((n = is.read(buffer)) > -1) { os.write(buffer, 0, n); // Don't allow any extra bytes to creep in, final write } os.close(); } ... |
If you are writing a big list of string as response, you can also wrap the server OutputStream with OutputStreamWriter like below:
1 2 3 4 5 6 7 8 9 10 11 12 |
StreamingOutput stream = new StreamingOutput() { @Override public void write(OutputStream os) throws IOException, WebApplicationException { Writer writer = new BufferedWriter(new OutputStreamWriter(os)); for (String str : largeStrList) { writer.write(str + "\n"); } writer.flush(); } }; |
Behind the scene, the content is sent to the client in chunk with size 16kB as it makes use of the chunked transfer encoding with packet size 16kB. You can confirm this thru tcpdump.
1 2 3 4 |
00:10:27.361521 IP localhost.7474 > localhost.55473: Flags [.], seq 6098196:6114528, ack 179, win 9175, options [nop,nop,TS val 784819663 ecr 784819662], length 16332 00:10:27.362278 IP localhost.7474 > localhost.55473: Flags [.], seq 6147374:6163706, ack 179, win 9175, options [nop,nop,TS val 784819663 ecr 784819663], length 16332 |
Content-Length and Transfer Encoding
When an HTTP client is reading a response message from a server it needs to know when it has reached the end of the message. This is particularly important with persistent (keep alive) connections, because a connection can only be re-used by another HTTP transaction after the response message has been fully received. The following sections describe the four ways in which an HTTP server can indicate the end of the response message:
- Connection Closed by Server – The connection can be closed at the end of the response message by the server, but this prevents connections being re-used.
- Content-Length Header – The length of the content after the response headers can be specified in bytes with the Content-Length header. You can do that if you know the full size of your response before sending it out.
- Implied Content Length – Some types of responses, such as 304, are defined to never have content and therefore the client can assume that the response message is terminated by the double CRLF after the headers.
- Chunked Encoding – The content can be broken up into a number of chunks; each of which is prefixed by its size in bytes. A zero size chunk indicates the end of the response message. If a server is using chunked encoding it must set the Transfer-Encoding header to “chunked”. Chunked encoding is useful when a large amount of data is being returned to the client and the total size of the response may not be known until the request has been fully processed. An example of this is generating an HTML table of results from a database query. If you wanted to use the Content-Length header you would have to buffer the whole result set before calculating the total content size. However, with chunked encoding you could just write the data one row at a time and write a zero sized chunk when the end of the query was reached.
Examples
Regular HTTP Response
1 2 3 4 5 6 7 8 |
HTTP/1.1 200 OK Date: Mon, 22 Mar 2004 11:15:03 GMT Content-Type: text/html Content-Length: 129 Expires: Sat, 27 Mar 2004 21:12:00 GMT <html><body><p>The file you requested is 3,400 bytes long and was last modified: Sat, 20 Mar 2004 21:12:00 GMT.</p></body></html> |
HTTP Response using chunked transfer encoding
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 |
HTTP/1.1 200 OK Date: Mon, 22 Mar 2004 11:15:03 GMT Content-Type: text/html Transfer-Encoding: chunked Trailer: Expires 29 <html><body><p>The file you requested is 5 3,400 23 bytes long and was last modified: 1d Sat, 20 Mar 2004 21:12:00 GMT 13 .</p></body></html> 0 Expires: Sat, 27 Mar 2004 21:12:00 GMT |
So What’s The Benefit Of Streaming?
Streaming doesn’t cut latency, neither it cuts the time a dynamic response needs to be generated. But since the application sends content right away instead of waiting for the whole response to be rendered, the client is able to request assets sooner. In particular, if you flush the head of an HTML document CSS and JavaScript files are going to be fetched in parallel, while the server works on generating content. The consequence is that pages load faster.