HttpClient 是 Apache Jakarta Common 下的子項目,能夠用來提供高效的、最新的、功能豐富的支持 HTTP 協議的客戶端編程工具包,而且它支持 HTTP 協議最新的版本和建議。HttpClient支持的功能以下:
java
支持Http0.九、Http1.0和Http1.1協議。編程
實現了Http所有的方法(GET,POST,PUT,HEAD 等)。安全
支持HTTPS協議。服務器
支持代理服務器。restful
提供安全認證方案。cookie
提供鏈接池以便重用鏈接。
多線程
鏈接管理器支持多線程應用。支持設置最大鏈接數,同時支持設置每一個主機的最大鏈接數,發現並關閉過時的鏈接。app
在http1.0和http1.1中利用KeepAlive保持長鏈接。框架
本文簡單談下HttpClient源碼,而HttpClient源碼包有700K,因此這裏只會挑重點介紹,詳細源碼你們能夠下載HttpClient的最新的代碼來研究。另外:本文所列舉的都是 HttpClient4.5 的源碼。less
HttpClient常常會用到鏈接池,以便重用tcp鏈接,特別是在使用長鏈接時能節約很多性能(避免了三次握手和四次解握)。HttpClient鏈接池的邏輯是:先根據域名route 來找是否有空閒的鏈接,若是有就取出來用,若是沒有則會建立一個新的鏈接,並綁定到route 上,這裏使用route 來獲取鏈接是爲了重用鏈接,假設一個長鏈接在同一個域名下屢次使用就不用屢次握手了。固然,其中還會校驗鏈接池的大小,等待的時間等。源碼以下:
private E getPoolEntryBlocking( final T route, final Object state, final long timeout, final TimeUnit tunit, final PoolEntryFuture<E> future) throws IOException, InterruptedException, TimeoutException { Date deadline = null; if (timeout > 0) { deadline = new Date (System.currentTimeMillis() + tunit.toMillis(timeout)); } this.lock.lock(); try { final RouteSpecificPool<T, C, E> pool = getPool(route); E entry = null; while (entry == null) { Asserts.check(!this.isShutDown, "Connection pool shut down"); for (;;) { entry = pool.getFree(state); if (entry == null) { break; } if (entry.isExpired(System.currentTimeMillis())) { entry.close(); } else if (this.validateAfterInactivity > 0) { if (entry.getUpdated() + this.validateAfterInactivity <= System.currentTimeMillis()) { if (!validate(entry)) { entry.close(); } } } if (entry.isClosed()) { this.available.remove(entry); pool.free(entry, false); } else { break; } } if (entry != null) { this.available.remove(entry); this.leased.add(entry); onReuse(entry); return entry; } // New connection is needed final int maxPerRoute = getMax(route); // Shrink the pool prior to allocating a new connection 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); } } if (pool.getAllocatedCount() < maxPerRoute) { final int totalUsed = this.leased.size(); final int freeCapacity = Math.max(this.maxTotal - totalUsed, 0); if (freeCapacity > 0) { final int totalAvailable = this.available.size(); 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); } } final C conn = this.connFactory.create(route); entry = pool.add(conn); this.leased.add(entry); return entry; } } boolean success = false; try { pool.queue(future); this.pending.add(future); success = future.await(deadline); } finally { // In case of 'success', we were woken up by the // connection pool and should now have a connection // waiting for us, or else we're shutting down. // Just continue in the loop, both cases are checked. pool.unqueue(future); this.pending.remove(future); } // check for spurious wakeup vs. timeout if (!success && (deadline != null) && (deadline.getTime() <= System.currentTimeMillis())) { break; } } throw new TimeoutException("Timeout waiting for connection"); } finally { this.lock.unlock(); } }
在發送數據前HttpClient 確定會和後臺的服務(好比restful服務)進行socket鏈接,創建socket時的參數好比connectTimeout、soTimeout等均可配置。另外,此時會開啓套接字的輸出流和輸入流:輸出流用於向後臺的restful服務輸出Request的參數、header、cookie和body等,該部分源碼以下:
@Override public void connect( final ManagedHttpClientConnection conn, final HttpHost host, final InetSocketAddress localAddress, final int connectTimeout, final SocketConfig socketConfig, final HttpContext context) throws IOException { final Lookup<ConnectionSocketFactory> registry = getSocketFactoryRegistry(context); final ConnectionSocketFactory sf = registry.lookup(host.getSchemeName()); if (sf == null) { throw new UnsupportedSchemeException(host.getSchemeName() + " protocol is not supported"); } final InetAddress[] addresses = host.getAddress() != null ? new InetAddress[] { host.getAddress() } : this.dnsResolver.resolve(host.getHostName()); final int port = this.schemePortResolver.resolve(host); for (int i = 0; i < addresses.length; i++) { final InetAddress address = addresses[i]; final boolean last = i == addresses.length - 1; Socket sock = sf.createSocket(context); sock.setSoTimeout(socketConfig.getSoTimeout()); sock.setReuseAddress(socketConfig.isSoReuseAddress()); sock.setTcpNoDelay(socketConfig.isTcpNoDelay()); sock.setKeepAlive(socketConfig.isSoKeepAlive()); final int linger = socketConfig.getSoLinger(); if (linger >= 0) { sock.setSoLinger(true, linger); } conn.bind(sock); final InetSocketAddress remoteAddress = new InetSocketAddress(address, port); if (this.log.isDebugEnabled()) { this.log.debug("Connecting to " + remoteAddress); } try { sock = sf.connectSocket( connectTimeout, sock, host, remoteAddress, localAddress, context); conn.bind(sock); if (this.log.isDebugEnabled()) { this.log.debug("Connection established " + conn); } return; } catch (final SocketTimeoutException ex) { if (last) { throw new ConnectTimeoutException(ex, host, addresses); } } catch (final ConnectException ex) { if (last) { final String msg = ex.getMessage(); if ("Connection timed out".equals(msg)) { throw new ConnectTimeoutException(ex, host, addresses); } else { throw new HttpHostConnectException(ex, host, addresses); } } } catch (final NoRouteToHostException ex) { if (last) { throw ex; } } if (this.log.isDebugEnabled()) { this.log.debug("Connect to " + remoteAddress + " timed out. " + "Connection will be retried using another IP address"); } } }
@Override public Socket connectSocket( final int connectTimeout, final Socket socket, final HttpHost host, final InetSocketAddress remoteAddress, final InetSocketAddress localAddress, final HttpContext context) throws IOException { final Socket sock = socket != null ? socket : createSocket(context); if (localAddress != null) { sock.bind(localAddress); } try { sock.connect(remoteAddress, connectTimeout); } catch (final IOException ex) { try { sock.close(); } catch (final IOException ignore) { } throw ex; } return sock; }
創建鏈接成功後就能夠發送數據了,像前面所說的那樣:httpclient是使用輸出流向後臺的restful服務輸出Request的參數、header、cookie和body等。源碼以下:
public HttpResponse execute( final HttpRequest request, final HttpClientConnection conn, final HttpContext context) throws IOException, HttpException { Args.notNull(request, "HTTP request"); Args.notNull(conn, "Client connection"); Args.notNull(context, "HTTP context"); try { HttpResponse response = doSendRequest(request, conn, context); if (response == null) { response = doReceiveResponse(request, conn, context); } return response; } catch (final IOException ex) { closeConnection(conn); throw ex; } catch (final HttpException ex) { closeConnection(conn); throw ex; } catch (final RuntimeException ex) { closeConnection(conn); throw ex; } }
@Override public void flush() throws IOException { flushBuffer(); flushStream(); } private void flushBuffer() throws IOException { final int len = this.buffer.length(); if (len > 0) { streamWrite(this.buffer.buffer(), 0, len); this.buffer.clear(); this.metrics.incrementBytesTransferred(len); } } private void flushStream() throws IOException { if (this.outstream != null) { this.outstream.flush(); } }
發送完數據後就會接受套接字輸入流的數據,上面源碼塊裏面的 response = doReceiveResponse(request, conn, context)就是用於接受套接字數據。源碼以下:
protected HttpResponse doReceiveResponse( final HttpRequest request, final HttpClientConnection conn, final HttpContext context) throws HttpException, IOException { Args.notNull(request, "HTTP request"); Args.notNull(conn, "Client connection"); Args.notNull(context, "HTTP context"); HttpResponse response = null; int statusCode = 0; while (response == null || statusCode < HttpStatus.SC_OK) { response = conn.receiveResponseHeader(); if (canResponseHaveBody(request, response)) { conn.receiveResponseEntity(response); } statusCode = response.getStatusLine().getStatusCode(); } // while intermediate response return response; }
其中:conn.receiveResponseHeader() 用於獲取Response的header數據,而conn.receiveResponseEntity(response)或獲取Response的body數據。固然,在獲取body數據前會先判斷Response的狀態碼是否合法,源碼以下:
protected boolean canResponseHaveBody(final HttpRequest request, final HttpResponse response) { if ("HEAD".equalsIgnoreCase(request.getRequestLine().getMethod())) { return false; } final int status = response.getStatusLine().getStatusCode(); return status >= HttpStatus.SC_OK && status != HttpStatus.SC_NO_CONTENT && status != HttpStatus.SC_NOT_MODIFIED && status != HttpStatus.SC_RESET_CONTENT; }
獲取Response數據後會處理這些數據,好比保持鏈接之類的設置,Httpclient會根據response返回的協議以及header裏的Connect等參數來設置是否保持鏈接,源碼以下:
if (reuseStrategy.keepAlive(response, context)) { // Set the idle duration of this connection 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); } connHolder.setValidFor(duration, TimeUnit.MILLISECONDS); connHolder.markReusable(); } else { connHolder.markNonReusable(); }
@Override public boolean keepAlive(final HttpResponse response, final HttpContext context) { Args.notNull(response, "HTTP response"); Args.notNull(context, "HTTP context"); // Check for a self-terminating entity. If the end of the entity will // be indicated by closing the connection, there is no keep-alive. final ProtocolVersion ver = response.getStatusLine().getProtocolVersion(); final Header teh = response.getFirstHeader(HTTP.TRANSFER_ENCODING); if (teh != null) { if (!HTTP.CHUNK_CODING.equalsIgnoreCase(teh.getValue())) { return false; } } else { if (canResponseHaveBody(response)) { final Header[] clhs = response.getHeaders(HTTP.CONTENT_LEN); // Do not reuse if not properly content-length delimited 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; } } } // Check for the "Connection" header. If that is absent, check for // the "Proxy-Connection" header. The latter is an unspecified and // broken but unfortunately common extension of HTTP. HeaderIterator hit = response.headerIterator(HTTP.CONN_DIRECTIVE); if (!hit.hasNext()) { hit = response.headerIterator("Proxy-Connection"); } if (hit.hasNext()) { try { final TokenIterator ti = createTokenIterator(hit); boolean keepalive = false; while (ti.hasNext()) { final String token = ti.nextToken(); if (HTTP.CONN_CLOSE.equalsIgnoreCase(token)) { return false; } else if (HTTP.CONN_KEEP_ALIVE.equalsIgnoreCase(token)) { // continue the loop, there may be a "close" afterwards keepalive = true; } } if (keepalive){ return true; // neither "close" nor "keep-alive", use default policy } } catch (final ParseException px) { // invalid connection header means no persistent connection // we don't have logging in HttpCore, so the exception is lost return false; } } // default since HTTP/1.1 is persistent, before it was non-persistent return !ver.lessEquals(HttpVersion.HTTP_1_0); }
而後若是Response返回302,時httpclient會根據策略來判斷並決定是否須要重定向,源碼以下:
for (int redirectCount = 0;;) { final CloseableHttpResponse response = requestExecutor.execute( currentRoute, currentRequest, context, execAware); try { if (config.isRedirectsEnabled() && this.redirectStrategy.isRedirected(currentRequest, response, context)) { if (redirectCount >= maxRedirects) { throw new RedirectException("Maximum redirects ("+ maxRedirects + ") exceeded"); } redirectCount++; final HttpRequest redirect = this.redirectStrategy.getRedirect( currentRequest, response, context); if (!redirect.headerIterator().hasNext()) { final HttpRequest original = request.getOriginal(); redirect.setHeaders(original.getAllHeaders()); } currentRequest = HttpRequestWrapper.wrap(redirect); if (currentRequest instanceof HttpEntityEnclosingRequest) { RequestEntityProxy.enhance((HttpEntityEnclosingRequest) currentRequest); } final URI uri = currentRequest.getURI(); final HttpHost newTarget = URIUtils.extractHost(uri); if (newTarget == null) { throw new ProtocolException("Redirect URI does not specify a valid host name: " + uri); } // Reset virtual host and auth states if redirecting to another host if (!currentRoute.getTargetHost().equals(newTarget)) { final AuthState targetAuthState = context.getTargetAuthState(); if (targetAuthState != null) { this.log.debug("Resetting target auth state"); targetAuthState.reset(); } final AuthState proxyAuthState = context.getProxyAuthState(); if (proxyAuthState != null) { final AuthScheme authScheme = proxyAuthState.getAuthScheme(); if (authScheme != null && authScheme.isConnectionBased()) { this.log.debug("Resetting proxy auth state"); proxyAuthState.reset(); } } } currentRoute = this.routePlanner.determineRoute(newTarget, currentRequest, context); if (this.log.isDebugEnabled()) { this.log.debug("Redirecting to '" + uri + "' via " + currentRoute); } EntityUtils.consume(response.getEntity()); response.close(); } else { return response; } } catch (final RuntimeException ex) { response.close(); throw ex; } catch (final IOException ex) { response.close(); throw ex; } catch (final HttpException ex) { // Protocol exception related to a direct. // The underlying connection may still be salvaged. try { EntityUtils.consume(response.getEntity()); } catch (final IOException ioex) { this.log.debug("I/O error while releasing connection", ioex); } finally { response.close(); } throw ex; } }
能夠看到,首先獲取Response,而後根據config.isRedirectsEnabled() 和this.redirectStrategy.isRedirected(currentRequest, response, context)的結果來進行重定向,都爲true則重定向,不然直接返回Response。固然了,這裏的redirectsEnabled和redirectStrategy都是能夠自定義的。
HttpClient 是Apache 下又一個十分優秀的開源框架,最經常使用用於封裝長鏈接、調用後臺的resetful接口等。然而它還有不少本文沒有提到的功能,好比安全認證、使用HTTPS協議等。你們感興趣的話能夠更深刻地瞭解下。