記錄 FTPClient 超時處理的相關問題

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 服務器的步驟就是這么幾個钦奋。

既然本篇主要是想理清超時為何沒生效,那么也就先來看看都有哪些設置超時的接口:

setTimeout

粗體字是 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錯誤列表 ,下面是部分截圖:

常見錯誤.png

大家好祖娘,我是 dasu失尖,歡迎關注我的公眾號(dasuAndroidTv),如果你覺得本篇內容有幫助到你,可以轉載但記得要關注掀潮,要標明原文哦菇夸,謝謝支持~


dasuAndroidTv2.png
?著作權歸作者所有,轉載或內容合作請聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市胧辽,隨后出現(xiàn)的幾起案子峻仇,更是在濱河造成了極大的恐慌公黑,老刑警劉巖邑商,帶你破解...
    沈念sama閱讀 216,544評論 6 501
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場離奇詭異凡蚜,居然都是意外死亡人断,警方通過查閱死者的電腦和手機,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 92,430評論 3 392
  • 文/潘曉璐 我一進店門朝蜘,熙熙樓的掌柜王于貴愁眉苦臉地迎上來恶迈,“玉大人,你說我怎么就攤上這事谱醇∠局伲” “怎么了?”我有些...
    開封第一講書人閱讀 162,764評論 0 353
  • 文/不壞的土叔 我叫張陵副渴,是天一觀的道長奈附。 經(jīng)常有香客問我,道長煮剧,這世上最難降的妖魔是什么斥滤? 我笑而不...
    開封第一講書人閱讀 58,193評論 1 292
  • 正文 為了忘掉前任,我火速辦了婚禮勉盅,結果婚禮上佑颇,老公的妹妹穿的比我還像新娘。我一直安慰自己草娜,他們只是感情好挑胸,可當我...
    茶點故事閱讀 67,216評論 6 388
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著宰闰,像睡著了一般茬贵。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上议蟆,一...
    開封第一講書人閱讀 51,182評論 1 299
  • 那天闷沥,我揣著相機與錄音,去河邊找鬼咐容。 笑死舆逃,一個胖子當著我的面吹牛,可吹牛的內容都是我干的。 我是一名探鬼主播路狮,決...
    沈念sama閱讀 40,063評論 3 418
  • 文/蒼蘭香墨 我猛地睜開眼虫啥,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了奄妨?” 一聲冷哼從身側響起涂籽,我...
    開封第一講書人閱讀 38,917評論 0 274
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎砸抛,沒想到半個月后评雌,有當?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 45,329評論 1 310
  • 正文 獨居荒郊野嶺守林人離奇死亡直焙,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內容為張勛視角 年9月15日...
    茶點故事閱讀 37,543評論 2 332
  • 正文 我和宋清朗相戀三年景东,在試婚紗的時候發(fā)現(xiàn)自己被綠了。 大學時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片奔誓。...
    茶點故事閱讀 39,722評論 1 348
  • 序言:一個原本活蹦亂跳的男人離奇死亡斤吐,死狀恐怖,靈堂內的尸體忽然破棺而出厨喂,到底是詐尸還是另有隱情和措,我是刑警寧澤,帶...
    沈念sama閱讀 35,425評論 5 343
  • 正文 年R本政府宣布蜕煌,位于F島的核電站派阱,受9級特大地震影響,放射性物質發(fā)生泄漏幌绍。R本人自食惡果不足惜颁褂,卻給世界環(huán)境...
    茶點故事閱讀 41,019評論 3 326
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望傀广。 院中可真熱鬧颁独,春花似錦、人聲如沸伪冰。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,671評論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至,卻和暖如春典予,著一層夾襖步出監(jiān)牢的瞬間辆它,已是汗流浹背彪杉。 一陣腳步聲響...
    開封第一講書人閱讀 32,825評論 1 269
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個月前我還...
    沈念sama閱讀 47,729評論 2 368
  • 正文 我出身青樓隔嫡,卻偏偏與公主長得像甸怕,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子腮恩,可洞房花燭夜當晚...
    茶點故事閱讀 44,614評論 2 353

推薦閱讀更多精彩內容

  • 告子曰:“生之謂性秸滴∥淦酰”這顯然不對,但其也有他本身的道理荡含,此句便涉及一個問題:何謂生之?”就是天生的東西到底...
    墻角天邊閱讀 688評論 0 0
  • 在一個實際需求中咒唆,需要對一批文件(如:文本、圖片)進行重命名内颗,按照數(shù)字編號钧排。正好借此熟悉了一下node的fs文件操...
    bestvist閱讀 817評論 5 2
  • 1.重構對軟件內部結構的一種調整敦腔,目的是在不改變軟件可觀察行為的前提下均澳,提供起可理解性,降低其修改成本符衔。重構是“整...
    薛云龍閱讀 307評論 0 0
  • 1.網(wǎng)盤庫 界面很友好干凈,首頁下方有提示正在熱播的劇集,都是第一時間更新百度云鏈接哦,右上角還有一個"最新分享"...
    theone_閱讀 2,546評論 0 3
  • “羅鍋”的尊嚴 (雜文) 文/金鋆鈴 2008年的8月8日找前,北京奧運會在人們的千呼萬喚中如期舉行。十六天的賽程...
    金鋆鈴閱讀 163評論 2 3