View Javadoc
1   /*
2    * ====================================================================
3    * Licensed to the Apache Software Foundation (ASF) under one
4    * or more contributor license agreements.  See the NOTICE file
5    * distributed with this work for additional information
6    * regarding copyright ownership.  The ASF licenses this file
7    * to you under the Apache License, Version 2.0 (the
8    * "License"); you may not use this file except in compliance
9    * with the License.  You may obtain a copy of the License at
10   *
11   *   http://www.apache.org/licenses/LICENSE-2.0
12   *
13   * Unless required by applicable law or agreed to in writing,
14   * software distributed under the License is distributed on an
15   * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
16   * KIND, either express or implied.  See the License for the
17   * specific language governing permissions and limitations
18   * under the License.
19   * ====================================================================
20   *
21   * This software consists of voluntary contributions made by many
22   * individuals on behalf of the Apache Software Foundation.  For more
23   * information on the Apache Software Foundation, please see
24   * <http://www.apache.org/>.
25   *
26   */
27  package org.apache.hc.core5.testing.compatibility.http2;
28  
29  import java.io.IOException;
30  import java.nio.charset.CodingErrorAction;
31  import java.nio.charset.StandardCharsets;
32  import java.util.Arrays;
33  import java.util.List;
34  import java.util.Objects;
35  import java.util.concurrent.CountDownLatch;
36  import java.util.concurrent.ExecutionException;
37  import java.util.concurrent.Future;
38  import java.util.concurrent.TimeoutException;
39  
40  import org.apache.hc.core5.concurrent.FutureCallback;
41  import org.apache.hc.core5.http.ContentType;
42  import org.apache.hc.core5.http.HttpException;
43  import org.apache.hc.core5.http.HttpHost;
44  import org.apache.hc.core5.http.HttpRequest;
45  import org.apache.hc.core5.http.HttpResponse;
46  import org.apache.hc.core5.http.HttpStatus;
47  import org.apache.hc.core5.http.Message;
48  import org.apache.hc.core5.http.Method;
49  import org.apache.hc.core5.http.config.CharCodingConfig;
50  import org.apache.hc.core5.http.impl.bootstrap.HttpAsyncRequester;
51  import org.apache.hc.core5.http.message.BasicHttpRequest;
52  import org.apache.hc.core5.http.nio.AsyncClientEndpoint;
53  import org.apache.hc.core5.http.nio.AsyncEntityProducer;
54  import org.apache.hc.core5.http.nio.entity.DiscardingEntityConsumer;
55  import org.apache.hc.core5.http.nio.entity.StringAsyncEntityConsumer;
56  import org.apache.hc.core5.http.nio.entity.StringAsyncEntityProducer;
57  import org.apache.hc.core5.http.nio.support.AbstractAsyncPushHandler;
58  import org.apache.hc.core5.http.nio.support.BasicRequestProducer;
59  import org.apache.hc.core5.http.nio.support.BasicResponseConsumer;
60  import org.apache.hc.core5.http2.HttpVersionPolicy;
61  import org.apache.hc.core5.http2.config.H2Config;
62  import org.apache.hc.core5.http2.impl.nio.bootstrap.H2RequesterBootstrap;
63  import org.apache.hc.core5.io.CloseMode;
64  import org.apache.hc.core5.reactor.IOReactorConfig;
65  import org.apache.hc.core5.testing.classic.LoggingConnPoolListener;
66  import org.apache.hc.core5.testing.nio.LoggingExceptionCallback;
67  import org.apache.hc.core5.testing.nio.LoggingH2StreamListener;
68  import org.apache.hc.core5.testing.nio.LoggingHttp1StreamListener;
69  import org.apache.hc.core5.testing.nio.LoggingIOSessionDecorator;
70  import org.apache.hc.core5.testing.nio.LoggingIOSessionListener;
71  import org.apache.hc.core5.util.TextUtils;
72  import org.apache.hc.core5.util.Timeout;
73  
74  public class H2CompatibilityTest {
75  
76      private final HttpAsyncRequester client;
77  
78      public static void main(final String... args) throws Exception {
79  
80          final HttpHost[] h2servers = new HttpHost[]{
81                  new HttpHost("http", "localhost", 8080),
82                  new HttpHost("http", "localhost", 8081)
83          };
84  
85          final HttpHost httpbin = new HttpHost("http", "localhost", 8082);
86  
87          final H2CompatibilityTest test = new H2CompatibilityTest();
88          try {
89              test.start();
90              for (final HttpHost h2server : h2servers) {
91                  test.executeH2(h2server);
92              }
93              test.executeHttpBin(httpbin);
94          } finally {
95              test.shutdown();
96          }
97      }
98  
99      H2CompatibilityTest() throws Exception {
100         this.client = H2RequesterBootstrap.bootstrap()
101                 .setIOReactorConfig(IOReactorConfig.custom()
102                         .setSoTimeout(TIMEOUT)
103                         .build())
104                 .setH2Config(H2Config.custom()
105                         .setPushEnabled(true)
106                         .build())
107                 .setStreamListener(LoggingHttp1StreamListener.INSTANCE_CLIENT)
108                 .setStreamListener(LoggingH2StreamListener.INSTANCE)
109                 .setConnPoolListener(LoggingConnPoolListener.INSTANCE)
110                 .setIOSessionDecorator(LoggingIOSessionDecorator.INSTANCE)
111                 .setExceptionCallback(LoggingExceptionCallback.INSTANCE)
112                 .setIOSessionListener(LoggingIOSessionListener.INSTANCE)
113                 .create();
114     }
115 
116     void start() throws Exception {
117         client.start();
118     }
119 
120     void shutdown() throws Exception {
121         client.close(CloseMode.GRACEFUL);
122     }
123 
124     private static final Timeout TIMEOUT = Timeout.ofSeconds(5);
125 
126     void executeH2(final HttpHost target) throws Exception {
127         {
128             System.out.println("*** HTTP/2 simple request execution ***");
129             final Future<AsyncClientEndpoint> connectFuture = client.connect(target, TIMEOUT, HttpVersionPolicy.FORCE_HTTP_2, null);
130             try {
131                 final AsyncClientEndpoint endpoint = connectFuture.get(TIMEOUT.getDuration(), TIMEOUT.getTimeUnit());
132 
133                 final CountDownLatch countDownLatch = new CountDownLatch(1);
134                 final HttpRequest httpget = new BasicHttpRequest(Method.GET, target, "/status.html");
135                 endpoint.execute(
136                         new BasicRequestProducer(httpget, null),
137                         new BasicResponseConsumer<>(new StringAsyncEntityConsumer()),
138                         new FutureCallback<Message<HttpResponse, String>>() {
139 
140                             @Override
141                             public void completed(final Message<HttpResponse, String> responseMessage) {
142                                 final HttpResponse response = responseMessage.getHead();
143                                 final int code = response.getCode();
144                                 if (code == HttpStatus.SC_OK) {
145                                     logResult(TestResult.OK, target, httpget, response,
146                                             Objects.toString(response.getFirstHeader("server")));
147                                 } else {
148                                     logResult(TestResult.NOK, target, httpget, response, "(status " + code + ")");
149                                 }
150                                 countDownLatch.countDown();
151                             }
152 
153                             @Override
154                             public void failed(final Exception ex) {
155                                 logResult(TestResult.NOK, target, httpget, null, "(" + ex.getMessage() + ")");
156                                 countDownLatch.countDown();
157                             }
158 
159                             @Override
160                             public void cancelled() {
161                                 logResult(TestResult.NOK, target, httpget, null, "(cancelled)");
162                                 countDownLatch.countDown();
163                             }
164 
165                         });
166                 if (!countDownLatch.await(TIMEOUT.getDuration(), TIMEOUT.getTimeUnit())) {
167                     logResult(TestResult.NOK, target, null, null, "(single request execution failed to complete in time)");
168                 }
169             } catch (final ExecutionException ex) {
170                 final Throwable cause = ex.getCause();
171                 logResult(TestResult.NOK, target, null, null, "(" + cause.getMessage() + ")");
172             } catch (final TimeoutException ex) {
173                 logResult(TestResult.NOK, target, null, null, "(time out)");
174             }
175         }
176         {
177             System.out.println("*** HTTP/2 multiplexed request execution ***");
178             final Future<AsyncClientEndpoint> connectFuture = client.connect(target, TIMEOUT, HttpVersionPolicy.FORCE_HTTP_2, null);
179             try {
180                 final AsyncClientEndpoint endpoint = connectFuture.get(TIMEOUT.getDuration(), TIMEOUT.getTimeUnit());
181 
182                 final int reqCount = 20;
183                 final CountDownLatch countDownLatch = new CountDownLatch(reqCount);
184                 for (int i = 0; i < reqCount; i++) {
185                     final HttpRequest httpget = new BasicHttpRequest(Method.GET, target, "/status.html");
186                     endpoint.execute(
187                             new BasicRequestProducer(httpget, null),
188                             new BasicResponseConsumer<>(new StringAsyncEntityConsumer()),
189                             new FutureCallback<Message<HttpResponse, String>>() {
190 
191                                 @Override
192                                 public void completed(final Message<HttpResponse, String> responseMessage) {
193                                     final HttpResponse response = responseMessage.getHead();
194                                     final int code = response.getCode();
195                                     if (code == HttpStatus.SC_OK) {
196                                         logResult(TestResult.OK, target, httpget, response,
197                                                 "multiplexed / " + response.getFirstHeader("server"));
198                                     } else {
199                                         logResult(TestResult.NOK, target, httpget, response, "(status " + code + ")");
200                                     }
201                                     countDownLatch.countDown();
202                                 }
203 
204                                 @Override
205                                 public void failed(final Exception ex) {
206                                     logResult(TestResult.NOK, target, httpget, null, "(" + ex.getMessage() + ")");
207                                     countDownLatch.countDown();
208                                 }
209 
210                                 @Override
211                                 public void cancelled() {
212                                     logResult(TestResult.NOK, target, httpget, null, "(cancelled)");
213                                     countDownLatch.countDown();
214                                 }
215 
216                             });
217                 }
218                 if (!countDownLatch.await(TIMEOUT.getDuration(), TIMEOUT.getTimeUnit())) {
219                     logResult(TestResult.NOK, target, null, null, "(multiplexed request execution failed to complete in time)");
220                 }
221             } catch (final ExecutionException ex) {
222                 final Throwable cause = ex.getCause();
223                 logResult(TestResult.NOK, target, null, null, "(" + cause.getMessage() + ")");
224             } catch (final TimeoutException ex) {
225                 logResult(TestResult.NOK, target, null, null, "(time out)");
226             }
227         }
228         {
229             System.out.println("*** HTTP/2 request execution with push ***");
230             final Future<AsyncClientEndpoint> connectFuture = client.connect(target, TIMEOUT, HttpVersionPolicy.FORCE_HTTP_2, null);
231             try {
232                 final AsyncClientEndpoint endpoint = connectFuture.get(TIMEOUT.getDuration(), TIMEOUT.getTimeUnit());
233 
234                 final CountDownLatch countDownLatch = new CountDownLatch(5);
235                 final HttpRequest httpget = new BasicHttpRequest(Method.GET, target, "/index.html");
236                 final Future<Message<HttpResponse, String>> future = endpoint.execute(
237                         new BasicRequestProducer(httpget, null),
238                         new BasicResponseConsumer<>(new StringAsyncEntityConsumer()),
239                         (request, context) -> new AbstractAsyncPushHandler<Message<HttpResponse, Void>>(
240                                 new BasicResponseConsumer<>(new DiscardingEntityConsumer<>())) {
241 
242                             @Override
243                             protected void handleResponse(
244                                     final HttpRequest promise,
245                                     final Message<HttpResponse, Void> responseMessage) throws IOException, HttpException {
246                                 final HttpResponse response = responseMessage.getHead();
247                                 logResult(TestResult.OK, target, promise, response,
248                                         "pushed / " + response.getFirstHeader("server"));
249                                 countDownLatch.countDown();
250                             }
251 
252                             @Override
253                             protected void handleError(
254                                     final HttpRequest promise,
255                                     final Exception cause) {
256                                 logResult(TestResult.NOK, target, promise, null, "(" + cause.getMessage() + ")");
257                                 countDownLatch.countDown();
258                             }
259                         },
260                         null,
261                         null);
262                 final Message<HttpResponse, String> message = future.get(TIMEOUT.getDuration(), TIMEOUT.getTimeUnit());
263                 final HttpResponse response = message.getHead();
264                 final int code = response.getCode();
265                 if (code == HttpStatus.SC_OK) {
266                     logResult(TestResult.OK, target, httpget, response,
267                             Objects.toString(response.getFirstHeader("server")));
268                     if (!countDownLatch.await(TIMEOUT.getDuration(), TIMEOUT.getTimeUnit())) {
269                         logResult(TestResult.NOK, target, null, null, "Push messages not received");
270                     }
271                 } else {
272                     logResult(TestResult.NOK, target, httpget, response, "(status " + code + ")");
273                 }
274             } catch (final ExecutionException ex) {
275                 final Throwable cause = ex.getCause();
276                 logResult(TestResult.NOK, target, null, null, "(" + cause.getMessage() + ")");
277             } catch (final TimeoutException ex) {
278                 logResult(TestResult.NOK, target, null, null, "(time out)");
279             }
280         }
281     }
282 
283     void executeHttpBin(final HttpHost target) throws Exception {
284         {
285             System.out.println("*** httpbin.org HTTP/1.1 simple request execution ***");
286 
287             final List<Message<HttpRequest, AsyncEntityProducer>> requestMessages = Arrays.asList(
288                     new Message<>(
289                             new BasicHttpRequest(Method.GET, target, "/headers"),
290                             null),
291                     new Message<>(
292                             new BasicHttpRequest(Method.POST, target, "/anything"),
293                             new StringAsyncEntityProducer("some important message", ContentType.TEXT_PLAIN)),
294                     new Message<>(
295                             new BasicHttpRequest(Method.PUT, target, "/anything"),
296                             new StringAsyncEntityProducer("some important message", ContentType.TEXT_PLAIN)),
297                     new Message<>(
298                             new BasicHttpRequest(Method.GET, target, "/drip"),
299                             null),
300                     new Message<>(
301                             new BasicHttpRequest(Method.GET, target, "/bytes/20000"),
302                             null),
303                     new Message<>(
304                             new BasicHttpRequest(Method.GET, target, "/delay/2"),
305                             null),
306                     new Message<>(
307                             new BasicHttpRequest(Method.POST, target, "/delay/2"),
308                             new StringAsyncEntityProducer("some important message", ContentType.TEXT_PLAIN)),
309                     new Message<>(
310                             new BasicHttpRequest(Method.PUT, target, "/delay/2"),
311                             new StringAsyncEntityProducer("some important message", ContentType.TEXT_PLAIN))
312             );
313 
314             for (final Message<HttpRequest, AsyncEntityProducer> message : requestMessages) {
315                 final CountDownLatch countDownLatch = new CountDownLatch(1);
316                 final HttpRequest request = message.getHead();
317                 final AsyncEntityProducer entityProducer = message.getBody();
318                 client.execute(
319                         new BasicRequestProducer(request, entityProducer),
320                         new BasicResponseConsumer<>(new StringAsyncEntityConsumer(CharCodingConfig.custom()
321                                 .setCharset(StandardCharsets.US_ASCII)
322                                 .setMalformedInputAction(CodingErrorAction.IGNORE)
323                                 .setUnmappableInputAction(CodingErrorAction.REPLACE)
324                                 .build())),
325                         TIMEOUT,
326                         new FutureCallback<Message<HttpResponse, String>>() {
327 
328                             @Override
329                             public void completed(final Message<HttpResponse, String> responseMessage) {
330                                 final HttpResponse response = responseMessage.getHead();
331                                 final int code = response.getCode();
332                                 if (code == HttpStatus.SC_OK) {
333                                     logResult(TestResult.OK, target, request, response,
334                                             Objects.toString(response.getFirstHeader("server")));
335                                 } else {
336                                     logResult(TestResult.NOK, target, request, response, "(status " + code + ")");
337                                 }
338                                 countDownLatch.countDown();
339                             }
340 
341                             @Override
342                             public void failed(final Exception ex) {
343                                 logResult(TestResult.NOK, target, request, null, "(" + ex.getMessage() + ")");
344                                 countDownLatch.countDown();
345                             }
346 
347                             @Override
348                             public void cancelled() {
349                                 logResult(TestResult.NOK, target, request, null, "(cancelled)");
350                                 countDownLatch.countDown();
351                             }
352                         });
353                 if (!countDownLatch.await(TIMEOUT.getDuration(), TIMEOUT.getTimeUnit())) {
354                     logResult(TestResult.NOK, target, null, null, "(httpbin.org tests failed to complete in time)");
355                 }
356             }
357         }
358         {
359             System.out.println("*** httpbin.org HTTP/1.1 pipelined request execution ***");
360 
361             final Future<AsyncClientEndpoint> connectFuture = client.connect(target, TIMEOUT);
362             final AsyncClientEndpoint streamEndpoint = connectFuture.get();
363 
364             final int n = 10;
365             final CountDownLatch countDownLatch = new CountDownLatch(n);
366             for (int i = 0; i < n; i++) {
367 
368                 final HttpRequest request;
369                 final AsyncEntityProducer entityProducer;
370                 if (i % 2 == 0) {
371                     request = new BasicHttpRequest(Method.GET, target, "/headers");
372                     entityProducer = null;
373                 } else {
374                     request = new BasicHttpRequest(Method.POST, target, "/anything");
375                     entityProducer = new StringAsyncEntityProducer("some important message", ContentType.TEXT_PLAIN);
376                 }
377 
378                 streamEndpoint.execute(
379                         new BasicRequestProducer(request, entityProducer),
380                         new BasicResponseConsumer<>(new StringAsyncEntityConsumer(CharCodingConfig.custom()
381                                 .setCharset(StandardCharsets.US_ASCII)
382                                 .setMalformedInputAction(CodingErrorAction.IGNORE)
383                                 .setUnmappableInputAction(CodingErrorAction.REPLACE)
384                                 .build())),
385                         new FutureCallback<Message<HttpResponse, String>>() {
386 
387                             @Override
388                             public void completed(final Message<HttpResponse, String> responseMessage) {
389                                 final HttpResponse response = responseMessage.getHead();
390                                 final int code = response.getCode();
391                                 if (code == HttpStatus.SC_OK) {
392                                     logResult(TestResult.OK, target, request, response,
393                                             "pipelined / " + response.getFirstHeader("server"));
394                                 } else {
395                                     logResult(TestResult.NOK, target, request, response, "(status " + code + ")");
396                                 }
397                                 countDownLatch.countDown();
398                             }
399 
400                             @Override
401                             public void failed(final Exception ex) {
402                                 logResult(TestResult.NOK, target, request, null, "(" + ex.getMessage() + ")");
403                                 countDownLatch.countDown();
404                             }
405 
406                             @Override
407                             public void cancelled() {
408                                 logResult(TestResult.NOK, target, request, null, "(cancelled)");
409                                 countDownLatch.countDown();
410                             }
411                         });
412             }
413             if (!countDownLatch.await(TIMEOUT.getDuration(), TIMEOUT.getTimeUnit())) {
414                 logResult(TestResult.NOK, target, null, null, "(httpbin.org tests failed to complete in time)");
415             }
416         }
417     }
418 
419     enum TestResult {OK, NOK}
420 
421     private void logResult(
422             final TestResult result,
423             final HttpHost httpHost,
424             final HttpRequest request,
425             final HttpResponse response,
426             final String message) {
427         final StringBuilder buf = new StringBuilder();
428         buf.append(result);
429         if (buf.length() == 2) {
430             buf.append(" ");
431         }
432         buf.append(": ").append(httpHost).append(" ");
433         if (response != null) {
434             buf.append(response.getVersion()).append(" ");
435         }
436         if (request != null) {
437             buf.append(request.getMethod()).append(" ").append(request.getRequestUri());
438         }
439         if (message != null && !TextUtils.isBlank(message)) {
440             buf.append(" -> ").append(message);
441         }
442         System.out.println(buf);
443     }
444 
445 }