关于 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);
    }

标签: none

添加新评论