Http Framework Overview
The Grizzly 2.3 HTTP framework provides codecs for marshalling/unmarshalling HTTP request/response data for both the server and clients. In addition to the codecs themselves, the framework includes base HTTP primitives and utilities to make working with the protocol simpler.
Components of the Framework
There are many components that provide the HTTP protocol handling within Grizzly. This section will cover the major components.
HttpCodecFilter (Server/Client)
The HttpCodecFilter is the base Filter that provides the ability to marshall/unmarshall HTTP primitives to and from the network. Note that this class does a majority of the heavy lifting when dealing with the HTTP protocol. However, Grizzly does have two subclasses of HttpCodecFilter to deal with different semantics depending on which side of the connection being dealt with.
HTTP Messages
In order to make working with HTTP easier for the developer, Grizzly abstracts the network representation of an http protocol request/response into various different message objects.
As seen from the diagram all messages produced/consumed by the HttpCodecFilter are simply HttpPackets. From there, there are several specialized packet representations:
HttpHeader | HttpHeaders represents the http message headers. |
HttpContent | Represents the entity body of a http message. |
HttpTrailer | This is a specialized form of HttpContent. It's only used when the chunked transfer encoding is used and represents the trailer chunk. |
HttpRequestPacket | A form of HttpHeader representing a HTTP request. |
HttpResponsePacket | A form of HttpHeader representing a HTTP response to a HTTP request. |
HttpRequestPacket, HttpResponsePacket, HttpContent, and HttpTrailer (if using the chunked transfer encoding) all have Builder objects in order to create new instances. For example, in order to create a response for a particular request:
final HttpResponsePacket.Builder builder = response.builder(request); final HttpResponsePacket response = builder.chunked(true).status(200).reasonPhrase("OK").build();
Building off the example above, a message body can be added to the response that was just created:
final HttpContent.Builder contentBuilder = response.httpContentBuilder(); final HttpContent = contentBuilder.setContent( Buffers.wrap(null, "<html><head></head><body>Hello!</body></html>")).setLast(true).build();
Finally, since the response body is chunked, a HttpTrailer needs to be included:
final HttpTrailer.Builder trailerBuilder = response.httpTrailerBuilder(); final HttpTrailer trailer = trailerBuilder.build();
Each of the created HttpPacket entities from above can then be written to client:
// assume we have access to the FilterChainContext via a variable called 'ctx'... ctx.write(response); ctx.write(content); ctx.write(trailer);
The examples above are there purely to demonstrate the different builder objects. However, the example above could have the step with the HttpContent.Builder eliminated completely since HttpTrailer is a specialized form of HttpContent:
final HttpResponsePacket.Builder builder = response.builder(request); final HttpResponsePacket response = builder.chunked(true).status(200).reasonPhrase("OK").build(); final HttpTrailer.Builder trailerBuilder = response.httpTrailerBuilder(); trailerBuilder.setContent(null, "<html><head></head><body>Hello!</body></html>").setLast(true).build(); ctx.write(response); ctx.write(trailer);
Transfer Encodings
Transfer encodings allow special transformation formats for communicating http messages between client and server. Grizzly includes two transfer encoding implementations:
Fixed-Length | Implemented by org.glassfish.grizzly.http.FixedLengthTransferEncoding. This transfer-encoing will be used when content-length has been specified, or if chunking has been disabled. In the case where chunking is disabled and no content length has been specified, the implementation will try to determine the content length on its own. |
Chunked | Implemented by org.glassfish.grizzly.ChunkedTransferEncoding. This transfer-encoding will be used when specified on a request or response. For more details on the chunked transfer-encoding, see: http://www.w3.org/Protocols/rfc2616/rfc2616-sec3.html#sec3.6.1. |
Custom transfer-encoding implementations may be provided by implementing the TransferEncoding interface:
public interface TransferEncoding { /** * Return <tt>true</tt> if this encoding should be used to parse the * content of the passed {@link HttpHeader}, or <tt>false</tt> otherwise. * * @param httpPacket {@link HttpHeader}. * @return <tt>true</tt> if this encoding should be used to parse the * content of the passed {@link HttpHeader}, or <tt>false</tt> otherwise. */ public boolean wantDecode(HttpHeader httpPacket); /** * Return <tt>true</tt> if this encoding should be used to serialize the * content of the passed {@link HttpHeader}, or <tt>false</tt> otherwise. * * @param httpPacket {@link HttpHeader}. * @return <tt>true</tt> if this encoding should be used to serialize the * content of the passed {@link HttpHeader}, or <tt>false</tt> otherwise. */ public boolean wantEncode(HttpHeader httpPacket); /** * This method will be called by {@link HttpCodecFilter} to let * <tt>TransferEncoding</tt> prepare itself for the content serialization. * At this time <tt>TransferEncoding</tt> is able to change, update HTTP * packet headers. * * @param ctx {@link FilterChainContext} * @param httpHeader HTTP packet headers. * @param content ready HTTP content (might be null). */ public void prepareSerialize(FilterChainContext ctx, HttpHeader httpHeader, HttpContent content); /** * Parse HTTP packet payload, represented by {@link Buffer} using specific * transfer encoding. * * @param ctx {@link FilterChainContext} * @param httpPacket {@link HttpHeader} with parsed headers. * @param buffer {@link Buffer} HTTP message payload. * @return {@link ParsingResult} */ public ParsingResult parsePacket(FilterChainContext ctx, HttpHeader httpPacket, Buffer buffer); /** * Serialize HTTP packet payload, represented by {@link HttpContent} * using specific transfer encoding. * * @param ctx {@link FilterChainContext} * @param httpContent {@link HttpContent} with parsed {@link HttpContent#getHttpHeader()}. * * @return serialized {@link Buffer} */ public Buffer serializePacket(FilterChainContext ctx, HttpContent httpContent); }
Custom TransferEncoding implementations may be registered by calling HttpCodecFilter.addTransferEncoding().
Content Encodings
Content encodings describe when encodings have been applied to the message body prior to their being transferred to the recipient. At the time that this was written, Grizzly 2.3 only supports gzip and lzma content encodings (implemented by org.glassfish.grizzly.http.GzipContentEncoding and org.glassfish.grizzly.http.LzmaContentEncoding).
Custom content-encodings can be provided by implementing the ContentEncoding interface:
/** * Abstraction, which represents HTTP content-encoding. * Implementation should take care of HTTP content encoding and decoding. * * @see GZipContentEncoding */ public interface ContentEncoding { /** * Get the <tt>ContentEncoding</tt> name. * * @return the <tt>ContentEncoding</tt> name. */ String getName(); /** * Get the <tt>ContentEncoding</tt> aliases. * * @return the <tt>ContentEncoding</tt> aliases. */ String[] getAliases(); /** * Method should implement the logic, which decides if HTTP packet with * the specific {@link HttpHeader} should be decoded using this <tt>ContentEncoding</tt>. * * @param header HTTP packet header. * @return <tt>true</tt>, if this <tt>ContentEncoding</tt> should be used to * decode the HTTP packet, or <tt>false</tt> otherwise. */ boolean wantDecode(HttpHeader header); /** * Method should implement the logic, which decides if HTTP packet with * the specific {@link HttpHeader} should be encoded using this <tt>ContentEncoding</tt>. * * @param header HTTP packet header. * @return <tt>true</tt>, if this <tt>ContentEncoding</tt> should be used to * encode the HTTP packet, or <tt>false</tt> otherwise. */ boolean wantEncode(HttpHeader header); /** * Decode HTTP packet content represented by {@link HttpContent}. * * @param connection {@link Connection}. * @param httpContent {@link HttpContent} to decode. * * @return {@link ParsingResult}, which represents the result of decoding. */ ParsingResult decode(Connection connection, HttpContent httpContent); /** * Encode HTTP packet content represented by {@link HttpContent}. * * @param connection {@link Connection}. * @param httpContent {@link HttpContent} to encode. * * @return encoded {@link HttpContent}. */ HttpContent encode(Connection connection, HttpContent httpContent); }
Custom ContentEncoding implementations may be registered by calling HttpCodecFilter.addContentEncoding().
HTTP Potpourri
Cookie | Represents a HTTP cookie. Supports version 0 and version 1 cookies. |
Cookies | A collection of Cookies. |
CookiesBuilder | A Builder implementation for created a List of Cookies from the message headers of a HttpHeader packet. This can be used on either the server or client side. |
LazyCookie | A wrapper for Cookie that delays parsing the header value into the values exposed by Cookie until needed. |
ByteChunk | Represents a portion of a byte array and exposes various operations to operate on said portion. |
BufferChunk | Represents a portion of a buffer and exposes various operations to operate on said portion. |
CharChunk | A utility class for dealing with character arrays in an efficent manner. |
DataChunk | Represents a chunk of data that may be a ByteChunk, BufferChunk, CharChunk, or String. |
Samples
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:
/** * Simple asynchronous HTTP client implementation, which downloads HTTP resource * and saves its content in a local file. */ public class Client { private static final Logger logger = Grizzly.logger(Client.class); public static void main(String[] args) throws IOException, URISyntaxException { // Check command line parameters if (args.length < 1) { System.out.println("To download the resource, please run: Client <url>"); System.exit(0); } final String url = args[0]; // Parse passed URL final URI uri = new URI(url); final String host = uri.getHost(); final int port = uri.getPort() > 0 ? uri.getPort() : 80; final FutureImpl<String> completeFuture = SafeFutureImpl.create(); // Build HTTP client filter chain FilterChainBuilder clientFilterChainBuilder = FilterChainBuilder.stateless(); // Add transport filter clientFilterChainBuilder.add(new TransportFilter()); // Add HttpClientFilter, which transforms Buffer <-> HttpContent clientFilterChainBuilder.add(new HttpClientFilter()); // Add HTTP client download filter, which is responsible for downloading // HTTP resource asynchronously clientFilterChainBuilder.add(new ClientDownloadFilter(uri, completeFuture)); // Initialize Transport final TCPNIOTransport transport = TCPNIOTransportBuilder.newInstance().build(); // Set filterchain as a Transport Processor transport.setProcessor(clientFilterChainBuilder.build()); try { // start the transport transport.start(); Connection connection = null; // Connecting to a remote Web server Future<Connection> connectFuture = transport.connect(host, port); try { // Wait until the client connect operation will be completed // Once connection will be established - downloading will // start @ ClientDownloadFilter.onConnect(...) connection = connectFuture.get(10, TimeUnit.SECONDS); // Wait until download will be completed String filename = completeFuture.get(); logger.log(Level.INFO, "File " + filename + " was successfully downloaded"); } catch (Exception e) { if (connection == null) { logger.log(Level.WARNING, "Can not connect to the target resource"); } else { logger.log(Level.WARNING, "Error downloading the resource"); } } finally { // Close the client connection if (connection != null) { connection.close(); } } } finally { logger.info("Stopping transport..."); // stop the transport transport.shutdownNow(); logger.info("Stopped transport..."); } } }
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:
/** * HTTP client download filter. * This Filter is responsible for asynchronous downloading of a HTTP resource and * saving its content in a local file. */ public class ClientDownloadFilter extends BaseFilter { private final static Logger logger = Grizzly.logger(ClientDownloadFilter.class); // URI of a remote resource private final URI uri; // local filename, where content will be saved private final String fileName; // Download completion future private FutureImpl<String> completeFuture; // local file channel, where we save resource content private volatile FileChannel output; // number of bytes downloaded private volatile int bytesDownloaded; private final String resourcePath; /** * <tt>ClientDownloadFilter</tt> constructor * * @param uri {@link URI} of a remote resource to download * @param completeFuture download completion handler ({@link FutureImpl}) */ public ClientDownloadFilter(URI uri, FutureImpl<String> completeFuture) { this.uri = uri; // Extracting resource path resourcePath = uri.getPath().trim().length() > 0 ? uri.getPath().trim() : "/"; int lastSlashIdx = resourcePath.lastIndexOf('/'); if (lastSlashIdx != -1 && lastSlashIdx < resourcePath.length() - 1) { // if the path contains a filename - take it as local filename fileName = resourcePath.substring(lastSlashIdx + 1); } else { // if the path doesn't contain filename - we will use default filename fileName = "download#" + System.currentTimeMillis() + ".txt"; } this.completeFuture = completeFuture; } /** * The method is called, when a client connection gets connected to a web * server. * When this method gets called by a framework - it means that client connection * has been established and we can send HTTP request to the web server. * * @param ctx Client connect processing context * * @return {@link NextAction} * @throws IOException */ @Override public NextAction handleConnect(FilterChainContext ctx) throws IOException { // Build the HttpRequestPacket, which will be sent to a server // We construct HTTP request version 1.1 and specifying the URL of the // resource we want to download final HttpRequestPacket httpRequest = HttpRequestPacket.builder().method("GET") .uri(resourcePath).protocol(Protocol.HTTP_1_1) .header("Host", uri.getHost()).build(); logger.log(Level.INFO, "Connected... Sending the request: {0}", httpRequest); // Write the request asynchronously ctx.write(httpRequest); // Return the stop action, which means we don't expect next filter to process // connect event return ctx.getStopAction(); } /** * The method is called, when we receive a {@link HttpContent} from a server. * Once we receive one - we save the content chunk to a local file. * * @param ctx Request processing context * * @return {@link NextAction} * @throws IOException */ @Override public NextAction handleRead(FilterChainContext ctx) throws IOException { try { // Cast message to a HttpContent final HttpContent httpContent = ctx.getMessage(); logger.log(Level.FINE, "Got HTTP response chunk"); if (output == null) { // If local file wasn't created - create it logger.log(Level.INFO, "HTTP response: {0}", httpContent.getHttpHeader()); logger.log(Level.FINE, "Create a file: {0}", fileName); FileOutputStream fos = new FileOutputStream(fileName); output = fos.getChannel(); } // Get HttpContent's Buffer final Buffer buffer = httpContent.getContent(); logger.log(Level.FINE, "HTTP content size: {0}", buffer.remaining()); if (buffer.remaining() > 0) { bytesDownloaded += buffer.remaining(); // save Buffer to a local file, represented by FileChannel ByteBuffer byteBuffer = buffer.toByteBuffer(); do { output.write(byteBuffer); } while (byteBuffer.hasRemaining()); // Dispose a content buffer buffer.dispose(); } if (httpContent.isLast()) { // it's last HttpContent - we close the local file and // notify about download completion logger.log(Level.FINE, "Downloaded done: {0} bytes", bytesDownloaded); completeFuture.result(fileName); close(); } } catch (IOException e) { close(); } // Return stop action, which means we don't expect next filter to process // read event return ctx.getStopAction(); } /** * The method is called, when the client connection will get closed. * Intercepting this method let's use release resources, like local FileChannel, * if it wasn't released before. * * @param ctx Request processing context * * @return {@link NextAction} * @throws IOException */ @Override public NextAction handleClose(FilterChainContext ctx) throws IOException { close(); return ctx.getStopAction(); } /** * Method closes the local file channel, and if download wasn't completed - * notify {@link FutureImpl} about download failure. * * @throws IOException If failed to close <em>localOutput</em>. */ private void close() throws IOException { final FileChannel localOutput = this.output; // close the local file channel if (localOutput != null) { localOutput.close(); } if (!completeFuture.isDone()) { //noinspection ThrowableInstanceNeverThrown completeFuture.failure(new IOException("Connection was closed")); } } }
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:
/** * Simple HTTP (Web) server, which listens on a specific TCP port and shares * static resources (files), located in a passed folder. */ public class Server { private static final Logger logger = Grizzly.logger(Server.class); // TCP Host public static final String HOST = "localhost"; // TCP port public static final int PORT = 7777; public static void main(String[] args) throws IOException { if (args.length < 1) { throw new IllegalStateException("Resources directory not specified."); } // Construct filter chain FilterChainBuilder serverFilterChainBuilder = FilterChainBuilder.stateless(); // Add transport filter serverFilterChainBuilder.add(new TransportFilter()); // Add IdleTimeoutFilter, which will close connections, which stay // idle longer than 10 seconds. serverFilterChainBuilder.add( new IdleTimeoutFilter( new DelayedExecutor(Executors.newCachedThreadPool()), 10, TimeUnit.SECONDS)); // Add HttpServerFilter, which transforms Buffer <-> HttpContent serverFilterChainBuilder.add(new HttpServerFilter()); // Simple server implementation, which locates a resource in a local file system // and transfers it via HTTP serverFilterChainBuilder.add(new WebServerFilter(args[0])); // Initialize Transport final TCPNIOTransport transport = TCPNIOTransportBuilder.newInstance().build(); // Set filterchain as a Transport Processor transport.setProcessor(serverFilterChainBuilder.build()); try { // binding transport to start listen on certain host and port transport.bind(HOST, PORT); // start the transport transport.start(); logger.info("Press any key to stop the server..."); System.in.read(); } finally { logger.info("Stopping transport..."); // stop the transport transport.shutdownNow(); logger.info("Stopped transport..."); } } }
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:
/** * Simple Web server implementation, which locates requested resources in a * local filesystem and transfers it asynchronously to a client. * * @author Alexey Stashok */ public class WebServerFilter extends BaseFilter { private static final Logger logger = Grizzly.logger(WebServerFilter.class); private final File rootFolderFile; /** * Construct a WebServer * @param rootFolder Root folder in a local filesystem, where server will look * for resources */ public WebServerFilter(String rootFolder) { this.rootFolderFile = new File(rootFolder); // check whether the root folder if (!rootFolderFile.isDirectory() || !rootFolderFile.canRead()) { throw new IllegalStateException("Directory " + rootFolder + " doesn't exist or can't be read"); } } /** * The method is called once we have received some {@link HttpContent}. * * Filter gets {@link HttpContent}, which represents a part or complete HTTP * request. If it's just a chunk of a complete HTTP request - filter checks * whether it's the last chunk, if not - swallows content and returns. * If incoming {@link HttpContent} represents complete HTTP request or it is * the last HTTP request - it initiates file download and sends the file * asynchronously to the client. * * @param ctx Request processing context * * @return {@link NextAction} * @throws IOException */ @Override public NextAction handleRead(FilterChainContext ctx) throws IOException { // Get the incoming message as HttpContent final HttpContent httpContent = ctx.getMessage(); // Get HTTP request message header final HttpRequestPacket request = (HttpRequestPacket) httpContent.getHttpHeader(); // Check if it's the last HTTP request chunk if (!httpContent.isLast()) { // if not // swallow content return ctx.getStopAction(); } // if entire request was parsed // extract requested resource URL path final String localURL = extractLocalURL(request); // Locate corresponding file final File file = new File(rootFolderFile, localURL); logger.log(Level.INFO, "Request file: {0}", file.getAbsolutePath()); if (!file.isFile()) { // If file doesn't exist - response 404 final HttpPacket response = create404(request); ctx.write(response); // return stop action return ctx.getStopAction(); } else { // if file exists // suspend HttpRequestPacket processing to send the HTTP response // asynchronously ctx.suspend(); final NextAction suspendAction = ctx.getSuspendAction(); // Start asynchronous file download downloadFile(ctx, request, file); // return suspend action return suspendAction; } } /** * Start asynchronous file download * * @param ctx HttpRequestPacket processing context * @param request HttpRequestPacket * @param file local file * * @throws IOException */ private void downloadFile(FilterChainContext ctx, HttpRequestPacket request, File file) throws IOException { // Create DownloadCompletionHandler, responsible for asynchronous // file transferring final DownloadCompletionHandler downloadHandler = new DownloadCompletionHandler(ctx, request, file); // Start the download downloadHandler.start(); } /** * Create a 404 HttpResponsePacket packet * @param request original HttpRequestPacket * * @return 404 HttpContent */ private static HttpPacket create404(HttpRequestPacket request) throws CharConversionException { // Build 404 HttpResponsePacket message headers final HttpResponsePacket responseHeader = HttpResponsePacket.builder(request). protocol(request.getProtocol()).status(404). reasonPhrase("Not Found").build(); // Build 404 HttpContent on base of HttpResponsePacket message header return responseHeader.httpContentBuilder(). content(Buffers.wrap(null, "Can not find file, corresponding to URI: " + request.getRequestURIRef().getDecodedURI())). build(); } /** * Extract URL path from the HttpRequestPacket * * @param request HttpRequestPacket message header * @return requested URL path */ private static String extractLocalURL(HttpRequestPacket request) throws CharConversionException { // Get requested URL String url = request.getRequestURIRef().getDecodedURI(); // Extract path final int idx; if ((idx = url.indexOf("://")) != -1) { final int localPartStart = url.indexOf('/', idx + 3); if (localPartStart != -1) { url = url.substring(localPartStart + 1); } else { url = "/"; } } return url; } /** * {@link org.glassfish.grizzly.CompletionHandler}, responsible for asynchronous file transferring * via HTTP protocol. */ private static class DownloadCompletionHandler extends EmptyCompletionHandler<WriteResult>{ // MemoryManager, used to allocate Buffers private final MemoryManager memoryManager; // Downloading FileInputStream private final InputStream in; // Suspended HttpRequestPacket processing context private final FilterChainContext ctx; // HttpResponsePacket message header private final HttpResponsePacket response; // Completion flag private volatile boolean isDone; /** * Construct a DownloadCompletionHandler * * @param ctx Suspended HttpRequestPacket processing context * @param request HttpRequestPacket message header * @param file local file to be sent * @throws FileNotFoundException */ public DownloadCompletionHandler(FilterChainContext ctx, HttpRequestPacket request, File file) throws FileNotFoundException { // Open file input stream in = new FileInputStream(file); this.ctx = ctx; // Build HttpResponsePacket message header (send file using chunked HTTP messages). response = HttpResponsePacket.builder(request). protocol(request.getProtocol()).status(200). reasonPhrase("OK").chunked(true).build(); memoryManager = ctx.getConnection().getTransport().getMemoryManager(); } /** * Start the file transferring * * @throws IOException */ public void start() throws IOException { sendFileChunk(); } /** * Send the next file chunk * @throws IOException */ public void sendFileChunk() throws IOException { // Allocate a new buffer final Buffer buffer = memoryManager.allocate(1024); // prepare byte[] for InputStream.read(...) final byte[] bufferByteArray = buffer.array(); final int offset = buffer.arrayOffset(); final int length = buffer.remaining(); // Read file chunk from the file input stream int bytesRead = in.read(bufferByteArray, offset, length); final HttpContent content; if (bytesRead == -1) { // if the file was completely sent // build the last HTTP chunk content = response.httpTrailerBuilder().build(); isDone = true; } else { // Prepare the Buffer buffer.limit(bytesRead); // Create HttpContent, based on HttpResponsePacket message header content = response.httpContentBuilder().content(buffer).build(); } // Send a file chunk asynchronously. // Once the chunk will be sent, the DownloadCompletionHandler.completed(...) method // will be called, or DownloadCompletionHandler.failed(...) is error will happen. ctx.write(content, this); } /** * Method gets called, when file chunk was successfully sent. * @param result the result */ @Override public void completed(WriteResult result) { try { if (!isDone) { // if transfer is not completed - send next file chunk sendFileChunk(); } else { // if transfer is completed - close the local file input stream. close(); // resume(finishing) HttpRequestPacket processing resume(); } } catch (IOException e) { failed(e); } } /** * The method will be called, when file transferring was canceled */ @Override public void cancelled() { // Close local file input stream close(); // resume the HttpRequestPacket processing resume(); } /** * The method will be called, if file transferring was failed. * @param throwable the cause */ @Override public void failed(Throwable throwable) { // Close local file input stream close(); // resume the HttpRequestPacket processing resume(); } /** * Returns <tt>true</tt>, if file transfer was completed, or * <tt>false</tt> otherwise. * * @return <tt>true</tt>, if file transfer was completed, or * <tt>false</tt> otherwise. */ public boolean isDone() { return isDone; } /** * Close the local file input stream. */ private void close() { try { in.close(); } catch (IOException e) { logger.fine("Error closing a downloading file"); } } /** * Resume the HttpRequestPacket processing */ private void resume() { // Resume the request processing ctx.resume(ctx.getStopAction()); } } }
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.4.0.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.4.0.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...
You can download, unjar (unzip) and use this sample from Maven-Central: https://repo1.maven.org/maven2/org/glassfish/grizzly/samples/grizzly-http-samples/2.4.4/grizzly-http-samples-2.4.4-sources.jar.