浅谈Java中的TCP超时

背景

在远程调用的世界里,Timeout的情况非常常见,几乎每段时间就会听到几个同事关于Timeout各种情况的讨论,偶尔的会出现不同开发语言间的同事的讨论,例如read timeout, 语言的隔阂使得大家讨论的都不知道是否是同一回事。

对于Java,各种远程调用,http,hessian,dubbo什么的,抛个timeout异常也是常见的事情,timeout是什么,一般追追源码,追到最后发现是个native方法,看着javadoc, 了解得不甚透彻。 所以本文尽量从Java到操作系统层面尝试说明常见的各种Timeout。

主要内容

现象

对于Java开发来说,最常见的异常莫过于SocketTimeoutException,从异常日志,一般会有两种情况

  • connect timed out
  • read timed out
1
2
3
4
5
6
7
8
9
10
11
12
13
Caused by: java.net.SocketTimeoutException: connect timed out
at java.net.PlainSocketImpl.socketConnect(Native Method)
at java.net.AbstractPlainSocketImpl.doConnect(AbstractPlainSocketImpl.java:345)
at java.net.AbstractPlainSocketImpl.connectToAddress(AbstractPlainSocketImpl.java:206)
at java.net.AbstractPlainSocketImpl.connect(AbstractPlainSocketImpl.java:188)
at java.net.SocksSocketImpl.connect(SocksSocketImpl.java:392)
at java.net.Socket.connect(Socket.java:589)

java.net.SocketTimeoutException: Read timed out
at java.net.SocketInputStream.socketRead0(Native Method)
at java.net.SocketInputStream.socketRead(SocketInputStream.java:116)
at java.net.SocketInputStream.read(SocketInputStream.java:170)
at java.net.SocketInputStream.read(SocketInputStream.java:141)

原理 connect timed out

“connect timed out”从字面上看就是连接的超时时间,那么超时时间是怎么控制的?

java.net.Socket

从Sokcet的connect方法可以看出,timeout参数会一致往下传递,最后到了PlainSocketImpl.socketConnect的native方法, java native方法是否真的很神秘?也不神秘,让我们一起看下JVM底层的实现,以下是jdk8-openjdk的源码

PlainSocketImpl.c

以下只截取部分重要的源码, 从源码上看,没设置超时时间时,jvm采用 connect的传统阻塞式方式,反之,则采用select/poll非阻塞式的方式, 由于poll/select都是得采用轮询的方式,在客户端没有设置超时的时候,采用轮询会带来不必要的开销,所以没设置超时时采用connect的阻塞方式是合理的

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
JNIEXPORT void JNICALL
Java_java_net_PlainSocketImpl_socketConnect(JNIEnv *env, jobject this,
jobject iaObj, jint port,
jint timeout)
{

if (timeout < 0 ) {
connect_rv = NET_Connect(fd, (struct sockaddr *)&him, len);
...
}
else {

#ifndef USE_SELECT
{
struct pollfd pfd;
pfd.fd = fd;
pfd.events = POLLOUT;

errno = 0;
connect_rv = NET_Poll(&pfd, 1, timeout);
}
#else
{
fd_set wr, ex;
struct timeval t;

t.tv_sec = timeout / 1000;
t.tv_usec = (timeout % 1000) * 1000;

FD_ZERO(&wr);
FD_SET(fd, &wr);
FD_ZERO(&ex);
FD_SET(fd, &ex);

errno = 0;
connect_rv = NET_Select(fd+1, 0, &wr, &ex, &t);
}
#endif

}

if (connect_rv == 0) {
JNU_ThrowByName(env, JNU_JAVANETPKG "SocketTimeoutException",
"connect timed out");

/*
* Timeout out but connection may still be established.
* At the high level it should be closed immediately but
* just in case we make the socket blocking again and
* shutdown input & output.
*/
SET_BLOCKING(fd);
JVM_SocketShutdown(fd, 2);
return;
}

/* has connection been established */
optlen = sizeof(connect_rv);
if (JVM_GetSockOpt(fd, SOL_SOCKET, SO_ERROR, (void*)&connect_rv,
&optlen) <0) {
connect_rv = errno;
}
}
}

原理 Read timed out

从下面这个时序图看, read timedout的原理就是通过系统调用 poll, 传入对应的socket文件句柄,在timeout时间内没有数据返回

当调用NET_Timeout没返回任何数据的时候, 根据情况会抛出 SocketTimeoutException或者SokcetException, 这个SocketTimeoutException就是我们经常遇到的read timed out

if (timeout) {
    nread = NET_Timeout(fd, timeout);
    if (nread <= 0) {
        if (nread == 0) {
            JNU_ThrowByName(env, JNU_JAVANETPKG "SocketTimeoutException",
                        "Read timed out");
        } else if (nread == JVM_IO_ERR) {
            if (errno == EBADF) {
                 JNU_ThrowByName(env, JNU_JAVANETPKG "SocketException", "Socket closed");
             } else {
                 NET_ThrowByNameWithLastError(env, JNU_JAVANETPKG "SocketException",
                                              "select/poll failed");
             }
        } else if (nread == JVM_IO_INTR) {
            JNU_ThrowByName(env, JNU_JAVAIOPKG "InterruptedIOException",
                        "Operation interrupted");
        }
        if (bufP != BUF) {
            free(bufP);
        }
        return -1;
    }
}

总结

  1. “connect timed out” 是在指定时间内TCP连接未创建成功时jdk抛出的异常
  2. “Read timed out”是在调用socketread后,指定时间内未收到响应时 jdk抛出的异常, 假如一个http响应10k, 每次socket read 4k, 那么就需要发起3次read的请求,假如timeout设置3秒,那么就允许每次read都等待3秒,最差的情况就是大概6秒读完数据,当然这得是极端的网络情况, 所以大部分情况下都是客户端发起请求后,在指定时间内收到的服务器的回包响应。
    ##