這裏簡單解釋一下httpclient一些關鍵參數的配置html
final RequestConfig requestConfig = RequestConfig.custom() .setSocketTimeout(SOCKET_TIMEOUT) .setConnectTimeout(CONNECTION_TIMEOUT) .setCookieSpec(CookieSpecs.IGNORE_COOKIES).build();
這裏設置了socket_timeout以及connection_timeoutjava
httpclient默認提供了一個策略spring
httpclient-4.5.3-sources.jar!/org/apache/http/impl/client/DefaultConnectionKeepAliveStrategy.javaapache
@Contract(threading = ThreadingBehavior.IMMUTABLE) public class DefaultConnectionKeepAliveStrategy implements ConnectionKeepAliveStrategy { public static final DefaultConnectionKeepAliveStrategy INSTANCE = new DefaultConnectionKeepAliveStrategy(); @Override public long getKeepAliveDuration(final HttpResponse response, final HttpContext context) { Args.notNull(response, "HTTP response"); final HeaderElementIterator it = new BasicHeaderElementIterator( response.headerIterator(HTTP.CONN_KEEP_ALIVE)); while (it.hasNext()) { final HeaderElement he = it.nextElement(); final String param = he.getName(); final String value = he.getValue(); if (value != null && param.equalsIgnoreCase("timeout")) { try { return Long.parseLong(value) * 1000; } catch(final NumberFormatException ignore) { } } } return -1; } }
默認的話,是從response裏頭讀timeout參數的,沒有讀到則設置爲-1,這個表明無窮,這樣設置是有點問題了,若是是https連接的話,則可能會常常報api
Caused by: 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) at java.net.SocketInputStream.read(SocketInputStream.java:141) at sun.security.ssl.InputRecord.readFully(InputRecord.java:465) at sun.security.ssl.InputRecord.read(InputRecord.java:503) at sun.security.ssl.SSLSocketImpl.readRecord(SSLSocketImpl.java:973) at sun.security.ssl.SSLSocketImpl.readDataRecord(SSLSocketImpl.java:930) at sun.security.ssl.AppInputStream.read(AppInputStream.java:105)
HTTP規範沒有肯定一個持久鏈接可能或應該保持活動多長時間。一些HTTP服務器使用非標準的頭部信息Keep-Alive來告訴客戶端它們想在服務器端保持鏈接活動的週期秒數。若是這個信息可用,HttClient就會利用這個它。若是頭部信息Keep-Alive在響應中不存在,HttpClient假設鏈接無限期的保持活動。然而許多現實中的HTTP服務器配置了在特定不活動週期以後丟掉持久鏈接來保存系統資源,每每這是不通知客戶端的。安全
這裏能夠在此基礎上重寫一個,這裏設置爲5秒服務器
ConnectionKeepAliveStrategy keepAliveStrategy = new DefaultConnectionKeepAliveStrategy() { @Override public long getKeepAliveDuration(final HttpResponse response, final HttpContext context) { long keepAlive = super.getKeepAliveDuration(response, context); if (keepAlive == -1) { keepAlive = 5000; } return keepAlive; } };
一個經典的阻塞 I/O 模型的主要缺點是網絡套接字僅當 I/O 操做阻塞時才能夠響應 I/O 事件。當一個鏈接被釋放返回管理器時,它能夠被保持活動狀態而卻不能監控套接字的狀態和響應任何 I/O 事件。若是鏈接在服務器端關閉,那麼客戶端鏈接也不能去偵測鏈接狀態中的變化和關閉本端的套接字去做出適當響應。網絡
HttpClient 經過測試鏈接是不是過期的來嘗試去減輕這個問題,這已經再也不有效了,由於它已經在服務器端關閉了,以前使用執行 HTTP 請求的鏈接。過期的鏈接檢查也並非 100%
的穩定,反而對每次請求執行還要增長10到30毫秒的開銷。惟一可行的而不涉及到每一個對空閒鏈接的套接字模型線程解決方案,是使用專用的監控線程來收回由於長時間不活動而被認爲是過時的鏈接。監控線程能夠週期地調用 ClientConnectionManager#closeExpiredConnections()方法來關閉全部過時的鏈接,從鏈接池中收回關閉的鏈接。它也能夠選擇性調用ClientConnectionManager#closeIdleConnections()方法來關閉全部已經空 閒超過給定時間週期的鏈接。併發
官方提供了一個實例app
public static class IdleConnectionMonitorThread extends Thread { private final HttpClientConnectionManager connMgr; private volatile boolean shutdown; public IdleConnectionMonitorThread(HttpClientConnectionManager connMgr) { super(); this.connMgr = connMgr; } @Override public void run() { try { while (!shutdown) { synchronized (this) { wait(5000); // Close expired connections connMgr.closeExpiredConnections(); // Optionally, close connections // that have been idle longer than 30 sec connMgr.closeIdleConnections(30, TimeUnit.SECONDS); } } } catch (InterruptedException ex) { // terminate } } public void shutdown() { shutdown = true; synchronized (this) { notifyAll(); } } }
在spring cloud netflix zuul中則提供了相似的定時器,只不過參數是寫死的
spring-cloud-netflix-core/1.2.6.RELEASE/spring-cloud-netflix-core-1.2.6.RELEASE-sources.jar!/org/springframework/cloud/netflix/zuul/filters/route/SimpleHostRoutingFilter.java
private final Timer connectionManagerTimer = new Timer( "SimpleHostRoutingFilter.connectionManagerTimer", true); @PostConstruct private void initialize() { this.httpClient = newClient(); SOCKET_TIMEOUT.addCallback(this.clientloader); CONNECTION_TIMEOUT.addCallback(this.clientloader); this.connectionManagerTimer.schedule(new TimerTask() { @Override public void run() { if (SimpleHostRoutingFilter.this.connectionManager == null) { return; } SimpleHostRoutingFilter.this.connectionManager.closeExpiredConnections(); } }, 30000, 5000); }
每30秒清理一次失效的connection,可是這裏的closeExpiredConnections是依賴connTimeToLive參數以及keep alive strategy設置的。
httpcore-4.4.6-sources.jar!/org/apache/http/pool/AbstractConnPool.java
/** * Closes expired connections and evicts them from the pool. */ public void closeExpired() { final long now = System.currentTimeMillis(); enumAvailable(new PoolEntryCallback<T, C>() { @Override public void process(final PoolEntry<T, C> entry) { if (entry.isExpired(now)) { entry.close(); } } }); }
httpcore-4.4.6-sources.jar!/org/apache/http/pool/PoolEntry.java
public synchronized boolean isExpired(final long now) { return now >= this.expiry; } public PoolEntry(final String id, final T route, final C conn, final long timeToLive, final TimeUnit tunit) { super(); Args.notNull(route, "Route"); Args.notNull(conn, "Connection"); Args.notNull(tunit, "Time unit"); this.id = id; this.route = route; this.conn = conn; this.created = System.currentTimeMillis(); this.updated = this.created; if (timeToLive > 0) { this.validityDeadline = this.created + tunit.toMillis(timeToLive); } else { this.validityDeadline = Long.MAX_VALUE; } this.expiry = this.validityDeadline; } public synchronized void updateExpiry(final long time, final TimeUnit tunit) { Args.notNull(tunit, "Time unit"); this.updated = System.currentTimeMillis(); final long newExpiry; if (time > 0) { newExpiry = this.updated + tunit.toMillis(time); } else { newExpiry = Long.MAX_VALUE; } this.expiry = Math.min(newExpiry, this.validityDeadline); }
若是是-1,則這裏expiry爲無窮大。那麼closeExpiredConnections實際上是無效的。可是還依賴一個keep alive strategy,它會去設置updateExpiry
httpclient-4.5.3-sources.jar!/org/apache/http/impl/execchain/MainClientExec.java
@Override public CloseableHttpResponse execute( final HttpRoute route, final HttpRequestWrapper request, final HttpClientContext context, final HttpExecutionAware execAware) throws IOException, HttpException { Args.notNull(route, "HTTP route"); Args.notNull(request, "HTTP request"); Args.notNull(context, "HTTP context"); AuthState targetAuthState = context.getTargetAuthState(); if (targetAuthState == null) { targetAuthState = new AuthState(); context.setAttribute(HttpClientContext.TARGET_AUTH_STATE, targetAuthState); } AuthState proxyAuthState = context.getProxyAuthState(); if (proxyAuthState == null) { proxyAuthState = new AuthState(); context.setAttribute(HttpClientContext.PROXY_AUTH_STATE, proxyAuthState); } if (request instanceof HttpEntityEnclosingRequest) { RequestEntityProxy.enhance((HttpEntityEnclosingRequest) request); } Object userToken = context.getUserToken(); final ConnectionRequest connRequest = connManager.requestConnection(route, userToken); if (execAware != null) { if (execAware.isAborted()) { connRequest.cancel(); throw new RequestAbortedException("Request aborted"); } else { execAware.setCancellable(connRequest); } } final RequestConfig config = context.getRequestConfig(); final HttpClientConnection managedConn; try { final int timeout = config.getConnectionRequestTimeout(); managedConn = connRequest.get(timeout > 0 ? timeout : 0, TimeUnit.MILLISECONDS); } catch(final InterruptedException interrupted) { Thread.currentThread().interrupt(); throw new RequestAbortedException("Request aborted", interrupted); } catch(final ExecutionException ex) { Throwable cause = ex.getCause(); if (cause == null) { cause = ex; } throw new RequestAbortedException("Request execution failed", cause); } context.setAttribute(HttpCoreContext.HTTP_CONNECTION, managedConn); if (config.isStaleConnectionCheckEnabled()) { // validate connection if (managedConn.isOpen()) { this.log.debug("Stale connection check"); if (managedConn.isStale()) { this.log.debug("Stale connection detected"); managedConn.close(); } } } final ConnectionHolder connHolder = new ConnectionHolder(this.log, this.connManager, managedConn); try { if (execAware != null) { execAware.setCancellable(connHolder); } HttpResponse response; for (int execCount = 1;; execCount++) { if (execCount > 1 && !RequestEntityProxy.isRepeatable(request)) { throw new NonRepeatableRequestException("Cannot retry request " + "with a non-repeatable request entity."); } if (execAware != null && execAware.isAborted()) { throw new RequestAbortedException("Request aborted"); } if (!managedConn.isOpen()) { this.log.debug("Opening connection " + route); try { establishRoute(proxyAuthState, managedConn, route, request, context); } catch (final TunnelRefusedException ex) { if (this.log.isDebugEnabled()) { this.log.debug(ex.getMessage()); } response = ex.getResponse(); break; } } final int timeout = config.getSocketTimeout(); if (timeout >= 0) { managedConn.setSocketTimeout(timeout); } if (execAware != null && execAware.isAborted()) { throw new RequestAbortedException("Request aborted"); } if (this.log.isDebugEnabled()) { this.log.debug("Executing request " + request.getRequestLine()); } if (!request.containsHeader(AUTH.WWW_AUTH_RESP)) { if (this.log.isDebugEnabled()) { this.log.debug("Target auth state: " + targetAuthState.getState()); } this.authenticator.generateAuthResponse(request, targetAuthState, context); } if (!request.containsHeader(AUTH.PROXY_AUTH_RESP) && !route.isTunnelled()) { if (this.log.isDebugEnabled()) { this.log.debug("Proxy auth state: " + proxyAuthState.getState()); } this.authenticator.generateAuthResponse(request, proxyAuthState, context); } response = requestExecutor.execute(request, managedConn, context); // The connection is in or can be brought to a re-usable state. if (reuseStrategy.keepAlive(response, context)) { // Set the idle duration of this connection // 這裏從keep alive strategy裏頭讀取duration final long duration = keepAliveStrategy.getKeepAliveDuration(response, context); if (this.log.isDebugEnabled()) { final String s; if (duration > 0) { s = "for " + duration + " " + TimeUnit.MILLISECONDS; } else { s = "indefinitely"; } this.log.debug("Connection can be kept alive " + s); } //這裏update valid connHolder.setValidFor(duration, TimeUnit.MILLISECONDS); connHolder.markReusable(); } else { connHolder.markNonReusable(); } if (needAuthentication( targetAuthState, proxyAuthState, route, response, context)) { // Make sure the response body is fully consumed, if present final HttpEntity entity = response.getEntity(); if (connHolder.isReusable()) { EntityUtils.consume(entity); } else { managedConn.close(); if (proxyAuthState.getState() == AuthProtocolState.SUCCESS && proxyAuthState.getAuthScheme() != null && proxyAuthState.getAuthScheme().isConnectionBased()) { this.log.debug("Resetting proxy auth state"); proxyAuthState.reset(); } if (targetAuthState.getState() == AuthProtocolState.SUCCESS && targetAuthState.getAuthScheme() != null && targetAuthState.getAuthScheme().isConnectionBased()) { this.log.debug("Resetting target auth state"); targetAuthState.reset(); } } // discard previous auth headers final HttpRequest original = request.getOriginal(); if (!original.containsHeader(AUTH.WWW_AUTH_RESP)) { request.removeHeaders(AUTH.WWW_AUTH_RESP); } if (!original.containsHeader(AUTH.PROXY_AUTH_RESP)) { request.removeHeaders(AUTH.PROXY_AUTH_RESP); } } else { break; } } if (userToken == null) { userToken = userTokenHandler.getUserToken(context); context.setAttribute(HttpClientContext.USER_TOKEN, userToken); } if (userToken != null) { connHolder.setState(userToken); } // check for entity, release connection if possible final HttpEntity entity = response.getEntity(); if (entity == null || !entity.isStreaming()) { // connection not needed and (assumed to be) in re-usable state connHolder.releaseConnection(); return new HttpResponseProxy(response, null); } else { return new HttpResponseProxy(response, connHolder); } } catch (final ConnectionShutdownException ex) { final InterruptedIOException ioex = new InterruptedIOException( "Connection has been shut down"); ioex.initCause(ex); throw ioex; } catch (final HttpException ex) { connHolder.abortConnection(); throw ex; } catch (final IOException ex) { connHolder.abortConnection(); throw ex; } catch (final RuntimeException ex) { connHolder.abortConnection(); throw ex; } }
主要看
httpclient-4.5.3-sources.jar!/org/apache/http/impl/execchain/ConnectionHolder.java
public void setValidFor(final long duration, final TimeUnit tunit) { synchronized (this.managedConn) { this.validDuration = duration; this.tunit = tunit; } } private void releaseConnection(final boolean reusable) { if (this.released.compareAndSet(false, true)) { synchronized (this.managedConn) { if (reusable) { this.manager.releaseConnection(this.managedConn, this.state, this.validDuration, this.tunit); } else { try { this.managedConn.close(); log.debug("Connection discarded"); } catch (final IOException ex) { if (this.log.isDebugEnabled()) { this.log.debug(ex.getMessage(), ex); } } finally { this.manager.releaseConnection( this.managedConn, null, 0, TimeUnit.MILLISECONDS); } } } } }
這裏設置了validDuration,會傳給releaseConnection
httpclient-4.5.3-sources.jar!/org/apache/http/impl/conn/PoolingHttpClientConnectionManager.java
public void releaseConnection( final HttpClientConnection managedConn, final Object state, final long keepalive, final TimeUnit tunit) { Args.notNull(managedConn, "Managed connection"); synchronized (managedConn) { final CPoolEntry entry = CPoolProxy.detach(managedConn); if (entry == null) { return; } final ManagedHttpClientConnection conn = entry.getConnection(); try { if (conn.isOpen()) { final TimeUnit effectiveUnit = tunit != null ? tunit : TimeUnit.MILLISECONDS; entry.setState(state); entry.updateExpiry(keepalive, effectiveUnit); if (this.log.isDebugEnabled()) { final String s; if (keepalive > 0) { s = "for " + (double) effectiveUnit.toMillis(keepalive) / 1000 + " seconds"; } else { s = "indefinitely"; } this.log.debug("Connection " + format(entry) + " can be kept alive " + s); } } } finally { this.pool.release(entry, conn.isOpen() && entry.isRouteComplete()); if (this.log.isDebugEnabled()) { this.log.debug("Connection released: " + format(entry) + formatStats(entry.getRoute())); } } } }
這裏去updateExpiry,至關於更新了timeToLive
這裏設置的是http鏈接池中connection的存活時間
httpclient-4.5.3-sources.jar!/org/apache/http/impl/client/HttpClientBuilder.java
/** * Sets maximum time to live for persistent connections * <p> * Please note this value can be overridden by the {@link #setConnectionManager( * org.apache.http.conn.HttpClientConnectionManager)} method. * </p> * * @since 4.4 */ public final HttpClientBuilder setConnectionTimeToLive(final long connTimeToLive, final TimeUnit connTimeToLiveTimeUnit) { this.connTimeToLive = connTimeToLive; this.connTimeToLiveTimeUnit = connTimeToLiveTimeUnit; return this; }
httpclient-4.5.3-sources.jar!/org/apache/http/impl/conn/PoolingHttpClientConnectionManager.java
public PoolingHttpClientConnectionManager( final Registry<ConnectionSocketFactory> socketFactoryRegistry, final HttpConnectionFactory<HttpRoute, ManagedHttpClientConnection> connFactory, final DnsResolver dnsResolver) { this(socketFactoryRegistry, connFactory, null, dnsResolver, -1, TimeUnit.MILLISECONDS); } public PoolingHttpClientConnectionManager( final Registry<ConnectionSocketFactory> socketFactoryRegistry, final HttpConnectionFactory<HttpRoute, ManagedHttpClientConnection> connFactory, final SchemePortResolver schemePortResolver, final DnsResolver dnsResolver, final long timeToLive, final TimeUnit tunit) { this( new DefaultHttpClientConnectionOperator(socketFactoryRegistry, schemePortResolver, dnsResolver), connFactory, timeToLive, tunit ); }
默認構造的話,timeToLive是-1,若是默認是-1的話,則不失效
4.0的ThreadSafeClientConnManager,在4.2版本的時候被廢棄了,默認使用PoolingHttpClientConnectionManager。這個manager有兩個重要的參數,一個是maxTotal,一個是defaultMaxPerRoute。
每一個默認的實現對每一個給定路由將會建立不超過2個的併發鏈接,而總共也不會超過 20 個鏈接。
this.pool = new CPool(new InternalConnectionFactory(this.configData, connFactory), 2, 20, timeToLive, tunit);
對於不少真實的應用程序,這個限制也證實很大的制約,特別是他們在服務中使用 HTTP 做爲 傳輸協議。鏈接限制,也可使用 HTTP 參數來進行調整。
spring cloud netflix zuul 裏頭默認配置是總共200鏈接,每一個route不超過20個鏈接
this.connectionManager = new PoolingHttpClientConnectionManager(registry); this.connectionManager.setMaxTotal(this.hostProperties.getMaxTotalConnections()); // 200 this.connectionManager.setDefaultMaxPerRoute(this.hostProperties.getMaxPerRouteConnections()); //20
httpclient-4.5.3-sources.jar!/org/apache/http/conn/routing/HttpRoute.java
HttpRoute對象是immutable的,包含的數據有目標主機、本地地址、代理鏈、是否tunnulled、是否layered、是不是安全路由。
@Contract(threading = ThreadingBehavior.IMMUTABLE) public final class HttpRoute implements RouteInfo, Cloneable { /** The target host to connect to. */ private final HttpHost targetHost; /** * The local address to connect from. * {@code null} indicates that the default should be used. */ private final InetAddress localAddress; /** The proxy servers, if any. Never null. */ private final List<HttpHost> proxyChain; /** Whether the the route is tunnelled through the proxy. */ private final TunnelType tunnelled; /** Whether the route is layered. */ private final LayerType layered; /** Whether the route is (supposed to be) secure. */ private final boolean secure; //... }
httpclient-4.5.3-sources.jar!/org/apache/http/impl/client/IdleConnectionEvictor.java
public IdleConnectionEvictor( final HttpClientConnectionManager connectionManager, final ThreadFactory threadFactory, final long sleepTime, final TimeUnit sleepTimeUnit, final long maxIdleTime, final TimeUnit maxIdleTimeUnit) { this.connectionManager = Args.notNull(connectionManager, "Connection manager"); this.threadFactory = threadFactory != null ? threadFactory : new DefaultThreadFactory(); this.sleepTimeMs = sleepTimeUnit != null ? sleepTimeUnit.toMillis(sleepTime) : sleepTime; this.maxIdleTimeMs = maxIdleTimeUnit != null ? maxIdleTimeUnit.toMillis(maxIdleTime) : maxIdleTime; this.thread = this.threadFactory.newThread(new Runnable() { @Override public void run() { try { while (!Thread.currentThread().isInterrupted()) { Thread.sleep(sleepTimeMs); connectionManager.closeExpiredConnections(); if (maxIdleTimeMs > 0) { connectionManager.closeIdleConnections(maxIdleTimeMs, TimeUnit.MILLISECONDS); } } } catch (final Exception ex) { exception = ex; } } }); }
builder默認會構造一個IdleConnectionEvictor
httpclient-4.5.3-sources.jar!/org/apache/http/impl/client/HttpClientBuilder.java
if (!this.connManagerShared) { if (closeablesCopy == null) { closeablesCopy = new ArrayList<Closeable>(1); } final HttpClientConnectionManager cm = connManagerCopy; if (evictExpiredConnections || evictIdleConnections) { final IdleConnectionEvictor connectionEvictor = new IdleConnectionEvictor(cm, maxIdleTime > 0 ? maxIdleTime : 10, maxIdleTimeUnit != null ? maxIdleTimeUnit : TimeUnit.SECONDS); closeablesCopy.add(new Closeable() { @Override public void close() throws IOException { connectionEvictor.shutdown(); } }); connectionEvictor.start(); } closeablesCopy.add(new Closeable() { @Override public void close() throws IOException { cm.shutdown(); } }); }
若是沒有指定maxIdleTime的話,可是有設置evictExpiredConnections的話,默認是10秒
public final HttpClientBuilder evictExpiredConnections() { evictExpiredConnections = true; return this; } public final HttpClientBuilder evictIdleConnections(final long maxIdleTime, final TimeUnit maxIdleTimeUnit) { this.evictIdleConnections = true; this.maxIdleTime = maxIdleTime; this.maxIdleTimeUnit = maxIdleTimeUnit; return this; }
設置從鏈接池獲取一個鏈接的請求超時時間(鏈接池中鏈接不夠用的時候等待超時時間
),單位毫秒,能夠設置爲500ms
httpclient-4.5.3-sources.jar!/org/apache/http/impl/execchain/MainClientExec.java
final RequestConfig config = context.getRequestConfig(); final HttpClientConnection managedConn; try { final int timeout = config.getConnectionRequestTimeout(); managedConn = connRequest.get(timeout > 0 ? timeout : 0, TimeUnit.MILLISECONDS); } catch(final InterruptedException interrupted) { Thread.currentThread().interrupt(); throw new RequestAbortedException("Request aborted", interrupted); } catch(final ExecutionException ex) { Throwable cause = ex.getCause(); if (cause == null) { cause = ex; } throw new RequestAbortedException("Request execution failed", cause); }
設置重試策略
httpclient-4.5.3-sources.jar!/org/apache/http/impl/client/DefaultHttpRequestRetryHandler.java
public DefaultHttpRequestRetryHandler(final int retryCount, final boolean requestSentRetryEnabled) { this(retryCount, requestSentRetryEnabled, Arrays.asList( InterruptedIOException.class, UnknownHostException.class, ConnectException.class, SSLException.class)); }
好比不重試,能夠這樣設置
httpClientBuilder.setConnectionManager(newConnectionManager()) .useSystemProperties().setDefaultRequestConfig(requestConfig) .setRetryHandler(new DefaultHttpRequestRetryHandler(0, false));
當getKeepAliveDuration爲-1以及connTimeToLive爲-1的時候,closeExpiredConnections方法實際上是沒有用的,查看debug日誌會出現
o.a.http.impl.client.DefaultHttpClient 509 : Connection can be kept alive indefinitely
當getKeepAliveDuration不爲-1的話,假設是5s,則日誌多是這樣的
o.a.http.impl.execchain.MainClientExec 285 : Connection can be kept alive for 5000 MILLISECONDS