背景
服务端以及客户端在开发过程中不可避免的会使用到网络请求,网络请求可以使用JAVA原生的URLConnection,也可以使用HttpClient。在日常工作中建议大家使用HttpClient。URLConnection需要自己去实现相关的方法,而HttpClient使用起来方便,且提供许多的API。
HttpClient
1、httpClient的特性
-
实现了所有 HTTP 的方法(GET,POST,PUT,HEAD 等)
-
支持自动转向
-
支持 HTTPS 协议
-
支持代理服务器等
目前httpClient的最新版本为4.5.2,可以通过如下maven获取Jar包,这里以本人常用的4.2.2为例;
<!-- https://mvnrepository.com/artifact/org.apache.httpcomponents/httpclient -->
<dependency>
<groupId>org.apache.httpcomponents</groupId>
<artifactId>httpclient</artifactId>
<version>4.2.2</version>
</dependency>
3、使用方法
package com.netease.mail.be.appads.web.controller;
import java.io.IOException;
import java.io.UnsupportedEncodingException;
import java.net.URI;
import java.net.URISyntaxException;
import java.util.List;
import org.apache.commons.lang.StringUtils;
import org.apache.http.HttpEntity;
import org.apache.http.HttpResponse;
import org.apache.http.client.ClientProtocolException;
import org.apache.http.client.HttpClient;
import org.apache.http.client.entity.UrlEncodedFormEntity;
import org.apache.http.client.methods.HttpPost;
import org.apache.http.entity.StringEntity;
import org.apache.http.impl.client.DefaultHttpClient;
import org.apache.http.message.BasicNameValuePair;
import org.apache.http.protocol.HTTP;
import org.apache.http.util.EntityUtils;
public class PostSample {
public String setPost(String url, String reqBody,
List<BasicNameValuePair> urlParams) {
// step1: 构造HttpClient的实例,类似于打开浏览器
HttpClient client = new DefaultHttpClient();
// step2: 创建POST方法的实例,类似于在浏览器地址栏输入url
HttpPost postMethod = new HttpPost(url);
String returnValue = "";
try {
// step3:设置url相关参数
if (null != urlParams && !urlParams.isEmpty()) {
String params = EntityUtils.toString(new UrlEncodedFormEntity(
urlParams, HTTP.UTF_8));
postMethod.setURI(new URI(postMethod.getURI().toString() + "?"
+ params));
}
// step4:设置请求body参数及头部相关参数
if (StringUtils.isNotBlank(reqBody)) {
StringEntity se = new StringEntity(reqBody, "UTF-8");
se.setContentType("application/text; charset=utf-8");
postMethod.setEntity(se);
}
// step5:执行发送请求
HttpResponse response = client.execute(postMethod);
int statusCode = response.getStatusLine().getStatusCode();
if (statusCode == 200) {
// 校验码
HttpEntity he = response.getEntity();
returnValue = new String(EntityUtils.toByteArray(he), "UTF-8");
System.out.println("Bid Request Send Succeed." + returnValue);
return returnValue;
} else if (statusCode == 204) {
System.out.println("Bid Request Send Error, No Content");
} else {
System.out.println("Error happens,http status code is "
+ statusCode + ".");
}
} catch (UnsupportedEncodingException e) {
System.out.println(Thread.currentThread().getName()
+ "发送BidRequest请求错误,编码错误:" + e.getMessage());
} catch (ClientProtocolException e) {
System.out.println(Thread.currentThread().getName()
+ "发送BidRequest请求错误,协议错误错误:" + e.getMessage());
} catch (IOException e) {
System.out.println(Thread.currentThread().getName()
+ "发送BidRequest请求错误,网络IO错误:" + e.getMessage());
} catch (URISyntaxException e) {
System.out.println(Thread.currentThread().getName()
+ "发送BidRequest请求错误,URL语法错误:" + e.getMessage());
} finally {
// 释放连接,很关键
postMethod.releaseConnection();
}
return returnValue;
}
}
注:此方法可以应付并发量不高的网络请求,但是对于高并发的网络请求,此方法存在很大的缺陷及性能问题。
具体原因如下:
- 此方法每次都会new一个httpClient对象及HttpPost对象,new的时候需要申请系统资源会消耗一定的时间;
- 虽然请求执行完毕后都执行了postMethod.releaseConnection方法去释放连接,但是这只是应用层的释放,而真正的释放端口需等待一段时间, 这个时间由系统决定。所以在高并发的时候,端口不断的被占用达到系统的最大值,导致后面的网络请求会直接抛出 Cannot assign requested address[不能分配端口]的异常],从而不能进行请求。
高并发的网络请求
在高并发时,需要用到HttpClient的连接池,连接的作用主要是减少创建链接的次数,请求可以复用之前的连接,所以就可以指定一定数量的连接数让连接池去创建,避免了多次创建新的连接并在系统回收连接时不能及时释放端口的情况下达到可用端口的最大值的问题。具体实现如下:
package com.netease.mail.be.appads.web.controller;
import java.io.IOException;
import java.io.UnsupportedEncodingException;
import java.net.URI;
import java.net.URISyntaxException;
import java.nio.charset.Charset;
import java.security.cert.CertificateException;
import java.security.cert.X509Certificate;
import java.util.List;
import javax.net.ssl.SSLContext;
import javax.net.ssl.TrustManager;
import javax.net.ssl.X509TrustManager;
import org.apache.commons.lang.StringUtils;
import org.apache.http.HttpEntity;
import org.apache.http.HttpResponse;
import org.apache.http.HttpVersion;
import org.apache.http.client.ClientProtocolException;
import org.apache.http.client.entity.UrlEncodedFormEntity;
import org.apache.http.client.methods.HttpGet;
import org.apache.http.client.methods.HttpPost;
import org.apache.http.conn.scheme.PlainSocketFactory;
import org.apache.http.conn.scheme.Scheme;
import org.apache.http.conn.scheme.SchemeRegistry;
import org.apache.http.conn.ssl.SSLSocketFactory;
import org.apache.http.entity.StringEntity;
import org.apache.http.impl.client.DefaultHttpClient;
import org.apache.http.impl.conn.PoolingClientConnectionManager;
import org.apache.http.message.BasicNameValuePair;
import org.apache.http.params.BasicHttpParams;
import org.apache.http.params.CoreConnectionPNames;
import org.apache.http.params.CoreProtocolPNames;
import org.apache.http.params.HttpParams;
import org.apache.http.util.EntityUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.stereotype.Service;
/**
* HTTP请求对象,支持post与get方法
*/
@Service("httpRequesterService")
public class HttpClientPoolSample {
private Logger logger = LoggerFactory.getLogger(HttpClientPoolSample.class);
private static int socketTimeout = 1000;// 设置等待数据超时时间5秒钟 根据业务调整
private static int connectTimeout = 2000;// 连接超时
private static int maxConnNum = 4000;// 连接池最大连接数
private static int maxPerRoute = 1500;// 每个主机的并发最多只有1500
private static PoolingClientConnectionManager cm;
private static HttpParams httpParams;
private static final String DEFAULT_ENCODING = Charset.defaultCharset()
.name();
static {
SchemeRegistry sr = new SchemeRegistry();
sr.register(new Scheme("http", 80, PlainSocketFactory
.getSocketFactory()));
SSLSocketFactory sslFactory;
try {
SSLContext sslContext = SSLContext.getInstance("SSL");
X509TrustManager tm = new X509TrustManager() {
@Override
public void checkClientTrusted(X509Certificate[] chain,
String authType) throws CertificateException {
}
@Override
public void checkServerTrusted(X509Certificate[] chain,
String authType) throws CertificateException {
}
@Override
public X509Certificate[] getAcceptedIssuers() {
return null;
}
};
sslContext.init(null, new TrustManager[] { tm },
new java.security.SecureRandom());
sslFactory = new SSLSocketFactory(sslContext,
SSLSocketFactory.ALLOW_ALL_HOSTNAME_VERIFIER);
sr.register(new Scheme("https", 443, sslFactory));
} catch (Exception e) {
e.printStackTrace();
}
// 初始化连接池
cm = new PoolingClientConnectionManager(sr);
cm.setMaxTotal(maxConnNum);
cm.setDefaultMaxPerRoute(maxPerRoute);
httpParams = new BasicHttpParams();
httpParams.setParameter(CoreProtocolPNames.PROTOCOL_VERSION,
HttpVersion.HTTP_1_1);
httpParams.setIntParameter(CoreConnectionPNames.CONNECTION_TIMEOUT,
connectTimeout);// 请求超时时间
httpParams.setIntParameter(CoreConnectionPNames.SO_TIMEOUT,
socketTimeout);// 读取数据超时时间
// 如果启用了NoDelay策略,httpclient和站点之间传输数据时将会尽可能及时地将发送缓冲区中的数据发送出去、而不考虑网络带宽的利用率,这个策略适合对实时性要求高的场景
httpParams.setBooleanParameter(CoreConnectionPNames.TCP_NODELAY, true);
httpParams.setBooleanParameter(
CoreConnectionPNames.STALE_CONNECTION_CHECK, true);
}
public DefaultHttpClient getHttpClient() {
return new DefaultHttpClient(cm, httpParams);
}
public String httpGet(String url, List<BasicNameValuePair> parameters) {
DefaultHttpClient client = getHttpClient();// 默认会到池中查询可用的连接,如果没有就新建
HttpGet getMethod = null;
String returnValue = "";
try {
getMethod = new HttpGet(url);
if (null != parameters) {
String params = EntityUtils.toString(new UrlEncodedFormEntity(
parameters, DEFAULT_ENCODING));
getMethod.setURI(new URI(getMethod.getURI().toString() + "?"
+ params));
logger.debug("httpGet-getUrl:{}", getMethod.getURI());
}
HttpResponse response = client.execute(getMethod);
int statusCode = response.getStatusLine().getStatusCode();
if (statusCode == 200) {
HttpEntity he = response.getEntity();
returnValue = new String(EntityUtils.toByteArray(he),
DEFAULT_ENCODING);
return returnValue;
}
} catch (UnsupportedEncodingException e) {
logger.error(Thread.currentThread().getName()
+ "httpGet Send Error,Code error:" + e.getMessage());
} catch (ClientProtocolException e) {
logger.error(Thread.currentThread().getName()
+ "httpGet Send Error,Protocol error:" + e.getMessage());
} catch (IOException e) {
logger.error(Thread.currentThread().getName()
+ "httpGet Send Error,IO error:" + e.getMessage());
} catch (URISyntaxException e) {
logger.error(Thread.currentThread().getName()
+ "httpGet Send Error,IO error:" + e.getMessage());
} finally {// 释放连接,将连接放回到连接池
getMethod.releaseConnection();
}
return returnValue;
}
public String httpPost(String url, List<BasicNameValuePair> parameters,
String requestBody) {
DefaultHttpClient client = getHttpClient();// 默认会到池中查询可用的连接,如果没有就新建
HttpPost postMethod = null;
String returnValue = "";
try {
postMethod = new HttpPost(url);
if (null != parameters) {
String params = EntityUtils.toString(new UrlEncodedFormEntity(
parameters, DEFAULT_ENCODING));
postMethod.setURI(new URI(postMethod.getURI().toString() + "?"
+ params));
logger.debug("httpPost-getUrl:{}", postMethod.getURI());
}
if (StringUtils.isNotBlank(requestBody)) {
StringEntity se = new StringEntity(requestBody,
DEFAULT_ENCODING);
postMethod.setEntity(se);
}
HttpResponse response = client.execute(postMethod);
int statusCode = response.getStatusLine().getStatusCode();
if (statusCode == 200) {
HttpEntity he = response.getEntity();
returnValue = new String(EntityUtils.toByteArray(he),
DEFAULT_ENCODING);
return returnValue;
}
} catch (UnsupportedEncodingException e) {
logger.error(Thread.currentThread().getName()
+ "httpPost Send Error,Code error:" + e.getMessage());
} catch (ClientProtocolException e) {
logger.error(Thread.currentThread().getName()
+ "httpPost Send Error,Protocol error:" + e.getMessage());
} catch (IOException e) {
logger.error(Thread.currentThread().getName()
+ "httpPost Send Error,IO error:" + e.getMessage());
} catch (URISyntaxException e) {
logger.error(Thread.currentThread().getName()
+ "httpPost Send Error,IO error:" + e.getMessage());
} finally {// 释放连接,将连接放回到连接池
postMethod.releaseConnection();
// 释放池子中的空闲连接
// client.getConnectionManager().closeIdleConnections(30L,
// TimeUnit.MILLISECONDS);
}
return returnValue;
}
}
一些参数的解释:
- socketTimeout:socket超时时间,这个超时时间不是连接的超时时间,而是读取返回值的超时时间,即响应超时时间
- connectTimeout:请求连接对方的超时时间
- maxConnNum: 连接池最大并发数。这个设置与应用的并发量有关。比如应用的1s的请求量1000,每个请求需要10ms,则需要的最大并发数为 1000/(1s/10ms)=10,理论上设置的值最好比这个稍大一点点。
- maxPerRoute:请求后端的最大连接数,比如说后端服务有2台机器,而maxConnNum=10,则maxPerRoute=5
根据如上的配置理论上最多只可能会存在4000个连接,对于空闲的连接连接池会根据默认的设置进行回收,当然也可以在程序中进行回收,回收方法client.getConnectionManager().closeIdleConnections(30000L,TimeUnit.MILLSECONDS)表示这个连接维持30秒的空闲时间后进行回收。如果我们将30000L改为0L,则表示立即进行回收,那就失去了连接池的意义,不建议这样做。