最近在使用RestTemplate的时候发现一个坑,就是某一次发送请求的时候突然卡住不动了,而且没有任何报错提示,重启下服务器就好,可见是因为某些特殊原因导致的,在网上找了下资料发现是因为没有设置超时时间,默认超时时间为永久,就想做一些试验复现下这个bug.
HttpClient试验
不设置HttpClient超时时间,看看发送请求的过程中如果目标服务突然挂掉会怎么样
public class HttpTimeoutTest {
@Test
public void testHttpClient() {
String url = "http://localhost:8081/httpTest/testSleep";
Map params = new HashMap();
params.put("time", 30);
String result = doGet(url, params, null);
System.out.println(result);
}
private static CloseableHttpClient buildClient() throws KeyStoreException, NoSuchAlgorithmException, KeyManagementException {
//设置连接超时时间
RequestConfig.Builder builder = RequestConfig.custom();
/*builder.setConnectTimeout(60 * 1000);
builder.setSocketTimeout(180 * 1000);*/
RequestConfig requestConfig = builder.build();
SSLContext sslContext = new SSLContextBuilder().loadTrustMaterial(null, new TrustStrategy() {
public boolean isTrusted(X509Certificate[] arg0, String arg1) throws CertificateException {
return true;
}
}).build();
SSLConnectionSocketFactory sslSocketFactory = new SSLConnectionSocketFactory(sslContext,
NoopHostnameVerifier.INSTANCE);
CloseableHttpClient httpclient = HttpClients.custom()
.setDefaultRequestConfig(requestConfig)
.setSSLSocketFactory(sslSocketFactory).build();
return httpclient;
}
public static String doGet(String url, Map<String, Object> params, Map<String, String> headersMap) {
String result = null;
CloseableHttpClient httpClient = null;
try {
httpClient = buildClient();
String apiUrl = url;
if (null != params && params.size() > 0) {
StringBuffer param = new StringBuffer();
int i = 0;
for (String key : params.keySet()) {
if (i == 0)
param.append("?");
else
param.append("&");
param.append(key).append("=").append(params.get(key));
i++;
}
apiUrl += param;
}
HttpGet httpGet = new HttpGet(apiUrl);
if (null != headersMap && headersMap.size() > 0) {
for (Map.Entry<String, String> entry : headersMap.entrySet()) {
String key = entry.getKey();
String value = entry.getValue();
httpGet.addHeader(new BasicHeader(key, value));
}
}
CloseableHttpResponse response = httpClient.execute(httpGet);
try {
HttpEntity entity = response.getEntity();
if (null != entity) {
result = EntityUtils.toString(entity, "utf-8");
}
} finally {
response.close();
}
} catch (Exception e) {
e.printStackTrace();
} finally {
IoUtil.close(httpClient);
}
return result;
}
}
发现抛NoHttpResponseException了,目标服务突然断开不会造成卡死
org.apache.http.NoHttpResponseException: The target server failed to respond
at org.apache.http.impl.conn.DefaultHttpResponseParser.parseHead(DefaultHttpResponseParser.java:141)
at org.apache.http.impl.conn.DefaultHttpResponseParser.parseHead(DefaultHttpResponseParser.java:56)
at org.apache.http.impl.io.AbstractMessageParser.parse(AbstractMessageParser.java:259)
at org.apache.http.impl.DefaultBHttpClientConnection.receiveResponseHeader(DefaultBHttpClientConnection.java:163)
at org.apache.http.impl.conn.CPoolProxy.receiveResponseHeader(CPoolProxy.java:157)
这是为什么呢?
我们知道,建立tcp连接需要三次握手,断开连接需要四次挥手,当进程主动close掉连接时,请求方是可以正常接收到,如果是进程突然挂掉呢?那操作系统就会去关闭连接,所以请求方还是能知道
那如果是服务器突然断网或者服务器挂掉了呢?这时服务器没法主动断开连接,请求方就会一直等待,这时就会造成卡死现象。
我们给HttpClient设置超时时间,再试试
builder.setConnectTimeout(5 * 1000); // 建立三次握手的超时时间
builder.setSocketTimeout(40 * 1000); // 没收到服务器数据的超时时间
可以发现超过40s还没返回,就会抛SocketTimeoutException
java.net.SocketTimeoutException: Read timed out
at java.net.SocketInputStream.socketRead0(Native Method)
at java.net.SocketInputStream.socketRead(SocketInputStream.java:116)
at java.net.SocketInputStream.read(SocketInputStream.java:170)
connectTimeout是建立连接的时间,如果服务器没挂,进程挂了,则3s就会报Connection refused: connect,否则就要20s,所以最好也设置下
RestTemplate超时时间
接下去我们用RestTemplate重复以上实验,发现会有同样的问题,这是就要设置超时时间了
@Test
public void testRestTemplate() {
RestTemplate restTemplate = restTemplate();
String url = "http://localhost:8081/httpTest/testSleep?time=30";
ResponseEntity responseEntity = restTemplate.getForEntity(url, String.class);
System.out.println(responseEntity.getBody());
}
public RestTemplate restTemplate() {
RestTemplate restTemplate = new RestTemplate();
restTemplate.setRequestFactory(clientHttpRequestFactory());
restTemplate.setErrorHandler(new DefaultResponseErrorHandler());
return restTemplate;
}
public HttpComponentsClientHttpRequestFactory clientHttpRequestFactory() {
try {
HttpClientBuilder httpClientBuilder = HttpClientBuilder.create();
SSLContext sslContext = new SSLContextBuilder().loadTrustMaterial(null, new org.apache.http.ssl.TrustStrategy() {
public boolean isTrusted(X509Certificate[] arg0, String arg1) throws CertificateException {
return true;
}
}).build();
httpClientBuilder.setSSLContext(sslContext);
HostnameVerifier hostnameVerifier = NoopHostnameVerifier.INSTANCE;
SSLConnectionSocketFactory sslConnectionSocketFactory = new SSLConnectionSocketFactory(sslContext, hostnameVerifier);
Registry<ConnectionSocketFactory> socketFactoryRegistry = RegistryBuilder.<ConnectionSocketFactory>create()
.register("http", PlainConnectionSocketFactory.getSocketFactory())
.register("https", sslConnectionSocketFactory).build();// 注册http和https请求
// 开始设置连接池
PoolingHttpClientConnectionManager poolingHttpClientConnectionManager = new PoolingHttpClientConnectionManager(socketFactoryRegistry);
poolingHttpClientConnectionManager.setMaxTotal(500); // 最大连接数500
poolingHttpClientConnectionManager.setDefaultMaxPerRoute(100); // 同路由并发数100
httpClientBuilder.setConnectionManager(poolingHttpClientConnectionManager);
HttpClient httpClient = httpClientBuilder.build();
HttpComponentsClientHttpRequestFactory clientHttpRequestFactory = new HttpComponentsClientHttpRequestFactory(httpClient); // httpClient连接配置
clientHttpRequestFactory.setConnectTimeout(20 * 1000); // 连接超时
clientHttpRequestFactory.setReadTimeout(30 * 1000); // 数据读取超时时间
clientHttpRequestFactory.setConnectionRequestTimeout(20 * 1000); // 连接不够用的等待时间
return clientHttpRequestFactory;
} catch (KeyManagementException | NoSuchAlgorithmException | KeyStoreException e) {
e.printStackTrace();
}
return null;
}