Read timed out深入分析

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

ReadTimedOut深入分析

  1. Read timed out就是已经连接成功,但是服务器没有及时返回数据,导致读超时。

  2. java程序模拟Read timed out

        服务端
        public class SimpleServer {
            public static void main(String[] args) throws IOException, InterruptedException {
                ServerSocket serverSocket = new ServerSocket(8888,200);
                Thread.sleep(6666666);
            }
        }
    
        //客户端程序
        @Test
        public void testReadTimeOut() {
            Socket socket = new Socket();
            long startTime = 0;
            try {
                socket.connect(new InetSocketAddress("127.0.0.1", 8888), 10000);
                System.out.println("socket连接成功....");
                socket.setSoTimeout(2000);
                startTime = System.currentTimeMillis();
                int read = socket.getInputStream().read();
            } catch (IOException e) {
                e.printStackTrace();
            } finally {
                long endTime = System.currentTimeMillis();
                System.out.println(endTime - startTime);
            }
    
        }           
    

    执行结果

    socket连接成功....
    执行时间:2008
    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)
        at java.net.SocketInputStream.read(SocketInputStream.java:223)
        at cn.jannal.net.port.SimpleClient.testReadTimeOut(SimpleClient.java:29)
  3. 模拟超时Read timed outPSH的意思是控制信息是可以正常传送的,也就是说握手是正常成功的,然后传输数据的时候,我们限制了服务器无法给客户端传送数据内容,与上面java程序模拟的原理类似,都是可以建立连接,但是不返回数据。

     防火墙增加-A OUTPUT -p tcp -m tcp --tcp-flags PSH PSH --sport 8888 -j DROP
  4. 执行流程

    client socket
     socket.setSoTimeout(int timeout)
        --> socketOptions.setOption(int optID, Object value)
         --> AbstractPlainSocketImpl.setOption(int opt, Object val) 
          --> PlainSocketImpl.socketSetOption(int cmd, boolean on, Object value)
            --> PlainSocketImpl.c 中的Java_java_net_PlainSocketImpl_socketSetOption

PlainSocketImpl.c

Java_java_net_PlainSocketImpl_socketSetOption

  1. 从以下代码分析可以看出,socket.setSoTimeout(int timeout)选项在Solaris/linux下根本就没有调用C库函数中的setSockOpt(SO_RCVTIMEO),那java中到底是怎么实现read timed out的呢?

        /*
         * Class:     java_net_PlainSocketImpl
         * Method:    socketSetOption
         * Signature: (IZLjava/lang/Object;)V
         */
        JNIEXPORT void JNICALL
        Java_java_net_PlainSocketImpl_socketSetOption(JNIEnv *env, jobject this,
                                                      jint cmd, jboolean on,
                                                      jobject value) {
            int fd;
            int level, optname, optlen;
            union {
                int i;
                struct linger ling;
            } optval;
    
            /*
             * Check that socket hasn't been closed
             */
            fd = getFD(env, this);
            if (fd < 0) {
                JNU_ThrowByName(env, JNU_JAVANETPKG "SocketException",
                                "Socket closed");
                return;
            }
    
            /*
             * SO_TIMEOUT is a NOOP on Solaris/Linux
             * ,在Solaris/linux下根本就没有调用C库函数中的setSockOpt(SO_RCVTIMEO)
             */
            if (cmd == java_net_SocketOptions_SO_TIMEOUT) {
                return;
            }
    
            /*
             * Map the Java level socket option to the platform specific
             * level and option name.
             */
            if (NET_MapSocketOption(cmd, &level, &optname)) {
                JNU_ThrowByName(env, JNU_JAVANETPKG "SocketException", "Invalid option");
                return;
            }
    
            switch (cmd) {
                case java_net_SocketOptions_SO_SNDBUF :
                case java_net_SocketOptions_SO_RCVBUF :
                case java_net_SocketOptions_SO_LINGER :
                case java_net_SocketOptions_IP_TOS :
                    {
                        jclass cls;
                        jfieldID fid;
    
                        cls = (*env)->FindClass(env, "java/lang/Integer");
                        CHECK_NULL(cls);
                        fid = (*env)->GetFieldID(env, cls, "value", "I");
                        CHECK_NULL(fid);
    
                        if (cmd == java_net_SocketOptions_SO_LINGER) {
                            if (on) {
                                optval.ling.l_onoff = 1;
                                optval.ling.l_linger = (*env)->GetIntField(env, value, fid);
                            } else {
                                optval.ling.l_onoff = 0;
                                optval.ling.l_linger = 0;
                            }
                            optlen = sizeof(optval.ling);
                        } else {
                            optval.i = (*env)->GetIntField(env, value, fid);
                            optlen = sizeof(optval.i);
                        }
    
                        break;
                    }
    
                /* Boolean -> int */
                default :
                    optval.i = (on ? 1 : 0);
                    optlen = sizeof(optval.i);
    
            }
    
            if (NET_SetSockOpt(fd, level, optname, (const void *)&optval, optlen) < 0) {
        #ifdef __solaris__
                if (errno == EINVAL) {
                    // On Solaris setsockopt will set errno to EINVAL if the socket
                    // is closed. The default error message is then confusing
                    char fullMsg[128];
                    jio_snprintf(fullMsg, sizeof(fullMsg), "Invalid option or socket reset by remote peer");
                    JNU_ThrowByName(env, JNU_JAVANETPKG "SocketException", fullMsg);
                    return;
                }
        #endif /* __solaris__ */
                NET_ThrowByNameWithLastError(env, JNU_JAVANETPKG "SocketException",
                                              "Error setting socket option");
            }
        }
    
    

