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() {
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() {
117         client.start();
118     }
119 
120     void shutdown() {
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<>(new BasicHttpRequest(Method.GET, target, "/headers")),
289                     new Message<>(
290                             new BasicHttpRequest(Method.POST, target, "/anything"),
291                             new StringAsyncEntityProducer("some important message", ContentType.TEXT_PLAIN)),
292                     new Message<>(
293                             new BasicHttpRequest(Method.PUT, target, "/anything"),
294                             new StringAsyncEntityProducer("some important message", ContentType.TEXT_PLAIN)),
295                     new Message<>(new BasicHttpRequest(Method.GET, target, "/drip")),
296                     new Message<>(new BasicHttpRequest(Method.GET, target, "/bytes/20000")),
297                     new Message<>(new BasicHttpRequest(Method.GET, target, "/delay/2")),
298                     new Message<>(
299                             new BasicHttpRequest(Method.POST, target, "/delay/2"),
300                             new StringAsyncEntityProducer("some important message", ContentType.TEXT_PLAIN)),
301                     new Message<>(
302                             new BasicHttpRequest(Method.PUT, target, "/delay/2"),
303                             new StringAsyncEntityProducer("some important message", ContentType.TEXT_PLAIN))
304             );
305 
306             for (final Message<HttpRequest, AsyncEntityProducer> message : requestMessages) {
307                 final CountDownLatch countDownLatch = new CountDownLatch(1);
308                 final HttpRequest request = message.getHead();
309                 final AsyncEntityProducer entityProducer = message.getBody();
310                 client.execute(
311                         new BasicRequestProducer(request, entityProducer),
312                         new BasicResponseConsumer<>(new StringAsyncEntityConsumer(CharCodingConfig.custom()
313                                 .setCharset(StandardCharsets.US_ASCII)
314                                 .setMalformedInputAction(CodingErrorAction.IGNORE)
315                                 .setUnmappableInputAction(CodingErrorAction.REPLACE)
316                                 .build())),
317                         TIMEOUT,
318                         new FutureCallback<Message<HttpResponse, String>>() {
319 
320                             @Override
321                             public void completed(final Message<HttpResponse, String> responseMessage) {
322                                 final HttpResponse response = responseMessage.getHead();
323                                 final int code = response.getCode();
324                                 if (code == HttpStatus.SC_OK) {
325                                     logResult(TestResult.OK, target, request, response,
326                                             Objects.toString(response.getFirstHeader("server")));
327                                 } else {
328                                     logResult(TestResult.NOK, target, request, response, "(status " + code + ")");
329                                 }
330                                 countDownLatch.countDown();
331                             }
332 
333                             @Override
334                             public void failed(final Exception ex) {
335                                 logResult(TestResult.NOK, target, request, null, "(" + ex.getMessage() + ")");
336                                 countDownLatch.countDown();
337                             }
338 
339                             @Override
340                             public void cancelled() {
341                                 logResult(TestResult.NOK, target, request, null, "(cancelled)");
342                                 countDownLatch.countDown();
343                             }
344                         });
345                 if (!countDownLatch.await(TIMEOUT.getDuration(), TIMEOUT.getTimeUnit())) {
346                     logResult(TestResult.NOK, target, null, null, "(httpbin.org tests failed to complete in time)");
347                 }
348             }
349         }
350         {
351             System.out.println("*** httpbin.org HTTP/1.1 pipelined request execution ***");
352 
353             final Future<AsyncClientEndpoint> connectFuture = client.connect(target, TIMEOUT);
354             final AsyncClientEndpoint streamEndpoint = connectFuture.get();
355 
356             final int n = 10;
357             final CountDownLatch countDownLatch = new CountDownLatch(n);
358             for (int i = 0; i < n; i++) {
359 
360                 final HttpRequest request;
361                 final AsyncEntityProducer entityProducer;
362                 if (i % 2 == 0) {
363                     request = new BasicHttpRequest(Method.GET, target, "/headers");
364                     entityProducer = null;
365                 } else {
366                     request = new BasicHttpRequest(Method.POST, target, "/anything");
367                     entityProducer = new StringAsyncEntityProducer("some important message", ContentType.TEXT_PLAIN);
368                 }
369 
370                 streamEndpoint.execute(
371                         new BasicRequestProducer(request, entityProducer),
372                         new BasicResponseConsumer<>(new StringAsyncEntityConsumer(CharCodingConfig.custom()
373                                 .setCharset(StandardCharsets.US_ASCII)
374                                 .setMalformedInputAction(CodingErrorAction.IGNORE)
375                                 .setUnmappableInputAction(CodingErrorAction.REPLACE)
376                                 .build())),
377                         new FutureCallback<Message<HttpResponse, String>>() {
378 
379                             @Override
380                             public void completed(final Message<HttpResponse, String> responseMessage) {
381                                 final HttpResponse response = responseMessage.getHead();
382                                 final int code = response.getCode();
383                                 if (code == HttpStatus.SC_OK) {
384                                     logResult(TestResult.OK, target, request, response,
385                                             "pipelined / " + response.getFirstHeader("server"));
386                                 } else {
387                                     logResult(TestResult.NOK, target, request, response, "(status " + code + ")");
388                                 }
389                                 countDownLatch.countDown();
390                             }
391 
392                             @Override
393                             public void failed(final Exception ex) {
394                                 logResult(TestResult.NOK, target, request, null, "(" + ex.getMessage() + ")");
395                                 countDownLatch.countDown();
396                             }
397 
398                             @Override
399                             public void cancelled() {
400                                 logResult(TestResult.NOK, target, request, null, "(cancelled)");
401                                 countDownLatch.countDown();
402                             }
403                         });
404             }
405             if (!countDownLatch.await(TIMEOUT.getDuration(), TIMEOUT.getTimeUnit())) {
406                 logResult(TestResult.NOK, target, null, null, "(httpbin.org tests failed to complete in time)");
407             }
408         }
409     }
410 
411     enum TestResult {OK, NOK}
412 
413     private void logResult(
414             final TestResult result,
415             final HttpHost httpHost,
416             final HttpRequest request,
417             final HttpResponse response,
418             final String message) {
419         final StringBuilder buf = new StringBuilder();
420         buf.append(result);
421         if (buf.length() == 2) {
422             buf.append(" ");
423         }
424         buf.append(": ").append(httpHost).append(" ");
425         if (response != null) {
426             buf.append(response.getVersion()).append(" ");
427         }
428         if (request != null) {
429             buf.append(request.getMethod()).append(" ").append(request.getRequestUri());
430         }
431         if (message != null && !TextUtils.isBlank(message)) {
432             buf.append(" -> ").append(message);
433         }
434         System.out.println(buf);
435     }
436 
437 }