上篇,通过简介和架构图,我们对HttpClient有了初步的了解。
本篇我们展示HttpClient的简单使用,同时为了说明httpclient的使用性能,我们将Httpclient的同步和异步模式与apache的Httpclient4作比较。。
以下基本是官方示例,分别展示了如何使用Get和Post请求。
HttpClient client = HttpClient.newBuilder() .version(Version.HTTP_1_1) //可以手动指定客户端的版本,如果不指定,那么默认是Http2 .followRedirects(Redirect.NORMAL) //设置重定向策略 .connectTimeout(Duration.ofSeconds(20)) //连接超时时间 .proxy(ProxySelector.of(new InetSocketAddress("proxy.example.com", 80))) //代理地址设置 .authenticator(Authenticator.getDefault()) //.executor(Executors.newFixedThreadPoolExecutor(8)) //可手动配置线程池 .build(); HttpRequest request = HttpRequest.newBuilder() .uri(URI.create("https://foo.com/")) //设置url地址 .GET() .build(); HttpResponse<String> response = client.send(request, BodyHandlers.ofString()); //同步发送 System.out.println(response.statusCode()); //打印响应状态码 System.out.println(response.body()); HttpRequest request = HttpRequest.newBuilder() .uri(URI.create("https://foo.com/")) .timeout(Duration.ofMinutes(2)) //设置连接超时时间 .header("Content-Type", "application/json") .POST(BodyPublishers.ofFile(Paths.get("file.json"))) //设置请求体来源 .build(); client.sendAsync(request, BodyHandlers.ofString()) //异步发送 .thenApply(HttpResponse::body) //发送结束打印响应体 .thenAccept(System.out::println);
可以看到,应用编写的代码相对流畅自然。不过,也有几个注意点
短短的几行代码只是实现了功能,那么,它的性能如何呢?我们把它和业界标杆——Apache 的HttpClient作对比。
为了简便,使用node.js的http模块运行一个简易的服务器。该服务器驻守在8080端口,每收到一个请求,停留500ms后返回响应。
let http = require("http") let server = http.createServer() server.addListener("request", (req, res) => { if (req.url.startsWith("/")) { //接到任意请求,停留0.5秒后返回 setTimeout(() => { res.end("haha") }, 500) } } ) server.listen(8080, () => console.log("启动成功!"))
使用node运行该js文件,提示已启动成功
首先定义公共的测试接口:
public interface Tester { //测试参数 class TestCommand { } /** * 测试主方法 * @param testCommand 测试参数 */ void test(TestCommand testCommand) throws Exception; /** * 重复测试多次 * @param testName 测试名称 * @param times 测试次数 * @param testCommand 每次测试的参数 */ default void testMultipleTimes(String testName, int times, TestCommand testCommand) throws Exception{ long startTime = System.currentTimeMillis(); System.out.printf(" ----- %s开始,共%s次 -----\n", testName, times); for (int i = 0; i < times; i++) { long currentStartTime = System.currentTimeMillis(); test(testCommand); System.out.printf("第%s次测试用时:%sms\n", i + 1, (System.currentTimeMillis() - currentStartTime)); } long usedTime = System.currentTimeMillis() - startTime; System.out.printf("%s次测试共用时:%sms,平均用时:%sms\n", times, usedTime, usedTime / times); } }
定义测试类,包含三个静态嵌套类,分别用作JDK httpclient的异步模式、同步模式和apache Httpclient的同步模式
public class HttpClientTester { /** Http请求的真正测试参数*/ static class HttpTestCommand extends Tester.TestCommand { /**目的url*/ String url; /**单次测试请求次数*/ int requestTimes; /**请求线程数*/ 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); } /** * 使用JDK Httpclient的同步模式进行测试 * @param url 请求的url * @param times 请求次数 * @param threadCount 开启的线程数量 * @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) { //阻塞直至所有请求返回 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); } /** * 使用JDK Httpclient的异步模式进行测试 * @param url 请求的url * @param times 请求次数 * @throws InterruptedException */ void testNonBlockly(String url, int times) throws InterruptedException { //给定16个线程,业务常用 2 * Runtime.getRuntime().availableProcessors() ExecutorService executor = Executors.newFixedThreadPool(16); HttpClient client = HttpClient.newBuilder() .executor(executor) .build(); //使用倒计时锁来保证所有请求完成 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(); }); } //阻塞直至所有请求完成 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); } /** * 使用Apache HttpClient进行测试 * @param url 请求的url * @param times 使用时长 * @param threadCount 开启的线程数量 * @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(); //设置apache Httpclient连接复用无限制,体现其最大性能 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) { //阻塞直至所有请求返回 String s = stringFuture.get(); } executorService.shutdown(); } }
测试的main方法:
public static void main(String[] args) { try { // HttpTestCommand testCommand = new HttpTestCommand("http://localhost:8080", 200, 16); //每个测试重复3轮,减少误差 final int testTimes = 3; new BlocklyHttpClientTester().testMultipleTimes("JDK HttpClient同步模式测试", testTimes, testCommand); new NonBlocklyHttpClientTester().testMultipleTimes("JDK HttpClient异步模式测试", testTimes, testCommand); new ApacheHttpClientTester().testMultipleTimes("Apache Httpclient同步模式测试", testTimes, testCommand); } catch (Exception e) { e.printStackTrace(); } }
----- JDK HttpClient同步模式测试开始,共3次 -----
第1次测试用时:4414ms
第2次测试用时:3580ms
第3次测试用时:3620ms
3次测试共用时:11620ms,平均用时:3873ms
----- JDK HttpClient异步模式测试开始,共3次 -----
第1次测试用时:568ms
第2次测试用时:595ms
第3次测试用时:579ms
3次测试共用时:1742ms,平均用时:580ms
----- Apache Httpclient同步模式测试开始,共3次 -----
第1次测试用时:3719ms
第2次测试用时:3557ms
第3次测试用时:3574ms
3次测试共用时:10851ms,平均用时:3617ms
可见,Httpclient同步模式与apacheHttpclient同步模式性能接近;异步模式由于充分利用了nio非阻塞的特性,在线程数相同的情况下,效率大幅优于同步模式。
需要注意的是,此处的“同步”“异步”并非I/O模型中的同步,而是指编程方式上的同步/异步。
通过以上示例代码,可以看出HttpClient具有编写流畅、性能优良的特点,也有可定制性不足的遗憾。
下一节,我们将深入客户端的构建和启动过程,接触选择器管理者这一角色,探寻它和Socket通道的交互的交互过程。