© Kishori Sharan 2017

Kishori Sharan, Java 9 Revealed, 10.1007/978-1-4842-2592-9_14

14. The HTTP/2 Client API

Kishori Sharan

(1)Montgomery, Alabama, USA

In this chapter, you will learn:

  • What the HTTP/2 Client API is

  • How to create HTTP clients

  • How to make HTTP requests

  • How to receive HTTP responses

  • How to create WebSocket endpoints

  • How to push unsolicited data from server to client

JDK 9 delivered the HTTP/2 Client API as an incubator module named jdk.incubator.httpclient. The module exports a jdk.incubator.http package that contains all public APIs. An incubator module is not part of the Java SE. In Java SE 10, either it will be standardized and become part of Java SE 10 or it will be removed. Refer to the web page at http://openjdk.java.net/jeps/11 to learn more about incubator modules in JDK.

An incubator module is not resolved by default at compile-time or runtime, so you will need to add the jdk.incubator.httpclient module to the default set of root modules using the --add-modules option, like so:

<javac|java|jmod...> -add-modules jdk.incubator.httpclient ...

An incubator module is resolved if it is read by another module and the second module is resolved. In this chapter, you will create a module that reads the jdk.incubator.httpclient module and you will not have to use the -add-modules option to resolve it.

Because the API provided by an incubator module is not final yet, a warning is printed on the standard error when you use an incubator module at compile-time or runtime. The warning message looks like this:

WARNING: Using incubator modules: jdk.incubator.httpclient

The names of an incubator module and packages containing an incubator API start with jdk.incubator. Once they are standardized and included in Java SE, their names will be changed to use the standard Java naming convention. For example, the module name jdk.incubator.httpclient may become java.httpclient in Java SE 10.

Because the jdk.incubator.httpclient module is not in Java SE yet, you will not find Javadoc for this module. I generated the Javadoc for this module and included it with the book’s source code. You can access the Javadoc using the Java9Revealed/jdk.incubator.httpclient/dist/javadoc/index.html file in the downloaded source code. I used the JDK 9 early access build 158 to generate the Javadoc. It is likely that the API may change and you may need to regenerate the Javadoc. Here is how you do this:

  1. The source code this book contains a jdk.incubator.httpclient NetBeans project in a directory with the same name as the project name.

  2. When you install JDK 9, its source code is copied as a src.zip file in the installation directory. Copy everything from the jdk.incubator.httpclient directory in the src.zip file to the Java9revealedjdk.incubator.httpclientsrc directory in the downloaded source code.

  3. Open the jdk.incubator.httpclient project in NetBeans.

  4. Right-click the project node in NetBeans and select the Generate Javadoc option. You will get errors and a warning that you can ignore. It will generate Javadoc in the Java9Revealed/jdk.incubator.httpclient/dist/javadoc directory. Open the index.html file in this directory to view the Javadoc for the jdk.incubator.httpclient module.

What Is the HTTP/2 Client API?

Java has supported HTTP/1.1 since JDK 1.0. The HTTP API consists of several types in the java.net package. The existing API had the following issues:

  • It was designed to support multiple protocols such as http, ftp, gopher, etc., many of which are not used anymore.

  • It was too abstract and hard to use.

  • It contained many undocumented behaviors.

  • It supported only one mode, blocking mode, which required you to have one thread per request/response.

In May 2015, the Internet Engineering Task Force (IETF) published a specification for HTTP/2. Refer to the web page at https://tools.ietf.org/html/rfc7540 for the complete text of the HTTP/2 specification. HTTP/2 does not modify the application-level semantics. That is, what you know about and have been using about the HTTP protocol in your application have not changed. It has a more efficient way of preparing the data packets and sending then over to the wire between the client and server. All you knew about HTTP before, such as HTTP headers, methods, status codes, URLs, etc., remains the same. HTTP/2 attempts to solve many problems related to performance faced by HTTP/1 connections:

  • HTTP/2 supports binary data exchange instead of textual data supported by HTTP/1.1.

  • HTTP/2 supports multiplexing and concurrency, which means that multiple data exchanges can occur concurrently in both directions of a TCP connection and responses to requests can be received out of order. This eliminates the overhead of having multiple connections between peers, which was typically the case while using HTTP/1.1. In HTTP/1.1, responses must be received in the same order as the requests were sent, which is called head-of-line blocking . HTTP/2 has solved the head-of-line blocking problem by enabling multiplexing over the same TCP connection.

  • The client can suggest the priority of a request, which may be honored by the server in prioritizing responses.

  • HTTP headers are compressed, which reduces the header size significantly, thus lowering latency.

  • It allows for unsolicited push of resources from server to clients .

Instead of updating the existing API for HTTP/1.1, JDK 9 provided a new HTTP/2 Client API that supports both HTTP/1.1 and HTTP/2. It is intended that the API will eventually replace the old one. The new API also contains classes and interfaces to develop client applications using the WebSocket protocol. Refer to the web page at https://tools.ietf.org/html/rfc6455 for the complete WebSocket protocol specification. The new HTTP/2 Client API has several benefits over the existing API:

  • It is simple and easy to learn and use for most common cases.

  • It provides event-based notifications. For example, it generates notifications when headers are received, the body is received, and errors occur.

  • It supports server push, which allows the server to push resources to the client without the client making an explicit request. It makes setting up WebSocket communication with servers simple.

  • It supports HTTP/2 and HTTPS/TLS protocols.

  • It works in both synchronous (blocking mode) and asynchronous (non-blocking mode) modes.

The new API consists of fewer than 20 types, four of which are the main types. Other types are used while you are using one of these four types. The new API also uses a few types from the old API. The new API is in the jdk.incubator.http package in the jdk.incubator.httpclient module. The main types are three abstract classes and one interface:

  • The HttpClient class

  • The HttpRequest class

  • The HttpResponse class

  • The WebSocket interface

An instance of the HttpClient class is a container for storing configurations that can be used for multiple HTTP requests instead of setting them separately for each. An instance of the HttpRequest class represents an HTTP request that can be sent to a server. An instance of the HttpResponse class represents an HTTP response. An instance of the WebSocket interface represents a WebSocket client. You can create a WebSocket server using the Java EE 7 WebSocket API. I show you an example of creating WebSocket client and server at the end of this chapter.

Instances of HttpClient, HttpRequest, and WebSocket are created using builders. Each type contains a nested class/interface named Builder that is used to build an instance of that type. Note that you do not create an HttpResponse; it is returned to you as part of an HTTP request that you make. The new HTTP/2 Client API is so simple to use that you can read an HTTP resource in just one statement! The following snippet of code reads the contents at the URL https://www.google.com/ as a string using a GET HTTP request:

String responseBody = HttpClient.newHttpClient()
         .send(HttpRequest.newBuilder(new URI("https://www.google.com/"))
               .GET()
               .build(), BodyHandler.asString())
         .body();

Typical steps in processing HTTP requests are as follows:

  • Create an HTTP client object to store HTTP configuration information.

  • Create an HTTP request object and populate it with information to be sent to the server.

  • Send the HTTP request to the server.

  • Receive an HTTP response object as a response from the server.

  • Process the HTTP response.

Setting Up Examples

I use many examples in this chapter that involve interacting with a web server. Instead of using a web application deployed on the Internet, I created a web application project in NetBeans that you can deploy locally. If you prefer using another web application, you need to change the URLs used in the examples.