read()时

  1. 再看java代码中读取数据

    InputStream inputStream = socket.getInputStream();
    inputStream.read()
  2. 这里inputStream的实现类是socketInputStream。执行流程如下,超时设置的值是在read()方法被调用的时候传入的

    -->read() ①
        --> read(b, off, length, impl.getTimeout());//最终调用的都是这个方法,此时传入SO_TIMEOUT ②
         -->native int socketRead0(FileDescriptor fd,
                                       byte b[], int off, int len,
                                       int timeout) ③
            --> SocketInputStream.c中的Java_java_net_SocketInputStream_socketRead0     ④                      
    Created with Raphaël 2.1.2 SocketInputStream SocketInputStream SocketInputStream.c SocketInputStream.c

Java_java_net_SocketInputStream_socketRead0

  1. 从下面代码分析可知,java中setSoTimeout()设置值最终是传给了linux下的select()函数,并没有通过C语言的socket选项来设置。只要在read()之前设置超时时间即可,计算超时的核心代码在NET_Timeout

        /*
         * Class:     java_net_SocketInputStream
         * Method:    socketRead0
         * Signature: (Ljava/io/FileDescriptor;[BIII)I
         */
        JNIEXPORT jint JNICALL
        Java_java_net_SocketInputStream_socketRead0(JNIEnv *env, jobject this,
                                                    jobject fdObj, jbyteArray data,
                                                    jint off, jint len, jint timeout)
        {
            char BUF[MAX_BUFFER_LEN];
            char *bufP;
            jint fd, nread;
    
            if (IS_NULL(fdObj)) {
                /* shouldn't this be a NullPointerException? -br */
                JNU_ThrowByName(env, JNU_JAVANETPKG "SocketException",
                                "Socket closed");
                return -1;
            } else {
                fd = (*env)->GetIntField(env, fdObj, IO_fd_fdID);
                /* Bug 4086704 - If the Socket associated with this file descriptor
                 * was closed (sysCloseFD), then the file descriptor is set to -1.
                 */
                if (fd == -1) {
                    JNU_ThrowByName(env, "java/net/SocketException", "Socket closed");
                    return -1;
                }
            }
    
            /*
             * If the read is greater than our stack allocated buffer then
             * we allocate from the heap (up to a limit)
             */
            if (len > MAX_BUFFER_LEN) {
                if (len > MAX_HEAP_BUFFER_LEN) {
                    len = MAX_HEAP_BUFFER_LEN;
                }
                bufP = (char *)malloc((size_t)len);
                if (bufP == NULL) {
                    bufP = BUF;
                    len = MAX_BUFFER_LEN;
                }
            } else {
                bufP = BUF;
            }
    
            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;
                }
            }
    
            nread = NET_Read(fd, bufP, len);
    
            if (nread <= 0) {
                if (nread < 0) {
    
                    switch (errno) {
                        case ECONNRESET:
                        case EPIPE:
                            JNU_ThrowByName(env, "sun/net/ConnectionResetException",
                                "Connection reset");
                            break;
    
                        case EBADF:
                            JNU_ThrowByName(env, JNU_JAVANETPKG "SocketException",
                                "Socket closed");
                            break;
    
                        case EINTR:
                             JNU_ThrowByName(env, JNU_JAVAIOPKG "InterruptedIOException",
                                   "Operation interrupted");
                             break;
    
                        default:
                            NET_ThrowByNameWithLastError(env,
                                JNU_JAVANETPKG "SocketException", "Read failed");
                    }
                }
            } else {
                (*env)->SetByteArrayRegion(env, data, off, nread, (jbyte *)bufP);
            }
    
            if (bufP != BUF) {
                free(bufP);
            }
            return nread;
        }
    
    

