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

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

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

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.建立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() 是相同的,區別僅在於後者會覆蓋掉前者設置的值。

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 主要是發送 FTP 協議的一些控制命令,由於鏈接已經建立成功,終端發送的 FTP 控制指令給 FTP 服務器,完成一些操做,好比登陸,好比建立目錄,進入某個指定路徑等等。

這些步驟過程當中,沒看到跟超時相關的處理,因此,看看最後一步上傳文件的操做:

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. 建立並鏈接用於傳輸 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。

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 輸入流中
     *    讀取數據的過程當中,若是線程進入了阻塞狀態,那麼此次阻塞的過程耗費的時間若是
     *    超過了設置的超時時間,就會拋出一個 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 文件上傳過程當中,常見的一些異常,便於針對性的進行分析:

1.storeFile() 上傳文件超時,該超時時間由 Linux 系統規定

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的行爲

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)

分析:該異常注意跟第一種場景的異常區分開,注意看異常棧中的第一個異常信息,這裏是因爲 read 過程的超時而拋出的異常,而這個超時就是對 Socket 設置了 setSoTimeout(),歸根到 FTPClient 的話,就是調用了 setDataTimeout() 設置了傳輸數據用的 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 在建立鏈接時超時的異常,一般是 TCP 的三次握手,這個鏈接對應着 FTPClient 的 connect() 方法,其實關鍵是 Socket 的 connect() 方法,在 FTPClient 的 stroreFile() 方法內部因爲須要建立用於傳輸的 Socket,也會有這個異常出現的可能。

另外,這個超時時長的設置由 FTPClient 的 setConnectTimeout() 決定。

3. 其餘 TCP 錯誤

參考:TCP/IP錯誤列表 ,下面是部分截圖:

常見錯誤.png


你們好,我是 dasu,歡迎關注個人公衆號(dasuAndroidTv),若是你以爲本篇內容有幫助到你,能夠轉載但記得要關注,要標明原文哦,謝謝支持~
dasuAndroidTv2.png

相關文章
相關標籤/搜索