The NetBeans web application is located in the webapp directory of the book’s source code. I tested the examples by deploying the web application on the GlassFish server 4.1.1 and Tomcat 8/9. You can download the NetBeans IDE with the GlassFish server from https://netbeans.org/ . I ran the HTTP listener in the GlassFish server at port 8080. If you run it on a different port, you need to change the port number in the example URLs.

All this chapter’s HTTP client programs are located in the com.jdojo.http.client directory in the book’s source code. They are in the com.jdojo.http.client module whose declaration is shown in Listing 14-1.

Listing 14-1. The Module Declaration for a Module Named com.jdojo.http.client
// module-info.java
module com.jdojo.http.client {
    requires jdk.incubator.httpclient;
}

Creating HTTP Clients

An HTTP request that is sent to a server needs to be configured so that the server knows which authenticator to use, the SSL configuration details, the cookie manager to be used, proxy information, the redirect policy if the server redirects the request, etc. An instance of the HttpClient class stores these request-specific configurations, and they can be reused for multiple requests. You can override a few of these configurations on a per-request basis. When you send an HTTP request, you need to specify the HttpClient object that will supply the configuration information for the request. An HttpClient holds the following pieces of information that are used for all HTTP requests: an authenticator, a cookie manager, an executor, a redirect policy, a request priority, a proxy selector, an SSL context, SSL parameters, and an HTTP version.

An authenticator is an instance of the java.net.Authenticator class. It is used for HTTP authentication. The default is to use no authenticator.

A cookie manager is used to manage HTTP cookies. It is an instance of the java.net.CookieManager class. The default is to use no cookie manager.

An executor is an instance of the java.util.concurrent.Executor interface, which is used to send and receive asynchronous HTTP requests and responses. A default executor is provided, if it is not specified.

A redirect policy is one of the constants of the HttpClient.Redirect enum that specifies how to handle redirects issues by the server. The default is NEVER, which means the redirects issued by the server are never followed.

A request priority is the default priority for HTTP/2 requests, which can be between 1 and 256 (inclusive). It is a hint for the server to prioritize the request processing. A higher value means a higher priority.

A proxy selector is an instance of the java.net.ProxySelector class that selects the proxy server to use. The default is not to use a proxy server.

An SSL context is an instance of the javax.net.ssl.SSLContext class that provides an implementation for the secure socket protocol. A default SSLContext is provided, which works when you do not need to specify protocols or do not need client authentication.

SSL parameters are parameters for SSL/TLS/DTLS connections. They are stored in an instance of the javax.net.ssl.SSLParameters class.

An HTTP version is the version of HTTP, which is 1.1 or 2. It is specified as one of the constants of the HttpClient.Version enum: HTTP_1_1 and HTTP_2. It requests a specific HTTP protocol version wherever possible. The default is HTTP_1_ 1.

Tip

An HttpClient is immutable. A few of the configurations stored in an HttpClient may be overridden for HTTP requests when such requests are built.

The HttpClient class is abstract and you cannot create its objects directly. There are two ways you can create an HttpClient object:

  • Using the newHttpClient() static method of the HttpClient class

  • Using the build() method of the HttpClient.Builder class

The following snippet of code gets a default HttpClient object:

// Get the default HttpClient
HttpClient defaultClient = HttpClient.newHttpClient();

You can also create an HttpClient using the HttpClient.Builder class. The HttpClient.newBuilder() static method returns a new instance of the HttpClient.Builder class. The HttpClient.Builder class provides a method for setting each configuration value. The value for the configuration is specified as the parameter of the method and the method returns the reference of the builder object itself, so you can chain multiple methods. In the end, call the build() method that returns an HttpClient object. The following statement creates an HttpClient with the redirect policy set to ALWAYS and the HTTP version set to HTTP_2:

// Create a custom HttpClient
HttpClient httpClient = HttpClient.newBuilder()                      .followRedirects(HttpClient.Redirect.ALWAYS)
                      .version(HttpClient.Version.HTTP_2)
                      .build();

The HttpClient class contains a method corresponding to each configuration setting that returns the value for that configuration. Those methods are as follows:

  • Optional<Authenticator> authenticator()

  • Optional<CookieManager> cookieManager()

  • Executor executor()

  • HttpClient.Redirect followRedirects()

  • Optional<ProxySelector> proxy()

  • SSLContext sslContext()

  • Optional<SSLParameters> sslParameters()

  • HttpClient.Version version()

Notice that there are no setters in the HttpClient class because it is immutable. You cannot use an HttpClient object by itself. You need to use an HttpRequest object before you use an HttpClient object to send a request to a server. I explain the HttpRequest class in the next section. The HttpClient class contains the following three methods to send a request to a server:

  • <T> HttpResponse<T> send(HttpRequest req, HttpResponse.BodyHandler<T> responseBodyHandler)

  • <T> CompletableFuture<HttpResponse<T>> sendAsync(HttpRequest req, HttpResponse.BodyHandler<T> responseBodyHandler)

  • <U,T> CompletableFuture<U> sendAsync(HttpRequest req, HttpResponse.MultiProcessor<U,T> multiProcessor)

The send() method sends the request synchronously, whereas the sendAsync() methods send the request asynchronously. I cover these methods in more detail in subsequent sections.

Processing HTTP Requests

A client application communicates with a web server using HTTP requests. It sends a request to the server and the server sends you back an HTTP response. An instance of the HttpRequest class represents an HTTP request. The following are the steps you need to perform to process an HTTP request:

  • Obtain an HTTP request builder

  • Set the parameters for the request

  • Create an HTTP request from the builder

  • Send the HTTP request to a server synchronously or asynchronously

  • Process the response from the server

Obtaining an HTTP Request Builder

You need to use a builder object, which is an instance of the HttpRequest.Builder class, to create an HttpRequest. You can get an HttpRequest.Builder using one of the following static methods of the HttpRequest class:

  • HttpRequest.Builder newBuilder()

  • HttpRequest.Builder newBuilder(URI uri)

The following snippet of code shows you how to use these methods to get an HttpRequest.Builder instance:

// A URI to point to google
URI googleUri = new URI("http://www.google.com");


// Get a builder for the google URI
HttpRequest.Builder builder1 = HttpRequest.newBuilder(googleUri);


// Get a builder without specifying a URI at this time
HttpRequest.Builder builder2 = HttpRequest.newBuilder();

Setting HTTP Request Parameters

Once you have an HTTP request builder, you can set different parameters for the request using the builder’s methods. All methods return the builder itself, so you can chain them. Those methods are as follows:

  • HttpRequest.Builder DELETE(HttpRequest.BodyProcessor body)

  • HttpRequest.Builder expectContinue(boolean enable)

  • HttpRequest.Builder GET()

  • HttpRequest.Builder header(String name, String value)

  • HttpRequest.Builder headers(String... headers)

  • HttpRequest.Builder method(String method, HttpRequest.BodyProcessor body)

  • HttpRequest.Builder POST(HttpRequest.BodyProcessor body)

  • HttpRequest.Builder PUT(HttpRequest.BodyProcessor body)

  • HttpRequest.Builder setHeader(String name, String value)

  • HttpRequest.Builder timeout(Duration duration)

  • HttpRequest.Builder uri(URI uri)

  • HttpRequest.Builder version(HttpClient.Version version)

