Socket笔记之深入分析java中的ConnectionTimedOut

文章目录

  • java中CTO(connection timed out)
    • PlainSocketImpl.c
      • Java_java_net_PlainSocketImpl_socketConnect
    • 超时重试次数与时间
    • java程序测试
    • 模拟超时
      • 设置防火墙模拟超时
      • 启动程序
  • 总结
  • 参考文献

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
    

    Socket笔记之深入分析java中的ConnectionTimedOut_第1张图片

  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下执行结果
    Socket笔记之深入分析java中的ConnectionTimedOut_第2张图片

  3. centos6.7下执行结果
    Socket笔记之深入分析java中的ConnectionTimedOut_第3张图片

模拟超时

设置防火墙模拟超时

  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下实现的问题(只是猜测)
    Socket笔记之深入分析java中的ConnectionTimedOut_第4张图片
    Socket笔记之深入分析java中的ConnectionTimedOut_第5张图片

  4. 客户端程序运行在centos6.7下,运行的结果是63s,与我们的预期结果一致
    Socket笔记之深入分析java中的ConnectionTimedOut_第6张图片

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

    Socket笔记之深入分析java中的ConnectionTimedOut_第7张图片

总结

  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

你可能感兴趣的:(#,java-socket)