使用httpclient实现http链接池与使用HttpURLConnection发送http请求的方法与性能对比
2016-07-15 15:49
661 查看
使用httpclient实现http链接池与使用HttpURLConnection发送http请求的方法与性能对比
在项目中需要使用http调用接口,实现了两套发送http请求的方法,一个是使用apache的httpclient提供的http链接池来发送http请求,另一个是使用java原生的HttpURLConnection来发送http请求,并对两者性能进行了对比。使用httpclient中的链接池发送http请求
使用最新的4.5.2版httpclient进行实现。在maven中引入<dependency> <groupId>org.apache.httpcomponents</groupId> <artifactId>httpclient</artifactId> <version>4.5.2</version> </dependency>
实现代码如下
package util; import org.apache.http.*; 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.client.methods.HttpRequestBase; import org.apache.http.client.protocol.HttpClientContext; import org.apache.http.config.Registry; import org.apache.http.config.RegistryBuilder; import org.apache.http.conn.ConnectionKeepAliveStrategy; import org.apache.http.conn.socket.ConnectionSocketFactory; import org.apache.http.conn.socket.LayeredConnectionSocketFactory; import org.apache.http.conn.socket.PlainConnectionSocketFactory; import org.apache.http.conn.ssl.SSLConnectionSocketFactory; import org.apache.http.impl.client.CloseableHttpClient; import org.apache.http.impl.client.HttpClients; import org.apache.http.impl.conn.PoolingHttpClientConnectionManager; import org.apache.http.message.BasicHeaderElementIterator; import org.apache.http.protocol.HTTP; import org.apache.http.protocol.HttpContext; import org.apache.http.util.EntityUtils; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.util.CollectionUtils; import java.io.IOException; import java.util.Map; /** * Created by xugang on 16/7/11. */ public class HttpClientUtil { private final static Logger logger = LoggerFactory.getLogger(HttpClientUtil.class); private int maxTotal = 1;//默认最大连接数 private int defaultMaxPerRoute = 1;//默认每个主机的最大链接数 private int connectionRequestTimeout = 3000;//默认请求超时时间 private int connectTimeout = 3000;//默认链接超时时间 private int socketTimeout = 3000;//默认socket超时时间 private HttpRequestRetryHandler httpRequestRetryHandler = new DefaultHttpRequestRetryHandler();//默认不进行重试处理 private CloseableHttpClient httpClient; public HttpClientUtil(){ init(); } public String sendGet(String url, Map<String, Object> params) throws Exception { StringBuffer sb = new StringBuffer(url); if(!CollectionUtils.isEmpty(params)) { for (Map.Entry<String, Object> entry : params.entrySet()) { sb.append(entry.getKey()) .append("=") .append(entry.getValue()) .append("&"); } } // no matter for the last '&' character HttpGet httpget = new HttpGet(sb.toString()); config(httpget); CloseableHttpResponse response = null; try { response = httpClient.execute(httpget, HttpClientContext.create()); } catch (IOException e) { logger.error("httpclient error:"+e.getMessage()); e.printStackTrace(); } HttpEntity entity = response.getEntity(); return EntityUtils.toString(entity, "utf-8"); } private void init() { ConnectionSocketFactory plainsf = PlainConnectionSocketFactory.getSocketFactory(); LayeredConnectionSocketFactory sslsf = SSLConnectionSocketFactory.getSocketFactory(); Registry<ConnectionSocketFactory> registry = RegistryBuilder.<ConnectionSocketFactory>create() .register("http", plainsf) .register("https", sslsf) .build(); PoolingHttpClientConnectionManager cm = new PoolingHttpClientConnectionManager(registry); // 设置最大连接数 cm.setMaxTotal(maxTotal); // 设置每个路由的默认连接数 cm.setDefaultMaxPerRoute(defaultMaxPerRoute); //连接保持时间 ConnectionKeepAliveStrategy myStrategy = new ConnectionKeepAliveStrategy() { public long getKeepAliveDuration(HttpResponse response, HttpContext context) { // Honor 'keep-alive' header HeaderElementIterator it = new BasicHeaderElementIterator( response.headerIterator(HTTP.CONN_KEEP_ALIVE)); while (it.hasNext()) { HeaderElement he = it.nextElement(); String param = he.getName(); String value = he.getValue(); if (value != null && param.equalsIgnoreCase("timeout")) { try { return Long.parseLong(value) * 1000; } catch(NumberFormatException ignore) { } } } return 30 * 1000; } }; this.httpClient = HttpClients.custom() .setConnectionManager(cm) .setRetryHandler(httpRequestRetryHandler) .setKeepAliveStrategy(myStrategy) .build(); } /** * http头信息的设置 * * @param httpRequestBase */ private void config(HttpRequestBase httpRequestBase) { httpRequestBase.setHeader("User-Agent", "Mozilla/5.0"); httpRequestBase.setHeader("Accept", "text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8"); httpRequestBase.setHeader("Accept-Language", "zh-CN,zh;q=0.8,en-US;q=0.5,en;q=0.3");//"en-US,en;q=0.5"); httpRequestBase.setHeader("Accept-Charset", "ISO-8859-1,utf-8,gbk,gb2312;q=0.7,*;q=0.7"); httpRequestBase.setHeader("connection", "Keep-Alive"); // 配置请求的超时设置 RequestConfig requestConfig = RequestConfig.custom() .setConnectionRequestTimeout(connectionRequestTimeout) .setConnectTimeout(connectTimeout) .setSocketTimeout(socketTimeout) .build(); httpRequestBase.setConfig(requestConfig); } /** * 请求重试处理 * 默认不进行任何重试 * 如需进行重试可参考下面进行重写 * if (executionCount >= 5) {// 如果已经重试了5次,就放弃 return false; } if (exception instanceof NoHttpResponseException) {// 如果服务器丢掉了连接,那么就重试 return true; } if (exception instanceof SSLHandshakeException) {// 不要重试SSL握手异常 return false; } if (exception instanceof InterruptedIOException) {// 超时 return false; } if (exception instanceof UnknownHostException) {// 目标服务器不可达 return false; } if (exception instanceof ConnectTimeoutException) {// 连接被拒绝 return false; } if (exception instanceof SSLException) {// ssl握手异常 return false; } HttpClientContext clientContext = HttpClientContext.adapt(context); HttpRequest request = clientContext.getRequest(); // 如果请求是幂等的,就再次尝试 if (!(request instanceof HttpEntityEnclosingRequest)) { return true; } */ private class DefaultHttpRequestRetryHandler implements HttpRequestRetryHandler { public boolean retryRequest(IOException exception, int executionCount, HttpContext context) { return false; } } }
使用了spring和slf4j,所以要直接用的话还需在你的pom中添加相关依赖,不想添加的话稍微改一下代码也很简单,就不说了。
<dependency> <groupId>org.slf4j</groupId> <artifactId>slf4j-api</artifactId> <version>1.7.13</version> </dependency> <dependency> <groupId>org.springframework</groupId> <artifactId>spring-core</artifactId> <version>4.1.1.RELEASE</version> </dependency> <dependency> <groupId>org.springframework</groupId> <artifactId>spring-beans</artifactId> <version>4.1.1.RELEASE</version> </dependency>
使用java原生的HttpURLConnection发送http链接
代码如下package util; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.util.StringUtils; import java.io.*; import java.net.HttpURLConnection; import java.net.URL; import java.net.URLConnection; import java.util.Map; public class HttpUtils { final static Logger logger = LoggerFactory.getLogger(HttpUtils.class); /** * Post 请求超时时间和读取数据的超时时间均为2000ms。 * * @param urlPath post请求地址 * @param parameterData post请求参数 * @return String json字符串,成功:code=1001,否者为其他值 * @throws Exception 链接超市异常、参数url错误格式异常 */ public static String doPost(String urlPath, String parameterData, String who, String ip) throws Exception { if (null == urlPath || null == parameterData) { // 避免null引起的空指针异常 return ""; } URL localURL = new URL(urlPath); URLConnection connection = localURL.openConnection(); HttpURLConnection httpURLConnection = (HttpURLConnection) connection; httpURLConnection.setDoOutput(true); if (!StringUtils.isEmpty(who)) { httpURLConnection.setRequestProperty("who", who); } if (!StringUtils.isEmpty(ip)) { httpURLConnection.setRequestProperty("clientIP", ip); } httpURLConnection.setRequestMethod("POST"); httpURLConnection.setRequestProperty("Accept-Charset", "utf-8"); httpURLConnection.setRequestProperty("Content-Type", "application/x-www-form-urlencoded"); httpURLConnection.setRequestProperty("Content-Length", String.valueOf(parameterData.length())); httpURLConnection.setConnectTimeout(18000); httpURLConnection.setReadTimeout(18000); OutputStream outputStream = null; OutputStreamWriter outputStreamWriter = null; InputStream inputStream = null; InputStreamReader inputStreamReader = null; BufferedReader reader = null; StringBuilder resultBuffer = new StringBuilder(); String tempLine = null; try { outputStream = httpURLConnection.getOutputStream(); outputStreamWriter = new OutputStreamWriter(outputStream); outputStreamWriter.write(parameterData.toString()); outputStreamWriter.flush(); if (httpURLConnection.getResponseCode() >= 300) { throw new Exception("HTTP Request is not success, Response code is " + httpURLConnection.getResponseCode()); } inputStream = httpURLConnection.getInputStream(); // 真正的发送请求到服务端 inputStreamReader = new InputStreamReader(inputStream); reader = new BufferedReader(inputStreamReader); while ((tempLine = reader.readLine()) != null) { resultBuffer.append(tempLine); } } finally { if (outputStreamWriter != null) { outputStreamWriter.close(); } if (outputStream != null) { outputStream.close(); } if (reader != null) { reader.close(); } if (inputStreamReader != null) { inputStreamReader.close(); } if (inputStream != null) { inputStream.close(); } } return resultBuffer.toString(); } public static String doPost(String url, Map<String, Object> params) throws Exception { StringBuffer sb = new StringBuffer(); for (Map.Entry<String, Object> entry : params.entrySet()) { sb.append(entry.getKey()) .append("=") .append(entry.getValue()) .append("&"); } // no matter for the last '&' character return doPost(url, sb.toString(), "", ""); } /** * 向指定URL发送GET方法的请求 * * @param url 发送请求的URL * @param param 请求参数,请求参数应该是 name1=value1&name2=value2 的形式。 * @return URL 所代表远程资源的响应结果 */ public static String sendGet(String url, String param, String who, String ip) { String result = ""; BufferedReader in = null; try { String urlNameString = url; if (!"".equals(param)) { urlNameString = urlNameString + "?" + param; } URL realUrl = new URL(urlNameString); // 打开和URL之间的连接 URLConnection connection = realUrl.openConnection(); // 设置通用的请求属性 if (!StringUtils.isEmpty(who)) { connection.setRequestProperty("who", who); } if (!StringUtils.isEmpty(ip)) { connection.setRequestProperty("clientIP", ip); } connection.setRequestProperty("accept", "*/*"); connection.setRequestProperty("connection", "Keep-Alive"); connection.setRequestProperty("user-agent", "Mozilla/4.0 (compatible; MSIE 6.0; Windows NT 5.1;SV1)"); // 建立实际的连接 connection.connect(); // 定义 BufferedReader输入流来读取URL的响应 in = new BufferedReader(new InputStreamReader( connection.getInputStream())); String line; while ((line = in.readLine()) != null) { result += line; } } catch (Exception e) { logger.warn("发送GET请求出现异常!", e); } // 使用finally块来关闭输入流 finally { try { if (in != null) { in.close(); } } catch (Exception e2) { logger.warn("fail to close inputStream.", e2); } } return result; } public static String sendGet(String url, Map<String, Object> params) throws Exception { StringBuffer sb = new StringBuffer(); for (Map.Entry<String, Object> entry : params.entrySet()) { sb.append(entry.getKey()) .append("=") .append(entry.getValue()) .append("&"); } // no matter for the last '&' character return sendGet(url, sb.toString(), "", ""); } }
也使用了sl4j打日志,不需要的话直接删掉吧。
两种链接方式的性能对比
测试代码如下HttpClientUtil httpClientUtil = new HttpClientUtil(); long start1 = System.currentTimeMillis(); for(int i = 0;i<1000;i++){ try { HttpUtils.sendGet("http://yop-console-qa.s.qima-inc.com/app/list", new HashMap<String, Object>()); } catch (Exception e) { e.printStackTrace(); } } long end1 = System.currentTimeMillis(); long start2 = System.currentTimeMillis(); for (int i = 0;i<1000;i++) { try { httpClientUtil.sendGet("http://yop-console-qa.s.qima-inc.com/app/list", null); } catch (Exception e) { e.printStackTrace(); } } long end2 = System.currentTimeMillis(); System.out.println("时间1:" + (end1 - start1) + "时间2:" + (end2 - start2));
两个都向同一地址发送1000个请求,最后输出为:
时间1:35637时间2:34868
即使用java原生的用时35637ms ,使用httpclient用时 34868ms。是不是感觉区别不大?但是要考虑到网络本来就不稳定,你下个片还时而100k时而50k呢,这种测试不能完全说明问题,下面我们来看看使用http链接池和直接使用java原生的http方式有什么区别
http链接池与直接链接的区别
首先我们要知道http是建立在tcp之上的应用层协议,因此在建立http请求前首先要进行tcp的三次握手过程:在http传输结束断开链接时要进行tcp的四次握手断开链接:
在看一下抓包得到的一次http请求过程可以更直观展现:
首先3次握手建立链接,链接建立后客服端提交一个http请求到服务器,然后是图中高亮的部分,服务器在接受到http请求后先回复一个tcp的确认请求给客服端,再回复http请求到客服端。最后四次握手断开链接。当使用java原生方式发送http请求时,是不是每一次请求都要经历3次握手建立链接-发送http请求并获取结果-4次握手释放链接这样的过程呢?来看图:
忽略图中的tcp segment和tcp window update。可以看到,第一次http结束后并没有断开链接,直接复用了,为了测试我还特意将HttpUtil中的这段代码注释掉了
//connection.setRequestProperty("connection", "Keep-Alive");
那么。。why?为什么这种情况下http链接还能被复用呢?
细心的读者可能注意到了,使用的http是1.1,在Http /1.1中:
在HTTP/1.1版本中,官方规定的Keep-Alive使用标准和在HTTP/1.0版本中有些不同,默认情况下所在HTTP1.1中所有连接都被保持,除非在请求头或响应头中指明要关闭:Connection:Close ,这也就是为什么Connection:
Keep-Alive字段再没有意义的原因。另外,还添加了一个新的字段Keep-Alive,但是因为这个字段并没有详细描述用来做什么,可忽略它
ok,答案很明显了,默认情况下该http链接便是可以被复用的,并且在java的客服端中有:
HttpURLConnection类自动实现了Keep-Alive,如果程序员没有介入去操作Keep-Alive,Keep-Alive会通过客户端内部的一个HttpURLConnection类的实例对象来自动实现。
在java的服务器端有:
HttpServlet、HttpServletRequest和HttpServletResponse类自动实现 了Keep-Alive
原来jdk已经帮我们做了这么多了^ ^
接下来还是再来看看使用链接池时进行多次http请求的情况吧:
看起来少了不少,但其实主流程一样,少的部分只是tcp segment和tcp window update,但是在使用链接池时几乎不会出现tcp segment和tcp window update,使用原生连接时有大量tcp segment出现,虽然对时间影响很小。
因此,单从时间上来说两种http方式耗时相差不大,如果只需简单的发送http请求用原生方法就够了,需要更强大的功能可考虑使用链接池。
相关文章推荐
- java对世界各个时区(TimeZone)的通用转换处理方法(转载)
- java-注解annotation
- java-模拟tomcat服务器
- java-用HttpURLConnection发送Http请求.
- java-WEB中的监听器Lisener
- Android IPC进程间通讯机制
- Android Native 绘图方法
- Android java 与 javascript互访(相互调用)的方法例子
- RPC failed; result=22, HTTP code = 411
- 介绍一款信息管理系统的开源框架---jeecg
- 聚类算法之kmeans算法java版本
- java实现 PageRank算法
- PropertyChangeListener简单理解
- c++11 + SDL2 + ffmpeg +OpenAL + java = Android播放器
- 插入排序
- 冒泡排序
- 堆排序
- 快速排序