An HttpRequest is sent to a server using an HttpClient. When you are building an HTTP request, you can set the HTTP version value through its the HttpRequest.Builder object using the version() method that will override the HTTP version set in the HttpClient when this request is sent. The following snippet of code sets the HTTP version to 2.0 for a request, which overrides the default value of NEVER in the default HttpClient object:

// By default a client uses HTTP 1.1. All requests sent using this
// HttpClient will use HTTP 1.1 unless overridden by the request
HttpClient client = HttpClient.newHttpClient();
        
// A URI to point to google
URI googleUri = new URI("http://www.google.com");


// Get an HttpRequest that uses HTTP 2.0
HttpRequest request = HttpRequest.newBuilder(googleUri)
                                 .version(HttpClient.Version.HTTP_2)
                                 .build();
// The client object contains HTTP version as 1.1 and the request
// object contains HTTP version 2.0. The following statement will
// send the request using HTTP 2.0, which is in the request object.
HttpResponse<String> r = client.send(request, BodyHandler.asString());

The timeout() method specifies a timeout for the request. If a response is not received within the specified timeout period, an HttpTimeoutException is thrown.

An HTTP request may contain a header named expect with its value "100-Continue". If this header is set, the client sends only headers to the server and the server is expected to send back an error response or a 100-Continue response. Upon receiving this response, the client sends the request body to the server. A client uses this techniques to check if the server can process the request based on the request’s headers, before the client sends the actual request body. This header is not set by default. You need to call the expectContinue(true) method of the request builder to enable this. Note that calling the header("expect", "100-Continue") method of the request builder does not enable this feature. You must use the expectContinue(true) method to enable it.

// Enable the expect=100-Continue header in the request
HttpRequest.Builder builder = HttpRequest.newBuilder()                                                               
                                         .expectContinue(true);

The following sections describe how to set the headers, body, and method of an HTTP request.

Setting Request Headers

A header in an HTTP request is a name-value pair. You can have multiple headers . You can use the header(), headers(), and setHeader() methods of the HttpRequest.Builder class to add headers to a request. The header() and headers() methods add headers if they are not already present. If headers were already added, these methods do nothing. The setHeader() method replaces the header if it was present; otherwise, it adds the header.

The header() and setHeader() methods let you add/set one header at a time, whereas the headers() method lets you add multiple headers. The headers() method takes a var-args argument, which should contain name-value pairs in sequence. The following snippet of code shows how to set headers for an HTTP request :

// Create a URI
URI calc = new URI("http://localhost:8080/webapp/Calculator");


// Use the header() method
HttpRequest.Builder builder1 = HttpRequest.newBuilder(calc)
    .header("Content-Type", "application/x-www-form-urlencoded")
    .header("Accept", "text/plain");


// Use the headers() method
HttpRequest.Builder builder2 = HttpRequest.newBuilder(calc)                
    .headers("Content-Type", "application/x-www-form-urlencoded",
             "Accept", "text/plain");


// Use the setHeader() method
HttpRequest.Builder builder3 = HttpRequest.newBuilder(calc)                
    .setHeader("Content-Type", "application/x-www-form-urlencoded")
    .setHeader("Accept", "text/plain");

Setting the Request Body

The body of some HTTP requests contain data such as requests using POST and PUT methods. The body of an HTTP request is set using a body processor, which is a static nested interface named HttpRequest.BodyProcessor. The HttpRequest.BodyProcessor interface contains the following static factory methods that return a body processor for HTTP requests that supply the request body from a specific type of source such as a String, a byte[], or a File:

  • HttpRequest.BodyProcessor fromByteArray(byte[] buf)

  • HttpRequest.BodyProcessor fromByteArray(byte[] buf, int offset, int length)

  • HttpRequest.BodyProcessor fromByteArrays(Iterable<byte[]> iter)

  • HttpRequest.BodyProcessor fromFile(Path path)

  • HttpRequest.BodyProcessor fromInputStream(Supplier<? extends InputStream> streamSupplier)

  • HttpRequest.BodyProcessor fromString(String body)

  • HttpRequest.BodyProcessor fromString(String s, Charset charset)

The first argument of these methods indicates the source of the data for the request’s body. For example, you use the fromString(String body) method to obtain a body processor if a String object supplies the request’s body.

Tip

The HttpRequest class contains a static noBody() method that returns an HttpRequest.BodyProcessor, which processes no request body. Typically, you will use this method with the method() method when an HTTP method does not accept a body, but the method() method requires you to pass a body processor.

Whether a request can have a body depends on the HTTP method used to send the request. The DELETE, POST, and PUT methods have a body, whereas the GET method does not. The HttpRequest.Builder class contains a method with the same name as the HTTP method’s name to set the method and the body of the request. For example, to use the POST method with a body, the builder has a POST(HttpRequest.BodyProcessor body) method .

There are many other HTTP methods such as HEAD and OPTIONS, which do not have a corresponding method the HttpRequest.Builder class. The class contains a method(String method, HttpRequest.BodyProcessor body) method that you can use for any HTTP method. When using the method() method, make sure to specify the method name in all uppercase—for example, GET, POST, HEAD, etc. The following is a list of those methods:

  • HttpRequest.Builder DELETE(HttpRequest.BodyProcessor body)

  • HttpRequest.Builder method(String method, HttpRequest.BodyProcessor body)

  • HttpRequest.Builder POST(HttpRequest.BodyProcessor body)

  • HttpRequest.Builder PUT(HttpRequest.BodyProcessor body)

The following snippet of code sets the body of an HTTP request from a String, which is typically done when you post an HTML form to a URL. The form data consists of three fields named n1, n2, and op.

URI calc = new URI("http://localhost:8080/webapp/Calculator");

// Compose the form data with n1 = 10, n2 = 20. And op = +      
String formData = "n1=" + URLEncoder.encode("10","UTF-8") +
                  "&n2=" + URLEncoder.encode("20","UTF-8") +
                  "&op=" + URLEncoder.encode("+","UTF-8")  ;


HttpRequest.Builder builder = HttpRequest.newBuilder(calc)                
    .header("Content-Type", "application/x-www-form-urlencoded")
    .header("Accept", "text/plain")
    .POST(HttpRequest.BodyProcessor.fromString(formData));

Creating HTTP Requests

In previous sections, you learned how to create an HttpRequest.Builder and how to use it to set different properties of an HTTP request. Creating an HTTP request is simply calling the build() method on an HttpRequest.Builder, which returns an HttpRequest object. The following snippet of code creates an HttpRequest that uses the HTTP GET method:

HttpRequest request = HttpRequest.newBuilder()
                                 .uri(new URI("http://www.google.com"))
                                 .GET()
                                 .build();

The following snippet of code builds an HttpRequest with headers and a body that uses an HTTP POST method :

// Build the URI and the form’s data
URI calc = new URI("http://localhost:8080/webapp/Calculator");               
String formData = "n1=" + URLEncoder.encode("10","UTF-8") +
                  "&n2=" + URLEncoder.encode("20","UTF-8") +
                  "&op=" + URLEncoder.encode("+","UTF-8");


