KB-Connection Closed Exception of FtpWebRequest
幾天前我寫了一篇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()
事到如今,要解決這個問題有幾種手段:
- 去找有實作NOOP Keep Alive機制的3rd Party .NET Component,如: Rebex FTP for .NET,但需考量採購成本
- 自己實作NOOP機制,但需要處理複雜的Control Connection與Data Connection問題,有陷入"重新發明輪子"迷思的疑慮,還不如去買現有元件
- 由於資料已順利傳完,實際上我們已不再需要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();