版权声明:本文是作者在学习与工作中的总结与笔记,如有内容是您的原创,请评论留下链接地址,我会在文章开头声明。 https://blog.csdn.net/usagoole/article/details/82586369
ReadTimedOut深入分析
Read timed out
就是已经连接成功,但是服务器没有及时返回数据,导致读超时。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)
模拟超时
Read timed out
,PSH
的意思是控制信息是可以正常传送的,也就是说握手是正常成功的,然后传输数据的时候,我们限制了服务器无法给客户端传送数据内容,与上面java程序模拟的原理类似,都是可以建立连接,但是不返回数据。防火墙增加-A OUTPUT -p tcp -m tcp --tcp-flags PSH PSH --sport 8888 -j DROP
执行流程
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
从以下代码分析可以看出,
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()时
再看java代码中读取数据
InputStream inputStream = socket.getInputStream(); inputStream.read()
这里
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 ④
Java_java_net_SocketInputStream_socketRead0
从下面代码分析可知,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
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; } } }
总结
Read timed out
表示已经连接成功(即三次握手已经完成),但是服务器没有及时返回数据(没有在设定的时间内返回数据),导致读超时。- java在linux中的
Read timed out
并不是通过C函数setSockOpt(SO_RCVTIMEO)
来设置的,而是通过select(s, timeout).
来实现定时器,并抛出JNI
异常来控制的 java socket
读超时的设置是在read()
方法被调用的时候传入的,所以只要在read()
调用之前设置即可