// Build the HttpRequest object
HttpRequest request = HttpRequest.newBuilder(calc)   
   .header("Content-Type", "application/x-www-form-urlencoded")
   .header("Accept", "text/plain")   
   .POST(HttpRequest.BodyProcessor.fromString(formData))
   .build();

Note that creating an HttpRequest object does not send the request to the server. You will need to call the send() or sendAsync() method of the HttpClient class to send the request to the server. In the previous example, the request object is like any other Java object. I explain how to send requests to a server in the next section.

The following snippet of code creates an HttpRequest object with HTTP HEAD request method. Note that it uses the method() method of the HttpRequest.Builder class to specify the HTTP method.

HttpRequest request =
    HttpRequest.newBuilder(new URI("http://www.google.com"))   
               .method("HEAD", HttpRequest.noBody())
               .build();

Processing HTTP Responses

Once you have an HttpRequest object, you can send the request to a server and receive the response synchronously or asynchronously. An instance of the HttpResponse<T> class represents a response received from a server, where the type parameter T indicates the type of the response body such as String, byte[], or Path. You can use one of the following methods of the HttpRequest class to send an HTTP request and receive an HTTP response:

  • <T> HttpResponse<T> send(HttpRequest req, HttpResponse.BodyHandler<T> responseBodyHandler)

  • <T> CompletableFuture<HttpResponse<T>> sendAsync(HttpRequest req, HttpResponse.BodyHandler<T> responseBodyHandler)

  • <U,T> CompletableFuture<U> sendAsync(HttpRequest req, HttpResponse.MultiProcessor<U,T> multiProcessor)

The send() method is synchronous. That is, it blocks until the response is received. The sendAsync() method processes the response asynchronously. It returns immediately with a CompletableFuture<HttpResponse>, which completes when the response is ready to be processed.

Processing Response Status and Headers

An HTTP response contains a status code, headers, and a body. An HttpResponse object is made available to you as soon as the status code and headers are received from the server, but before the body is received. The statusCode() method of the HttpResponse class returns the status code of the response as an int. The headers() method of the HttpResponse class returns the headers of the response as an instance of the HttpHeaders interface. The HttpHeaders interface contains the following methods to conveniently retrieve header’s values by name or all headers as a Map<String,List<String>>:

  • List<String> allValues(String name)

  • Optional<String> firstValue(String name)

  • Optional<Long> firstValueAsLong(String name)

  • Map<String,List<String>> map()

Listing 14-2 contains a complete program to send a request to the URL, http://www.google.com , with a HEAD request. It prints the received response’s status code and headers. You may get different output.

Listing 14-2. Processing an HTTP Response’s Status Code and Headers
// GoogleHeadersTest.java
package com.jdojo.http.client;


import java.io.IOException;
import java.net.URI;
import java.net.URISyntaxException;
import jdk.incubator.http.HttpClient;
import jdk.incubator.http.HttpRequest;
import jdk.incubator.http.HttpResponse;


public class GoogleHeadersTest {
    public static void main(String[] args) {
        try {
            URI googleUri = new URI("http://www.google.com");
            HttpClient client = HttpClient.newHttpClient();
            HttpRequest request =
                HttpRequest.newBuilder(googleUri)
                           .method("HEAD", HttpRequest.noBody())
                           .build();
            HttpResponse<?> response =
              client.send(request, HttpResponse.BodyHandler.discard(null));


            // Print the response status code and headers
            System.out.println("Response Status Code:" +
                               response.statusCode());


            System.out.println("Response Headers are:");
            response.headers()
                    .map()
                    .entrySet()
                    .forEach(System.out::println);
        } catch (URISyntaxException | InterruptedException |
                 IOException e) {
            e.printStackTrace();
        }
    }
}
WARNING: Using incubator modules: jdk.incubator.httpclient
Response Status Code:200
Response Headers are:
accept-ranges=[none]
cache-control=[private, max-age=0]
content-type=[text/html; charset=ISO-8859-1]
date=[Sun, 26 Feb 2017 16:39:36 GMT]
expires=[-1]
p3p=[CP="This is not a P3P policy! See https://www.google.com/support/accounts/answer/151657?hl=en for more info."]
server=[gws]
set-cookie=[NID=97=Kmz52m8Zdf4lsNDsnMyrJomx_2kD7lnWYcNEuwPWsFTFUZ7yli6DbCB98Wv-SlxOfKA0OoOBIBgysuZw3ALtgJjX67v7-mC5fPv88n8VpwxrNcjVGCfFrxVro6gRNIrye4dAWZvUVfY28eOM; expires=Mon, 28-Aug-2017 16:39:36 GMT; path=/; domain=.google.com; HttpOnly]
transfer-encoding=[chunked]
vary=[Accept-Encoding]
x-frame-options=[SAMEORIGIN]
x-xss-protection=[1; mode=block]
Processing the Response Body

Processing the body of an HTTP response is a two-step process:

  • You need to specify a response body handler, which is an instance of the HttpResponse.BodyHandler<T> interface, when you send a request using the send() or sendAsync() method of the HttpClient class.

  • When the response status code and headers are received, the apply() method of the response body handler is called. The response status code and headers are passed to the apply() method. The apply() method returns an instance of the HttpResponse.BodyProcessor<T> interface, which reads the response body and converts the read data into the type T.

Don’t worry about these details of processing a response body. Several implementations of the HttpResponse.BodyHandler<T> are provided. You can use one of the following static factory methods of the HttpResponse.BodyHandler interface to gets its instance for a different type parameter T:

  • HttpResponse.BodyHandler<byte[]> asByteArray()

  • HttpResponse.BodyHandler<Void> asByteArrayConsumer(Consumer<Optional<byte[]>> consumer)

  • HttpResponse.BodyHandler<Path> asFile(Path file)

  • HttpResponse.BodyHandler<Path> asFile(Path file, OpenOption... openOptions)

  • HttpResponse.BodyHandler<Path> asFileDownload(Path directory, OpenOption... openOptions)

  • HttpResponse.BodyHandler<String> asString()

  • HttpResponse.BodyHandler<String> asString(Charset charset)

  • <U> HttpResponse.BodyHandler<U> discard(U value)

These methods’ signatures are intuitive enough to tell you what type of response body they handle. For example, if you want to get the response body as a String, use the asString() method to get a body handler. The discard(U value) method returns a body handler, which will discard the response body and return the specified value as the body.

The body() method of the HttpResponse<T> class returns the response body, which is of type T.

The following snippet of code sends a GET request to the http://google.com URL and retrieves the response body as a String. I ignored the exception handling logic .

import java.net.URI;
import jdk.incubator.http.HttpClient;
import jdk.incubator.http.HttpRequest;
import jdk.incubator.http.HttpResponse;
import static jdk.incubator.http.HttpResponse.BodyHandler.asString;
...
// Build the request
HttpRequest request = HttpRequest.newBuilder()
                .uri(new URI("http://google.com"))
                .GET()
                .build();


// Send the request and get a Response
HttpResponse<String> response = HttpClient.newHttpClient()
                                          .send(request, asString());


// Get the response body and print it
String body = response.body();
System.out.println(body);<HTML><HEAD><meta http-equiv="content-type"
WARNING: Using incubator modules: jdk.incubator.httpclient
<HTML><HEAD><meta http-equiv="content-type" content="text/html;charset=utf-8">
<TITLE>301 Moved</TITLE></HEAD><BODY>
<H1>301 Moved</H1>
The document has moved
<A HREF="http://www.google.com/">here</A>.
</BODY></HTML>

