The following example demonstrates the flexibility of the http module by using the module for both the client and the server. To begin, we'll start with the the client:
1 /** 2 * Simple asynchronous HTTP client implementation, which downloads HTTP resource 3 * and saves its content in a local file. 4 * 5 * @author Alexey Stashok 6 */ 7 public class Client { 8 private static final Logger logger = Grizzly.logger(Client.class); 9 10 public static void main(String[] args) throws IOException, URISyntaxException { 11 // Check command line parameters 12 if (args.length < 1) { 13 System.out.println("To download the resource, please run: Client <url>"); 14 System.exit(0); 15 } 16 17 final String url = args[0]; 18 19 // Parse passed URL 20 final URI uri = new URI(url); 21 final String host = uri.getHost(); 22 final int port = uri.getPort() > 0 ? uri.getPort() : 80; 23 24 final FutureImpl<String> completeFuture = SafeFutureImpl.create(); 25 26 // Build HTTP client filter chain 27 FilterChainBuilder clientFilterChainBuilder = FilterChainBuilder.stateless(); 28 // Add transport filter 29 clientFilterChainBuilder.add(new TransportFilter()); 30 // Add IdleTimeoutFilter, which will close connections, which stay 31 // idle longer than 10 seconds. 32 clientFilterChainBuilder.add(new IdleTimeoutFilter(10, TimeUnit.SECONDS)); 33 // Add HttpClientFilter, which transforms Buffer <-> HttpContent 34 clientFilterChainBuilder.add(new HttpClientFilter()); 35 // Add HTTP client download filter, which is responsible for downloading 36 // HTTP resource asynchronously 37 clientFilterChainBuilder.add(new ClientDownloadFilter(uri, completeFuture)); 38 39 // Initialize Transport 40 final TCPNIOTransport transport = 41 TCPNIOTransportBuilder.newInstance().build(); 42 // Set filterchain as a Transport Processor 43 transport.setProcessor(clientFilterChainBuilder.build()); 44 45 try { 46 // start the transport 47 transport.start(); 48 49 Connection connection = null; 50 51 // Connecting to a remote Web server 52 Future<Connection> connectFuture = transport.connect(host, port); 53 try { 54 // Wait until the client connect operation will be completed 55 // Once connection will be established - downloading will 56 // start @ ClientDownloadFilter.onConnect(...) 57 connection = connectFuture.get(10, TimeUnit.SECONDS); 58 // Wait until download will be completed 59 String filename = completeFuture.get(); 60 logger.log(Level.INFO, "File " + filename + " was successfully downloaded"); 61 } catch (Exception e) { 62 if (connection == null) { 63 logger.log(Level.WARNING, "Can not connect to the target resource"); 64 } else { 65 logger.log(Level.WARNING, "Error downloading the resource"); 66 } 67 } finally { 68 // Close the client connection 69 if (connection != null) { 70 connection.close(); 71 } 72 } 73 } finally { 74 logger.info("Stopping transport..."); 75 // stop the transport 76 transport.stop(); 77 78 logger.info("Stopped transport..."); 79 } 80 } 81 }
The documentation within the example above should be sufficient to get an understanding of what is going on within the client. However, the ClientDownloadFilter, added at line 37 warrants a look:
1 /** 2 * HTTP client download filter. 3 * This Filter is responsible for asynchronous downloading of a HTTP resource and 4 * saving its content in a local file. 5 * 6 * @author Alexey Stashok 7 */ 8 public class ClientDownloadFilter extends BaseFilter { 9 private final static Logger logger = Grizzly.logger(ClientDownloadFilter.class); 10 11 // URI of a remote resource 12 private final URI uri; 13 // local filename, where content will be saved 14 private final String fileName; 15 16 // Download completion future 17 private FutureImpl<String> completeFuture; 18 19 // local file channel, where we save resource content 20 private volatile FileChannel output; 21 // number of bytes downloaded 22 private volatile int bytesDownloaded; 23 24 private final String resourcePath; 25 26 /** 27 * <tt>ClientDownloadFilter</tt> constructor 28 * 29 * @param uri {@link URI} of a remote resource to download 30 * @param completeFuture download completion handler ({@link FutureImpl}) 31 */ 32 public ClientDownloadFilter(URI uri, FutureImpl<String> completeFuture) { 33 this.uri = uri; 34 35 // Extracting resource path 36 resourcePath = 37 uri.getPath().trim().length() > 0 ? uri.getPath().trim() : "/"; 38 39 int lastSlashIdx = resourcePath.lastIndexOf('/'); 40 if (lastSlashIdx != -1 && lastSlashIdx < resourcePath.length() - 1) { 41 // if the path contains a filename - take it as local filename 42 fileName = resourcePath.substring(lastSlashIdx + 1); 43 } else { 44 // if the path doesn't contain filename - we will use default filename 45 fileName = "download#" + System.currentTimeMillis() + ".txt"; 46 } 47 48 this.completeFuture = completeFuture; 49 } 50 51 /** 52 * The method is called, when a client connection gets connected to a web 53 * server. 54 * When this method gets called by a framework - it means that client connection 55 * has been established and we can send HTTP request to the web server. 56 * 57 * @param ctx Client connect processing context 58 * 59 * @return {@link NextAction} 60 * @throws IOException 61 */ 62 @Override 63 public NextAction handleConnect(FilterChainContext ctx) throws IOException { 64 // Build the HttpRequestPacket, which will be sent to a server 65 // We construct HTTP request version 1.1 and specifying the URL of the 66 // resource we want to download 67 final HttpRequestPacket httpRequest = HttpRequestPacket.builder().method("GET") 68 .uri(resourcePath).protocol(Protocol.HTTP_1_1) 69 .header("Host", uri.getHost()).build(); 70 logger.log(Level.INFO, "Connected... Sending the request: {0}", httpRequest); 71 72 // Write the request asynchronously 73 ctx.write(httpRequest); 74 75 // Return the stop action, which means we don't expect next filter to process 76 // connect event 77 return ctx.getStopAction(); 78 } 79 80 /** 81 * The method is called, when we receive a {@link HttpContent} from a server. 82 * Once we receive one - we save the content chunk to a local file. 83 * 84 * @param ctx Request processing context 85 * 86 * @return {@link NextAction} 87 * @throws IOException 88 */ 89 @Override 90 public NextAction handleRead(FilterChainContext ctx) throws IOException { 91 try { 92 // Cast message to a HttpContent 93 final HttpContent httpContent = (HttpContent) ctx.getMessage(); 94 95 logger.log(Level.FINE, "Got HTTP response chunk"); 96 if (output == null) { 97 // If local file wasn't created - create it 98 logger.log(Level.INFO, "HTTP response: {0}", httpContent.getHttpHeader()); 99 logger.log(Level.FINE, "Create a file: {0}", fileName); 100 FileOutputStream fos = new FileOutputStream(fileName); 101 output = fos.getChannel(); 102 } 103 104 // Get HttpContent's Buffer 105 final Buffer buffer = httpContent.getContent(); 106 107 logger.log(Level.FINE, "HTTP content size: {0}", buffer.remaining()); 108 if (buffer.remaining() > 0) { 109 bytesDownloaded += buffer.remaining(); 110 111 // save Buffer to a local file, represented by FileChannel 112 ByteBuffer byteBuffer = buffer.toByteBuffer(); 113 do { 114 output.write(byteBuffer); 115 } while (byteBuffer.hasRemaining()); 116 117 // Dispose a content buffer 118 buffer.dispose(); 119 } 120 121 if (httpContent.isLast()) { 122 // it's last HttpContent - we close the local file and 123 // notify about download completion 124 logger.log(Level.FINE, "Downloaded done: {0} bytes", bytesDownloaded); 125 completeFuture.result(fileName); 126 close(); 127 } 128 } catch (IOException e) { 129 close(); 130 } 131 132 // Return stop action, which means we don't expect next filter to process 133 // read event 134 return ctx.getStopAction(); 135 } 136 137 /** 138 * The method is called, when the client connection will get closed. 139 * Intercepting this method let's use release resources, like local FileChannel, 140 * if it wasn't released before. 141 * 142 * @param ctx Request processing context 143 * 144 * @return {@link NextAction} 145 * @throws IOException 146 */ 147 @Override 148 public NextAction handleClose(FilterChainContext ctx) throws IOException { 149 close(); 150 return ctx.getStopAction(); 151 } 152 153 /** 154 * Method closes the local file channel, and if download wasn't completed - 155 * notify {@link FutureImpl} about download failure. 156 * 157 * @throws IOException If failed to close <em>localOutput</em>. 158 */ 159 private void close() throws IOException { 160 final FileChannel localOutput = this.output; 161 // close the local file channel 162 if (localOutput != null) { 163 localOutput.close(); 164 } 165 166 if (!completeFuture.isDone()) { 167 //noinspection ThrowableInstanceNeverThrown 168 completeFuture.failure(new IOException("Connection was closed")); 169 } 170 } 171 }
Now for the server side. Like the client, there are two parts. The server itself and the custom Filter. Let's start with the server:
1 /** 2 * Simple HTTP (Web) server, which listens on a specific TCP port and shares 3 * static resources (files), located in a passed folder. 4 * 5 * @author Alexey Stashok 6 */ 7 public class Server { 8 private static final Logger logger = Grizzly.logger(Server.class); 9 10 // TCP Host 11 public static final String HOST = "localhost"; 12 // TCP port 13 public static final int PORT = 7777; 14 15 public static void main(String[] args) throws IOException { 16 // Construct filter chain 17 FilterChainBuilder serverFilterChainBuilder = FilterChainBuilder.stateless(); 18 // Add transport filter 19 serverFilterChainBuilder.add(new TransportFilter()); 20 // Add IdleTimeoutFilter, which will close connetions, which stay 21 // idle longer than 10 seconds. 22 serverFilterChainBuilder.add(new IdleTimeoutFilter(10, TimeUnit.SECONDS)); 23 // Add HttpServerFilter, which transforms Buffer <-> HttpContent 24 serverFilterChainBuilder.add(new HttpServerFilter()); 25 // Simple server implementation, which locates a resource in a local file system 26 // and transfers it via HTTP 27 serverFilterChainBuilder.add(new WebServerFilter(".")); 28 29 // Initialize Transport 30 final TCPNIOTransport transport = 31 TCPNIOTransportBuilder.newInstance().build(); 32 // Set filterchain as a Transport Processor 33 transport.setProcessor(serverFilterChainBuilder.build()); 34 35 try { 36 // binding transport to start listen on certain host and port 37 transport.bind(HOST, PORT); 38 39 // start the transport 40 transport.start(); 41 42 logger.info("Press any key to stop the server..."); 43 System.in.read(); 44 } finally { 45 logger.info("Stopping transport..."); 46 // stop the transport 47 transport.stop(); 48 49 logger.info("Stopped transport..."); 50 } 51 } 52 }
Line 27, a custom Filter, WebServerFilter, is added to serve resources from the directory in which the server was started. Let's take a look at that Filter:
1 /** 2 * Simple Web server implementation, which locates requested resources in a 3 * local filesystem and transfers it asynchronously to a client. 4 * 5 * @author Alexey Stashok 6 */ 7 public class WebServerFilter extends BaseFilter { 8 private static final Logger logger = Grizzly.logger(WebServerFilter.class); 9 private final File rootFolderFile; 10 11 /** 12 * Construct a WebServer 13 * @param rootFolder Root folder in a local filesystem, where server will look 14 * for resources 15 */ 16 public WebServerFilter(String rootFolder) { 17 if (rootFolder != null) { 18 this.rootFolderFile = new File(rootFolder); 19 } else { 20 // if root folder wasn't specified - use the current folder 21 this.rootFolderFile = new File("."); 22 } 23 24 // check whether the root folder 25 if (!rootFolderFile.isDirectory()) { 26 throw new IllegalStateException("Directory " + rootFolder + " doesn't exist"); 27 } 28 } 29 30 /** 31 * The method is called once we have received some {@link HttpContent}. 32 * 33 * Filter gets {@link HttpContent}, which represents a part or complete HTTP 34 * request. If it's just a chunk of a complete HTTP request - filter checks 35 * whether it's the last chunk, if not - swallows content and returns. 36 * If incoming {@link HttpContent} represents complete HTTP request or it is 37 * the last HTTP request - it initiates file download and sends the file 38 * asynchronously to the client. 39 * 40 * @param ctx Request processing context 41 * 42 * @return {@link NextAction} 43 * @throws IOException 44 */ 45 @Override 46 public NextAction handleRead(FilterChainContext ctx) 47 throws IOException { 48 49 // Get the incoming message 50 final Object message = ctx.getMessage(); 51 52 // Check if this is DownloadCompletionHandler, which means download has 53 // been completed and HTTP request processing was resumed. 54 if (message instanceof DownloadCompletionHandler) { 55 // Download completed 56 return ctx.getStopAction(); 57 } 58 59 // Otherwise cast message to a HttpContent 60 final HttpContent httpContent = (HttpContent) ctx.getMessage(); 61 // Get HTTP request message header 62 final HttpRequestPacket request = (HttpRequestPacket) httpContent.getHttpHeader(); 63 64 // Check if it's the last HTTP request chunk 65 if (!httpContent.isLast()) { 66 // if not 67 // swallow content 68 return ctx.getStopAction(); 69 } 70 71 // if entire request was parsed 72 // extract requested resource URL path 73 final String localURL = extractLocalURL(request); 74 75 // Locate corresponding file 76 final File file = new File(rootFolderFile, localURL); 77 78 logger.log(Level.INFO, "Request file: {0}", file.getAbsolutePath()); 79 80 if (!file.isFile()) { 81 // If file doesn't exist - response 404 82 final HttpPacket response = create404(request); 83 ctx.write(response); 84 85 // return stop action 86 return ctx.getStopAction(); 87 } else { 88 // if file exists 89 // suspend HttpRequestPacket processing to send the HTTP response 90 // asynchronously 91 ctx.suspend(); 92 final NextAction suspendAction = ctx.getSuspendAction(); 93 94 // Start asynchronous file download 95 downloadFile(ctx, request, file); 96 // return suspend action 97 return suspendAction; 98 } 99 } 100 101 /** 102 * Start asynchronous file download 103 * 104 * @param ctx HttpRequestPacket processing context 105 * @param request HttpRequestPacket 106 * @param file local file 107 * 108 * @throws IOException 109 */ 110 private void downloadFile(FilterChainContext ctx, 111 HttpRequestPacket request, File file) throws IOException { 112 // Create DownloadCompletionHandler, responsible for asynchronous 113 // file transferring 114 final DownloadCompletionHandler downloadHandler = 115 new DownloadCompletionHandler(ctx, request, file); 116 // Start the download 117 downloadHandler.start(); 118 } 119 120 /** 121 * Create a 404 HttpResponsePacket packet 122 * @param request original HttpRequestPacket 123 * 124 * @return 404 HttpContent 125 */ 126 private static HttpPacket create404(HttpRequestPacket request) 127 throws CharConversionException { 128 // Build 404 HttpResponsePacket message headers 129 final HttpResponsePacket responseHeader = HttpResponsePacket.builder(request). 130 protocol(request.getProtocol()).status(404). 131 reasonPhrase("Not Found").build(); 132 133 // Build 404 HttpContent on base of HttpResponsePacket message header 134 return responseHeader.httpContentBuilder(). 135 content(Buffers.wrap(null, 136 "Can not find file, corresponding to URI: " 137 + request.getRequestURIRef().getDecodedURI())). 138 build(); 139 } 140 141 /** 142 * Extract URL path from the HttpRequestPacket 143 * 144 * @param request HttpRequestPacket message header 145 * @return requested URL path 146 */ 147 private static String extractLocalURL(HttpRequestPacket request) 148 throws CharConversionException { 149 // Get requested URL 150 String url = request.getRequestURIRef().getDecodedURI(); 151 152 // Extract path 153 final int idx; 154 if ((idx = url.indexOf("://")) != -1) { 155 final int localPartStart = url.indexOf('/', idx + 3); 156 if (localPartStart != -1) { 157 url = url.substring(localPartStart + 1); 158 } else { 159 url = "/"; 160 } 161 } 162 163 return url; 164 } 165 166 /** 167 * {@link org.glassfish.grizzly.CompletionHandler}, responsible for asynchronous file transferring 168 * via HTTP protocol. 169 */ 170 private static class DownloadCompletionHandler 171 extends EmptyCompletionHandler<WriteResult>{ 172 173 // MemoryManager, used to allocate Buffers 174 private final MemoryManager memoryManager; 175 // Downloading FileInputStream 176 private final InputStream in; 177 // Suspended HttpRequestPacket processing context 178 private final FilterChainContext ctx; 179 // HttpResponsePacket message header 180 private final HttpResponsePacket response; 181 182 // Completion flag 183 private volatile boolean isDone; 184 185 /** 186 * Construct a DownloadCompletionHandler 187 * 188 * @param ctx Suspended HttpRequestPacket processing context 189 * @param request HttpRequestPacket message header 190 * @param file local file to be sent 191 * @throws FileNotFoundException 192 */ 193 public DownloadCompletionHandler(FilterChainContext ctx, 194 HttpRequestPacket request, File file) throws FileNotFoundException { 195 196 // Open file input stream 197 in = new FileInputStream(file); 198 this.ctx = ctx; 199 // Build HttpResponsePacket message header (send file using chunked HTTP messages). 200 response = HttpResponsePacket.builder(request). 201 protocol(request.getProtocol()).status(200). 202 reasonPhrase("OK").chunked(true).build(); 203 memoryManager = ctx.getConnection().getTransport().getMemoryManager(); 204 } 205 206 /** 207 * Start the file tranferring 208 * 209 * @throws IOException 210 */ 211 public void start() throws IOException { 212 sendFileChunk(); 213 } 214 215 /** 216 * Send the next file chunk 217 * @throws IOException 218 */ 219 public void sendFileChunk() throws IOException { 220 // Allocate a new buffer 221 final Buffer buffer = memoryManager.allocate(1024); 222 223 // prepare byte[] for InputStream.read(...) 224 final byte[] bufferByteArray = buffer.toByteBuffer().array(); 225 final int offset = buffer.toByteBuffer().arrayOffset(); 226 final int length = buffer.remaining(); 227 228 // Read file chunk from the file input stream 229 int bytesRead = in.read(bufferByteArray, offset, length); 230 final HttpContent content; 231 232 if (bytesRead == -1) { 233 // if the file was completely sent 234 // build the last HTTP chunk 235 content = response.httpTrailerBuilder().build(); 236 isDone = true; 237 } else { 238 // Prepare the Buffer 239 buffer.limit(bytesRead); 240 // Create HttpContent, based on HttpResponsePacket message header 241 content = response.httpContentBuilder().content(buffer).build(); 242 } 243 244 // Send a file chunk asynchronously. 245 // Once the chunk will be sent, the DownloadCompletionHandler.completed(...) method 246 // will be called, or DownloadCompletionHandler.failed(...) is error will happen. 247 ctx.write(content, this); 248 } 249 250 /** 251 * Method gets called, when file chunk was successfully sent. 252 * @param result the result 253 */ 254 @Override 255 public void completed(WriteResult result) { 256 try { 257 if (!isDone) { 258 // if transfer is not completed - send next file chunk 259 sendFileChunk(); 260 } else { 261 // if transfer is completed - close the local file input stream. 262 close(); 263 // resume(finishing) HttpRequestPacket processing 264 resume(); 265 } 266 } catch (IOException e) { 267 failed(e); 268 } 269 } 270 271 /** 272 * The method will be called, when file transferring was canceled 273 */ 274 @Override 275 public void cancelled() { 276 // Close local file input stream 277 close(); 278 // resume the HttpRequestPacket processing 279 resume(); 280 } 281 282 /** 283 * The method will be called, if file transferring was failed. 284 * @param throwable the cause 285 */ 286 @Override 287 public void failed(Throwable throwable) { 288 // Close local file input stream 289 close(); 290 // resume the HttpRequestPacket processing 291 resume(); 292 } 293 294 /** 295 * Returns <tt>true</tt>, if file transfer was completed, or 296 * <tt>false</tt> otherwise. 297 * 298 * @return <tt>true</tt>, if file transfer was completed, or 299 * <tt>false</tt> otherwise. 300 */ 301 public boolean isDone() { 302 return isDone; 303 } 304 305 /** 306 * Close the local file input stream. 307 */ 308 private void close() { 309 try { 310 in.close(); 311 } catch (IOException e) { 312 logger.fine("Error closing a downloading file"); 313 } 314 } 315 316 /** 317 * Resume the HttpRequestPacket processing 318 */ 319 private void resume() { 320 // Set this DownloadCompletionHandler as message 321 ctx.setMessage(this); 322 // Resume the request processing 323 // After resume will be called - filter chain will execute 324 // WebServerFilter.handleRead(...) again with the ctx as FilterChainContext. 325 ctx.resume(); 326 } 327 } 328 }
Again, we'll let the inline documentation in the examples provide the details on what's going on here. Let's see the output of the examples in action. Here's the server start:
[586][target]$ java -cp grizzly-http-samples-2.2.10.jar org.glassfish.grizzly.samples.http.download.Server /tmp Jan 28, 2011 1:11:17 PM org.glassfish.grizzly.samples.http.download.Server main INFO: Press any key to stop the server...
And on the client side:
[574][ryanlubke.lunasa: target]$ java -cp grizzly-http-samples-2.2.10.jar org.glassfish.grizzly.samples.http.download.Client http://localhost:7777/test.html Jan 28, 2011 1:54:49 PM org.glassfish.grizzly.samples.http.download.ClientDownloadFilter handleConnect INFO: Connected... Sending the request: HttpRequestPacket ( method=GET url=/test.html query=null protocol=HTTP_1_1 content-length=-1 headers=[ Host=localhost] ) Jan 28, 2011 1:54:49 PM org.glassfish.grizzly.samples.http.download.ClientDownloadFilter handleRead INFO: HTTP response: HttpResponsePacket (status=200 reason=OK protocol=HTTP_1_1 content-length=-1 headers==== MimeHeaders === date = Fri, 28 Jan 2011 21:54:49 GMT transfer-encoding = chunked committed=false) Jan 28, 2011 1:54:49 PM org.glassfish.grizzly.samples.http.download.Client main INFO: File test.html was successfully downloaded Jan 28, 2011 1:54:49 PM org.glassfish.grizzly.samples.http.download.Client main INFO: Stopping transport... Jan 28, 2011 1:54:49 PM org.glassfish.grizzly.samples.http.download.Client main INFO: Stopped transport...
This example within the java.net maven repository: https://maven.java.net/content/repositories/releases/org/glassfish/grizzly/samples/grizzly-http-samples/2.2.10.