1. Background
HTTP protocol is a stateless protocol, that is, each request is independent of each other. Therefore, its initial implementation is that every http request will open a tcp socket connection, and the connection will be closed after the interaction is completed.
The HTTP protocol is a full duplex protocol, so it takes three handshakes and four waves to establish and disconnect. Obviously, in this design, every time I send an Http request, it consumes a lot of extra resources, namely the establishment and destruction of the connection.
Therefore, the HTTP protocol has also been developed, and socket connection multiplexing is performed through persistent connection methods.
From the picture, you can see:
There are two implementations of persistent connections: keep-alive and persistent connections of HTTP/1.1 for HTTP/1.0+.
2. Keep-Alive for HTTP/1.0+
Since 1996, many HTTP/1.0 browsers and servers have extended the protocol, that is, the "keep-alive" extension protocol.
Note that this extension protocol appears as a complement to 1.0 "experimental persistent connection". Keep-alive is no longer used, and it is not explained in the latest HTTP/1.1 specification, but many applications have continued.
Clients using HTTP/1.0 add "Connection:Keep-Alive" to the header, and request the server to keep a connection open. If the server is willing to keep this connection open, it will include the same header in the response. If the response does not contain the "Connection:Keep-Alive" header, the client will think that the server does not support keep-alive and will close the current connection after sending the response message.
Through the keep-alive supplementary protocol, a persistent connection is completed between the client and the server, but there are still some problems:
3. Persistent connection of HTTP/1.1
HTTP/1.1 takes a persistent connection method to replace Keep-Alive.
HTTP/1.1 connections are persistent by default. If you want to explicitly close, you need to add the Connection:Close header to the message. That is, in HTTP/1.1, all connections are multiplexed.
However, like Keep-Alive, idle persistent connections can be closed by the client and the server at any time. Not sending Connection:Close does not mean that the server promises that the connection will remain open forever.
4. How to generate persistent connections by HttpClient
HttpClien uses a connection pool to manage the holding connections. On the same TCP link, connections can be reused. HttpClient connects persistence through connection pooling.
In fact, the "pool" technology is a general design, and its design idea is not complicated:
All connection pools have this idea, but when we look at the HttpClient source code, we mainly focus on two points:
4.1 Implementation of HttpClient connection pool
HttpClient's processing of persistent connections can be reflected in the following code. The following is to extract the parts related to the connection pool from MainClientExec and remove other parts:
public class MainClientExec implements ClientExecChain { @Override public CloseableHttpResponse execute( final HttpRoute route, final HttpRequestWrapper request, final HttpClientContext context, final HttpExecutionAware execAware) throws IOException, HttpException { //Get a connection request from the connection manager HttpClientConnectionManager ConnectionRequest final ConnectionRequest connRequest = connManager.requestConnection(route, userToken);final HttpClientConnection managedConn; final int timeout = config.getConnectionRequestTimeout(); //Get a managed connection from the connection request ConnectionRequestHttpClientConnection managedConn = connRequest.get(timeout > 0 ? timeout : 0, TimeUnit.MILLISECONDS); //Submit the connection manager HttpClientConnectionManager and the managed connection HttpClientConnection to a ConnectionHolder holds final ConnectionHolder connHolder = new ConnectionHolder(this.log, this.connManager, managedConn); try { HttpResponse response; if (!managedConn.isOpen()) { //If the currently managed connection is not in an open state, you need to re-establish the connection established(proxyAuthState, managedConn, route, request, context); } //Send request through the connection HttpClientConnection response = requestExecutor.execute(request, managedConn, context); //Distinguish whether the connection can be reused through the connection reuse strategy if (reuseStrategy.keepAlive(response, context)) { //Get the connection validity period final long duration = keepAliveStrategy.getKeepAliveDuration(response, context); //Set the connection validity period connHolder.setValidFor(duration, TimeUnit.MILLISECONDS); //Mark the current connection as reusable state connHolder.markReusable(); } else { connHolder.markNonReusable(); } } final HttpEntity entity = response.getEntity(); if (entity == null || !entity.isStreaming()) { //Release the current connection to the pool for the next call to connHolder.releaseConnection(); return new HttpResponseProxy(response, null); } else { return new HttpResponseProxy(response, connHolder); }}Here we see that the processing of connections during the Http request process is consistent with the protocol specifications. Here we will discuss the specific implementation.
PoolingHttpClientConnectionManager is the default connection manager of HttpClient. First, obtain a connection request through requestConnection(). Note that this is not a connection.
public ConnectionRequest requestConnection( final HttpRoute route, final Object state) {final Future<CPoolEntry> future = this.pool.lease(route, state, null); return new ConnectionRequest() { @Override public boolean cancel() { return future.cancel(true); } @Override public HttpClientConnection get( final long timeout, final TimeUnit tunit) throws InterruptedException, ExecutionException, ConnectionPoolTimeoutException { final HttpClientConnection conn = leaseConnection(future, timeout, tunit); if (conn.isOpen()) { final HttpHost host; if (route.getProxyHost() != null) { host = route.getProxyHost(); } else { host = route.getTargetHost(); } final SocketConfig socketConfig = resolveSocketConfig(host); conn.setSocketTimeout(socketConfig.getSoTimeout()); } return conn; } }; }You can see that the returned ConnectionRequest object is actually a real connection instance that holds Future<CPoolEntry>, which is managed by the connection pool.
From the above code we should focus on:
Future<CPoolEntry> future = this.pool.lease(route, state, null)
How to get an asynchronous connection from a connection pool CPool, Future<CPoolEntry>
HttpClientConnection conn = leaseConnection(future, timeout, tunit)
How to get a true connection by asynchronous connection to Future<CPoolEntry>
4.2 Future<CPoolEntry>
Let's take a look at how CPool releases a Future<CPoolEntry>. The core code of AbstractConnPool is as follows:
private E getPoolEntryBlocking( final T route, final Object state, final long timeout, final TimeUnit tunit, final Future<E> future) throws IOException, InterruptedException, TimeoutException { //First lock the current connection pool. The current lock is a reentrantLockthis.lock.lock(); try { //Get a connection pool corresponding to the current HttpRoute. For the connection pool of HttpClient, the total pool has a size, and the connection corresponding to each route is also a pool, so it is a "pool in the pool" final RouteSpecificPool<T, C, E> pool = getPool(route); E entry; for (;;) { Asserts.check(!this.isShutDown, "Connection pool shut down"); //Get connections from the pool corresponding to the route, which may be null, or a valid connection entry = pool.getFree(state); //If you get null, exit the loop if (entry == null) { break; } //If you get an expired connection or the connection has been closed, release the resource and continue to loop to get if (entry.isExpired(System.currentTimeMillis())) { entry.close(); } if (entry.isClosed()) { this.available.remove(entry); pool.free(entry, false); } else { //If you get a valid connection, exit the loop break; } } //If you get a valid connection, exit if (entry != null) { this.available.remove(entry); this.leased.add(entry); onReuse(entry); return entry; } //To here prove that no valid connection was obtained, you need to generate a final int maxPerRoute = getMax(route); //The maximum number of connections corresponding to each route is configurable. If it exceeds, you need to clean up some connections through LRU final int excess = Math.max(0, pool.getAllocatedCount() + 1 - maxPerRoute); if (excess > 0) { for (int i = 0; i < excess; i++) { final E lastUsed = pool.getLastUsed(); if (lastUsed == null) { break; } lastUsed.close(); this.available.remove(lastUsed); pool.remove(lastUsed); } } //The number of connections in the current route pool has not reached the online if (pool.getAllocatedCount() < maxPerRoute) { final int totalUsed = this.leased.size(); final int freeCapacity = Math.max(this.maxTotal - totalUsed, 0); //Judge whether the connection pool exceeds the online line. If it exceeds it, you need to clean up some connections through LRU if (freeCapacity > 0) { final int totalAvailable = this.available.size(); //If the number of free connections is already greater than the remaining available space, you need to clean up the free connection if (totalAvailable > freeCapacity - 1) { if (!this.available.isEmpty()) { final E lastUsed = this.available.removeLast(); lastUsed.close(); final RouteSpecificPool<T, C, E> otherpool = getPool(lastUsed.getRoute()); otherpool.remove(lastUsed); } } //Create a connection based on route final C conn = this.connFactory.create(route); //Put this connection into the "small pool" corresponding to route entry = pool.add(conn); //Put this connection into the "big pool" this.leased.add(entry); return entry; } } //To this end, it is proved that there is no valid connection from the route pool obtained, and when you want to establish a connection yourself, the current route connection pool has reached its maximum value, that is, there is already a connection in use, but it is not available for the current thread boolean success = false; try { if (future.isCancelled()) { throw new InterruptedException("Operation interrupted"); } //Put future into the route pool waiting for pool.queue(future); //Put future into the large connection pool waiting for this.pending.add(future); //If you wait for the notification of the semaphore, success is true if (deadline != null) { success = this.condition.awaitUntil(deadline); } else { this.condition.await(); success = true; } if (future.isCancelled()) { throw new InterruptedException("Operation interrupted"); } } finally { //Remove pool.unqueue(future); this.pending.remove(future); } //If the semaphore notification is not waited and the current time has timed out, the loop is exited if (!success && (deadline != null && deadline.getTime() <= System.currentTimeMillis())) { break; } } //In the end, no semaphore notification was received and no available connection was obtained, an exception was thrown. throw new TimeoutException("Timeout waiting for connection"); } finally { //Release the lock on the large connection pool this.lock.unlock(); } }There are several important points in the above code logic:
So far, the program has obtained an available CPoolEntry instance, or terminated the program by throwing an exception.
4.3 HttpClientConnection
protected HttpClientConnection leaseConnection( final Future<CPoolEntry> future, final long timeout, final TimeUnit tunit) throws InterruptedException, ExecutionException, ConnectionPoolTimeoutException { final CPoolEntry entry; try { //Get CPoolEntry entry from asynchronous operation Future<CPoolEntry> entry = future.get(timeout, tunit); if (entry == null || future.isCancelled()) { throw new InterruptedException(); } Asserts.check(entry.getConnection() != null, "Pool entry with no connection"); if (this.log.isDebugEnabled()) { this.log.debug("Connection spared: " + format(entry) + formatStats(entry.getRoute())); } //Get a proxy object of CPoolEntry, and the operations are all done using the same underlying HttpClientConnection return CPoolProxy.newProxy(entry); } catch (final TimeoutException ex) { throw new ConnectionPoolTimeoutException("Timeout waiting for connection from pool"); } } 5. How to reuse persistent connections in HttpClient?
In the previous chapter, we saw that HttpClient gets connections through connection pools, and gets them from the pool when it is necessary to use connections.
Corresponding to the third chapter:
In Chapter 4, we saw how HttpClient handles the problems of 1 and 3, so how do we deal with the second question?
That is, how does HttpClient determine whether a connection should be closed after use, or should it be placed in a pool for others to reuse? Look at the code of MainClientExec
//Send Http connection response = requestExecutor.execute(request, managedConn, context); //Defend whether the current connection is to be reused based on the reuse strategy if (reuseStrategy.keepAlive(response, context)) { //The connection that needs to be reused, get the connection timeout time, based on the timeout in the response final long duration = keepAliveStrategy.getKeepAliveDuration(response, context); if (this.log.isDebugEnabled()) { final String s; //Timeout is the number of milliseconds, if not set, it is -1, that is, there is no timeout if (duration > 0) { s = "for " + duration + " " + TimeUnit.MILLISECONDS; } else { s = "indefinitely"; } this.log.debug("Connection can be kept alive " + s); } //Set the timeout time. When the request ends, the connection manager will decide whether to close or put it back into the pool based on the timeout time connHolder.setValidFor(duration, TimeUnit.MILLISECONDS); //Sign up the connection as reusable connHolder.markReusable(); } else { //Sign up the connection as non-reusable connHolder.markNonReusable(); }It can be seen that after a request occurs using a connection, there is a connection retry policy to decide whether the connection is to be reused. If it is reused, it will be handed over to the HttpClientConnectionManager after the end.
So what is the logic of the connection multiplexing policy?
public class DefaultClientConnectionReuseStrategy extends DefaultConnectionReuseStrategy { public static final DefaultClientConnectionReuseStrategy INSTANCE = new DefaultClientConnectionReuseStrategy(); @Override public boolean keepAlive(final HttpResponse response, final HttpContext context) { //Get request final HttpRequest from the context final HttpRequest request = (HttpRequest) context.getAttribute(HttpCoreContext.HTTP_REQUEST); if (request != null) { //Get the Header final Header[] connHeaders = request.getHeaders(HttpHeaders.CONNECTION); if (connHeaders.length != 0) { final TokenIterator ti = new BasicTokenIterator(new BasicHeaderIterator(connHeaders, null)); while (ti.hasNext()) { final String token = ti.nextToken(); //If the Connection:Close header is included, it means that the request does not intend to keep the connection, and the response's intention will be ignored. This header is the specification of HTTP/1.1 if (HTTP.CONN_CLOSE.equalsIgnoreCase(token)) { return false; } } } } } //Use the reuse strategy of the parent class to return super.keepAlive(response, context); }}Take a look at the parent class's reuse strategy
if (canResponseHaveBody(request, response)) { final Header[] clhs = response.getHeaders(HTTP.CONTENT_LEN); //If the Content-Length of the response is not set correctly, the connection will not be reused//Because for persistent connections, there is no need to re-establish the connection between the two transmissions, you need to confirm which request the content belongs to based on Content-Length to correctly handle the "stick packet" phenomenon // Therefore, the response connection without correctly setting Content-Length cannot be reused if (clhs.length == 1) { final Header clh = clhs[0]; try { final int contentLen = Integer.parseInt(clh.getValue()); if (contentLen < 0) { return false; } } catch (final NumberFormatException ex) { return false; } } else { return false; } } if (headerIterator.hasNext()) { try { final TokenIterator ti = new BasicTokenIterator(headerIterator); boolean keepalive = false; while (ti.hasNext()) { final String token = ti.nextToken(); //If the response has a Connection:Close header, it is explicitly stated that it is to be closed, and if (HTTP.CONN_CLOSE.equalsIgnoreCase(token)) { return false; //If the response has a Connection:Keep-Alive header, it is explicitly stated that it is to be persisted, it is reused} else if (HTTP.CONN_KEEP_ALIVE.equalsIgnoreCase(token)) { keepalive = true; } } if (keepalive) { return true; } } catch (final ParseException px) { return false; } } } //If there is no relevant Connection header description in the response, all connections higher than HTTP/1.0 versions will be reused to return !ver.lessEquals(HttpVersion.HTTP_1_0);To summarize:
As can be seen from the code, its implementation strategy is consistent with the constraints of our Chapter 2 and Chapter 3 protocol layers.
6. How to clean out expired connections from HttpClient
Before HttpClient 4.4, when reusing the connection from the connection pool, it will check whether it expires, and clean it if it expires.
The subsequent version will be different. There will be a separate thread to scan the connections in the connection pool. After finding that there is a last use of the time that has been set, it will be cleaned up. The default timeout is 2 seconds.
public CloseableHttpClient build() { //If you specify that you want to clean out expired and idle connections, the cleaning thread will be started. The default is not started if (evictExpiredConnections || evictIdleConnections) { //Create a clean thread for a connection pool final IdleConnectionEvictor connectionEvictor = new IdleConnectionEvictor(cm, maxIdleTime > 0 ? maxIdleTime : 10, maxIdleTimeUnit != null ? maxIdleTimeUnit : TimeUnit.SECONDS, maxIdleTime, maxIdleTimeUnit); closeablesCopy.add(new Closeable() { @Override public void close() throws IOException { connectionEvictor.shutdown(); try { connectionEvictor.awaitTermination(1L, TimeUnit.SECONDS); } catch (final InterruptedException interrupted) { Thread.currentThread().interrupt(); } } }); //Execute the cleaning thread connectionEvictor.start();}You can see that when HttpClientBuilder is building, if the cleaning function is enabled is specified, a connection pool cleaning thread will be created and run it.
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 { //The dead loop, the thread keeps executing while (!Thread.currentThread().isInterrupted()) { //Execute after a few seconds of rest, default to 10 seconds Thread.sleep(sleepTimeMs); //Clean expired connection connectionManager.closeExpiredConnections(); //If the maximum idle time is specified, clean up the idle connection if (maxIdleTimeMs > 0) { connectionManager.closeIdleConnections(maxIdleTimeMs, TimeUnit.MILLISECONDS); } } } catch (final Exception ex) { exception = ex; } } }); }To summarize:
7. Summary of this article
The above research is based on personal understanding of the HttpClient source code. If there is any error, I hope everyone will leave a message to discuss it actively.
Okay, the above is the entire content of this article. I hope that the content of this article has certain reference value for everyone's study or work. If you have any questions, you can leave a message to communicate. Thank you for your support to Wulin.com.