apache 有个开源库:commons-net,这个开源库中包括了各类基础的网络工具类,我使用了这个开源库中的 FTP 工具。html
但碰到一些问题,并非说是开源库的 bug,可能锅得算在产品头上吧,各类奇怪需求。java
当将网络限速成 1KB/S 时,使用 commons-net 开源库中的 FTPClient 上传本地文件到 FTP 服务器上,FTPClient 源码内部是经过 Socket 来实现传输的,当终端和服务器创建了链接,调用 storeFile()
开始上传文件时,因为网络限速问题,一直没有接收到是否传输结束的反馈,致使此时,当前线程一直卡在 storeFile()
,后续代码一直没法执行。linux
若是这个时候去 FTP 服务器上查看一下,会发现,新建立了一个 0KB 的文件,但本地文件中的数据内容就是没有上传上来。git
产品要求,须要有个超时处理,好比上传工做超过了 30s 就当作上传失败,超时处理。但我明明调用了 FTPClient 的相关超时设置接口,就是没有一个会生效。github
一句话简述下上述的场景问题:apache
网络限速时,为什么 FTPClient 设置了超时时间,但文件上传过程当中超时机制却一直没生效?编程
一气之下,干脆跟进 FTPClient 源码内部,看看为什么设置的超时失效了,没有起做用。promise
因此,本篇也就是梳理下 FTPClient 中相关超时接口的含义,以及如何处理上述场景中的超时功能。服务器
先来说讲对 FTPClient 的浅入学习过程吧,若是不感兴趣,直接跳过该节,看后续小节的结论就能够了。网络
ps:本篇所使用的 commons-net 开源库版本为 3.6
首先,先来看看,使用 FTPClient 上传文件到 FTP 服务器大概须要哪些步骤:
//1.与 FTP 服务器建立链接 ftpClient.connect(hostUrl, port); //2.登陆 ftpClient.login(username, password); //3.进入到指定的上传目录中 ftpClient.makeDirectory(remotePath); ftpClient.changeWorkingDirectory(remotePath); //4.开始上传文件到FTP ftpClient.storeFile(file.getName(), fis);
固然,中间省略其余的配置项,好比设置主动模式、被动模式,设置每次读取本地文件的缓冲大小,设置文件类型,设置超时等等。但大致上,使用 FTPClient 来上传文件到 FTP 服务器的步骤就是这么几个。
既然本篇主要是想理清超时为什么没生效,那么也就先来看看都有哪些设置超时的接口:
粗体字是 FTPClient 类中提供的方法,而 FTPClient 的继承关系以下:
FTPClient extends FTP extends SocketClient
非粗体字的方法都是 SocketClient 中提供的方法。
好,先清楚有这么几个设置超时的接口存在,后面再从跟进源码过程当中,一个个来了解它们。
那么,就先看看第一步的 connect()
:
//SocketClient#connect() public void connect(String hostname, int port) throws SocketException, IOException { _hostname_ = hostname; _connect(InetAddress.getByName(hostname), port, null, -1); } //SocketClient#_connect() private void _connect(InetAddress host, int port, InetAddress localAddr, int localPort) throws SocketException, IOException { //1.建立socket _socket_ = _socketFactory_.createSocket(); //2.设置发送窗口和接收窗口的缓冲大小 if (receiveBufferSize != -1) { _socket_.setReceiveBufferSize(receiveBufferSize); } if (sendBufferSize != -1) { _socket_.setSendBufferSize(sendBufferSize); } //3.socket(套接字:ip 和 port 组成) if (localAddr != null) { _socket_.bind(new InetSocketAddress(localAddr, localPort)); } //4.链接,这里出现 connectTimeout 了 _socket_.connect(new InetSocketAddress(host, port), connectTimeout); _connectAction_(); }
因此, FTPClient 调用的 connect()
方法实际上是调用父类的方法,这个过程会去建立客户端 Socket,并和指定的服务端的 ip 和 port 建立链接,这个过程当中,出现了一个 connectTimeout,与之对应的 FTPClient 的超时接口:
//SocketClient#setConnectTimeout() public void setConnectTimeout(int connectTimeout) { this.connectTimeout = connectTimeout; }
至于内部是如何建立计时器,并在超时后是如何抛出 SocketTimeoutException 异常的,就不跟进了,有兴趣自行去看,这里就看一下接口的注释:
/** * Connects this socket to the server with a specified timeout value. * A timeout of zero is interpreted as an infinite timeout. The connection * will then block until established or an error occurs. * (用该 socket 与服务端建立链接,并设置一个指定的超时时间,若是超时时间是0,表示超时时间为无穷大, * 建立链接这个过程会进入阻塞状态,直到链接建立成功,或者发生某个异常错误) * @param endpoint the {@code SocketAddress} * @param timeout the timeout value to be used in milliseconds. * @throws IOException if an error occurs during the connection * @throws SocketTimeoutException if timeout expires before connecting * @throws java.nio.channels.IllegalBlockingModeException * if this socket has an associated channel, * and the channel is in non-blocking mode * @throws IllegalArgumentException if endpoint is null or is a * SocketAddress subclass not supported by this socket * @since 1.4 * @spec JSR-51 */ public void connect(SocketAddress endpoint, int timeout) throws IOException { }
注释有大概翻译了下,总之到这里,先搞清一个超时接口的做用了,虽然从方法命名上也能够看出来了:
setConnectTimeout()
: 用于设置终端和服务器创建链接这个过程的超时时间。
还有一点须要注意,当终端和服务端创建链接这个过程当中,当前线程会进入阻塞状态,即常说的同步请求操做,直到链接成功或失败,后续代码才会继续进行。
当链接建立成功后,会调用 _connectAction_()
,看看:
//SocketClient#_connectAction_() protected void _connectAction_() throws IOException { _socket_.setSoTimeout(_timeout_); //... }
这里又出现一个 _timeout_ 了,看看它对应的 FTPClient 的超时接口:
//SocketClient#setDefaultTimeout() public void setDefaultTimeout(int timeout){ _timeout_ = timeout; }
setDefaultTimeout()
:用于当终端与服务端建立完链接后,初步对用于传输控制命令的 Socket 调用 setSoTimeout()
设置超时,因此,这个超时具体是何做用,取决于 Socket 的 setSoTimeout()
。
另外,还记得 FTPClient 也有这么个超时接口么:
//SocketClient#setSoTimeout() public void setSoTimeout(int timeout) throws SocketException { _socket_.setSoTimeout(timeout); }
因此,对于 FTPClient 而言,setDefaultTimeout()
超时的工做跟 setSoTimeout()
是相同的,区别仅在于后者会覆盖掉前者设置的值。
接下去看看其余步骤的方法:
//FTPClient#login() public boolean login(String username, String password) throws IOException { //... user(username); //... return FTPReply.isPositiveCompletion(pass(password)); } //FTP#user() public int user(String username) throws IOException { return sendCommand(FTPCmd.USER, username); } //FTP#pass() public int pass(String password) throws IOException { return sendCommand(FTPCmd.PASS, password); }
因此,login 主要是发送 FTP 协议的一些控制命令,由于链接已经建立成功,终端发送的 FTP 控制指令给 FTP 服务器,完成一些操做,好比登陆,好比建立目录,进入某个指定路径等等。
这些步骤过程当中,没看到跟超时相关的处理,因此,看看最后一步上传文件的操做:
//FTPClient#storeFile() public boolean storeFile(String remote, InputStream local) throws IOException { return __storeFile(FTPCmd.STOR, remote, local); } //FTPClient#__storeFile() private boolean __storeFile(FTPCmd command, String remote, InputStream local) throws IOException { return _storeFile(command.getCommand(), remote, local); } //FTPClient#_storeFile() protected boolean _storeFile(String command, String remote, InputStream local) throws IOException { //1. 建立并链接用于传输 FTP 数据的 Socket Socket socket = _openDataConnection_(command, remote); //... //2. 设置传输监听,这里出现了一个timeout CSL csl = null; if (__controlKeepAliveTimeout > 0) { csl = new CSL(this, __controlKeepAliveTimeout, __controlKeepAliveReplyTimeout); } // Treat everything else as binary for now try { //3.开始发送本地数据到FTP服务器 Util.copyStream(local, output, getBufferSize(), CopyStreamEvent.UNKNOWN_STREAM_SIZE, __mergeListeners(csl), false); } //... }
咱们在学习 FTP 协议的端口时,还记得么,一般 20 端口是数据端口,21 端口是控制端口,固然这并不固定。但整体上,整个过程分两步:一是先创建用于传输控制命令的链接,二是再创建用于传输数据的链接。
因此,当调用 _storeFile()
上传文件时,会再经过 _openDataConnection_()
建立一个用于传输数据的 Socket,并与服务端链接,链接成功后,就会经过 Util 的 copyStream()
将本地文件 copy 到用于传输数据的这个 Socket 的 OutputStream 输出流上,此时,Socket 底层会自动去按照 TCP 协议往发送窗口中写数据来发给服务器。
这个步骤涉及到不少超时处理的地方,因此就来看看,首先是 _openDataConnection_()
:
//FTPClient#_openDataConnection_() protected Socket _openDataConnection_(String command, String arg) throws IOException { //... Socket socket; //... //1. 根据被动模式或主动模式建立不一样的 Socket 配置 if (__dataConnectionMode == ACTIVE_LOCAL_DATA_CONNECTION_MODE) { //... } else { // We must be in PASSIVE_LOCAL_DATA_CONNECTION_MODE //... //2. 我项目中使用的是被动模式,因此我只看这个分支了 //3. 建立用于传输数据的 Socket socket = _socketFactory_.createSocket(); //... //4. 对这个传输数据的 Socket 设置了 SoTimeout 超时 if (__dataTimeout >= 0) { socket.setSoTimeout(__dataTimeout); } //5. 跟服务端创建链接,指定超时处理 socket.connect(new InetSocketAddress(__passiveHost, __passivePort), connectTimeout); //... } //... return socket; }
因此,建立用于传输数据的 Socket 跟传输控制命令的 Socket 区别不是很大,当跟服务端创建链接时也都是用的 FTPClient 的 setConnectTimeout()
设置的超时时间处理。
有点区别的地方在于,传输控制命令的 Socket 是当在与服务端创建完链接后才会去设置 Socket 的 SoTimeout,而这个超时时间则来自于调用 FTPClient 的 setDefaultTimeout()
,和 setSoTimeout()
,后者设置的值优先。
而传输数据的 Socket 则是在与服务端创建链接以前就设置了 Socket 的 SoTimeout,超时时间值来自于 FTPClient 的 setDataTimeout()
。
那么,setDataTimeout()
也清楚一半了,设置用于传输数据的 Socket 的 SoTimeout 值。
因此,只要能搞清楚,Socket 的 setSoTimeout()
超时究竟指的是对哪一个工做过程的超时处理,那么就可以理清楚 FTPClient 的这些超时接口的用途:setDefaultTimeout()
,setSoTimeout()
,setDataTimeout()
。
这个先放一边,继续看 _storeFile()
流程的第二步:
//FTPClient#_storeFile() protected boolean _storeFile(String command, String remote, InputStream local) throws IOException { //... //2. 设置传输监听 CSL csl = null; if (__controlKeepAliveTimeout > 0) { csl = new CSL(this, __controlKeepAliveTimeout, __controlKeepAliveReplyTimeout); } // Treat everything else as binary for now try { //3.开始发送本地数据到FTP服务器 Util.copyStream(local, output, getBufferSize(), CopyStreamEvent.UNKNOWN_STREAM_SIZE, __mergeListeners(csl), false); } } //FTPClient#setControlKeepAliveTimeout() public void setControlKeepAliveTimeout(long controlIdle){ __controlKeepAliveTimeout = controlIdle * 1000; } //FTPClient#setControlKeepAliveReplyTimeout() public void setControlKeepAliveReplyTimeout(int timeout) { __controlKeepAliveReplyTimeout = timeout; }
FTPClient 的最后两个超时接口也找到使用的地方了,那么就看看 CSL 内部类是如何处理这两个 timeout 的:
//FTPClient$CSL private static class CSL implements CopyStreamListener { CSL(FTPClient parent, long idleTime, int maxWait) throws SocketException { this.idle = idleTime; //... parent.setSoTimeout(maxWait); } //每次读取文件的过程,都让传输控制命令的 Socket 发送一个无任何操做的 NOOP 命令,以便让这个 Socket keep alive @Override public void bytesTransferred(long totalBytesTransferred, int bytesTransferred, long streamSize) { long now = System.currentTimeMillis(); if ((now - time) > idle) { try { parent.__noop(); } catch (SocketTimeoutException e) { notAcked++; } catch (IOException e) { // Ignored } time = now; } } }
CSL 是监听 copyStream()
这个过程的,由于本地文件要上传到服务器,首先,须要先读取本地文件的内容,而后写入到传输数据的 Socket 的输出流中,这个过程不多是一次性完成的,确定是每次读取一些、写一些,默认每次是读取 1KB,可配置。而 Socket 的输出流缓冲区也不可能能够一直往里写的,它有一个大小限制。底层的具体实现其实也就是 TCP 的发送窗口,那么这个窗口中的数据天然须要在接收到服务器的 ACK 确认报文后才会清空,腾出位置以即可以继续写入。
因此,copyStream()
是一个会进入阻塞的操做,由于须要取决于网络情况。而 setControlKeepAliveTimeout()
方法命名中虽然带有 timeout 关键字,但实际上它的用途并非用于处理传输超时工做的。它的用途,其实将方法的命名翻译下就是了:
setControlKeepAliveTimeout()
:用于设置传输控制命令的 Socket 的 alive 状态,注意单位为 s。
由于 FTP 上传文件过程当中,须要用到两个 Socket,一个用于传输控制命令,一个用于传输数据,那当处于传输数据过程当中时,传输控制命令的 Socket 会处于空闲状态,有些路由器可能监控到这个 Socket 链接处于空闲状态超过必定时间,会进行一些断开等操做。因此,在传输过程当中,每读取一次本地文件,传输数据的 Socket 每要发送一次报文给服务端时,根据 setControlKeepAliveTimeout()
设置的时间阈值,来让传输控制命令的 Socket 也发送一个无任何操做的命令 NOOP,以便让路由器觉得这个 Socket 也处于工做状态。这些就是 bytesTransferred()
方法中的代码干的事。
setControlKeepAliveReplyTimeout()
:这个只有在调用了 setControlKeepAliveTimeout()
方法,并传入一个大于 0 的值后,才会生效,用于在 FTP 传输数据这个过程,对传输控制命令的 Socket 设置 SoTimeout,这个传输过程结束后会恢复传输控制命令的 Socket 本来的 SoTimeout 配置。
那么,到这里能够稍微来小结一下:
FTPClient 一共有 6 个用于设置超时的接口,而终端与 FTP 通讯过程会建立两个 Socket,一个用于传输控制命令,一个用于传输数据。这 6 个超时接口与两个 Socket 之间的关系:
setConnectTimeout()
:用于设置两个 Socket 与服务器创建链接这个过程的超时时间,单位 ms。
setDefaultTimeout()
:用于设置传输控制命令的 Socket 的 SoTimeout,单位 ms。
setSoTimeout()
:用于设置传输控制命令的 Socket 的 SoTimeout,单位 ms,值会覆盖上个方法设置的值。
setDataTimeout()
:被动模式下,用于设置传输数据的 Socket 的 SoTimeout,单位 ms。
setControlKeepAliveTimeout()
:用于在传输数据过程当中,也可让传输控制命令的 Socket 伪装保持处于工做状态,防止被路由器干掉,注意单位是 s。
setControlKeepAliveReplyTimeout()
:只有调用上个方法后,该方法才能生效,用于设置在传输数据这个过程当中,暂时替换掉传输控制命令的 Socket 的 SoTimeout,传输过程结束恢复这个 Socket 本来的 SoTimeout。
大部分超时接口最后设置的对象都是 Socket 的 SoTimeout,因此,接下来,学习下这个是什么:
//Socket#setSoTimeout() /** * Enable/disable {@link SocketOptions#SO_TIMEOUT SO_TIMEOUT} * with the specified timeout, in milliseconds. With this option set * to a non-zero timeout, a read() call on the InputStream associated with * this Socket will block for only this amount of time. If the timeout * expires, a <B>java.net.SocketTimeoutException</B> is raised, though the * Socket is still valid. The option <B>must</B> be enabled * prior to entering the blocking operation to have effect. The * timeout must be {@code > 0}. * A timeout of zero is interpreted as an infinite timeout. * (设置一个超时时间,用来当这个 Socket 调用了 read() 从 InputStream 输入流中 * 读取数据的过程当中,若是线程进入了阻塞状态,那么此次阻塞的过程耗费的时间若是 * 超过了设置的超时时间,就会抛出一个 SocketTimeoutException 异常,但只是将 * 线程从读数据这个过程当中断掉,并不影响 Socket 的后续使用。 * 若是超时时间为0,表示无限长。) * (注意,并非读取输入流的整个过程的超时时间,而仅仅是每一次进入阻塞等待输入流中 * 有数据可读的超时时间) * @param timeout the specified timeout, in milliseconds. * @exception SocketException if there is an error * in the underlying protocol, such as a TCP error. * @since JDK 1.1 * @see #getSoTimeout() */ public synchronized void setSoTimeout(int timeout) throws SocketException { //... } //SocketOptions#SO_TIMEOUT /** Set a timeout on blocking Socket operations: * (设置一个超时时间,用于处理一些会陷入阻塞的 Socket 操做的超时处理,好比:) * <PRE> * ServerSocket.accept(); * SocketInputStream.read(); * DatagramSocket.receive(); * </PRE> * * <P> The option must be set prior to entering a blocking * operation to take effect. If the timeout expires and the * operation would continue to block, * <B>java.io.InterruptedIOException</B> is raised. The Socket is * not closed in this case. * (设置这个超时的操做必需要在 Socket 那些会陷入阻塞的操做以前才能生效, * 当超时时间到了,而当前还处于阻塞状态,那么会抛出一个异常,但此时 Socket 并无被关闭) * * <P> Valid for all sockets: SocketImpl, DatagramSocketImpl * * @see Socket#setSoTimeout * @see ServerSocket#setSoTimeout * @see DatagramSocket#setSoTimeout */ @Native public final static int SO_TIMEOUT = 0x1006;
以上的翻译是基于个人理解,我自行的翻译,也许不那么正确,大家也能够直接看英文。
或者是看看这篇文章:关于 Socket 设置 setSoTimeout 误用的说明,文中有一句解释:
读取数据时阻塞链路的超时时间
我再基于他的基础上理解一波,我以为他这句话中有两个重点,一是:读取,二是:阻塞。
这两个重点是理解 SoTimeout 超时机制的关键,就像那篇文中所说,不少人将 SoTimeout 理解成链路的超时时间,或者这一次传输过程的总超时时间,但这种理解是错误的。
第一点,SoTimeout 并非传输过程的总超时时间,无论是上传文件仍是下载文件,服务端和终端确定是要分屡次报文传输的,我对 SoTimeout 的理解是,它是针对每一次的报文传输过程而已,而不是总的传输过程。
第二点,SoTimeout 只针对从 Socket 输入流中读取数据的操做。什么意思,若是是终端下载 FTP 服务器的文件,那么服务端会往终端的 Socket 的输入流中写数据,若是终端接收到了这些数据,那么 FTPClient 就能够去这个 Socket 的输入流中读取数据写入到本地文件的输出流。而若是反过来,终端上传文件到 FTP 服务器,那么 FTPClient 是读取本地文件写入终端的 Socket 的输出流中发送给终端,这时就不是对 Socket 的输入流操做了。
总之,setSoTimeout()
用于设置从 Socket 的输入流中读取数据时每次陷入阻塞过程的超时时间。
那么,在 FTPClient 中,所对应的就是,setSoTimeout()
对下述方法有效:
retrieveFile()
retrieveFileStream()
相反的,下述这些方法就无效了:
storeFile()
storeFileStream()
这样就能够解释得通,开头我所提的问题了,在网络被限速之下,因为 sotreFile()
会陷入阻塞,而且设置的 setDataTimeout()
超时因为这是一个上传文件的操做,不是对 Socket 的输入流的读取操做,因此无效。因此,也才会出现线程进入阻塞状态,后续代码一直得不到执行,UI 层迟迟接收不到上传成功与否的回调通知。
最后个人处理是,在业务层面,本身写了超时处理。
注意,以上分析的场景是:FTP 被动模式的上传文件的场景下,相关接口的超时处理。因此不少表述都是基于这个场景的前提下,有一些源码,如 Util 的 copyStream()
不只在文件上传中使用,在下载 FTP 上的文件时也一样使用,因此对于文件上传来讲,这方法就是用来读取本地文件写入传输数据的 Socket 的输出流;而对于下载 FTP 文件的场景来讲,这方法的做用就是用于读取传输数据的 Socket 的输入流,写入到本地文件的输出流中。以此类推。
总结来讲,若是是对于网络开发这方面领域内的来讲,这些超时接口的用途应该都是基础,但对于咱们这些不多接触 Socket 的来讲,若是单凭接口注释文档没法理解的话,那能够尝试翻阅下源码,理解下。
梳理以后,FTPClient 一共有 6 个设置超时的接口,而无论是文件上传或下载,这过程,FTP 都会建立两个 Socket,一个用于传输控制命令,一个用于传输文件数据,超时接口和这两个 Socket 之间的关系以下:
setConnectTimeout()
用于设置终端 Socket 与 FTP 服务器创建链接这个过程的超时时间。setDefaultTimeout()
用于设置终端的传输控制命令的 Socket 的 SoTimeout,即针对传输控制命令的 Socket 的输入流作读取操做时每次陷入阻塞的超时时间。setSoTimeout()
做用跟上个方法同样,区别仅在于该方法设置的超时会覆盖掉上个方法设置的值。setDataTimeout()
用于设置终端的传输数据的 Socket 的 Sotimeout,即针对传输文件数据的 Socket 的输入流作读取操做时每次陷入阻塞的超时时间。setControlKeepAliveTimeout()
用于设置当处于传输数据过程当中,按指定的时间阈值按期让传输控制命令的 Socket 发送一个无操做命令 NOOP 给服务器,让它 keep alive。setControlKeepAliveReplyTimeout()
:只有调用上个方法后,该方法才能生效,用于设置在传输数据这个过程当中,暂时替换掉传输控制命令的 Socket 的 SoTimeout,传输过程结束恢复这个 Socket 本来的 SoTimeout。超时接口大概的用途明确了,那么再稍微来说讲该怎么用:
针对使用 FTPClient 下载 FTP 文件,通常只需使用两个超时接口,一个是 setConnectTimeout()
,用于设置创建链接过程当中的超时处理,而另外一个则是 setDataTimeout()
,用于设置下载 FTP 文件过程当中的超时处理。
针对使用 FTPClient 上传文件到 FTP 服务器,创建链接的超时一样须要使用 setConnectTimeout()
,但文件上传过程当中,建议自行利用 Android 的 Handler 或其余机制实现超时处理,由于 setDataTimeout()
这个设置对上传的过程无效。
另外,使用 setDataTimeout()
时须要注意,这个超时不是指下载文件整个过程的超时处理,而是仅针对终端 Socket 从输入流中,每一次可进行读取操做以前陷入阻塞的超时。
以上,是我所碰到的问题,及梳理的结论,我只以我所遇的现象来理解,由于我对网络编程,对 Socket 不熟,若是有错误的地方,欢迎指证一下。
最后附上 FTPClient 文件上传过程当中,常见的一些异常,便于针对性的进行分析:
org.apache.commons.net.io.CopyStreamException: IOException caught while copying. at org.apache.commons.net.io.Util.copyStream(Util.java:136) at org.apache.commons.net.ftp.FTPClient._storeFile(FTPClient.java:675) at org.apache.commons.net.ftp.FTPClient.__storeFile(FTPClient.java:639) at org.apache.commons.net.ftp.FTPClient.storeFile(FTPClient.java:2030) at com.chinanetcenter.component.log.FtpUploadTask.run(FtpUploadTask.java:121) Caused by: java.net.SocketException: sendto failed: ETIMEDOUT (Connection timed out) at libcore.io.IoBridge.maybeThrowAfterSendto(IoBridge.java:546) at libcore.io.IoBridge.sendto(IoBridge.java:515) at java.net.PlainSocketImpl.write(PlainSocketImpl.java:504) at java.net.PlainSocketImpl.access$100(PlainSocketImpl.java:37) at java.net.PlainSocketImpl$PlainSocketOutputStream.write(PlainSocketImpl.java:266) at java.io.BufferedOutputStream.write(BufferedOutputStream.java:174) at
分析:异常的关键信息:ETIMEOUT。
可能的场景:因为网络被限速 1KB/S,终端的 Socket 发给服务端的报文一直收不到 ACK 确认报文(缘由不懂),致使发送缓冲区一直处于满的状态,致使 FTPClient 的 storeFile()
一直陷入阻塞。而若是一个 Socket 一直处于阻塞状态,TCP 的 keeplive 机制一般会每隔 75s 发送一次探测包,一共 9 次,若是都没有回应,则会抛出如上异常。
可能还有其余场景,上述场景是我所碰到的,FTPClient 的 setDataTimeout()
设置了超时,但没生效,缘由上述已经分析过了,最后过了十来分钟本身抛了超时异常,至于为何会抛了一次,看了下篇文章里的分析,感受对得上我这种场景。
具体原理参数:浅谈TCP/IP网络编程中socket的行为
org.apache.commons.net.io.CopyStreamException: IOException caught while copying. at org.apache.commons.net.io.Util.copyStream(Util.java:136) at org.apache.commons.net.ftp.FTPClient._retrieveFile(FTPClient.java:1920) at org.apache.commons.net.ftp.FTPClient.retrieveFile(FTPClient.java:1885) at com.chinanetcenter.component.log.FtpUploadTask.run(FtpUploadTask.java:143) Caused by: java.net.SocketTimeoutException at java.net.PlainSocketImpl.read(PlainSocketImpl.java:488) at java.net.PlainSocketImpl.access$000(PlainSocketImpl.java:37) at java.net.PlainSocketImpl$PlainSocketInputStream.read(PlainSocketImpl.java:237) at java.io.InputStream.read(InputStream.java:162) at java.io.BufferedInputStream.fillbuf(BufferedInputStream.java:149) at java.io.BufferedInputStream.read(BufferedInputStream.java:234) at java.io.PushbackInputStream.read(PushbackInputStream.java:146)
分析:该异常注意跟第一种场景的异常区分开,注意看异常栈中的第一个异常信息,这里是因为 read 过程的超时而抛出的异常,而这个超时就是对 Socket 设置了 setSoTimeout()
,归根到 FTPClient 的话,就是调用了 setDataTimeout()
设置了传输数据用的 Socket 的 SoTimeout,因为是文件下载操做,是对 Socket 的输入流进行的操做,因此这个超时机制能够正常运行。
java.net.SocketTimeoutException: failed to connect to /123.103.23.202 (port 2121) after 500ms at libcore.io.IoBridge.connectErrno(IoBridge.java:169) at libcore.io.IoBridge.connect(IoBridge.java:122) at java.net.PlainSocketImpl.connect(PlainSocketImpl.java:183) at java.net.PlainSocketImpl.connect(PlainSocketImpl.java:456) at java.net.Socket.connect(Socket.java:882) at org.apache.commons.net.SocketClient._connect(SocketClient.java:243) at org.apache.commons.net.SocketClient.connect(SocketClient.java:202) at com.chinanetcenter.component.log.FtpUploadTask.run(FtpUploadTask.java:93)
分析:这是因为 Socket 在建立链接时超时的异常,一般是 TCP 的三次握手,这个链接对应着 FTPClient 的 connect()
方法,其实关键是 Socket 的 connect()
方法,在 FTPClient 的 stroreFile()
方法内部因为须要建立用于传输的 Socket,也会有这个异常出现的可能。
另外,这个超时时长的设置由 FTPClient 的 setConnectTimeout()
决定。
参考:TCP/IP错误列表 ,下面是部分截图:
你们好,我是 dasu,欢迎关注个人公众号(dasuAndroidTv),若是你以为本篇内容有帮助到你,能够转载但记得要关注,要标明原文哦,谢谢支持~