netty的http客户端与apache的http客户端比较
之前学习了netty和http异步连接池,跟仓颉大神问的结果是netty的http客户端性能比apache的好。
咱今儿就用三种http连接池进行测试。
首先是pom.xml:
1 <project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" 2 xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd"> 3 <modelVersion>4.0.0</modelVersion> 4 5 <groupId>com.company</groupId> 6 <artifactId>websocket_demo</artifactId> 7 <version>0.0.1-SNAPSHOT</version> 8 <packaging>jar</packaging> 9 10 <name>websocket_demo</name> 11 <url>http://maven.apache.org</url> 12 13 <properties> 14 <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding> 15 <junit.version>4.12</junit.version> 16 <log4j.version>1.2.17</log4j.version> 17 <netty.version>4.1.36.Final</netty.version> 18 <google.gson.version>2.3</google.gson.version> 19 <guava.version>19.0</guava.version> 20 </properties> 21 22 <dependencies> 23 <!-- junit --> 24 <dependency> 25 <groupId>junit</groupId> 26 <artifactId>junit</artifactId> 27 <version>${junit.version}</version> 28 <scope>test</scope> 29 </dependency> 30 31 <!-- log4j --> 32 <dependency> 33 <groupId>log4j</groupId> 34 <artifactId>log4j</artifactId> 35 <version>${log4j.version}</version> 36 </dependency> 37 38 <!-- netty --> 39 <dependency> 40 <groupId>io.netty</groupId> 41 <artifactId>netty-all</artifactId> 42 <version>${netty.version}</version> 43 </dependency> 44 45 <!-- gson --> 46 <dependency> 47 <groupId>com.google.code.gson</groupId> 48 <artifactId>gson</artifactId> 49 <version>${google.gson.version}</version> 50 </dependency> 51 52 <!-- guava --> 53 <dependency> 54 <groupId>com.google.guava</groupId> 55 <artifactId>guava</artifactId> 56 <version>${guava.version}</version> 57 </dependency> 58 59 60 61 <dependency> 62 <groupId>org.apache.httpcomponents</groupId> 63 <artifactId>httpclient</artifactId> 64 <version>4.5.8</version> 65 </dependency> 66 67 <dependency> 68 <groupId>org.apache.commons</groupId> 69 <artifactId>commons-io</artifactId> 70 <version>1.3.2</version> 71 </dependency> 72 73 </dependencies> 74 75 <build> 76 <finalName>Netty_WebSocket</finalName> 77 <plugins> 78 <!--编译版本--> 79 <plugin> 80 <artifactId>maven-compiler-plugin</artifactId> 81 <version>2.3.1</version> 82 <configuration> 83 <source>1.8</source> 84 <target>1.8</target> 85 <encoding>UTF-8</encoding> 86 <compilerArguments> 87 <extdirs>src/main/webapp/WEB-INF/lib</extdirs> 88 </compilerArguments> 89 </configuration> 90 </plugin> 91 </plugins> 92 </build> 93 94 </project>
导入好包之后,创建一个用于测试http连接池连接时间的基类BaseHttpClientTest(在仓颉大神的基础之上做的测试,测试时发现某些数据有问题,就再观察下载的网页代码长度):
1 package com.company.client; 2 3 import java.util.ArrayList; 4 import java.util.List; 5 import java.util.concurrent.atomic.AtomicInteger; 6 7 /** 8 * 连接池基类 9 * 10 * @author 五月的仓颉https://www.cnblogs.com/xrq730/p/10963689.html 11 */ 12 public class BaseHttpClientTest { 13 14 protected static final int REQUEST_COUNT = 15; 15 16 protected static final String SEPERATOR = " "; 17 18 protected static final AtomicInteger NOW_COUNT = new AtomicInteger(0); 19 20 protected static final StringBuilder EVERY_REQ_COST = new StringBuilder(200); 21 22 protected static final String TEST_URL = "http://sohu.com"; 23 24 protected static long averageLength = 0; 25 26 /** 27 * 获取待运行的线程 28 */ 29 protected List<Thread> getRunThreads(Runnable runnable) { 30 List<Thread> tList = new ArrayList<Thread>(REQUEST_COUNT); 31 32 for (int i = 0; i < REQUEST_COUNT; i++) { 33 tList.add(new Thread(runnable)); 34 } 35 36 return tList; 37 } 38 39 /** 40 * 启动所有线程 41 */ 42 protected void startUpAllThreads(List<Thread> tList) { 43 for (Thread t : tList) { 44 t.start(); 45 // 这里需要加一点延迟,保证请求按顺序发出去 46 try { 47 Thread.sleep(300); 48 } catch (InterruptedException e) { 49 e.printStackTrace(); 50 } 51 } 52 } 53 54 protected synchronized void addContentLength(long len) { 55 averageLength += len; 56 } 57 58 protected synchronized void addCost(long cost) { 59 EVERY_REQ_COST.append(cost); 60 EVERY_REQ_COST.append("ms"); 61 EVERY_REQ_COST.append(SEPERATOR); 62 } 63 64 }
不用连接池的子类HttpClientWithoutPoolTest:
1 package com.company.client; 2 3 import java.io.InputStream; 4 5 import org.apache.commons.io.IOUtils; 6 import org.apache.http.client.methods.CloseableHttpResponse; 7 import org.apache.http.client.methods.HttpGet; 8 import org.apache.http.impl.client.CloseableHttpClient; 9 import org.apache.http.impl.client.HttpClients; 10 import org.junit.Test; 11 12 /** 13 * 不使用连接池测试 14 * 15 * @author 五月的仓颉https://www.cnblogs.com/xrq730/p/10963689.html 16 */ 17 public class HttpClientWithoutPoolTest extends BaseHttpClientTest { 18 19 @Test 20 public void test() throws Exception { 21 startUpAllThreads(getRunThreads(new HttpThread())); 22 // 等待线程运行 23 for (;;); 24 } 25 26 private class HttpThread implements Runnable { 27 28 @Override 29 public void run() { 30 /** 31 * HttpClient是线程安全的,因此HttpClient正常使用应当做成全局变量,但是一旦全局共用一个,HttpClient内部构建的时候会new一个连接池 32 * 出来,这样就体现不出使用连接池的效果,因此这里每次new一个HttpClient,保证每次都不通过连接池请求对端 33 */ 34 CloseableHttpClient httpClient = HttpClients.custom().build(); 35 HttpGet httpGet = new HttpGet(TEST_URL); 36 37 long startTime = System.currentTimeMillis(); 38 try { 39 CloseableHttpResponse response = httpClient.execute(httpGet); 40 if (response != null) { 41 addContentLength(IOUtils.toString(response.getEntity().getContent()).length()); 42 response.close(); 43 } 44 } catch (Exception e) { 45 e.printStackTrace(); 46 } finally { 47 addCost(System.currentTimeMillis() - startTime); 48 49 if (NOW_COUNT.incrementAndGet() == REQUEST_COUNT) { 50 System.out.println(EVERY_REQ_COST.toString()); 51 System.out.println(averageLength / REQUEST_COUNT); 52 } 53 } 54 } 55 56 } 57 58 }
异步连接池的子类HttpclientWithPoolTest:
package com.company.client; import java.io.ByteArrayOutputStream; import java.io.InputStream; import java.net.MalformedURLException; import java.net.URL; import java.nio.charset.StandardCharsets; import java.util.Timer; import java.util.TimerTask; import java.util.concurrent.TimeUnit; import org.apache.commons.io.IOUtils; import org.apache.http.HttpHost; import org.apache.http.client.HttpRequestRetryHandler; import org.apache.http.client.config.RequestConfig; import org.apache.http.client.methods.CloseableHttpResponse; import org.apache.http.client.methods.HttpGet; import org.apache.http.conn.routing.HttpRoute; import org.apache.http.impl.client.CloseableHttpClient; import org.apache.http.impl.client.HttpClients; import org.apache.http.impl.client.StandardHttpRequestRetryHandler; import org.apache.http.impl.conn.PoolingHttpClientConnectionManager; import org.junit.Before; import org.junit.Test; /** * 使用连接池测试 * * @author 五月的仓颉https://www.cnblogs.com/xrq730/p/10963689.html */ public class HttpclientWithPoolTest extends BaseHttpClientTest { private CloseableHttpClient httpClient = null; PoolingHttpClientConnectionManager connectionManager = null; @Before public void before() { initHttpClient(); } @Test public void test() throws Exception { startUpAllThreads(getRunThreads(new HttpThread())); connectionManager.shutdown(); } private class HttpThread implements Runnable { @Override public void run() { HttpGet httpGet = new HttpGet(TEST_URL); // 长连接标识,不加也没事,HTTP1.1默认都是Connection: keep-alive的 httpGet.addHeader("Connection", "keep-alive"); long startTime = System.currentTimeMillis(); try { CloseableHttpResponse response = httpClient.execute(httpGet); if (response != null) { addContentLength(IOUtils.toString(response.getEntity().getContent()).length()); response.close(); } } catch (Exception e) { e.printStackTrace(); } finally { addCost(System.currentTimeMillis() - startTime); if (NOW_COUNT.incrementAndGet() == REQUEST_COUNT) { System.out.println(EVERY_REQ_COST.toString()); System.out.println(averageLength / REQUEST_COUNT); } } } } private void initHttpClient() { connectionManager = new PoolingHttpClientConnectionManager(); // 总连接池数量 connectionManager.setMaxTotal(1); // 可为每个域名设置单独的连接池数量 try { connectionManager.setMaxPerRoute(new HttpRoute(new HttpHost(new URL(TEST_URL).getHost())), 1); } catch (MalformedURLException e) { e.printStackTrace(); } // setConnectTimeout表示设置建立连接的超时时间 // setConnectionRequestTimeout表示从连接池中拿连接的等待超时时间 // setSocketTimeout表示发出请求后等待对端应答的超时时间 RequestConfig requestConfig = RequestConfig.custom().setConnectTimeout(10000).setConnectionRequestTimeout(20000) .setSocketTimeout(30000).build(); // 重试处理器,StandardHttpRequestRetryHandler这个是官方提供的,看了下感觉比较挫,很多错误不能重试,可自己实现HttpRequestRetryHandler接口去做 HttpRequestRetryHandler retryHandler = new StandardHttpRequestRetryHandler(); httpClient = HttpClients.custom().setConnectionManager(connectionManager).setDefaultRequestConfig(requestConfig) .setRetryHandler(retryHandler).build(); // 服务端假设关闭了连接,对客户端是不透明的,HttpClient为了缓解这一问题,在某个连接使用前会检测这个连接是否过时,如果过时则连接失效,但是这种做法会为每个请求 // 增加一定额外开销,因此有一个定时任务专门回收长时间不活动而被判定为失效的连接,可以某种程度上解决这个问题 Timer timer = new Timer(); timer.schedule(new TimerTask() { @Override public void run() { try { // 关闭失效连接并从连接池中移除 connectionManager.closeExpiredConnections(); // 关闭30秒钟内不活动的连接并从连接池中移除,空闲时间从交还给连接管理器时开始 connectionManager.closeIdleConnections(20, TimeUnit.SECONDS); } catch (Throwable t) { t.printStackTrace(); } } }, 0 , 1000 * 5); } }
使用netty管理http客户端连接的HttpclientNettyTest:
1 package com.company.client; 2 3 import java.net.MalformedURLException; 4 import java.net.URL; 5 6 import org.apache.commons.io.IOUtils; 7 import org.junit.Before; 8 import org.junit.Test; 9 10 import io.netty.bootstrap.Bootstrap; 11 import io.netty.channel.Channel; 12 import io.netty.channel.ChannelHandlerContext; 13 import io.netty.channel.ChannelInitializer; 14 import io.netty.channel.ChannelOption; 15 import io.netty.channel.ChannelPipeline; 16 import io.netty.channel.EventLoopGroup; 17 import io.netty.channel.SimpleChannelInboundHandler; 18 import io.netty.channel.nio.NioEventLoopGroup; 19 import io.netty.channel.socket.SocketChannel; 20 import io.netty.channel.socket.nio.NioSocketChannel; 21 import io.netty.handler.codec.http.DefaultFullHttpRequest; 22 import io.netty.handler.codec.http.DefaultHttpContent; 23 import io.netty.handler.codec.http.DefaultHttpResponse; 24 import io.netty.handler.codec.http.FullHttpRequest; 25 import io.netty.handler.codec.http.HttpClientCodec; 26 import io.netty.handler.codec.http.HttpContentDecompressor; 27 import io.netty.handler.codec.http.HttpHeaderNames; 28 import io.netty.handler.codec.http.HttpHeaderValues; 29 import io.netty.handler.codec.http.HttpMethod; 30 import io.netty.handler.codec.http.HttpObjectAggregator; 31 import io.netty.handler.codec.http.HttpVersion; 32 import io.netty.handler.codec.http.LastHttpContent; 33 34 /** 35 * 使用连接池测试 36 * 37 * @author 五月的仓颉https://www.cnblogs.com/xrq730/p/10963689.html 38 */ 39 public class HttpclientNettyTest extends BaseHttpClientTest { 40 41 //线程组 42 private static EventLoopGroup bossGroup = null; 43 44 //启动类 45 private static Bootstrap bootstrap = null; 46 47 @Before 48 public void before() { 49 initHttpClient(); 50 } 51 52 public static void closePool() { 53 if(null != bossGroup) { 54 //优雅退出,释放线程池资源 55 bossGroup.shutdownGracefully(); 56 } 57 else { 58 System.out.println("is null"); 59 } 60 } 61 62 public static Channel getChannel(String url) throws MalformedURLException, InterruptedException { 63 URL urlObj = new URL(url); 64 String host = urlObj.getHost(); 65 int port = urlObj.getPort(); 66 if(port == -1 && urlObj.getProtocol().equals("http")) { 67 port = 80; 68 } 69 if(port == -1 && urlObj.getProtocol().equals("https")) { 70 port = 443; 71 } 72 return bootstrap.connect(host, port).sync().channel(); 73 } 74 75 public static void get(Channel channel, String url) throws MalformedURLException, InterruptedException { 76 URL urlObj = new URL(url); 77 String path = urlObj.getFile(); 78 if(path == "") { 79 path = "/"; 80 } 81 FullHttpRequest request = new DefaultFullHttpRequest(HttpVersion.HTTP_1_1, HttpMethod.GET, url); 82 request.headers().set(HttpHeaderNames.HOST, urlObj.getHost()); 83 request.headers().set(HttpHeaderNames.CONNECTION, HttpHeaderValues.CLOSE); 84 request.headers().set(HttpHeaderNames.ACCEPT_ENCODING, HttpHeaderValues.GZIP + ", deflate"); 85 channel.writeAndFlush(request); 86 } 87 88 @Test 89 public void test() throws Exception { 90 startUpAllThreads(getRunThreads(new HttpThread())); 91 closePool(); 92 } 93 94 private class HttpThread implements Runnable { 95 96 @Override 97 public void run() { 98 Channel channel; 99 long startTime = System.currentTimeMillis(); 100 try { 101 channel = getChannel(TEST_URL); 102 get(channel, TEST_URL); 103 channel.closeFuture().sync(); 104 } catch (Exception e) { 105 e.printStackTrace(); 106 } finally { 107 addCost(System.currentTimeMillis() - startTime); 108 if (NOW_COUNT.incrementAndGet() == REQUEST_COUNT) { 109 System.out.println(EVERY_REQ_COST.toString()); 110 System.out.println(averageLength / REQUEST_COUNT); 111 } 112 } 113 } 114 115 } 116 117 private void initHttpClient() { 118 try { 119 bossGroup = new NioEventLoopGroup(); 120 bootstrap = new Bootstrap(); 121 bootstrap.group(bossGroup) 122 .option(ChannelOption.CONNECT_TIMEOUT_MILLIS, 30000) 123 .option(ChannelOption.SO_KEEPALIVE, true) 124 .channel(NioSocketChannel.class) 125 .handler(new ChannelInitializer<SocketChannel>() { 126 @Override 127 protected void initChannel(SocketChannel ch) throws Exception { 128 ChannelPipeline p = ch.pipeline(); 129 p.addLast(new HttpClientCodec()); 130 p.addLast(new HttpContentDecompressor());//这里要添加解压,不然打印时会乱码 131 p.addLast(new HttpObjectAggregator(1234330));//添加HttpObjectAggregator, HttpClientMsgHandler才会收到FullHttpResponse 132 p.addLast("handler", new SimpleChannelInboundHandler<Object>() { 133 @Override 134 protected void channelRead0(ChannelHandlerContext ctx, Object msg) throws Exception { 135 LastHttpContent d = (LastHttpContent)msg; 136 addContentLength(d.content().toString(io.netty.util.CharsetUtil.UTF_8).length()); 137 } 138 }); 139 } 140 }); 141 }catch(Exception e) { 142 e.printStackTrace(); 143 } 144 } 145 146 }
在测试不同网址时,观察到一些想不到的情况。
442ms 443ms 449ms 460ms 178ms 153ms 131ms 146ms 161ms 145ms 135ms 128ms 142ms 145ms 140ms 23149
384ms 249ms 165ms 161ms 158ms 103ms 128ms 129ms 89ms 147ms 143ms 144ms 126ms 146ms 155ms 23149
372ms 73ms 67ms 61ms 69ms 64ms 107ms 61ms 68ms 61ms 137ms 68ms 72ms 59ms 57ms 256
可以看到,在连接cnblogs这种https站时,异步连接的优势明显,下载性能从优到劣是:netty>异步连接池>不用连接池。
但是如果是http站呢?
咱们把基类第22行的网页url改成"http://mini.eastday.com"
101ms 100ms 102ms 27ms 22ms 23ms 21ms 20ms 21ms 21ms 20ms 21ms 29ms 20ms 22ms 31126
106ms 44ms 34ms 506ms 359ms 157ms 60ms 44ms 37ms 38ms 34ms 36ms 37ms 36ms 40ms 31126
539ms 239ms 43ms 317ms 82ms 46ms 38ms 100ms 42ms 35ms 41ms 40ms 43ms 35ms 48ms 31126
对于东方头条门户这种加载速度本身就快的网页,使用异步机制对整体加载速度影响不大。
但是如果需要加载大一点的网页呢?
咱们把基类第22行的网页url改成"https://mini.eastday.com/a/190919091345041.html"
无连接池的东方头条新闻网页:
510ms 374ms 526ms 539ms 124ms 74ms 72ms 62ms 70ms 75ms 70ms 75ms 207ms 86ms 74ms 65325
有连接池的东方头条新闻网页:
424ms 185ms 92ms 63ms 74ms 77ms 77ms 66ms 92ms 64ms 107ms 65ms 70ms 51ms 90ms 65325
netty的东方头条新闻网页:
318ms 44ms 33ms 41ms 42ms 41ms 47ms 34ms 49ms 38ms 30ms 34ms 47ms 40ms 44ms 264
如果下载比较大的网页,异步机制确实能让系统总体的加载速度加快。