apache 有個開源庫:commons-net,這個開源庫中包括了各種基礎的網(wǎng)絡工具類冒萄,我使用了這個開源庫中的 FTP 工具片效。
但碰到一些問題,并不是說是開源庫的 bug激况,可能鍋得算在產(chǎn)品頭上吧作彤,各種奇怪需求。
問題
當將網(wǎng)絡限速成 1KB/S 時乌逐,使用 commons-net 開源庫中的 FTPClient 上傳本地文件到 FTP 服務器上竭讳,F(xiàn)TPClient 源碼內部是通過 Socket 來實現(xiàn)傳輸?shù)模斀K端和服務器建立了連接浙踢,調用 storeFile()
開始上傳文件時代咸,由于網(wǎng)絡限速問題,一直沒有接收到是否傳輸結束的反饋成黄,導致此時呐芥,當前線程一直卡在 storeFile()
逻杖,后續(xù)代碼一直無法執(zhí)行。
如果這個時候去 FTP 服務器上查看一下思瘟,會發(fā)現(xiàn)鄙币,新創(chuàng)建了一個 0KB 的文件,但本地文件中的數(shù)據(jù)內容就是沒有上傳上來渔伯。
產(chǎn)品要求饶唤,需要有個超時處理,比如上傳工作超過了 30s 就當做上傳失敗光绕,超時處理女嘲。但我明明調用了 FTPClient 的相關超時設置接口,就是沒有一個會生效诞帐。
一句話簡述下上述的場景問題:
網(wǎng)絡限速時欣尼,為何 FTPClient 設置了超時時間,但文件上傳過程中超時機制卻一直沒生效停蕉?
一氣之下愕鼓,干脆跟進 FTPClient 源碼內部,看看為何設置的超時失效了慧起,沒有起作用菇晃。
所以,本篇也就是梳理下 FTPClient 中相關超時接口的含義蚓挤,以及如何處理上述場景中的超時功能磺送。
源碼跟進
先來講講對 FTPClient 的淺入學習過程吧,如果不感興趣灿意,直接跳過該節(jié)册着,看后續(xù)小節(jié)的結論就可以了。
ps:本篇所使用的 commons-net 開源庫版本為 3.6
使用
首先脾歧,先來看看甲捏,使用 FTPClient 上傳文件到 FTP 服務器大概需要哪些步驟:
//1.與 FTP 服務器創(chuàng)建連接
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 中提供的方法付材。
好朦拖,先清楚有這么幾個設置超時的接口存在,后面再從跟進源碼過程中厌衔,一個個來了解它們璧帝。
跟進
1. connect()
那么,就先看看第一步的 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.創(chuàng)建socket
_socket_ = _socketFactory_.createSocket();
//2.設置發(fā)送窗口和接收窗口的緩沖大小
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.連接富寿,這里出現(xiàn) connectTimeout 了
_socket_.connect(new InetSocketAddress(host, port), connectTimeout);
_connectAction_();
}
所以睬隶, FTPClient 調用的 connect()
方法其實是調用父類的方法,這個過程會去創(chuàng)建客戶端 Socket页徐,并和指定的服務端的 ip 和 port 創(chuàng)建連接苏潜,這個過程中,出現(xiàn)了一個 connectTimeout变勇,與之對應的 FTPClient 的超時接口:
//SocketClient#setConnectTimeout()
public void setConnectTimeout(int connectTimeout) {
this.connectTimeout = connectTimeout;
}
至于內部是如何創(chuàng)建計時器恤左,并在超時后是如何拋出 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 與服務端創(chuàng)建連接滤蝠,并設置一個指定的超時時間豌熄,如果超時時間是0,表示超時時間為無窮大物咳,
* 創(chuàng)建連接這個過程會進入阻塞狀態(tài)锣险,直到連接創(chuàng)建成功,或者發(fā)生某個異常錯誤)
* @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()
: 用于設置終端和服務器建立連接這個過程的超時時間压鉴。
還有一點需要注意崖咨,當終端和服務端建立連接這個過程中,當前線程會進入阻塞狀態(tài)油吭,即常說的同步請求操作击蹲,直到連接成功或失敗,后續(xù)代碼才會繼續(xù)進行婉宰。
當連接創(chuàng)建成功后歌豺,會調用 _connectAction_()
,看看:
//SocketClient#_connectAction_()
protected void _connectAction_() throws IOException {
_socket_.setSoTimeout(_timeout_);
//...
}
這里又出現(xiàn)一個 _timeout_ 了心包,看看它對應的 FTPClient 的超時接口:
//SocketClient#setDefaultTimeout()
public void setDefaultTimeout(int timeout){
_timeout_ = timeout;
}
setDefaultTimeout()
:用于當終端與服務端創(chuàng)建完連接后类咧,初步對用于傳輸控制命令的 Socket 調用 setSoTimeout()
設置超時,所以,這個超時具體是何作用痕惋,取決于 Socket 的 setSoTimeout()
区宇。
另外,還記得 FTPClient 也有這么個超時接口么:
//SocketClient#setSoTimeout()
public void setSoTimeout(int timeout) throws SocketException {
_socket_.setSoTimeout(timeout);
}
所以血巍,對于 FTPClient 而言萧锉,setDefaultTimeout()
超時的工作跟 setSoTimeout()
是相同的,區(qū)別僅在于后者會覆蓋掉前者設置的值述寡。
2. login()
接下去看看其他步驟的方法:
//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 主要是發(fā)送 FTP 協(xié)議的一些控制命令,因為連接已經(jīng)創(chuàng)建成功鲫凶,終端發(fā)送的 FTP 控制指令給 FTP 服務器禀崖,完成一些操作,比如登錄螟炫,比如創(chuàng)建目錄波附,進入某個指定路徑等等。
這些步驟過程中昼钻,沒看到跟超時相關的處理掸屡,所以,看看最后一步上傳文件的操作:
3. storeFile
//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. 創(chuàng)建并連接用于傳輸 FTP 數(shù)據(jù)的 Socket
Socket socket = _openDataConnection_(command, remote);
//...
//2. 設置傳輸監(jiān)聽然评,這里出現(xiàn)了一個timeout
CSL csl = null;
if (__controlKeepAliveTimeout > 0) {
csl = new CSL(this, __controlKeepAliveTimeout, __controlKeepAliveReplyTimeout);
}
// Treat everything else as binary for now
try {
//3.開始發(fā)送本地數(shù)據(jù)到FTP服務器
Util.copyStream(local, output, getBufferSize(), CopyStreamEvent.UNKNOWN_STREAM_SIZE, __mergeListeners(csl), false);
}
//...
}
我們在學習 FTP 協(xié)議的端口時仅财,還記得么,通常 20 端口是數(shù)據(jù)端口碗淌,21 端口是控制端口盏求,當然這并不固定。但總體上亿眠,整個過程分兩步:一是先建立用于傳輸控制命令的連接碎罚,二是再建立用于傳輸數(shù)據(jù)的連接。
所以纳像,當調用 _storeFile()
上傳文件時荆烈,會再通過 _openDataConnection_()
創(chuàng)建一個用于傳輸數(shù)據(jù)的 Socket,并與服務端連接竟趾,連接成功后憔购,就會通過 Util 的 copyStream()
將本地文件 copy 到用于傳輸數(shù)據(jù)的這個 Socket 的 OutputStream 輸出流上,此時潭兽,Socket 底層會自動去按照 TCP 協(xié)議往發(fā)送窗口中寫數(shù)據(jù)來發(fā)給服務器倦始。
這個步驟涉及到很多超時處理的地方,所以就來看看山卦,首先是 _openDataConnection_()
:
//FTPClient#_openDataConnection_()
protected Socket _openDataConnection_(String command, String arg) throws IOException {
//...
Socket socket;
//...
//1. 根據(jù)被動模式或主動模式創(chuàng)建不同的 Socket 配置
if (__dataConnectionMode == ACTIVE_LOCAL_DATA_CONNECTION_MODE) {
//...
} else { // We must be in PASSIVE_LOCAL_DATA_CONNECTION_MODE
//...
//2. 我項目中使用的是被動模式鞋邑,所以我只看這個分支了
//3. 創(chuàng)建用于傳輸數(shù)據(jù)的 Socket
socket = _socketFactory_.createSocket();
//...
//4. 對這個傳輸數(shù)據(jù)的 Socket 設置了 SoTimeout 超時
if (__dataTimeout >= 0) {
socket.setSoTimeout(__dataTimeout);
}
//5. 跟服務端建立連接诵次,指定超時處理
socket.connect(new InetSocketAddress(__passiveHost, __passivePort), connectTimeout);
//...
}
//...
return socket;
}
所以,創(chuàng)建用于傳輸數(shù)據(jù)的 Socket 跟傳輸控制命令的 Socket 區(qū)別不是很大枚碗,當跟服務端建立連接時也都是用的 FTPClient 的 setConnectTimeout()
設置的超時時間處理逾一。
有點區(qū)別的地方在于,傳輸控制命令的 Socket 是當在與服務端建立完連接后才會去設置 Socket 的 SoTimeout肮雨,而這個超時時間則來自于調用 FTPClient 的 setDefaultTimeout()
遵堵,和 setSoTimeout()
,后者設置的值優(yōu)先怨规。
而傳輸數(shù)據(jù)的 Socket 則是在與服務端建立連接之前就設置了 Socket 的 SoTimeout陌宿,超時時間值來自于 FTPClient 的 setDataTimeout()
。
那么波丰,setDataTimeout()
也清楚一半了壳坪,設置用于傳輸數(shù)據(jù)的 Socket 的 SoTimeout 值。
所以掰烟,只要能搞清楚爽蝴,Socket 的 setSoTimeout()
超時究竟指的是對哪個工作過程的超時處理,那么就能夠理清楚 FTPClient 的這些超時接口的用途:setDefaultTimeout()
纫骑,setSoTimeout()
蝎亚,setDataTimeout()
。
這個先放一邊先馆,繼續(xù)看 _storeFile()
流程的第二步:
//FTPClient#_storeFile()
protected boolean _storeFile(String command, String remote, InputStream local) throws IOException {
//...
//2. 設置傳輸監(jiān)聽
CSL csl = null;
if (__controlKeepAliveTimeout > 0) {
csl = new CSL(this, __controlKeepAliveTimeout, __controlKeepAliveReplyTimeout);
}
// Treat everything else as binary for now
try {
//3.開始發(fā)送本地數(shù)據(jù)到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 發(fā)送一個無任何操作的 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 是監(jiān)聽 copyStream()
這個過程的缤底,因為本地文件要上傳到服務器顾患,首先番捂,需要先讀取本地文件的內容,然后寫入到傳輸數(shù)據(jù)的 Socket 的輸出流中江解,這個過程不可能是一次性完成的设预,肯定是每次讀取一些、寫一些犁河,默認每次是讀取 1KB鳖枕,可配置。而 Socket 的輸出流緩沖區(qū)也不可能可以一直往里寫的桨螺,它有一個大小限制宾符。底層的具體實現(xiàn)其實也就是 TCP 的發(fā)送窗口,那么這個窗口中的數(shù)據(jù)自然需要在接收到服務器的 ACK 確認報文后才會清空灭翔,騰出位置以便可以繼續(xù)寫入魏烫。
所以,copyStream()
是一個會進入阻塞的操作,因為需要取決于網(wǎng)絡狀況哄褒。而 setControlKeepAliveTimeout()
方法命名中雖然帶有 timeout 關鍵字稀蟋,但實際上它的用途并不是用于處理傳輸超時工作的。它的用途呐赡,其實將方法的命名翻譯下就是了:
setControlKeepAliveTimeout()
:用于設置傳輸控制命令的 Socket 的 alive 狀態(tài)退客,注意單位為 s。
因為 FTP 上傳文件過程中链嘀,需要用到兩個 Socket萌狂,一個用于傳輸控制命令,一個用于傳輸數(shù)據(jù)怀泊,那當處于傳輸數(shù)據(jù)過程中時粥脚,傳輸控制命令的 Socket 會處于空閑狀態(tài),有些路由器可能監(jiān)控到這個 Socket 連接處于空閑狀態(tài)超過一定時間包个,會進行一些斷開等操作刷允。所以,在傳輸過程中碧囊,每讀取一次本地文件树灶,傳輸數(shù)據(jù)的 Socket 每要發(fā)送一次報文給服務端時,根據(jù) setControlKeepAliveTimeout()
設置的時間閾值糯而,來讓傳輸控制命令的 Socket 也發(fā)送一個無任何操作的命令 NOOP天通,以便讓路由器以為這個 Socket 也處于工作狀態(tài)。這些就是 bytesTransferred()
方法中的代碼干的事熄驼。
setControlKeepAliveReplyTimeout()
:這個只有在調用了 setControlKeepAliveTimeout()
方法像寒,并傳入一個大于 0 的值后,才會生效瓜贾,用于在 FTP 傳輸數(shù)據(jù)這個過程诺祸,對傳輸控制命令的 Socket 設置 SoTimeout,這個傳輸過程結束后會恢復傳輸控制命令的 Socket 原本的 SoTimeout 配置祭芦。
那么筷笨,到這里可以稍微來小結一下:
FTPClient 一共有 6 個用于設置超時的接口,而終端與 FTP 通信過程會創(chuàng)建兩個 Socket龟劲,一個用于傳輸控制命令胃夏,一個用于傳輸數(shù)據(jù)。這 6 個超時接口與兩個 Socket 之間的關系:
setConnectTimeout()
:用于設置兩個 Socket 與服務器建立連接這個過程的超時時間昌跌,單位 ms仰禀。
setDefaultTimeout()
:用于設置傳輸控制命令的 Socket 的 SoTimeout,單位 ms蚕愤。
setSoTimeout()
:用于設置傳輸控制命令的 Socket 的 SoTimeout答恶,單位 ms囊榜,值會覆蓋上個方法設置的值。
setDataTimeout()
:被動模式下亥宿,用于設置傳輸數(shù)據(jù)的 Socket 的 SoTimeout卸勺,單位 ms。
setControlKeepAliveTimeout()
:用于在傳輸數(shù)據(jù)過程中烫扼,也可以讓傳輸控制命令的 Socket 假裝保持處于工作狀態(tài)曙求,防止被路由器干掉,注意單位是 s映企。
setControlKeepAliveReplyTimeout()
:只有調用上個方法后悟狱,該方法才能生效,用于設置在傳輸數(shù)據(jù)這個過程中堰氓,暫時替換掉傳輸控制命令的 Socket 的 SoTimeout挤渐,傳輸過程結束恢復這個 Socket 原本的 SoTimeout。
4. 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 輸入流中
* 讀取數(shù)據(jù)的過程中软免,如果線程進入了阻塞狀態(tài),那么這次阻塞的過程耗費的時間如果
* 超過了設置的超時時間焚挠,就會拋出一個 SocketTimeoutException 異常膏萧,但只是將
* 線程從讀數(shù)據(jù)這個過程中斷掉,并不影響 Socket 的后續(xù)使用蝌衔。
* 如果超時時間為0榛泛,表示無限長。)
* (注意噩斟,并不是讀取輸入流的整個過程的超時時間曹锨,而僅僅是每一次進入阻塞等待輸入流中
* 有數(shù)據(jù)可讀的超時時間)
* @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 那些會陷入阻塞的操作之前才能生效艘希,
* 當超時時間到了硼身,而當前還處于阻塞狀態(tài)硅急,那么會拋出一個異常,但此時 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 誤用的說明荚板,文中有一句解釋:
讀取數(shù)據(jù)時阻塞鏈路的超時時間
我再基于他的基礎上理解一波凤壁,我覺得他這句話中有兩個重點,一是:讀取跪另,二是:阻塞拧抖。
這兩個重點是理解 SoTimeout 超時機制的關鍵,就像那篇文中所說免绿,很多人將 SoTimeout 理解成鏈路的超時時間唧席,或者這一次傳輸過程的總超時時間,但這種理解是錯誤的嘲驾。
第一點淌哟,SoTimeout 并不是傳輸過程的總超時時間,不管是上傳文件還是下載文件辽故,服務端和終端肯定是要分多次報文傳輸?shù)耐讲郑覍?SoTimeout 的理解是,它是針對每一次的報文傳輸過程而已誊垢,而不是總的傳輸過程掉弛。
第二點,SoTimeout 只針對從 Socket 輸入流中讀取數(shù)據(jù)的操作喂走。什么意思狰晚,如果是終端下載 FTP 服務器的文件,那么服務端會往終端的 Socket 的輸入流中寫數(shù)據(jù)缴啡,如果終端接收到了這些數(shù)據(jù)壁晒,那么 FTPClient 就可以去這個 Socket 的輸入流中讀取數(shù)據(jù)寫入到本地文件的輸出流。而如果反過來业栅,終端上傳文件到 FTP 服務器秒咐,那么 FTPClient 是讀取本地文件寫入終端的 Socket 的輸出流中發(fā)送給終端,這時就不是對 Socket 的輸入流操作了碘裕。
總之携取,setSoTimeout()
用于設置從 Socket 的輸入流中讀取數(shù)據(jù)時每次陷入阻塞過程的超時時間。
那么帮孔,在 FTPClient 中雷滋,所對應的就是,setSoTimeout()
對下述方法有效:
retrieveFile()
retrieveFileStream()
相反的文兢,下述這些方法就無效了:
storeFile()
storeFileStream()
這樣就可以解釋得通晤斩,開頭我所提的問題了,在網(wǎng)絡被限速之下姆坚,由于 sotreFile()
會陷入阻塞澳泵,并且設置的 setDataTimeout()
超時由于這是一個上傳文件的操作,不是對 Socket 的輸入流的讀取操作兼呵,所以無效兔辅。所以腊敲,也才會出現(xiàn)線程進入阻塞狀態(tài),后續(xù)代碼一直得不到執(zhí)行维苔,UI 層遲遲接收不到上傳成功與否的回調通知碰辅。
最后我的處理是,在業(yè)務層面介时,自己寫了超時處理乎赴。
注意,以上分析的場景是:FTP 被動模式的上傳文件的場景下潮尝,相關接口的超時處理榕吼。所以很多表述都是基于這個場景的前提下,有一些源碼勉失,如 Util 的 copyStream()
不僅在文件上傳中使用羹蚣,在下載 FTP 上的文件時也同樣使用,所以對于文件上傳來說乱凿,這方法就是用來讀取本地文件寫入傳輸數(shù)據(jù)的 Socket 的輸出流顽素;而對于下載 FTP 文件的場景來說,這方法的作用就是用于讀取傳輸數(shù)據(jù)的 Socket 的輸入流徒蟆,寫入到本地文件的輸出流中胁出。以此類推。
結論
總結來說段审,如果是對于網(wǎng)絡開發(fā)這方面領域內的來說全蝶,這些超時接口的用途應該都是基礎,但對于我們這些很少接觸 Socket 的來說寺枉,如果單憑接口注釋文檔無法理解的話抑淫,那可以嘗試翻閱下源碼,理解下姥闪。
梳理之后始苇,F(xiàn)TPClient 一共有 6 個設置超時的接口,而不管是文件上傳或下載筐喳,這過程催式,F(xiàn)TP 都會創(chuàng)建兩個 Socket,一個用于傳輸控制命令避归,一個用于傳輸文件數(shù)據(jù)荣月,超時接口和這兩個 Socket 之間的關系如下:
-
setConnectTimeout()
用于設置終端 Socket 與 FTP 服務器建立連接這個過程的超時時間。 -
setDefaultTimeout()
用于設置終端的傳輸控制命令的 Socket 的 SoTimeout槐脏,即針對傳輸控制命令的 Socket 的輸入流做讀取操作時每次陷入阻塞的超時時間喉童。 -
setSoTimeout()
作用跟上個方法一樣,區(qū)別僅在于該方法設置的超時會覆蓋掉上個方法設置的值顿天。 -
setDataTimeout()
用于設置終端的傳輸數(shù)據(jù)的 Socket 的 Sotimeout堂氯,即針對傳輸文件數(shù)據(jù)的 Socket 的輸入流做讀取操作時每次陷入阻塞的超時時間。 -
setControlKeepAliveTimeout()
用于設置當處于傳輸數(shù)據(jù)過程中牌废,按指定的時間閾值定期讓傳輸控制命令的 Socket 發(fā)送一個無操作命令 NOOP 給服務器咽白,讓它 keep alive。 -
setControlKeepAliveReplyTimeout()
:只有調用上個方法后鸟缕,該方法才能生效晶框,用于設置在傳輸數(shù)據(jù)這個過程中,暫時替換掉傳輸控制命令的 Socket 的 SoTimeout懂从,傳輸過程結束恢復這個 Socket 原本的 SoTimeout授段。
超時接口大概的用途明確了,那么再稍微來講講該怎么用:
針對使用 FTPClient 下載 FTP 文件番甩,一般只需使用兩個超時接口侵贵,一個是 setConnectTimeout()
,用于設置建立連接過程中的超時處理缘薛,而另一個則是 setDataTimeout()
窍育,用于設置下載 FTP 文件過程中的超時處理。
針對使用 FTPClient 上傳文件到 FTP 服務器宴胧,建立連接的超時同樣需要使用 setConnectTimeout()
漱抓,但文件上傳過程中,建議自行利用 Android 的 Handler 或其他機制實現(xiàn)超時處理恕齐,因為 setDataTimeout()
這個設置對上傳的過程無效乞娄。
另外,使用 setDataTimeout()
時需要注意显歧,這個超時不是指下載文件整個過程的超時處理补胚,而是僅針對終端 Socket 從輸入流中,每一次可進行讀取操作之前陷入阻塞的超時追迟。
以上溶其,是我所碰到的問題,及梳理的結論敦间,我只以我所遇的現(xiàn)象來理解瓶逃,因為我對網(wǎng)絡編程,對 Socket 不熟廓块,如果有錯誤的地方厢绝,歡迎指證一下。
常見異常
最后附上 FTPClient 文件上傳過程中带猴,常見的一些異常昔汉,便于針對性的進行分析:
1.storeFile() 上傳文件超時,該超時時間由 Linux 系統(tǒng)規(guī)定
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拴清。
可能的場景:由于網(wǎng)絡被限速 1KB/S靶病,終端的 Socket 發(fā)給服務端的報文一直收不到 ACK 確認報文(原因不懂)会通,導致發(fā)送緩沖區(qū)一直處于滿的狀態(tài),導致 FTPClient 的 storeFile()
一直陷入阻塞娄周。而如果一個 Socket 一直處于阻塞狀態(tài)涕侈,TCP 的 keeplive 機制通常會每隔 75s 發(fā)送一次探測包,一共 9 次煤辨,如果都沒有回應裳涛,則會拋出如上異常。
可能還有其他場景众辨,上述場景是我所碰到的端三,F(xiàn)TPClient 的 setDataTimeout()
設置了超時,但沒生效鹃彻,原因上述已經(jīng)分析過了郊闯,最后過了十來分鐘自己拋了超時異常,至于為什么會拋了一次浮声,看了下篇文章里的分析虚婿,感覺對得上我這種場景。
具體原理參數(shù):淺談TCP/IP網(wǎng)絡編程中socket的行為
2. retrieveFile 下載文件超時
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)
分析:該異常注意跟第一種場景的異常區(qū)分開泳挥,注意看異常棧中的第一個異常信息然痊,這里是由于 read 過程的超時而拋出的異常,而這個超時就是對 Socket 設置了 setSoTimeout()
屉符,歸根到 FTPClient 的話剧浸,就是調用了 setDataTimeout()
設置了傳輸數(shù)據(jù)用的 Socket 的 SoTimeout,由于是文件下載操作矗钟,是對 Socket 的輸入流進行的操作唆香,所以這個超時機制可以正常運行。
2. 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 在創(chuàng)建連接時超時的異常吨艇,通常是 TCP 的三次握手躬它,這個連接對應著 FTPClient 的 connect()
方法,其實關鍵是 Socket 的 connect()
方法东涡,在 FTPClient 的 stroreFile()
方法內部由于需要創(chuàng)建用于傳輸?shù)?Socket冯吓,也會有這個異常出現(xiàn)的可能。
另外疮跑,這個超時時長的設置由 FTPClient 的 setConnectTimeout()
決定组贺。
3. 其他 TCP 錯誤
參考:TCP/IP錯誤列表 ,下面是部分截圖:
大家好祖娘,我是 dasu失尖,歡迎關注我的公眾號(dasuAndroidTv),如果你覺得本篇內容有幫助到你,可以轉載但記得要關注掀潮,要標明原文哦菇夸,謝謝支持~