Socket笔记之Read timed out深入分析

文章目录

  • ReadTimedOut深入分析
    • PlainSocketImpl.c
      • Java_java_net_PlainSocketImpl_socketSetOption
    • read()时
      • Java_java_net_SocketInputStream_socketRead0
      • solaris/native/java/net/bsd_close.c
  • 总结

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     ④                      
    
    SocketInputStream SocketInputStream.c ① ② ③ ④ SocketInputStream 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()调用之前设置即可

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