JDK Httpclient usage and performance test

Posted by JehovahsWord on Tue, 04 Jan 2022 11:47:37 +0100

Httpclient usage and performance testing

In the first part, through the introduction and architecture diagram, we have a preliminary understanding of HttpClient.
In this article, we show the simple use of httpclient. At the same time, in order to illustrate the performance of httpclient, we compare the synchronous and asynchronous modes of httpclient with httpclient 4 of apache..

1. HttpClient sample code

The following are basically official examples showing how to use Get and Post requests respectively.

HttpClient client = HttpClient.newBuilder()
    .version(Version.HTTP_1_1)	//You can manually specify the version of the client. If not, the default is Http2
    .followRedirects(Redirect.NORMAL)	//Set redirection policy
    .connectTimeout(Duration.ofSeconds(20))	//Connection timeout
    .proxy(ProxySelector.of(new InetSocketAddress("proxy.example.com", 80)))	//Proxy address settings
    .authenticator(Authenticator.getDefault()) 
    //. executor(Executors.newFixedThreadPoolExecutor(8)) / / thread pool can be configured manually
    .build();   

HttpRequest request = HttpRequest.newBuilder()       
    .uri(URI.create("https://foo.com / ") / / set url address
    .GET()
    .build();
HttpResponse<String> response = client.send(request, BodyHandlers.ofString());  	//Synchronous transmission
System.out.println(response.statusCode()); 	//Print response status code
System.out.println(response.body());  


HttpRequest request = HttpRequest.newBuilder()
       .uri(URI.create("https://foo.com/")) 
       .timeout(Duration.ofMinutes(2))	//Set connection timeout
       .header("Content-Type", "application/json")	
       .POST(BodyPublishers.ofFile(Paths.get("file.json")))    //Set request body source
       .build();   
client.sendAsync(request, BodyHandlers.ofString()) 		//Asynchronous transmission
    .thenApply(HttpResponse::body) 	//Send end print response body
    .thenAccept(System.out::println);  

As you can see, the code written by the application is relatively smooth and natural. However, there are a few points to note

  • Http connection pool does not support manual configuration. It is infinitely multiplexed by default
  • The number of retries does not support manual configuration
  • If the Http client or requested version is not specified, the Http 2 mode will be used for connection by default, and will be degraded after frustration
  • The synchronous send mode (send) of the request actually opens another thread in the background

Just a few lines of code only realize the function, so how about its performance? We compare it with Apache's HttpClient, an industry benchmark.

2. Server test code writing

For simplicity, use node JS http module runs a simple server. The server is stationed at port 8080. Every time a request is received, it will stay for 500ms and return a response.

let http = require("http")
let server = http.createServer()
server.addListener("request", (req, res) => {
    if (req.url.startsWith("/")) {
        //After receiving any request, stay for 0.5 seconds and return
        setTimeout(() => {
            res.end("haha")
        }, 500)
    }
}
)
server.listen(8080, () => console.log("Start successfully!"))

Run the js file with node and you will be prompted that it has been started successfully

3. JDK httpclient and apache Httpclient test code

First, define the common test interface:

public interface Tester {

    //Test parameters
    class TestCommand {

    }

    /**
     * Test master method
     * @param testCommand Test parameters
     */
    void test(TestCommand testCommand) throws Exception;

    /**
     * Repeat the test several times
     * @param testName Test name
     * @param times Number of tests
     * @param testCommand Parameters for each test
     */
    default void testMultipleTimes(String testName, int times, TestCommand testCommand) throws Exception{
        long startTime = System.currentTimeMillis();
        System.out.printf(" ----- %s Start, total%s second -----\n", testName, times);
        for (int i = 0; i < times; i++) {
            long currentStartTime = System.currentTimeMillis();
            test(testCommand);
            System.out.printf("The first%s Test time:%sms\n", i + 1, (System.currentTimeMillis() - currentStartTime));
        }
        long usedTime = System.currentTimeMillis() - startTime;
        System.out.printf("%s When shared by the next test:%sms,Average time:%sms\n", times, usedTime, usedTime / times);
    }
}

Define the test class, including three static nested classes, which are used as asynchronous mode, synchronous mode of JDK httpclient and synchronous mode of apache Httpclient respectively

public class HttpClientTester {

    /** Http Requested true test parameters*/
    static class HttpTestCommand extends Tester.TestCommand {

        /**Destination url*/
        String url;
        /**Number of single test requests*/
        int requestTimes;
        /**Number of request threads*/
        int threadCount;

        public HttpTestCommand(String url, int requestTimes, int threadCount) {
            this.url = url;
            this.requestTimes = requestTimes;
            this.threadCount = threadCount;
        }
    }


    static class BlocklyHttpClientTester implements Tester {

        @Override
        public void test(TestCommand testCommand) throws Exception {
            HttpTestCommand httpTestCommand = (HttpTestCommand) testCommand;
            testBlockly(httpTestCommand.url, httpTestCommand.requestTimes,httpTestCommand.threadCount);
        }

        /**
         * Test using the synchronization mode of JDK Httpclient
         * @param url Requested url
         * @param times Number of requests
         * @param threadCount Number of threads open
         * @throws ExecutionException
         * @throws InterruptedException
         */
        void testBlockly(String url, int times, int threadCount) throws ExecutionException, InterruptedException {
            threadCount = threadCount <= 0 ? 1 : threadCount;
            ExecutorService executorService = Executors.newFixedThreadPool(threadCount);
            HttpClient client = HttpClient.newBuilder().build();
            Callable<String> callable1 = () -> {
                HttpRequest request = HttpRequest.newBuilder(URI.create(url)).GET().build();
                HttpResponse<String> response = client.send(request, HttpResponse.BodyHandlers.ofString());
                return response.body();
            };
            List<Future<String>> futureList1 = new ArrayList<>();
            for (int i = 0; i < times; i++) {
                Future<String> future1 = executorService.submit(callable1);
                futureList1.add(future1);
            }
            for (Future<String> stringFuture : futureList1) {
                //Block until all requests return
                String s = stringFuture.get();
            }
            executorService.shutdown();
        }
    }


    static class NonBlocklyHttpClientTester implements Tester {


        @Override
        public void test(TestCommand testCommand) throws Exception {
            HttpTestCommand httpTestCommand = (HttpTestCommand) testCommand;
            testNonBlockly(httpTestCommand.url, httpTestCommand.requestTimes);
        }

        /**
         * Test using the asynchronous mode of JDK Httpclient
         * @param url Requested url
         * @param times Number of requests
         * @throws InterruptedException
         */
        void testNonBlockly(String url, int times) throws InterruptedException {
            //Given 16 threads, business uses 2 * runtime getRuntime(). availableProcessors()
            ExecutorService executor = Executors.newFixedThreadPool(16);
            HttpClient client = HttpClient.newBuilder()
                    .executor(executor)
                    .build();
            //Use the countdown lock to ensure that all requests are completed
            CountDownLatch countDownLatch = new CountDownLatch(times);
            HttpRequest request = HttpRequest.newBuilder(URI.create(url)).GET().build();
            while (times-- >= 0) {
                client.sendAsync(request, HttpResponse.BodyHandlers.ofString())
                        .whenComplete((stringHttpResponse, throwable) -> {
                            if (throwable != null) {
                                throwable.printStackTrace();
                            }
                            if (stringHttpResponse != null) {
                                stringHttpResponse.body();
                            }
                            countDownLatch.countDown();
                        });
            }

            //Block until all requests are completed
            countDownLatch.await();
            executor.shutdown();
        }
    }

    static class ApacheHttpClientTester implements Tester {

        @Override
        public void test(TestCommand testCommand) throws Exception {
            HttpTestCommand httpTestCommand = (HttpTestCommand) testCommand;
            testBlocklyWithApacheClient(httpTestCommand.url, httpTestCommand.requestTimes,httpTestCommand.threadCount);
        }
        /**
         * Testing with Apache HttpClient
         * @param url Requested url
         * @param times Service duration
         * @param threadCount Number of threads open
         * @throws ExecutionException
         * @throws InterruptedException
         */
        void testBlocklyWithApacheClient(String url, int times, int threadCount) throws ExecutionException, InterruptedException {

            threadCount = threadCount <= 0 ? 1 : threadCount;
            ExecutorService executorService = Executors.newFixedThreadPool(threadCount);
            PoolingHttpClientConnectionManager connectionManager = new PoolingHttpClientConnectionManager();
            //Set unlimited connection reuse of apache Httpclient to reflect its maximum performance
            connectionManager.setDefaultMaxPerRoute(Integer.MAX_VALUE);
            connectionManager.setMaxTotal(Integer.MAX_VALUE);
            CloseableHttpClient httpClient = HttpClientBuilder.create().setConnectionManager(connectionManager).build();
            Callable<String> callable1 = () -> {
                HttpGet httpGet = new HttpGet(url);
                CloseableHttpResponse response = httpClient.execute(httpGet);
                return EntityUtils.toString(response.getEntity());
            };
            List<Future<String>> futureList1 = new ArrayList<>();
            for (int i = 0; i < times; i++) {
                Future<String> future1 = executorService.submit(callable1);
                futureList1.add(future1);
            }
            for (Future<String> stringFuture : futureList1) {
                //Block until all requests return
                String s = stringFuture.get();
            }
            executorService.shutdown();
        }
    }

main method of test:

    public static void main(String[] args) {
        try {
            //
            HttpTestCommand testCommand = new HttpTestCommand("http://localhost:8080", 200, 16);
            //Repeat each test for 3 rounds to reduce errors
            final int testTimes = 3;
            new BlocklyHttpClientTester().testMultipleTimes("JDK HttpClient Synchronous mode test", testTimes, testCommand);
            new NonBlocklyHttpClientTester().testMultipleTimes("JDK HttpClient Asynchronous mode test", testTimes, testCommand);
            new ApacheHttpClientTester().testMultipleTimes("Apache Httpclient Synchronous mode test", testTimes, testCommand);
        } catch (Exception e) {
            e.printStackTrace();
        }
    }

4. Test results

-----JDK HttpClient synchronization mode test starts for 3 times-----
Time for the first test: 4414ms
Time for the second test: 3580ms
Time for the third test: 3620ms
Shared time for 3 tests: 11620ms, average time: 3873ms
-----JDK HttpClient asynchronous mode test started for 3 times-----
Time for the first test: 568ms
Time for the second test: 595ms
Time for the third test: 579ms
Shared time for 3 tests: 1742ms, average time: 580ms
-----Apache Httpclient synchronization mode test started for 3 times-----
Time for the first test: 3719ms
Time for the second test: 3557ms
Time for the third test: 3574ms
Shared time for 3 tests: 10851ms, average time: 3617ms

It can be seen that the performance of Httpclient synchronization mode is close to that of Apache Httpclient synchronization mode; Asynchronous mode makes full use of the non blocking characteristics of nio, and its efficiency is much better than synchronous mode when the number of threads is the same.

It should be noted that "synchronous" and "asynchronous" here are not synchronous in the I/O model, but synchronous / asynchronous in programming mode.

5. Summary

From the above example code, we can see that HttpClient has the characteristics of smooth writing and excellent performance, and it also has the regret of insufficient customizability.

In the next section, we will go deep into the construction and startup process of the client, contact the role of selector manager, and explore its interaction process with the Socket channel.

Topics: Java http