之前学习了netty和http异步连接池,跟仓颉大神问的结果是netty的http客户端性能比apache的好。

  咱今儿就用三种http连接池进行测试。

  首先是pom.xml:

python netty 客户端 netty http客户端_java

python netty 客户端 netty http客户端_apache_02

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>

pom.xml

  导入好包之后,创建一个用于测试http连接池连接时间的基类BaseHttpClientTest(在仓颉大神的基础之上做的测试,测试时发现某些数据有问题,就再观察下载的网页代码长度):

python netty 客户端 netty http客户端_java

python netty 客户端 netty http客户端_apache_02

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 五月的仓颉
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 }

View Code

  不用连接池的子类HttpClientWithoutPoolTest:

python netty 客户端 netty http客户端_java

python netty 客户端 netty http客户端_apache_02

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 五月的仓颉
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 }

View Code

  异步连接池的子类HttpclientWithPoolTest:

python netty 客户端 netty http客户端_java

python netty 客户端 netty http客户端_apache_02

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 五月的仓颉
 */
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);
    }
    
}

View Code

  使用netty管理http客户端连接的HttpclientNettyTest:

python netty 客户端 netty http客户端_java

python netty 客户端 netty http客户端_apache_02

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 五月的仓颉
 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 }

View Code

  在测试不同网址时,观察到一些想不到的情况。

python netty 客户端 netty http客户端_java

python netty 客户端 netty http客户端_apache_02

442ms   443ms   449ms   460ms   178ms   153ms   131ms   146ms   161ms   145ms   135ms   128ms   142ms   145ms   140ms   
23149

无连接池的cnblogs连接

python netty 客户端 netty http客户端_java

python netty 客户端 netty http客户端_apache_02

384ms   249ms   165ms   161ms   158ms   103ms   128ms   129ms   89ms   147ms   143ms   144ms   126ms   146ms   155ms   
23149

有连接池的cnblogs连接

python netty 客户端 netty http客户端_java

python netty 客户端 netty http客户端_apache_02

372ms   73ms   67ms   61ms   69ms   64ms   107ms   61ms   68ms   61ms   137ms   68ms   72ms   59ms   57ms   
256

netty的cnblogs连接

  可以看到,在连接cnblogs这种https站时,异步连接的优势明显,下载性能从优到劣是:netty>异步连接池>不用连接池。

  但是如果是http站呢?

  咱们把基类第22行的网页url改成"http://mini.eastday.com"

python netty 客户端 netty http客户端_java

python netty 客户端 netty http客户端_apache_02

101ms   100ms   102ms   27ms   22ms   23ms   21ms   20ms   21ms   21ms   20ms   21ms   29ms   20ms   22ms   
31126

无连接池的东方头条连接

python netty 客户端 netty http客户端_java

python netty 客户端 netty http客户端_apache_02

106ms   44ms   34ms   506ms   359ms   157ms   60ms   44ms   37ms   38ms   34ms   36ms   37ms   36ms   40ms   
31126

有连接池的东方头条连接

python netty 客户端 netty http客户端_java

python netty 客户端 netty http客户端_apache_02

539ms   239ms   43ms   317ms   82ms   46ms   38ms   100ms   42ms   35ms   41ms   40ms   43ms   35ms   48ms   
31126

netty的东方头条连接

  对于东方头条门户这种加载速度本身就快的网页,使用异步机制对整体加载速度影响不大。

  但是如果需要加载大一点的网页呢?

  咱们把基类第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

  如果下载比较大的网页,异步机制确实能让系统总体的加载速度加快。