幾天前我寫了一篇Post介紹如何用System.Net.FtpWebRequest開發一個支援續傳功能的FTP Client。

在專案中開始使用它來傳大檔時,卻發現不知FtpWebRequest是不是為了炫耀它的續傳功能,在花了半小時傳完一個400MB的ZIP檔之後,都會觸發一個"The underlying connection was closed: An unexpected error occurred on a receive."的Exception,但是檔案的大小正確,代表每一個Byte都順利傳回來了,但就是逼得我得Run第二次"續傳餘下的0 Byte",程式才會正常結束。

同樣的程式若下載的是5MB的ZIP檔,則不會發生問題。因為"Size Matters",所以我推斷可能可能與Timeout之類的屬性有關。雖然發現FtpWebRequest.Timeout官方文件寫錯了,預設值是100,000ms而不是無限大,但是測試發現它並不是導致問題的原因。

後來想到另一件事,傳統的FTP軟體的訊息視窗中總會看到定期送出NOOP指令來保持連線不被Server切斷(依據這篇文件,一般的FTP Server,只要5分鐘沒收到Control Connection傳來的任何訊息,就會中斷連線),而FtpWebRequest並沒有實做這類的機制。因此,只要下載時間超過FTP Server的Idle Timeout,在Stream.Close()會觸發檢查Connection的Logic,雖然資料已順利傳完,但由於Control Connection已經Idle過久被切了,於是拋出以下錯誤:

Error: The underlying connection was closed: An unexpected error occurred on a receive.
   at System.Net.FtpWebRequest.SyncRequestCallback(Object obj)
   at System.Net.FtpWebRequest.RequestCallback(Object obj)
   at System.Net.CommandStream.Abort(Exception e)
   at System.Net.CommandStream.CheckContinuePipeline()
   at System.Net.FtpWebRequest.DataStreamClosed(CloseExState closeState)
   at System.Net.FtpDataStream.System.Net.ICloseEx.CloseEx(CloseExState closeState)
   at System.Net.FtpDataStream.Dispose(Boolean disposing)
   at System.IO.Stream.Close()

事到如今,要解決這個問題有幾種手段:

  1. 去找有實作NOOP Keep Alive機制的3rd Party .NET Component,如: Rebex FTP for .NET,但需考量採購成本
  2. 自己實作NOOP機制,但需要處理複雜的Control Connection與Data Connection問題,有陷入"重新發明輪子"迷思的疑慮,還不如去買現有元件
  3. 由於資料已順利傳完,實際上我們已不再需要Control Connection,而是Stream.Close()時所觸發的制式檢查,那麼忽略Connection已斷的事實又何妨?
    雖說將Exception納為正常流程有違反一般的效能準則,但進入Exception流程所多出的時間應該不及1ms,相較於超過5分鐘的下載時間,倒是可以被忽略。

偷懶的我決定用方法3),將先前Post程式的片段修改如下:

//2007-06-27 因為沒有NOOP的Keep Alive機制
//當下載時間超過FTP Server的Idel限制,會在Stream.Close()時
//發生以下錯誤:
//The underlying connection was closed: 
//An unexpected error occurred on a receive.
//若檔案已順利下載完成,則忽略此一WebException
bool bFinish = false;
 
try
{
    using (Stream stm = ftpResp.GetResponseStream())
    {
        //由於檔案頗大,因此分Block寫入
        byte[] buff = new byte[2048];
        int len = 0;
        //取得要下載的檔案大小
        long totalSize = ftpResp.ContentLength;
        while (fs.Length < totalSize)
        {
            len = stm.Read(buff, 0, buff.Length);
            fs.Write(buff, 0, len);
        }
        fs.Flush();
        //檔案完整傳完, bFinish=true
        bFinish = (fs.Length == totalSize);
        fs.Close();
        //下載時間過久時,stm.Close()會觸發Exception
        stm.Close();
    }
}
catch (System.Net.WebException we)
{
    //若未傳完才要觸發Exception
    if (!bFinish) throw we;
}
ftpResp.Close();

Comments

# by SGY

long totalSize = ftpResp.ContentLength; 我用XP SP2 Pro +IIS 6(FTP) VS2005 C# 2.0 測試 ftpResp.ContentLength; 回 -1 導致 while (fs.Length < totalSize) 離開無法下載 別人的Code 可以下載 public void Download( ) { FtpWebRequest reqFTP; try { FileStream outputStream = new FileStream(@"c:\aa.wmv", FileMode.Create); reqFTP = (FtpWebRequest)FtpWebRequest.Create(new Uri("ftp://127.0.0.1/media/MOVIE/Amazing_Caves_1080.wmv")); reqFTP.Method = WebRequestMethods.Ftp.DownloadFile; reqFTP.UseBinary = true; reqFTP.Credentials = new NetworkCredential("rb", "rb"); FtpWebResponse response = (FtpWebResponse)reqFTP.GetResponse(); Stream ftpStream = response.GetResponseStream(); long cl = response.ContentLength; int bufferSize = 2048; int readCount; byte[] buffer = new byte[bufferSize]; readCount = ftpStream.Read(buffer, 0, bufferSize); while (readCount > 0) { outputStream.Write(buffer, 0, readCount); readCount = ftpStream.Read(buffer, 0, bufferSize); } ftpStream.Close(); outputStream.Close(); response.Close(); } catch (Exception ex) { Console.WriteLine(ex.Message); } } 問題是為什麼會回-1 ?

