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.