在实际开发中经常会碰到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
openjdk
源码下载地址https://download.java.net/openjdk/jdk8
. PlainSocketImpl.c
的位置在openjdk/jdk/src/solaris/native/java/net
阻塞模式: 只有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才会把断开这个连接。
修改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 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));
}
}
三次握手,当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下实现的问题(只是猜测)
从上面的结果来看,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
时超时(即在应用层设置时无效的)