深入分析java中的Connection timed out

版权声明:本文是作者在学习与工作中的总结与笔记,如有内容是您的原创,请评论留下链接地址,我会在文章开头声明。 https://blog.csdn.net/usagoole/article/details/81317966

深入分析java中的Connection timed out

java中CTO(connection timed out)

  1. 在实际开发中经常会碰到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)
  2. Connection timed outclient发出sync包,server端在指定的时间内没有回复ack导致的.没有回复ack的原因可能是网络丢包、防火墙阻止服务端返回syn的ack包等

  3. 执行流程

        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

  1. openjdk源码下载地址https://download.java.net/openjdk/jdk8. PlainSocketImpl.c的位置在openjdk/jdk/src/solaris/native/java/net

Java_java_net_PlainSocketImpl_socketConnect

  1. 阻塞模式: 只有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
    
    }
  2. 当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;
        }
    }
    
    

超时重试次数与时间

  1. 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
  2. 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

  3. 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 次幂),如果超时,则重试
  4. 修改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程序测试

  1. 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));
        }
    }
  2. mac下执行结果

  3. centos6.7下执行结果

模拟超时

设置防火墙模拟超时

  1. 三次握手,当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   # 重启防火墙
    
    

启动程序

  1. 服务端程序,如果不启动远程服务,会直接报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();
            }
    
        }
    }
    
  2. 客户端程序

    /**
     * @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));
        }
    }
    
  3. 客户端程序运行在macox下,运行结果是25-26秒,这个与我们的预期结果(75s)并不一致,并且抓包来看与macox使用telnet测试时使用wireshark抓包的结果(重试的时间间隔)也不一致.经过多次测试,mac下java的socket程序,如果不指定超时时间,默认超时时间基本都是26秒左右,这个可能是JDK mac下实现的问题(只是猜测)

  4. 客户端程序运行在centos6.7下,运行的结果是63s,与我们的预期结果一致

  5. 从上面的结果来看,macox并没有等待75秒之后(net.inet.tcp.keepinit: 75000)才超时,大约在25-26秒左右就超时了。目前不知道什么原因,可能与jdk底层不同平台的实现有关,毕竟我们只能看到openjdk里linux的实现。

    • [ ] 遗留问题:macox(Sierra 10.12.6)为什么不是75s?(目前只能猜测是jdk在mac下的实现有关)
  6. 如果在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));
                }
            }

总结

  1. java中socket
    • 不指定timeout,超时时间是通过操作系统底层tcpip参数决定的,不同操作系统的参数不一样(比如centos和mac的参数就不同)
    • 指定timeout,此时如果timeout设置的时间小于操作系统内核中设置的时间,则以指定的timeout为准。如果timeout设置的时间大于操作系统内核中的设置的时间,比如在centos6.7中设置sysctl net.ipv4.tcp_syn_retries=1(3s),此时即使在java的socket参数上设置大于3s的值程序还是会在3s时超时(即在应用层设置时无效的)

参考文献

  1. https://www.unix.com/man-page/osx/4/tcp/
  2. 《UNIX网络编程.卷1,套接字网络API第三版》
  3. https://www.freebsd.org/cgi/man.cgi?query=tcp

猜你喜欢

转载自blog.csdn.net/usagoole/article/details/81317966