关于 Java SocketReadTimeout
在诊断Java应用程序的诊断过程中, 竟然会遇到 Connect timeout
, Socket read timeout
. 但是经常会遇到有些有些开发人员对这些概念有些误解. 本文就涉及到一些细节使用一些例子做些说明, 使大家更容易理解.
一个简单的例子
Jersey
做为 Jax-RS
的参考实现, 被广泛用于 Java 应用开发. 下面使用 Jersey
开发一个客户端的例子.
import java.net.URI;
import javax.ws.rs.client.Client;
import javax.ws.rs.client.ClientBuilder;
import javax.ws.rs.client.WebTarget;
import javax.ws.rs.core.MediaType;
import javax.ws.rs.core.Response;
import javax.ws.rs.core.UriBuilder;
import org.glassfish.jersey.client.ClientConfig;
import org.glassfish.jersey.client.ClientProperties;
...
public String callRestSvc() {
ClientConfig config = new ClientConfig();
config.property(ClientProperties.CONNECT_TIMEOUT, 500);
config.property(ClientProperties.READ_TIMEOUT, 3000);
Client client = ClientBuilder.newClient(config);
WebTarget target = client.target(UriBuilder.fromUri("http://localhost:8080/").build());
try {
return target.path("rest").
request().
accept(MediaType.TEXT_PLAIN).
async().
get(String.class).get(10, TimeUnit.SECONDS);
} catch (InterruptedException | ExecutionException e) {
throw new RuntimeException(e);
} catch (TimeoutException e) {
throw new RuntimeException(e);
}
}
服务端的 Socket Read/Write timeout
Tomcat NIO
在 SocketTimeoutException
的初始化方法上设置断点, 然后可以看到下面的 Tomcat 异常栈.
SocketTimeoutException.<init>() (java.net.SocketTimeoutException:49)
NioEndpoint$Poller.timeout() (org.apache.tomcat.util.net.NioEndpoint$Poller:1086)
NioEndpoint$Poller.run() (org.apache.tomcat.util.net.NioEndpoint$Poller:852)
Thread.run() (java.lang.Thread:829)
如何判断是不是 timeout
Tomcat 每次在读取(read())的时候, 记录当前时间. 每次循环的时候(link)就检查是不是有 timeout
: 检查当前时间和上次读的时间的差值, 如果大于设置的 timeout
值, 就设置 error: timeout exception.
code: https://github.com/apache/tomcat/blob/ec8ef7a3a2fa56afb4db4261ebdc0aba848f23ff/java/org/apache/tomcat/util/net/NioEndpoint.java#L1017-L1050
if (socketWrapper.interestOpsHas(SelectionKey.OP_READ)) {
long delta = now - socketWrapper.getLastRead();
long timeout = socketWrapper.getReadTimeout();
if (timeout > 0 && delta > timeout) {
readTimeout = true;
}
}
// Check for write timeout
if (!readTimeout && socketWrapper.interestOpsHas(SelectionKey.OP_WRITE)) {
long delta = now - socketWrapper.getLastWrite();
long timeout = socketWrapper.getWriteTimeout();
if (timeout > 0 && delta > timeout) {
writeTimeout = true;
}
}
Tomcat NIO 2
Tomcat NIO 2 的 Endpoint 直接使用的是 java.util.concurrent.Future
的 timeout 设置:
link: https://github.com/apache/tomcat/blob/ec8ef7a3a2fa56afb4db4261ebdc0aba848f23ff/java/org/apache/tomcat/util/net/Nio2Endpoint.java#L1130-L1162
if (block) {
try {
integer = getSocket().read(to);
long timeout = getReadTimeout();
if (timeout > 0) {
nRead = integer.get(timeout, TimeUnit.MILLISECONDS).intValue();
} else {
nRead = integer.get().intValue();
}
} catch (ExecutionException e) {
if (e.getCause() instanceof IOException) {
throw (IOException) e.getCause();
} else {
throw new IOException(e);
}
} catch (InterruptedException e) {
throw new IOException(e);
} catch (TimeoutException e) {
integer.cancel(true);
throw new SocketTimeoutException();
} finally {
// Blocking read so need to release here since there will
// not be a callback to a completion handler.
readPending.release();
}
} else {
startInline();
getSocket().read(to, toTimeout(getReadTimeout()), TimeUnit.MILLISECONDS, to,
readCompletionHandler);
endInline();
if (readPending.availablePermits() == 1) {
nRead = to.position();
}
}
客户端的 Socket read timeout
BIO
HttpUrlConnection
HttpUrlConnection 就是使用的 BIO, 它自己是使用native 代码实现的 timeout.
NIO
例子的改正
这样即实现了最长等多少秒, 又不忘在后来的response 时候消费掉 entity, 保证连接释放.
public String asyncQuery() throws UnsupportedEncodingException, ExecutionException, InterruptedException, TimeoutException {
WebTarget target = logsTarget.path("/").queryParam("style", "increase");
System.out.println("url " + target.getUri().toString());
CompletableFuture<String> result = new CompletableFuture<>();
target.request(MediaType.APPLICATION_JSON).async().get(new InvocationCallback<String>() {
@Override
public void completed(String response) {
System.out.println("I get response: " + response);
result.complete(response);
}
@Override
public void failed(Throwable throwable) {
System.out.println("faied with URL " + target.getUri().toString() + " " + throwable.getMessage());
result.completeExceptionally(throwable);
}
});
return result.get(10, TimeUnit.SECONDS);
}