本文介绍: apache有个开源库: commonsnet,这个开源库中包括了各种基础的网络工具类,我使用了这个开源库中的FTP工具。但碰到一些问题,并不是说是开源库的 bug可能锅得算在产品头上吧,各种奇怪需求问题当将网络速成1KB/S时,使用commons-net开源库中的FTPClient 上传本地文件到FTP服务器上,FTPClient源码内部通过Socket实现传输的,当终端服务器建立了连接调用storeFile()开始上传文件时,由于网络限速问题,一直没有收到是否传输结束反馈

apache有个开源库: commons-net,这个开源库中包括了各种基础的网络工具类,我使用了这个开源库中的FTP工具
但碰到一些问题,并不是说是开源库的 bug可能锅得算在产品头上吧,各种奇怪需求

问题

当将网络限速成1KB/S时,使用commons-net开源库中的FTPClient 上传本地文件到FTP服务器上,FTPClient源码内部通过Socket 来实现传输的,当终端服务器建立了连接调用storeFile()开始上传文件时,由于网络限速问题,一直没有收到是否传输结束反馈,导致此时,当前线程一直卡在storeFile(),后续代码一直无法执行
如果这个时候去FTP服务器查看一下,会发现,新创建一个OKB的文件,但本地文件中的数据内容就是没有上传上来。
产品要求,需要有个超时处理比如上传工作超过了30s就当做上传失败超时处理。但我明明调用了FTPClient的相关超时设置接口,就是没有一个生效
—句话简述下上述的场景问题:
网络限速时,为何 FTPClient 设置了超时时间,但文件上传过程中超时机制却一直没生效?
一气之下,干脆跟进FTPClient源码内部,看看为何设置的超时失效了,没有起作用
所以,本篇也就是梳理下FTPClient 中相关超时接口的含义,以及如何处理上述场景中的超时功能

源码跟进

先来讲讲对FTPClient的浅入学习过程吧,如果不感兴趣,直接跳过该节,看后续小节的结论就可以了。
ps:本篇所使用的commons-net开源库版本为3.6

使用

首先,先来看看,使用FTPClient上传文件到FTP服务器大概需要哪些步骤:

当然,中间省略其他的配置项,比如设置主动模式、被动模式,设置每次读取本地文件的缓冲大小,设置文件类型,设置超时等等。但大体上,使用FTPClient来上传文件到FTP服务器步骤就是这么几个。
既然本篇主要是想理清超时为何没生效,那么也就先来看看都有哪些设置超时的接口:

粗体字是 FTPClient类中提供的方法,而FTPClient的继承关系如下:

非粗体字的方法都是SocketClient中提供的方法
好,先清楚有这么几个设置超时的接口存在,后面再从跟进源码过程中,一个个来了解它们。

跟进

1.connect()

那么,就先看看一步connect() :

所以,FTPClient调用connect()方法其实是调用父类方法,这个过程会去创建客户端Socket,并和指定服务端ipport创建连接,这个过程中,出现了一个connectTimeout,与之对应的FTPClient 的超时接口:;

至于内部是如何创建计时器,并在超时后是如何抛出SocketTimeoutException异常的,就不跟进了,有兴趣自行去看,这里就看—下接口注释:

注释有大概翻译了下,总之到这里,先搞清一个超时接口作用了,虽然从方法命名上也可以看出来了:setConnectTimeout():用于设置终端服务器建立连接这个过程的超时时间。
还有一点需要注意,当终端服务端建立连接这个过程中,当前线程进入阻塞状态,即常说的同步请求操作,直到连接成功或失败,后续代码才会继续进行。
连接创建成功后,会调用_connectAction_(,看看:

这里又出现一个_timeout_ 了,看看它对应的FTPClient 的超时接口;

setDefaultTimeout()∶用于终端服务端创建完连接后,初步对用于传输控制命令的Socket 调用setSoTimeout()设置超时,所以,这个超时具体是何作用,取决于Socket 的setSoTimeout()。
另外,还记得 FTPClient也有这么个超时接口么:

所以,对于FTPClient而言,setDefaultTimeout()超时的工作setSoTimeout()是相同的,区别仅在于后者会覆盖掉前者设置的值。
2.login()
接下去看看其他步骤方法:

所以,login主要是发送FTP协议的一些控制命令,因为连接已经创建成功,终端发送的FTP控制指令给FTP服务器,完成一些操作比如登录比如创建目录进入某个指定路径等等。
这些步骤过程中,没看到跟超时相关的处理,所以,看看最后一步上传文件的操作:
3. storeFile

所以,创建用于传输数据的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 的最后两个超时接口也找到使用的地方了,那么就看看CSL内部类是如何处理这两个timeout的:

 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。

4.SoTimeout
部分超时接口最后设置的对象都是Socket的SoTimeout,所以,接下来学习下这个是什么:

 

以上的翻译基于我的理解,我自行的翻译,也许不那么正确,你们也可以直接看英文
或者是看看这篇文章:关于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层迟迟接收不到上传成功与否的回调通知
最后我的处理是,在业务层面,自己写了超时处理。

那么,在 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文件上传过程中,常见的一些异常,便于针对性的进行分析:
1.storeFile()上传文件超时,该超时时间由Linux系统规定

分析:异常的关键信息:ETIMEOUT。
可能的场景:由于网络被限速1KB/S,终端的Socket 发给服务端的报文一直收不到ACK确认报文原因不懂),导致发送缓冲区一直处于满的状态,导致FTPClient的storeFile()一直陷入阻塞。而如果一个Socket一直处于阻塞状态,TCP的 keeplive机制通常会每隔75s 发送一次探测包,一共9次,如果都没有回应,则会抛出如上异常。
可能还有其他场景,上述场景是我所碰到的,FTPClient的setDataTimeout()设置了超时,但没生效,原因上述已经分析过了,最后过了十来分钟自己抛了超时异常,至于为什么会抛了一次,看了下篇文章里的分析,感觉对得上我这种场景。
具体原理参数:浅谈TCP/IP网络编程socket行为
2.retrieveFile下载文件超时

分析:该异常注意跟第一种场景的异常区分开,注意看异常栈中的第一个异常信息这里是由于read过程的超时而抛出的异常,而这个超时就是对Socket设置了setSoTimeout(),归根到FTPClient的话,就是调用了setDataTimeout()设置了传输数据用的Socket的 SoTimeout,由于是文件下载操作,是对Socket的输入流进行的操作,所以这个超时机制可以正常运行
2.Socket建立连接超时异常

 分析:这是由于Socket在创建连接时超时的异常,通常是TCP的三次握手,这个连接对应着FTPClient的connect()方法,其实关键是Socket的connect()方法,在FTPClient的stroreFile()方法内部由于需要创建用于传输的Socket,也会有这个异常出现的可能

另外,这个超时时长的设置由FTPClient 的setConnectTimeout()决定。
3.其他 TCP错误
参考:TCP/IP错误列表,下面是部分截图:

原文地址:https://blog.csdn.net/m1195900241/article/details/124969872

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任

如若转载,请注明出处:http://www.7code.cn/show_28358.html

如若内容造成侵权/违法违规/事实不符,请联系代码007邮箱suwngjj01@126.com进行投诉反馈,一经查实,立即删除

发表回复

您的邮箱地址不会被公开。 必填项已用 * 标注