The example returns a response body with a status code of 301, which indicates that the URL has moved. The output also contains the moved URL. If we had set the following redirects policy in the HttpClient to ALWAYS, the request would have been resubmitted to the moved URL. The following snippet of code fixes this issue:

// The request will follow the redirects issues by the server       
HttpResponse<String> response = HttpClient.newBuilder()
    .followRedirects(HttpClient.Redirect.ALWAYS)
    .build()
    .send(request, asString());

Listing 14-3 contains a complete program that shows you how to use a POST request with a body and process the response asynchronously. The web application in the source code for this book contains a servlet named Calculator. The source code for the Calculator servlet is not shown here to save space. The servlet accepts three parameters in the request, which are named n1, n2, and op, where n1 and n2 are two numbers and op is an operator (+, -, *, or /). The response is a plain text and summarizes the operator and its result. The URL in the program assumes that you have deployed the servlet locally on your machine and the web server is running on port 8080. If these assumptions are not true, modify the program accordingly. You will get the output shown here if the servlet is called successfully. Otherwise, you will get different output .

Listing 14-3. Processing an HTTP Response’s Body Asynchronously
// CalculatorTest.java
package com.jdojo.http.client;


import java.io.IOException;
import java.net.URI;
import java.net.URISyntaxException;
import java.net.URLEncoder;
import jdk.incubator.http.HttpClient;
import jdk.incubator.http.HttpRequest;
import static jdk.incubator.http.HttpRequest.BodyProcessor.fromString;
import jdk.incubator.http.HttpResponse;


public class CalculatorTest {
    public static void main(String[] args) {
        try {
            URI calcUri =
                new URI("http://localhost:8080/webapp/Calculator");


            String formData = "n1=" + URLEncoder.encode("10","UTF-8") +
                              "&n2=" + URLEncoder.encode("20","UTF-8") +
                              "&op=" + URLEncoder.encode("+","UTF-8")  ;


            // Create a request
            HttpRequest request = HttpRequest.newBuilder()
                .uri(calcUri)
                .header("Content-Type", "application/x-www-form-urlencoded")
                .header("Accept", "text/plain")                
                .POST(fromString(formData))
                .build();


            // Process the response asynchronously. When the response
            // is ready, the processResponse() method of this class will
            // be called.
            HttpClient.newHttpClient()
                      .sendAsync(request,
                                 HttpResponse.BodyHandler.asString())
                      .whenComplete(CalculatorTest::processResponse);


            try {
                // Let the current thread sleep for 5 seconds,
                // so the async response processing is complete
                Thread.sleep(5000);
            } catch (InterruptedException ex) {
                ex.printStackTrace();
            }
        } catch (URISyntaxException | IOException e) {
            e.printStackTrace();
        }
    }


    private static void processResponse(HttpResponse<String> response,
                                       Throwable t) {


         if (t == null ) {
             System.out.println("Response Status Code: " +  
                                 response.statusCode());
             System.out.println("Response Body: " + response.body());
         } else {
            System.out.println("An exception occurred while " +
                "processing the HTTP request. Error: " +  t.getMessage());
         }
     }
}
WARNING: Using incubator modules: jdk.incubator.httpclient
Response Status Code: 200
Response Body: 10 + 20 = 30.0

Using the response body handler saves the developer a lot of work. In one statement, you can download and save the contents of a URL in a file. The following snippet of code saves the contents of the URL http://www.google.com as a file named google.html in your current directory. When the download is complete, the path of the downloaded file is printed. If an error occurs, the stack trace of the exception is printed.

HttpClient.newBuilder()
          .followRedirects(HttpClient.Redirect.ALWAYS)
          .build()
          .sendAsync(HttpRequest.newBuilder()           
                                .uri(new URI("http://www.google.com"))
                                .GET()
                                .build(),
                                asFile(Paths.get("google.html")))
           .whenComplete((HttpResponse<Path> response,
                          Throwable exception) -> {
               if(exception == null) {
                  System.out.println("File saved to " +
                                     response.body().toAbsolutePath());
              } else {
                  exception.printStackTrace();
              }
            });
Processing Response Trailers

HTTP trailers are lists of name-value pairs like HTTP headers that are sent by the server at the end of an HTTP response, after the response body has been sent. HTTP trailers are typically not used by many servers. The HttpResponse class contains a trailers() method that returns response trailers as an instance of the CompletableFuture<HttpHeaders>. Notice the name of the returned object type—HttpHeaders. The HTTP/2 Client API does have a type named HttpTrailers. You need to retrieve the response body before you can retrieve the trailers. At the time of this writing, the HTTP/2 Client API does not support processing HTTP trailers. The following snippet of code shows you how to print all response trailers when the API supports it:

// Get an HTTP response
HttpResponse<String> response = HttpClient.newBuilder()
                  .followRedirects(HttpClient.Redirect.ALWAYS)
                  .build()
                  .send(HttpRequest.newBuilder()           
                                   .uri(new URI("http://www.google.com"))
                                   .GET()
                                   .build(),
                                   asString());
// Read the response body
String body = response.body();
// Process trailers
response.trailers()
        .whenComplete((HttpHeaders trailers, Throwable t) -> {
             if(t == null) {
                 trailers.map()
                         .entrySet()
                         .forEach(System.out::println);
             } else {
                  t.printStackTrace();
             }
         });

Setting the Request Redirection Policy

In response to an HTTP request, a web server may return a 3XX response status code, where X is a digit between 0 and 9. This status code indicates that an additional action is required on the client’s part to fulfill the request. For example, a status code of 301 indicates that the URL has permanently moved to a new location. The response body contains alternative locations. By default, upon receiving a 3XX status code, the request is not resubmitted to a new location. You can set one of the following constants of the HttpClient.Redirect enum as a policy for your HttpClient to act in case the returned response contains a 3XX response status code:

  • ALWAYS

  • NEVER

  • SAME_PROTOCOL

  • SECURE

ALWAYS indicates that the redirects should always be followed. That is, the request should be resubmitted to the new location.

NEVER indicates that the redirects should never be followed. This is the default.

SAME_PROTOCOL indicates that the redirection may occur if the old and new locations use the same protocol—for example, HTTP to HTTP or HTTPS to HTTPS.

SECURE indicates that the redirection should always occur, except when the old location uses HTTPS and the new one uses HTTP.

I covered how to use the follow redirect configuration setting on an HttpClient in previous sections.

Using the WebSocket Protocol

The WebSocket protocol provides a two-way communication between two endpoints—a client endpoint and a server endpoint. The term endpoint means any of the two sides of the connection that use the WebSocket protocol. The client endpoint initiates a connection and the server endpoint accepts the connection. The connection is bi-directional, which means the server endpoint can push messages to the client endpoint on its own. You will also come across another term in this context, which is called a peer. A peer is simply the other end of the connection. For example, for a client endpoint, the server endpoint is a peer and, for a server endpoint, the client endpoint is a peer. A WebSocket session represents a sequence of interactions between an endpoint and a single peer.

The WebSocket protocol can be broken into three parts :

  • Opening handshake

  • Data exchanges

  • Closing handshake