# by Jeffrey

To SGY, 依據MSDN Library裡的說明(http://tinyurl.com/6znqtq),"當 FTP 伺服器傳回回應資料流時,ContentLength 屬性會包含資料流中的位元組數。如果回應中未傳回任何資料,或伺服器未傳送內容長度資訊,則 ContentLength 會傳回 -1。" 我想IIS裡的FTP Server應該不致不支援ContentLength,而其他的Code可以跑,應該也不是回應中未傳回任何資料。我懷疑會不會這反而是IIS FTP Server為了配合*.wmv可串流播放的特性才故意做的調整。 建議你做個測試,用同樣的Code改下載一般的txt, zip,是否ContentLength仍等於-1? 如果有結果也讓大家知一下

# by SGY

我用另一個URL (對方不是IISFTP) ftp://ftpsv.cwb.gov.tw/pub/forecast/W002.txt 可回傳正確長度 本機 :驗測結果 如xml File還是回-1 解決: 可能策略需改變 先取得 WebRequestMethods.Ftp.GetFileSize; 可回傳正確FileSize 再作 WebRequestMethods.Ftp.DownloadFile; 另Rebex FTP for .NET Demo FTP 似乎都很正常的運作(來源在我的本機,我懷疑它也是先要FileSize,不然ProcessBar 應該無法計算進度)

# by SGY

(IIS 6)我下載不會有此例外 (下載約30min) catch (System.Net.WebException we){ //若未傳完才要觸發Exception if (!bFinish) throw we; } (IIS 6)另一定要先要 FileSize , 不然會回-1 如上是我驗測後的結果 還有一點 ,FTP 連線會共用 不管你下多少Request. 關掉AP FTP 的 Session 才會斷 不知板主用的TEST FTP Server是那個? 關於續傳 : 我會先下載至Tmp file Check SERVER FileDateTime 與Local FileDateTime 與Size 不同才會下載 下載會在更改LocalFile Lastwrite DateTime 至於(temp-->LocalFile)File Copy 若目標File 在使用時Copy File會失敗 失敗 後:採用MovefileEX API(Reboot 會自動班你更版) <---不知.NET 是否有相對的API 這樣fileUpdate就很完善了 此為個人見解

# by Jeffrey

To SGY, 謝謝你提供這麼詳盡的測試心得。 我當初測試的FTP Server是廠商提供資訊下載的FTP主機,看起來像是UNIX。由你我的測試結果來看,看來各家(甚至各版本)FTP Server的行為差異頗大(若這是個大困擾,或許一些適應力強的元件就值得花錢買了),晚點有時間我再來研究一下IIS FTP不能傳回ContentLength的原因。謝謝囉!

# by LittleMonkey

請問一下...若為Upload的話..該怎改呢? 因我也是用FtpWebRequest設計,也一樣有WebException的問題 >< Please..help me!! Thank you!!

# by DeltaCat

我的想法: FTP是依託於TCP協議,任何一次簡單的請求,都需要經過三次握手的確認,非常的啰嗦。即使是發送NoOP命令,也必定是要經過這個過程的。在FTP空閒時,執行NoOP的動作相對簡單,但是在文件transfering過程中,如果要執行NoOP動作,則必須重新開一個到FTP服務器的連接,或者暫停當前的傳輸,然後才能執行NOOP。 既然這樣,雖然MS沒有提供NOOP的功能,我們自己是否可以變相的實現。實現的方案如下: 因為FtpWebRequest是重複利用其Session的,所以,在下載大文件的過程中,我們可以起一個thread,這個thread定時執行針對正在下載的文件的 WebRequestMethods.Ftp.GetDateTimestamp 動作,來起到NOOP的作用。 此方法我沒有實際去測試,歡迎探討。

# by VanHazard

我也遇到 SGY 本機FTP的問題,客戶要求我一定要解決...~"~ 不論本機傳送任何檔案,ContentLength都是傳回 -1 這個問題困擾很久,後來改為 Active Mode 就解決了, ftpReq.UsePassive = false; 應該是 IIS 本身的問題,測試過 XP、2003 都一樣, 提供各位參考

Post a comment