深入分析java中的Connection timed out
java中CTO(connection timed out)
在实际开发中经常会碰到
Connection timed out
的问题java.net.ConnectException: Connection timed out (Connection timed out) at java.net.PlainSocketImpl.socketConnect(Native Method) at java.net.AbstractPlainSocketImpl.doConnect(AbstractPlainSocketImpl.java:350) 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) at java.net.Socket.connect(Socket.java:538) at java.net.Socket.<init>(Socket.java:434) at java.net.Socket.<init>(Socket.java:211) at ClientSocketTimeout.main(ClientSocketTimeout.java:8)
Connection timed out
是client
发出sync
包,server端在指定的时间内没有回复ack
导致的.没有回复ack
的原因可能是网络丢包、防火墙阻止服务端返回syn的ack包等执行流程
socksSocketImpl.connect(endpoint,timeout) --> remainingMillis(deadlineMillis) //java代码先计算剩余是否超时 --> connectToAddress(this.address, port, timeout) --> doConnect(address, port, timeout) --> native PlainSocketImpl.socketConnect (调用本地方法) --> PlainSocketImpl.c 中的Java_java_net_PlainSocketImpl_socketConnect
PlainSocketImpl.c
openjdk
源码下载地址https://download.java.net/openjdk/jdk8
.PlainSocketImpl.c
的位置在openjdk/jdk/src/solaris/native/java/net
Java_java_net_PlainSocketImpl_socketConnect
阻塞模式: 只有timeout小于等于0,才会执行这段代码,可以看出阻塞模式的超时,如果不设置则依赖底层操作系统的超时机制,如果在java层面设置超时,则本质上是通过非阻塞模式实现的
if (timeout <= 0) { connect_rv = NET_Connect(fd, (struct sockaddr *)&him, len); #ifdef __solaris__ if (connect_rv == JVM_IO_ERR && errno == EINPROGRESS ) { /* This can happen if a blocking connect is interrupted by a signal. * See 6343810. */ while (1) { #ifndef USE_SELECT { struct pollfd pfd; pfd.fd = fd; pfd.events = POLLOUT; connect_rv = NET_Poll(&pfd, 1, -1); } #else { fd_set wr, ex; // 清空fdset与所有文件句柄的联系。 FD_ZERO(&wr); //建立文件句柄fd与fdset的联系。 FD_SET(fd, &wr); FD_ZERO(&ex); // FD_SET(fd, &ex); //错误返回-1,超时返回0,大于0表示已经准备好描述符,fd的值是从0开始 connect_rv = NET_Select(fd+1, 0, &wr, &ex, 0); } #endif if (connect_rv == JVM_IO_ERR) { //EINTR(Interrupted system call)系统调用被中断 if (errno == EINTR) { continue; } else { break; } } if (connect_rv > 0) { int optlen; /* has connection been established */ optlen = sizeof(connect_rv); //获取与某个套接字关联的选项,成功执行时,返回0。失败返回-1 if (JVM_GetSockOpt(fd, SOL_SOCKET, SO_ERROR, (void*)&connect_rv, &optlen) <0) { connect_rv = errno; } if (connect_rv != 0) { /* restore errno */ errno = connect_rv; connect_rv = JVM_IO_ERR; } break; } } } #endif }
当timeout大于0时执行调用非阻塞API来处理。
select()
在没有文件描述符监视的情况下,会等待timeout的时间,时间到了select
会返回0/* * A timeout was specified. We put the socket into non-blocking * mode, connect, and then wait for the connection to be * established, fail, or timeout. */ SET_NONBLOCKING(fd); /* no need to use NET_Connect as non-blocking */ connect_rv = connect(fd, (struct sockaddr *)&him, len); /* connection not established immediately 在一个非阻塞的TCP SOCKET上调用connect时,connect将立即返回一个`EINPROGRESS`错误(此时已经发起的TCP三次握手继续执行) */ if (connect_rv != 0) { int optlen; jlong prevTime = JVM_CurrentTimeMillis(env, 0); //EINPROGRESS表示连接建立已经启动但是尚未完成 if (errno != EINPROGRESS) { NET_ThrowByNameWithLastError(env, JNU_JAVANETPKG "ConnectException", "connect failed"); SET_BLOCKING(fd); return; } /* * Wait for the connection to be established or a * timeout occurs. poll/select needs to handle EINTR in * case lwp sig handler redirects any process signals to * this thread. */ while (1) { jlong newTime; #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 /** 1. 如果 select() 返回 0,表示在 select() 超时,超时时间内未能成功建立连接 2. 如果 select() 返回大于0的值(准备好的描述符个数),则说明检测到可读或可写的套接字描述符(可读、可写、异常)。 3. 如果select()小于0表示发生错误,如果不是 if (errno != EINTR) (Interrupted system call)系统调用被中断,则跳出循环报告错误 **/ if (connect_rv >= 0) { break; } if (errno != EINTR) { break; } /* * The poll was interrupted so adjust timeout and * restart * 如果被中断,计算剩余的超时时间,如果小于等于0,则退出循环,如果大于0则重启连接,确保应用层设置的timeout时间运行完毕才退出循环 */ newTime = JVM_CurrentTimeMillis(env, 0); timeout -= (newTime - prevTime); if (timeout <= 0) { connect_rv = 0; break; } prevTime = newTime; } /* while */ /*jvm抛出我们最常见的SocketTimeoutException*/ 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 如果 select 返回大于0 的值,则说明检测到可读、可写或异常的套接字描述符存在;此时我们可以通过调用 getsockopt 来检测集合中的套接口上是否存在待处理的错误,如果连接建立是成功的,则通过 getsockopt(sockfd,SOL_SOCKET,SO_ERROR,(char *)&error,&len) 获取的 error 值将是0 ,如果建立连接时遇到错误,则 error 的值是连接错误所对应的 errno 值 */ optlen = sizeof(connect_rv); if (JVM_GetSockOpt(fd, SOL_SOCKET, SO_ERROR, (void*)&connect_rv, &optlen) <0) { connect_rv = errno; } } /* make socket blocking again */ SET_BLOCKING(fd); /* restore errno */ if (connect_rv != 0) { errno = connect_rv; connect_rv = JVM_IO_ERR; } }
超时重试次数与时间
telnet测试
centos6.7
下的超时时间,这里指定一个不存在的ip地址和端口号[root@jannal Desktop]# date "+%Y-%m-%d %H:%M:%S"; telnet 192.168.111.11 7777; date "+%Y-%m-%d %H:%M:%S" 输出结果: 2018-07-27 23:42:13 Trying 192.168.111.11... telnet: connect to address 192.168.111.11: Connection timed out 2018-07-27 23:43:16 大约63s [root@jannal Desktop]# sysctl -a | grep tcp_syn_retries 输出结果net.ipv4.tcp_syn_retries = 5
mac下,运行上面的telnet获得的时间大约是75s
mac:~ jannal$ date "+%Y-%m-%d %H:%M:%S"; telnet 192.168.111.11 7777; date "+%Y-%m-%d %H:%M:%S" 输出结果: 2018-07-28 10:22:51 Trying 192.168.111.11... telnet: connect to address 192.168.111.11: Operation timed out telnet: Unable to connect to remote host 2018-07-28 10:24:07 大约75-76s mac:~ jannal$ sysctl net.inet.tcp | grep net.inet.tcp.keepinit 输出: net.inet.tcp.keepinit: 75000 mac下net.inet.tcp.keepinit参数表示的就是timeout for establishing syn,默认是75s
在
centos6.7
下(不同的linux发行版设置的时间可能不一样),默认重试次数为5次,重试的间隔时间从1s开始每次都翻倍,5次的重试时间间隔为1s, 2s, 4s, 8s, 16s,总共31s,第5次发出后还要等32s都知道第5次也超时了,所以,总共需要1s + 2s + 4s+ 8s+ 16s + 32s = 2^6 -1 = 63s
,TCP才会把断开这个连接。- 第 1 次发送 SYN 报文后等待 1s(2 的 0 次幂),如果超时,则重试
- 第 2 次发送后等待 2s(2 的 1 次幂),如果超时,则重试
- 第 3 次发送后等待 4s(2 的 2 次幂),如果超时,则重试
- 第 4 次发送后等待 8s(2 的 3 次幂),如果超时,则重试
- 第 5 次发送后等待 16s(2 的 4 次幂),如果超时,则重试
- 第 6 次发送后等待 32s(2 的 5 次幂),如果超时,则重试
修改centos的重试次数
[root@jannal ~]# sysctl net.ipv4.tcp_syn_retries=2 [root@jannal ~]# date "+%Y-%m-%d %H:%M:%S"; telnet 192.168.111.11 7777; date "+%Y-%m-%d %H:%M:%S" 输出结果: 2018-07-27 23:49:34 Trying 192.168.111.11... telnet: connect to address 192.168.111.11: Connection timed out 2018-07-27 23:49:41 可以看到7秒就超时(1s+2s+4s)
java程序测试
java socket程序
public class ClientSocketTimeout { public static void main(String[] args) { long start = System.currentTimeMillis(); Socket socket = null; try { /**此构造方法,最终会执行connect(endpoint, 0);即超时时间以来与操作系统内核的tcpip时间 * 这里指定一个不存在的ip地址和端口号 */ socket = new Socket("192.168.111.11", 7777); } catch (IOException e) { e.printStackTrace(); } long end = System.currentTimeMillis(); System.out.println("执行时间:"+(end-start)); } }
mac下执行结果
centos6.7下执行结果
模拟超时
设置防火墙模拟超时
三次握手,当client向server发送syn包,此时server没有ack,此时会触发重试并一直等到时间超时。根据这个原因我们可以通过设置防火墙来不让server丢弃SYN的握手信息,以达到模拟握手超时的目的
在centos6.7 下 [root@jannal ~]# vim /etc/sysconfig/iptables 添加防火墙规则 -A INPUT -m state --state NEW -m tcp -p tcp --dport 8888 -j ACCEPT # 开放8888端口 -A OUTPUT -p tcp -m tcp --tcp-flags SYN SYN --sport 8888 -j DROP # 丢弃SYN握手信息 [root@jannal ~]# service iptables restart # 重启防火墙
启动程序
服务端程序,如果不启动远程服务,会直接报
java.net.ConnectException: Connection refused
,所以这里我们写一个远程的服务,服务端程序运行在centos6.7
下public class TCPEchoServer { private static final int BUFSIZE = 32; public static void main(String[] args) throws IOException { int servPort = 8080; ServerSocket servSock = new ServerSocket(servPort); int recvMsgSize; byte[] receiveBuf = new byte[BUFSIZE]; while (true) { System.out.println("等待连接"); Socket clntSock = servSock.accept(); System.out.println("开始数据接收"); SocketAddress clientAddress = clntSock.getRemoteSocketAddress(); System.out.println("Handling client at " + clientAddress); InputStream in = clntSock.getInputStream(); OutputStream out = clntSock.getOutputStream(); while ((recvMsgSize = in.read(receiveBuf)) != -1) { out.write(receiveBuf, 0, recvMsgSize); } clntSock.close(); } } }
客户端程序
/** * @author jannal **/ public class ClientSocketTimeout { public static void main(String[] args) { long start = System.currentTimeMillis(); Socket socket = null; try { socket = new Socket("192.168.1.106", 8888); } catch (IOException e) { e.printStackTrace(); } long end = System.currentTimeMillis(); System.out.println("执行时间:"+(end-start)); } }
客户端程序运行在
macox
下,运行结果是25-26秒,这个与我们的预期结果(75s)并不一致,并且抓包来看与macox
使用telnet
测试时使用wireshark抓包的结果(重试的时间间隔)也不一致.经过多次测试,mac下java的socket程序,如果不指定超时时间,默认超时时间基本都是26秒左右,这个可能是JDK mac下实现的问题(只是猜测)
客户端程序运行在
centos6.7
下,运行的结果是63s,与我们的预期结果一致
从上面的结果来看,
macox
并没有等待75秒之后(net.inet.tcp.keepinit: 75000
)才超时,大约在25-26秒左右就超时了。目前不知道什么原因,可能与jdk底层不同平台的实现有关,毕竟我们只能看到openjdk里linux的实现。- [ ] 遗留问题:
macox(Sierra 10.12.6)
为什么不是75s?(目前只能猜测是jdk在mac下的实现有关)
- [ ] 遗留问题:
如果在java层面设置超时时间大于系统内核的时间会出现什么情况呢?比如以在
centos6.7
下设置sysctl net.ipv4.tcp_syn_retries=1
(代表3s),超时时间会以centos6.7
内核的超时时间为准备public class ClientSocketTimeout { public static void main(String[] args) { long start = System.currentTimeMillis(); Socket socket = new Socket(); try { socket.connect(new InetSocketAddress("192.168.1.106", 8888), 20000); } catch (Exception e) { e.printStackTrace(); } long end = System.currentTimeMillis(); System.out.println("执行时间:" + (end - start)); } }
总结
- java中socket
- 不指定
timeout
,超时时间是通过操作系统底层tcpip参数决定的,不同操作系统的参数不一样(比如centos和mac的参数就不同) - 指定
timeout
,此时如果timeout
设置的时间小于操作系统内核中设置的时间,则以指定的timeout
为准。如果timeout
设置的时间大于操作系统内核中的设置的时间,比如在centos6.7中设置sysctl net.ipv4.tcp_syn_retries=1
(3s),此时即使在java的socket参数上设置大于3s
的值程序还是会在3s
时超时(即在应用层设置时无效的)
- 不指定
参考文献
- https://www.unix.com/man-page/osx/4/tcp/
- 《UNIX网络编程.卷1,套接字网络API第三版》
- https://www.freebsd.org/cgi/man.cgi?query=tcp