The client initiates an opening handshake with the server. The handshake occurs using HTTP with an upgrade request to the WebSocket protocol. The server responds to the opening handshake with an upgrade response. Once the handshake is successful, the client and the server exchange messages. The message exchanges may be initiated by either the client or the server. In the end, either endpoint can send a closing handshake; the peer responds with the closing handshake. Once the closing handshake is successful, the WebSocket is closed.

The HTTP/2 Client API in JDK 9 supports creating WebSocket client endpoints. To have a complete example of using the WebSocket protocol, you will need to have a server endpoint and a client endpoint. The following sections cover creating both.

Creating a Server Endpoint

Creating a server endpoint requires using Java EE. This book is focused on Java SE 9. I explain briefly how to create a server endpoint to be used in the example in this section. Without covering the details, I use Java EE 7 annotations to create a WebSocket server endpoint.

Listing 14-4 contains the code for a class named TimeServerEndPoint. The class is included in the web application in the webapp directory of the book’s source code. When you deploy the web application to a web server, this class will be deployed as a server endpoint .

Listing 14-4. A WebSocket Server Endpoint
// TimeServerEndPoint.java
package com.jdojo.ws;


import java.io.IOException;
import java.time.ZonedDateTime;
import java.util.concurrent.TimeUnit;
import javax.websocket.CloseReason;
import javax.websocket.OnMessage;
import javax.websocket.OnOpen;
import javax.websocket.OnClose;
import javax.websocket.OnError;
import javax.websocket.Session;
import javax.websocket.server.ServerEndpoint;
import static javax.websocket.CloseReason.CloseCodes.NORMAL_CLOSURE;


@ServerEndpoint("/servertime")
public class TimeServerEndPoint {
    @OnOpen
    public void onOpen(Session session) {                
        System.out.println("Client connected. ");
    }
    @OnClose
    public void onClose(Session session) {        
        System.out.println("Connection closed.");
    }
    @OnError
    public void onError(Session session, Throwable t) {
        System.out.println("Error occurred:" + t.getMessage());
    }
    @OnMessage
    public void onMessage(String message, Session session) {
        System.out.println("Client: " + message);                
        // Send messages to the client
        sendMessages(session);
    }
    private void sendMessages(Session session) {
        /* Start a new thread and send 3 messages to the
           client. Each message contains the current date and
           time with zone.
        */
        new Thread(() -> {
            for(int i = 0; i < 3; i++) {
                String currentTime =
                    ZonedDateTime.now().toString();
                try {
                    session.getBasicRemote()
                           .sendText(currentTime, true);
                    TimeUnit.SECONDS.sleep(5);
                } catch(InterruptedException | IOException e) {
                    e.printStackTrace();
                    break;
                }
            }
            try {
                // Let us close the WebSocket
                session.close(new CloseReason(NORMAL_CLOSURE,
                                              "Done"));
            } catch (IOException e) {
                e.printStackTrace();
            }
        })
        .start();
    }
}

Using the @ServerEndpoint("/servertime") annotation on the TimeServerEndPoint class makes this class a server endpoint when it is deployed to a web server. The value of the value element of the annotation is /servertime, which will make the web server publish this endpoint at this URL.

The class contains four methods, which have been annotated with @onOpen, @onMessage, @onClose, and @onError annotations. I named these methods the same as these annotations. These methods are called at different points in the lifecycle of the server endpoint. They take a Session object as an argument. The Session object represents the interaction of this endpoint with its peer, which would be the client endpoint.

The onOpen() method is called when an opening handshake with a peer is successful. The method prints a message that a client is connected .

The onMessage() is called when a message is received from the peer. The method prints the message it receives and calls a private method named sendMessages(). The sendMessages() method starts a new thread and sends three messages to the peer. The thread sleeps for five seconds after sending each message. The message contains the current date and time with the time zone. You can send messages to peers synchronously or asynchronously. To send a message, you need to get a reference of the RemoteEndpoint interface that represents the conversation with a peer. Use the getBasicRemote() and getAsyncRemote() methods on the Session instance to get a RemoteEndpoint.Basic and RemoteEndpont.Async instances that can send messages synchronously and asynchronously, respectively. Once you get a reference of the peer (the remote endpoint), you can call several of its sendXxx() methods to send different types of data to the peer.

// Send a synchronous text message to the peer
session.getBasicRemote()
       .sendText(currentTime, true);

The second argument in the sendText() method indicates whether it is the last part of the partial message being sent. If your message is complete, use true.

After all messages are sent to the peer, a close message is sent using the sendClose() method. The method takes an object of the CloseReason class that encapsulates a close code and a close reason. When the peer receives a close message, the peer needs to respond with a close message, and, after that, the WebSocket connection is closed. Note that, after sending a close message, the server endpoint is not supposed to send more messages to its peer.

The onError() method is called when an error occurs that is not handled by the WebSocket protocol.

You cannot use this endpoint by itself. You need create a client endpoint, which I cover in detail in the next section. You will see this endpoint in action in the section entitled “Running the WebSocket Program,” later in this chapter.

Creating a Client Endpoint

Developing a WebSocket client endpoint involves using the WebSocket interface, which is part of the HTTP/2 Client API in JDK 9. The WebSocket interface contains the following nested types:

  • WebSocket.Builder

  • WebSocket.Listener

  • WebSocket.MessagePart

An instance of the WebSocket interface represents a WebSocket client endpoint. A builder, which is an instance of the WebSocket.Builder interface, is used to create a WebSocket instance. The newWebSocketBuilder(URI uri, WebSocket.Listener listener) method of the HttpClient class returns an instance of the WebSocket.Builder interface.

When events occur on a client endpoint, for example, the completion of the opening handshake, a message arrival, closing handshake, etc., notifications are sent to a listener, which is an instance of the WebSocket.Listener interface. The interface contains a default method for each type of notification. You will need to create a class that implements this interface. You implement only those methods that correspond to the events of which you are interested in receiving notifications. You need to specify a listener when you create a WebSocket instance.

When you send a close message to a peer, you may specify a close status code. The WebSocket interface contains the following constants of type int that can be used as the WebSocket Close message status code:

  • CLOSED_ABNORMALLY: Represents a WebSocket Close message status code (1006), which means that the connection was closed abnormally, for example, without sending or receiving a Close message.

  • NORMAL_CLOSURE: Represents a WebSocket Close message status code (1000), which means that the connection was closed normally. This means that the purpose for which the connection was established was fulfilled.

The server endpoint may send partial messages. Messages are marked as first, part, last, or whole, which indicates their position. The WebSocket.MessagePart enum defines four constants corresponding to a message’s position: FIRST, PART, LAST, and WHOLE. You will receive these values as part of a message when your listener receives a notification that a message has been received.

The following sections describe each step in setting up a client endpoint in detail.

Creating a Listener

A listener is an instance of the WebSocket.Listener interface. Creating a listener involves creating a class that implements this interface. The interface contains the following default methods:

  • CompletionStage<?> onBinary(WebSocket webSocket, ByteBuffer message, WebSocket.MessagePart part)

  • CompletionStage<?> onClose(WebSocket webSocket, int statusCode, String reason)

  • void onError(WebSocket webSocket, Throwable error)

  • void onOpen(WebSocket webSocket)

  • CompletionStage<?> onPing(WebSocket webSocket, ByteBuffer message)

  • CompletionStage<?> onPong(WebSocket webSocket, ByteBuffer message)

  • CompletionStage<?> onText(WebSocket webSocket, CharSequence message, WebSocket.MessagePart part)

