序
本文主要研究一下httpclient的connect timeout異常
例項程式碼
@Test
public void testConnectTimeout() throws IOException, InterruptedException {
HttpClient client = HttpClient.newBuilder()
.build();
HttpRequest request = HttpRequest.newBuilder()
.uri(URI.create("https://twitter.com"))
.build();
long start = System.currentTimeMillis();
try{
HttpResponse<String> result = client.send(request, HttpResponse.BodyHandlers.ofString());
System.out.println(result.body());
}finally {
long cost = System.currentTimeMillis() - start;
System.out.println("cost:"+cost);
}
}
複製程式碼
異常日誌如下:
cost:75814
java.net.ConnectException: Operation timed out
at java.net.http/jdk.internal.net.http.HttpClientImpl.send(HttpClientImpl.java:561)
at java.net.http/jdk.internal.net.http.HttpClientFacade.send(HttpClientFacade.java:119)
at com.example.HttpClientTest.testConnectTimeout(HttpClientTest.java:464)
at java.base/jdk.internal.reflect.NativeMethodAccessorImpl.invoke0(Native Method)
at java.base/jdk.internal.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:62)
at java.base/jdk.internal.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43)
at java.base/java.lang.reflect.Method.invoke(Method.java:566)
at org.junit.runners.model.FrameworkMethod$1.runReflectiveCall(FrameworkMethod.java:50)
at org.junit.internal.runners.model.ReflectiveCallable.run(ReflectiveCallable.java:12)
at org.junit.runners.model.FrameworkMethod.invokeExplosively(FrameworkMethod.java:47)
at org.junit.internal.runners.statements.InvokeMethod.evaluate(InvokeMethod.java:17)
at org.junit.runners.ParentRunner.runLeaf(ParentRunner.java:325)
at org.junit.runners.BlockJUnit4ClassRunner.runChild(BlockJUnit4ClassRunner.java:78)
at org.junit.runners.BlockJUnit4ClassRunner.runChild(BlockJUnit4ClassRunner.java:57)
at org.junit.runners.ParentRunner$3.run(ParentRunner.java:290)
at org.junit.runners.ParentRunner$1.schedule(ParentRunner.java:71)
at org.junit.runners.ParentRunner.runChildren(ParentRunner.java:288)
at org.junit.runners.ParentRunner.access$000(ParentRunner.java:58)
at org.junit.runners.ParentRunner$2.evaluate(ParentRunner.java:268)
at org.junit.runners.ParentRunner.run(ParentRunner.java:363)
at org.junit.runner.JUnitCore.run(JUnitCore.java:137)
at com.intellij.junit4.JUnit4IdeaTestRunner.startRunnerWithArgs(JUnit4IdeaTestRunner.java:68)
at com.intellij.rt.execution.junit.IdeaTestRunner$Repeater.startRunnerWithArgs(IdeaTestRunner.java:47)
at com.intellij.rt.execution.junit.JUnitStarter.prepareStreamsAndStart(JUnitStarter.java:242)
at com.intellij.rt.execution.junit.JUnitStarter.main(JUnitStarter.java:70)
at java.base/jdk.internal.reflect.NativeMethodAccessorImpl.invoke0(Native Method)
at java.base/jdk.internal.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:62)
at java.base/jdk.internal.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43)
at java.base/java.lang.reflect.Method.invoke(Method.java:566)
at com.intellij.rt.execution.application.AppMainV2.main(AppMainV2.java:131)
Caused by: java.net.ConnectException: Operation timed out
at java.base/sun.nio.ch.SocketChannelImpl.checkConnect(Native Method)
at java.base/sun.nio.ch.SocketChannelImpl.finishConnect(SocketChannelImpl.java:779)
at java.net.http/jdk.internal.net.http.PlainHttpConnection$ConnectEvent.handle(PlainHttpConnection.java:128)
at java.net.http/jdk.internal.net.http.HttpClientImpl$SelectorManager.handleEvent(HttpClientImpl.java:957)
at java.net.http/jdk.internal.net.http.HttpClientImpl$SelectorManager.lambda$run$3(HttpClientImpl.java:912)
at java.base/java.util.ArrayList.forEach(ArrayList.java:1540)
at java.net.http/jdk.internal.net.http.HttpClientImpl$SelectorManager.run(HttpClientImpl.java:912)
複製程式碼
Exchange.responseAsync
java.net.http/jdk/internal/net/http/Exchange.java
public CompletableFuture<Response> responseAsync() {
return responseAsyncImpl(null);
}
CompletableFuture<Response> responseAsyncImpl(HttpConnection connection) {
SecurityException e = checkPermissions();
if (e != null) {
return MinimalFuture.failedFuture(e);
} else {
return responseAsyncImpl0(connection);
}
}
CompletableFuture<Response> responseAsyncImpl0(HttpConnection connection) {
Function<ExchangeImpl<T>, CompletableFuture<Response>> after407Check;
bodyIgnored = null;
if (request.expectContinue()) {
request.addSystemHeader("Expect", "100-Continue");
Log.logTrace("Sending Expect: 100-Continue");
// wait for 100-Continue before sending body
after407Check = this::expectContinue;
} else {
// send request body and proceed.
after407Check = this::sendRequestBody;
}
// The ProxyAuthorizationRequired can be triggered either by
// establishExchange (case of HTTP/2 SSL tunneling through HTTP/1.1 proxy
// or by sendHeaderAsync (case of HTTP/1.1 SSL tunneling through HTTP/1.1 proxy
// Therefore we handle it with a call to this checkFor407(...) after these
// two places.
Function<ExchangeImpl<T>, CompletableFuture<Response>> afterExch407Check =
(ex) -> ex.sendHeadersAsync()
.handle((r,t) -> this.checkFor407(r, t, after407Check))
.thenCompose(Function.identity());
return establishExchange(connection)
.handle((r,t) -> this.checkFor407(r,t, afterExch407Check))
.thenCompose(Function.identity());
}
// get/set the exchange impl, solving race condition issues with
// potential concurrent calls to cancel() or cancel(IOException)
private CompletableFuture<? extends ExchangeImpl<T>>
establishExchange(HttpConnection connection) {
if (debug.on()) {
debug.log("establishing exchange for %s,%n\t proxy=%s",
request, request.proxy());
}
// check if we have been cancelled first.
Throwable t = getCancelCause();
checkCancelled();
if (t != null) {
return MinimalFuture.failedFuture(t);
}
CompletableFuture<? extends ExchangeImpl<T>> cf, res;
cf = ExchangeImpl.get(this, connection);
// We should probably use a VarHandle to get/set exchangeCF
// instead - as we need CAS semantics.
synchronized (this) { exchangeCF = cf; };
res = cf.whenComplete((r,x) -> {
synchronized(Exchange.this) {
if (exchangeCF == cf) exchangeCF = null;
}
});
checkCancelled();
return res.thenCompose((eimpl) -> {
// recheck for cancelled, in case of race conditions
exchImpl = eimpl;
IOException tt = getCancelCause();
checkCancelled();
if (tt != null) {
return MinimalFuture.failedFuture(tt);
} else {
// Now we're good to go. Because exchImpl is no longer
// null cancel() will be able to propagate directly to
// the impl after this point ( if needed ).
return MinimalFuture.completedFuture(eimpl);
} });
}
複製程式碼
- responseAsync最後呼叫ExchangeImpl.get(this, connection)
ExchangeImpl.get
java.net.http/jdk/internal/net/http/ExchangeImpl.java
/**
* Initiates a new exchange and assigns it to a connection if one exists
* already. connection usually null.
*/
static <U> CompletableFuture<? extends ExchangeImpl<U>>
get(Exchange<U> exchange, HttpConnection connection)
{
if (exchange.version() == HTTP_1_1) {
if (debug.on())
debug.log("get: HTTP/1.1: new Http1Exchange");
return createHttp1Exchange(exchange, connection);
} else {
Http2ClientImpl c2 = exchange.client().client2(); // #### improve
HttpRequestImpl request = exchange.request();
CompletableFuture<Http2Connection> c2f = c2.getConnectionFor(request, exchange);
if (debug.on())
debug.log("get: Trying to get HTTP/2 connection");
return c2f.handle((h2c, t) -> createExchangeImpl(h2c, t, exchange, connection))
.thenCompose(Function.identity());
}
}
複製程式碼
- 這裡呼叫Http2ClientImpl.getConnectionFor獲取連線
Http2ClientImpl.getConnectionFor
java.net.http/jdk/internal/net/http/Http2ClientImpl.java
/**
* When HTTP/2 requested only. The following describes the aggregate behavior including the
* calling code. In all cases, the HTTP2 connection cache
* is checked first for a suitable connection and that is returned if available.
* If not, a new connection is opened, except in https case when a previous negotiate failed.
* In that case, we want to continue using http/1.1. When a connection is to be opened and
* if multiple requests are sent in parallel then each will open a new connection.
*
* If negotiation/upgrade succeeds then
* one connection will be put in the cache and the others will be closed
* after the initial request completes (not strictly necessary for h2, only for h2c)
*
* If negotiate/upgrade fails, then any opened connections remain open (as http/1.1)
* and will be used and cached in the http/1 cache. Note, this method handles the
* https failure case only (by completing the CF with an ALPN exception, handled externally)
* The h2c upgrade is handled externally also.
*
* Specific CF behavior of this method.
* 1. completes with ALPN exception: h2 negotiate failed for first time. failure recorded.
* 2. completes with other exception: failure not recorded. Caller must handle
* 3. completes normally with null: no connection in cache for h2c or h2 failed previously
* 4. completes normally with connection: h2 or h2c connection in cache. Use it.
*/
CompletableFuture<Http2Connection> getConnectionFor(HttpRequestImpl req,
Exchange<?> exchange) {
URI uri = req.uri();
InetSocketAddress proxy = req.proxy();
String key = Http2Connection.keyFor(uri, proxy);
synchronized (this) {
Http2Connection connection = connections.get(key);
if (connection != null) {
try {
if (connection.closed || !connection.reserveStream(true)) {
if (debug.on())
debug.log("removing found closed or closing connection: %s", connection);
deleteConnection(connection);
} else {
// fast path if connection already exists
if (debug.on())
debug.log("found connection in the pool: %s", connection);
return MinimalFuture.completedFuture(connection);
}
} catch (IOException e) {
// thrown by connection.reserveStream()
return MinimalFuture.failedFuture(e);
}
}
if (!req.secure() || failures.contains(key)) {
// secure: negotiate failed before. Use http/1.1
// !secure: no connection available in cache. Attempt upgrade
if (debug.on()) debug.log("not found in connection pool");
return MinimalFuture.completedFuture(null);
}
}
return Http2Connection
.createAsync(req, this, exchange)
.whenComplete((conn, t) -> {
synchronized (Http2ClientImpl.this) {
if (conn != null) {
try {
conn.reserveStream(true);
} catch (IOException e) {
throw new UncheckedIOException(e); // shouldn't happen
}
offerConnection(conn);
} else {
Throwable cause = Utils.getCompletionCause(t);
if (cause instanceof Http2Connection.ALPNException)
failures.add(key);
}
}
});
}
複製程式碼
- 如果沒有連線會新建立一個,走的是Http2Connection.createAsync
Http2Connection.createAsync
java.net.http/jdk/internal/net/http/Http2Connection.java
// Requires TLS handshake. So, is really async
static CompletableFuture<Http2Connection> createAsync(HttpRequestImpl request,
Http2ClientImpl h2client,
Exchange<?> exchange) {
assert request.secure();
AbstractAsyncSSLConnection connection = (AbstractAsyncSSLConnection)
HttpConnection.getConnection(request.getAddress(),
h2client.client(),
request,
HttpClient.Version.HTTP_2);
// Expose the underlying connection to the exchange's aborter so it can
// be closed if a timeout occurs.
exchange.connectionAborter.connection(connection);
return connection.connectAsync(exchange)
.thenCompose(unused -> connection.finishConnect())
.thenCompose(unused -> checkSSLConfig(connection))
.thenCompose(notused-> {
CompletableFuture<Http2Connection> cf = new MinimalFuture<>();
try {
Http2Connection hc = new Http2Connection(request, h2client, connection);
cf.complete(hc);
} catch (IOException e) {
cf.completeExceptionally(e);
}
return cf; } );
}
複製程式碼
- 這裡先是呼叫了HttpConnection.getConnection獲取連線,然後呼叫connectAsync進行連線
AsyncSSLConnection
java.net.http/jdk/internal/net/http/AsyncSSLConnection.java
@Override
public CompletableFuture<Void> connectAsync(Exchange<?> exchange) {
return plainConnection
.connectAsync(exchange)
.thenApply( unused -> {
// create the SSLTube wrapping the SocketTube, with the given engine
flow = new SSLTube(engine,
client().theExecutor(),
client().getSSLBufferSupplier()::recycle,
plainConnection.getConnectionFlow());
return null; } );
}
複製程式碼
- 這裡委託給plainConnection.connectAsync
PlainHttpConnection.connectAsync
java.net.http/jdk/internal/net/http/PlainHttpConnection.java
@Override
public CompletableFuture<Void> connectAsync(Exchange<?> exchange) {
CompletableFuture<Void> cf = new MinimalFuture<>();
try {
assert !connected : "Already connected";
assert !chan.isBlocking() : "Unexpected blocking channel";
boolean finished;
connectTimerEvent = newConnectTimer(exchange, cf);
if (connectTimerEvent != null) {
if (debug.on())
debug.log("registering connect timer: " + connectTimerEvent);
client().registerTimer(connectTimerEvent);
}
PrivilegedExceptionAction<Boolean> pa =
() -> chan.connect(Utils.resolveAddress(address));
try {
finished = AccessController.doPrivileged(pa);
} catch (PrivilegedActionException e) {
throw e.getCause();
}
if (finished) {
if (debug.on()) debug.log("connect finished without blocking");
cf.complete(null);
} else {
if (debug.on()) debug.log("registering connect event");
client().registerEvent(new ConnectEvent(cf));
}
} catch (Throwable throwable) {
cf.completeExceptionally(Utils.toConnectException(throwable));
try {
close();
} catch (Exception x) {
if (debug.on())
debug.log("Failed to close channel after unsuccessful connect");
}
}
return cf;
}
複製程式碼
- 這裡如果client有設定connectTimeout的話,則會建立一個connectTimerEvent
- 呼叫chan.connect進行連線,如果連線未完成,則註冊ConnectEvent
SocketChannelImpl.connect
java.base/sun/nio/ch/SocketChannelImpl.java
@Override
public boolean connect(SocketAddress sa) throws IOException {
InetSocketAddress isa = Net.checkAddress(sa);
SecurityManager sm = System.getSecurityManager();
if (sm != null)
sm.checkConnect(isa.getAddress().getHostAddress(), isa.getPort());
InetAddress ia = isa.getAddress();
if (ia.isAnyLocalAddress())
ia = InetAddress.getLocalHost();
try {
readLock.lock();
try {
writeLock.lock();
try {
int n = 0;
boolean blocking = isBlocking();
try {
beginConnect(blocking, isa);
do {
n = Net.connect(fd, ia, isa.getPort());
} while (n == IOStatus.INTERRUPTED && isOpen());
} finally {
endConnect(blocking, (n > 0));
}
assert IOStatus.check(n);
return n > 0;
} finally {
writeLock.unlock();
}
} finally {
readLock.unlock();
}
} catch (IOException ioe) {
// connect failed, close the channel
close();
throw SocketExceptions.of(ioe, isa);
}
}
複製程式碼
- 通過Net.connect呼叫本地方法進行連線
ConnectEvent
java.net.http/jdk/internal/net/http/PlainHttpConnection.java
final class ConnectEvent extends AsyncEvent {
private final CompletableFuture<Void> cf;
ConnectEvent(CompletableFuture<Void> cf) {
this.cf = cf;
}
@Override
public SelectableChannel channel() {
return chan;
}
@Override
public int interestOps() {
return SelectionKey.OP_CONNECT;
}
@Override
public void handle() {
try {
assert !connected : "Already connected";
assert !chan.isBlocking() : "Unexpected blocking channel";
if (debug.on())
debug.log("ConnectEvent: finishing connect");
boolean finished = chan.finishConnect();
assert finished : "Expected channel to be connected";
if (debug.on())
debug.log("ConnectEvent: connect finished: %s Local addr: %s",
finished, chan.getLocalAddress());
// complete async since the event runs on the SelectorManager thread
cf.completeAsync(() -> null, client().theExecutor());
} catch (Throwable e) {
Throwable t = Utils.toConnectException(e);
client().theExecutor().execute( () -> cf.completeExceptionally(t));
close();
}
}
@Override
public void abort(IOException ioe) {
client().theExecutor().execute( () -> cf.completeExceptionally(ioe));
close();
}
}
複製程式碼
- SelectorManager對準備好的事件觸發handle操作,對於ConnectEvent,就是呼叫ConnectEvent.handle
- ConnectEvent的handle方法執行chan.finishConnect(),如果捕獲到異常,則呼叫cf.completeExceptionally(t)
SocketChannelImpl.finishConnect
java.base/sun/nio/ch/SocketChannelImpl.java
@Override
public boolean finishConnect() throws IOException {
try {
readLock.lock();
try {
writeLock.lock();
try {
// no-op if already connected
if (isConnected())
return true;
boolean blocking = isBlocking();
boolean connected = false;
try {
beginFinishConnect(blocking);
int n = 0;
if (blocking) {
do {
n = checkConnect(fd, true);
} while ((n == 0 || n == IOStatus.INTERRUPTED) && isOpen());
} else {
n = checkConnect(fd, false);
}
connected = (n > 0);
} finally {
endFinishConnect(blocking, connected);
}
assert (blocking && connected) ^ !blocking;
return connected;
} finally {
writeLock.unlock();
}
} finally {
readLock.unlock();
}
} catch (IOException ioe) {
// connect failed, close the channel
close();
throw SocketExceptions.of(ioe, remoteAddress);
}
}
複製程式碼
- checkConnect是一個本地方法,如果是連線超時,則丟擲java.net.ConnectException: Operation timed out
tcp連線syn超時(net.ipv4.tcp_syn_retries
)
當client端與server端建立連線,client發出syn包,如果等待一定時間沒有收到server端發來的SYN+ACK,則會進行重試,重試次數由具體由net.ipv4.tcp_syn_retries決定
/ # sysctl -a | grep tcp_syn_retries
sysctl: error reading key 'net.ipv6.conf.all.stable_secret': I/O error
net.ipv4.tcp_syn_retries = 6
sysctl: error reading key 'net.ipv6.conf.default.stable_secret': I/O error
sysctl: error reading key 'net.ipv6.conf.eth0.stable_secret': I/O error
sysctl: error reading key 'net.ipv6.conf.lo.stable_secret': I/O error
複製程式碼
linux預設是6次,第一次傳送等待2^0秒沒收到回包則重試第一次,之後等待2^1,以此類推,第六次重試等待2^6秒,因此一共是1s+2s+4s+8s+16s+32s+64s=127s,因而在linux平臺下,如果httpclient沒有設定connect timeout,則依賴系統tcp的syn超時,即127s之後超時,java的本地呼叫丟擲java.net.ConnectException: Operation timed out
如果是mac系統,根據Overriding the default Linux kernel 20-second TCP socket connect timeout的描述,超時是75s,與本例項程式碼輸出的75814ms近似一致。
小結
- 使用jdk httpclient進行連線,如果沒有設定client的connectTimeout,則具體的超時時間依賴系統的tcp相關設定
- 如果client端sync傳送超時,則依賴tcp_syn_retries的配置來決定本地方法丟擲java.net.ConnectException: Operation timed out異常的時間
- linux下預設tcp_syn_retries預設為6,即重試6次,一共需要1s+2s+4s+8s+16s+32s+64s=127s,若再沒有收到server端發來的SYN+ACK則丟擲java.net.ConnectException: Operation timed out異常