solaris/native/java/net/bsd_close.c

  1. NET_timeout代码是对select(s, timeout)的封装,使用select函数实现定时器,来判断是否超过设置的时间(setSoTimeout())

        /*
         * Wrapper for select(s, timeout). We are using select() on Mac OS due to Bug 7131399.
         * Auto restarts with adjusted timeout if interrupted by
         * signal other than our wakeup signal.
         */
        int NET_Timeout(int s, long timeout) {
            long prevtime = 0, newtime;
            struct timeval t, *tp = &t;
            fd_set fds;
            fd_set* fdsp = NULL;
            int allocated = 0;
            threadEntry_t self;
            fdEntry_t *fdEntry = getFdEntry(s);
    
            /*
             * Check that fd hasn't been closed.
             */
            if (fdEntry == NULL) {
                errno = EBADF;
                return -1;
            }
    
            /*
             * Pick up current time as may need to adjust timeout
             */
            if (timeout > 0) {
                /* Timed */
                struct timeval now;
                gettimeofday(&now, NULL);
                prevtime = now.tv_sec * 1000  +  now.tv_usec / 1000;
                t.tv_sec = timeout / 1000;
                t.tv_usec = (timeout % 1000) * 1000;
            } else if (timeout < 0) {
                /* Blocking */
                tp = 0;
            } else {
                /* Poll */
                t.tv_sec = 0;
                t.tv_usec = 0;
            }
    
            if (s < FD_SETSIZE) {
                fdsp = &fds;
                FD_ZERO(fdsp);
            } else {
                int length = (howmany(s+1, NFDBITS)) * sizeof(int);
                fdsp = (fd_set *) calloc(1, length);
                if (fdsp == NULL) {
                    return -1;   // errno will be set to ENOMEM
                }
                allocated = 1;
            }
            FD_SET(s, fdsp);
    
            for(;;) {
                int rv;
    
                /*
                 * call select on the fd. If interrupted by our wakeup signal
                 * errno will be set to EBADF.
                 */
    
                startOp(fdEntry, &self);
                rv = select(s+1, fdsp, 0, 0, tp);
                endOp(fdEntry, &self);
    
                /*
                 * If interrupted then adjust timeout. If timeout
                 * has expired return 0 (indicating timeout expired).
                 */
                if (rv < 0 && errno == EINTR) {
                    if (timeout > 0) {
                        struct timeval now;
                        gettimeofday(&now, NULL);
                        newtime = now.tv_sec * 1000  +  now.tv_usec / 1000;
                        timeout -= newtime - prevtime;
                        if (timeout <= 0) {
                            if (allocated != 0)
                                free(fdsp);
                            //返回0表示超时    
                            return 0;
                        }
                        prevtime = newtime;
                        t.tv_sec = timeout / 1000;
                        t.tv_usec = (timeout % 1000) * 1000;
                    }
                } else {
                    if (allocated != 0)
                        free(fdsp);
                    return rv;
                }
    
            }
        }

总结

  1. Read timed out表示已经连接成功(即三次握手已经完成),但是服务器没有及时返回数据(没有在设定的时间内返回数据),导致读超时。
  2. java在linux中的 Read timed out并不是通过C函数setSockOpt(SO_RCVTIMEO)来设置的,而是通过select(s, timeout).来实现定时器,并抛出JNI异常来控制的
  3. java socket读超时的设置是在read()方法被调用的时候传入的,所以只要在read()调用之前设置即可

猜你喜欢

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