The onOpen() method is called when the client endpoint is connected to the peer whose reference is passed to this method as the first argument. The default implementation requests one message, which means that this listener can receive one more message. The request for messages is made using the request(long n) method of the WebSocket interface:

// Allow one more message to be received
webSocket.request(1);
Tip

If a server sends more messages than requested, messages are queued on the TCP connection and may eventually force the sender to stop sending more messages through TCP flow control. It is important that you call the request(long n) method at the appropriate time with an appropriate argument value, so your listener keeps receiving messages from the server. It is a common mistake to override the onOpen() method in your listener and not call the webSocket.request(1) method, which prevents you from receiving messages from the server .

The onClose() method is called when the endpoint receives a close message from the peer. It is the last notification to the listener. An exception thrown from this method is ignored. The default implementation does not do anything. Typically, you need to send a close message to the peer to complete the closing handshake.

The onPing() method is called when this endpoint receives a Ping message from the peer. A Ping message can be sent by both client and server endpoints. The default implementation sends a Pong message with the same message contents to the peer.

The onPong() method is called when the endpoint receives a Pong message from the peer. A Pong message is typically received as a response to a previously sent Ping message. An endpoint may also receive an unsolicited Pong message. The default implementation of the onPong() method requests one more message on the listener and performs no other actions.

The onError() method is called when an I/O or protocol error occurs on the WebSocket. An exception thrown from this method is ignored. The listener receives no more notifications after this method is called. The default implementation does nothing.

The onBinary() and onText() methods are called when a binary message and a text message are received from the peer, respectively. Make sure to check the last argument of these methods, which indicates the position of the message. If you receive partial messages, you will need to assemble them to get the whole message. Returning null from these methods indicates that the message processing is complete. Otherwise, a CompletionStage<?> is returned and it completes when the message processing is complete .

The following snippet of code creates a WebSocket listener that is ready to receive text messages:

WebSocket.Listener listener =  new WebSocket.Listener() {
    @Override
    public CompletionStage<?> onText(WebSocket webSocket,
                                     CharSequence message,
                                     WebSocket.MessagePart part) {


        // Allow one message to be received by the listener
        webSocket.request(1);
        // Print the message received from the server
        System.out.println("Server: " + message);
        // Return null indicating that we are done processing this message
        return null;
     }
};

Building an Endpoint

You need to build an instance of the WebSocket interface that acts as a client endpoint. The instance is used to connect and exchange messages with the server endpoint. A WebSocket instance is built using a WebSocket.Builder. You can use the following method of the HttpClient class to get a builder:

WebSocket.Builder newWebSocketBuilder(URI uri, WebSocket.Listener listener)                  

The HttpClient instance used to obtain the WebSocket builder supplies the connection configuration for the WebSocket. The specified uri is the URI of the server endpoint. The listener is a listener for the endpoint being built, as described in the previous section. Once you have a builder, you can call one of the following methods of the builder to configure the endpoint:

  • WebSocket.Builder connectTimeout(Duration timeout)

  • WebSocket.Builder header(String name, String value)

  • WebSocket.Builder subprotocols(String mostPreferred, String... lesserPreferred)

The connectTimeout() method lets you specify a timeout for the opening handshake. If the opening handshake does not complete within the specified duration, the CompletableFuture returned from the buildAsync() method of the WebSocket.Builder completes exceptionally with an HttpTimeoutException. You can add any custom headers for the opening handshake using the header() method. You can specify a request for given sub-protocols during the opening handshake using the subprotocols() method—only one of them will be selected by the server. The sub-protocols are defined by the application. The client and the server need to agree to work on specific sub-protocols and their details.

Finally, call the buildAsync() method of the WebSocket.Builder interface to build the endpoint. It returns CompletableFuture<WebSocket>, which completes normally when this endpoint is connected to the server endpoint; it completes exceptionally when there was an error. The following snippet of code shows how to build and connect a client endpoint. Notice that the URI for the server starts with ws, which indicates the WebSocket protocol.

URI serverUri = new URI("ws://localhost:8080/webapp/servertime");

// Get a listener
WebSocket.Listener listener = ...;
// Build an endpoint using the default HttpClient
HttpClient.newHttpClient()
          .newWebSocketBuilder(serverUri, listener)
          .buildAsync()
          .whenComplete((WebSocket webSocket, Throwable t) -> {
               // More code goes here
           });

Sending Messages to a Peer

Once a client endpoint is connected to a peer , both exchange messages. An instance of the WebSocket interface represents a client endpoint and the interface contains the following methods to send messages to a peer:

  • CompletableFuture<WebSocket> sendBinary(ByteBuffer message, boolean isLast)

  • CompletableFuture<WebSocket> sendClose()

  • CompletableFuture<WebSocket> sendClose(int statusCode, String reason)

  • CompletableFuture<WebSocket> sendPing(ByteBuffer message)

  • CompletableFuture<WebSocket> sendPong(ByteBuffer message)

  • CompletableFuture<WebSocket> sendText(CharSequence message)

  • CompletableFuture<WebSocket> sendText(CharSequence message, boolean isLast)

The sendText() method is used to send a text message to the peer. If you are sending a partial message, use the version of this method that takes two arguments. If the second argument is false, it indicates part of the partial message. If the second argument is true, it indicates the last part of a partial message. If there were no partial messages sent before, a true in the second argument indicates a whole message. The sendText(CharSequence message) is a convenience method that calls the second version of the method using true as the second argument.

The sendBinary() method sends a binary message to the peer.

The sendPing() and sendPong() methods send a Ping and a Pong message to the peer, respectively.

The sendClose() method sends a Close message to the peer. You can send a Close message as part of a closing handshake initiated by a peer or you can send it to initiate a closing handshake with the peer .

Tip

If you want to close the WebSocket abruptly, use the abort() method of the WebSocket interface.

Running the WebSocket Program

It is time to see a WebSocket client endpoint and a WebSocket server endpoint exchanging messages. Listing 14-5 contains code for a class named WebSocketClient that encapsulates a client endpoint . Its intended use is as follows:

// Create a client WebSocket
WebSocketClient wsClient = new WebSocketClient(new URI(“<server-uri>”));
// Connect to the server and exchange messages
wsClient.connect();
Listing 14-5. A Class That Encapsulates a Client Endpoint and Its Operations
// WebSocketClient.java
package com.jdojo.http.client;


import java.net.URI;
import java.util.concurrent.CompletionStage;
import jdk.incubator.http.HttpClient;
import jdk.incubator.http.WebSocket;


public class WebSocketClient {
    private WebSocket webSocket;
    private final URI serverUri;
    private boolean inError = false;
    public WebSocketClient(URI serverUri) {
        this.serverUri = serverUri;
    }


    public boolean isClosed() {
        return (webSocket != null && webSocket.isClosed())
               ||
               this.inError;        
    }


    public void connect() {
        HttpClient.newHttpClient()
                  .newWebSocketBuilder(serverUri, this.getListener())
                  .buildAsync()
                  .whenComplete(this::statusChanged);
    }


    private void statusChanged(WebSocket webSocket, Throwable t) {
        this.webSocket = webSocket;
        if (t == null) {        
            this.talkToServer();
        } else {
            this.inError = true;
            System.out.println("Could not connect to the server." +
                               " Error: " + t.getMessage());
        }
    }
    private void talkToServer() {
        // Allow one message to be received by the listener
        webSocket.request(1);
        // Send the server a request for time
        webSocket.sendText("Hello");
    }


    private WebSocket.Listener getListener() {
        return new WebSocket.Listener() {
            @Override
            public void onOpen(WebSocket webSocket) {
                // Allow one more message to be received by the listener
                webSocket.request(1);
                // Notify the user that we are connected
                System.out.println("A WebSocket has been opened.");                
            }


            @Override
            public CompletionStage<?> onClose(WebSocket webSocket,
                             int statusCode, String reason) {
                // Server closed the web socket. Let us respond to
                // the close message from the server
                webSocket.sendClose();
                System.out.println("The WebSocket is closed." +
                                   " Close Code: " + statusCode +
                                   ", Close Reason: " + reason);
                // Return null indicating that this WebSocket
                // can be closed immediately
                return null;
            }
            @Override
            public void onError(WebSocket webSocket, Throwable t) {
                System.out.println("An error occurred: " + t.getMessage());
            }


            @Override
            public CompletionStage<?> onText(WebSocket WebSocket,
                CharSequence message, WebSocket.MessagePart part) {


                // Allow one more message to be received by the listener
                webSocket.request(1);
                // Print the message received from the server
                System.out.println("Server: " + message);
                // Return null indicating that we are done
                // processing this message
                return null;
            }
        };
    }
}

The WebSocketClient class works as follows:

  • The webSocket instance variable holds the reference of the client endpoint.

  • The serverUri instance variable stores the URI of the server endpoint.

  • The isError instance variable stores an indicator whether this endpoint is in error or not.

  • The isClosed() method checks whether the endpoint is already closed or is in error.

  • The webSocket instance variable will be null until the opening handshake is successful. Its value is updated inside the statusChanged() method.

  • The connect() method builds a WebSocket and starts an opening handshake. Note that it calls the statusChanged() method when the opening handshake is complete, irrespective of the connection status.

  • The statusChanged() method talks to the server by calling the talkToServer() method when the opening handshake is successful. Otherwise, it prints an error message and sets the isError flag to true.

  • The talkToServer() method allows for one more message to be received by the listener and sends a text message to the server endpoint. Note that the server endpoint sends three messages with a five-second interval when it receives a text message from a client endpoint. Sending this message from the talkToServer() method initiates the message exchange between the two endpoints.

  • The getListener() method creates and returns a WebSocket.Listener instance. The server endpoint will send three messages followed by a close message. The onClose() method in the listener responds to the close message from the server by sending an empty close message, which will end the client endpoint operations.

Listing 14-6 contains the program to run the client endpoint. If you run the WebSocketClientTest class, make sure that your web application with the server endpoint is running. You will also need to modify the SERVER_URI static variable to match the URI of the server endpoint for your web application. Refer to the next section, “Troubleshooting the WebSocket Application,” if you do not get output similar to the one shown. The output prints the current date and time with the time zone, so you may get different output.

Listing 14-6. A Program to Run the Client Endpoint
// WebSocketClientTest.java
package com.jdojo.http.client;


import java.net.URI;
import java.net.URISyntaxException;
import java.util.concurrent.TimeUnit;


public class WebSocketClientTest {    
    // Please change the URI to point to your server endpoint
    static final String SERVER_URI ="ws://localhost:8080/webapp/servertime";
    public static void main(String[] args)
       throws URISyntaxException, InterruptedException {
        // Create a client WebSocket
        WebSocketClient wsClient = new WebSocketClient(new URI(SERVER_URI));
        // Connect to the Server
        wsClient.connect();
        // Wait until the WebSocket is closed
        while(!wsClient.isClosed()) {            
            TimeUnit.SECONDS.sleep(1);
        }


        // Need to exit
        System.exit(0);
    }
}
A WebSocket has been opened.
Server: 2016-12-15T14:19:53.311-06:00[America/Chicago]
Server: 2016-12-15T14:19:58.312-06:00[America/Chicago]
Server: 2016-12-15T14:20:03.313-06:00[America/Chicago]
The WebSocket is closed.  Close Code: 1000, Close Reason: Done

Troubleshooting the WebSocket Application

A number of things can go wrong when you are trying to test the WebSocket application . Table 14-1 lists a few of these problems and their solutions.

Table 14-1. Possible Errors and Solutions for the WebSocket Application

Error Message

Solution

Could not connect to the server. Error: java.net.ConnectException: Connection refused: no further information

Indicates that the web server is not running or your server URI is incorrect. Try running the webserver and checking the server URI, which is specified in the WebSocketClientTest class in its SERVER_URI static variable.

Could not connect to the server. Error: java.net.http.WebSocketHandshakeException: 404: RFC 6455 1.3. Unable to complete handshake; HTTP response status code 404

Indicates that the server URI is not pointing to the correct endpoint on the server. Verify that the value of the SERVER_URI static variable in the WebSocketClientTest class is correct.

A WebSocket has been opened.

Dec 15, 2016 2:58:03 PM java.net.http.WS$1 onError

WARNING: Failing connection java.net.http.WS@162532d6[CONNECTED], reason: 'RFC 6455 7.2.1. Stream ended before a Close frame has been received'

An error occurred: null

Indicates that after the opening handshake the server is automatically closing the server endpoint. This is typically done by an antivirus program running on your machine. You need to configure your antivirus program to allow the HTTP connections on the specified port or run the web server with an HTTP listener on another port that is not blocked by your antivirus program.

A WebSocket has been opened.

Server: 2016-12-16T07:15:04.586-06:00[America/Chicago]

In this case, the application prints one or two lines of output and waits forever. This happens when you do not have webSocket.request(1) calls in your client endpoint logic. The server is sending messages, which are queued because you have not allowed for more messages. Calling the request(n) method in the onOpen, onText, and other events fixes this problem.

Summary

JDK 9 added an HTTP/2 Client API that lets you work with HTTP requests and responses in Java applications. The API provides classes and interfaces to develop WebSocket client endpoints with authentication and TLS. The API is in the jdk.incubator.http package, which is in the jdk.incubator.httpclient module.

Three abstract classes, HttpClient, HttpRequest, and HttpResponse, and the WebSocket interface are central to the HTTP/2 Client API. Instances of these types are created using builders. The HttpClient class is immutable. An instance of the HttpClient class stores HTTP connection configurations that can be reused for multiple HTTP requests. An instance of the HttpRequest class represents an HTTP request. An instance of the HttpResponse class represents an HTTP response received from a server. You can send and receive HTTP requests and responses synchronously or asynchronously.

An instance of the WebSocket interface represents a WebSocket client endpoint. Communication with the WebSocket server endpoint is accomplished asynchronously. The WebSocket API is event-based. You need to specify a listener, which is an instance of the WebSocket.Listener interface, for a WebSocket client endpoint. The listener is notified—by invoking its appropriate methods—when an event occurs on the endpoint, for example, the listener is notified when the opening handshake with a peer is completed successfully by calling the onOpen() method of the listener. The API supports exchanging text as well as binary messages with peers. Messages can be exchanged in parts.

..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset
3.144.3.43