之前發過片段的內容,都沒給出完整的項目代碼,不少人來要,故整理下,做個完整的演示項目出來,花了4天的時間調試,真要命。另外如果是IIS承載的WCF服務,建議直接寫一個繼承IHttpHandler的類來實現,那樣效率更高,控制更精細,用WCF服務來做僅僅是在不使用IIS的情況下的妥協方法。注:必須安裝.NET4.0 以上版本
先簡單介紹下該程序實現的功能:
一個服務端,使用WCF自承載,默認采用了REST模式,直接提供了HTTP下載,也可以開啟NET TCP等其它模式。HTTP下載時,IE是不支持斷點續傳的,下載軟件中,只有網絡傳送帶支持斷點續傳,其它的軟件都不支持,主要是我的WCF服務采用了流模式傳輸,客戶端軟件獲取不到要接收的數據總長度,以為不支持斷點續傳,故而直接不考慮續傳,但是網絡傳送帶就不同,它能夠繼續嘗試續傳請求,看服務端如何響應,因此只有網絡傳送帶支持斷點續傳。為了解決下載工具不支持斷點續傳的情況,我自己在客戶端里增加了一個HTTP下載方式,輸入下載地址,即可開始下載,中間暫停后還可以恢復,從斷點處恢復。另外提供直接程序下載方式,那個功能比較簡單,同時客戶端無法中斷下載,除非網絡異常造成的中斷,一旦中斷可以重試,繼續從斷點處續傳,但是手動中斷不可以,這個是WCF消息機制密封裝導致的,我們無法主動打斷消息的傳輸(文件下載就是在接收消息)。
這是服務端界面

點擊“開啟服務”后,按鈕變為“正在運行”,此時服務打開,服務監聽端口默認為12251,如果不想用這個端口,可以打開源碼重新編譯,在Form1里面有個baseAddress變量,修改那個即可,也可以設置到配置文件中,演示項目我就不搞那么麻煩了。
服務開啟后,要先點擊“文件目錄” ,將當前提供文件下載服務的目錄設置下,那個當前目錄會顯示當前設置的位置。然后點擊“產生鏈接”,這是會出現文件列表,只有一層,未做遞歸處理。只有產生了鏈接后的文件,才能被下載到,否則都是非法訪問,不給與下載,保證了系統安全。點擊“復制選中鏈接”,你可以直接把下載鏈接復制到剪貼板,可以直接從IE下載試試效果了。
那個HTTP幫助的鏈接,點擊后可以看到服務的詳細調用說明,如果要改變幫助,可以修改config文件。
這是客戶端界面:

服務地址輸入后,點擊“獲取下載列表 ”,一旦獲取成功,服務地址將不可改變。此時直接用“下載選中文件”功能,則通過WCF的接口函數直接下載文件,和底層傳輸協議無關。如果先“復制選中鏈接”,然后“打開HTTP下載窗口”,則通過HTTP地址下載文件,此時是通過WCF提供的RESF服務進行的下載。
整個演示項目涉及到的知識點很多,故而做了很長時間,下面簡單說明下項目中涉及到的幾個要點。
一、消息的流封裝。
以前曾經寫過一篇隨筆,提到自定義文件流,現在考慮得更加成熟了,使用自定義讀取流,可以對任何流進行封裝,比如內存流,因為可能我們要傳輸的內容是要進行預處理的,一邊處理,一邊傳輸,這樣就必須對內存流進行封裝傳輸。
自定義讀取流(只讀)
/// 自定義讀取流(只讀)
/// </summary>
internal class CusStreamReader : Stream
{
long _endPosition; // 結束位置
Stream innerStream;
/// <summary>
/// 參數為當前流的斷點
/// </summary>
public event Action< long> Reading;
/// <summary>
/// 直接使用原始流。
/// </summary>
/// <param name="stream"> 原始流 </param>
public CusStreamReader(Stream stream)
{
this.innerStream = stream;
_endPosition = stream.Length;
}
/// <summary>
/// 使用流當前位置,指定長度初始化自定義流
/// </summary>
/// <param name="stream"> 原始流 </param>
/// <param name="count"> 使用長度 </param>
public CusStreamReader(Stream stream, long count)
{
this.innerStream = stream;
_endPosition = stream.Position + count;
if (_endPosition > stream.Length)
_endPosition = stream.Length;
}
/// <summary>
/// 指定初始位置、長度初始化自定義流
/// </summary>
/// <param name="stream"> 原始流 </param>
/// <param name="offset"> 初始位置 </param>
/// <param name="count"> 使用長度 </param>
public CusStreamReader(Stream stream, long offset, long count)
{
stream.Position = offset > stream.Length ? stream.Length : offset;
this.innerStream = stream;
_endPosition = offset + count;
if (_endPosition > stream.Length)
_endPosition = stream.Length;
}
/// <summary>
/// 從自定義流讀取指定長度到array,但是不超過初始化時設定的長度。
/// </summary>
/// <returns> 讀取的字節數 </returns>
public override int Read( byte[] array, int offset, int count)
{
int readcount = 0;
if (Position + count > this._endPosition)
readcount = innerStream.Read(array, offset, ( int)( this._endPosition - Position));
else
readcount = innerStream.Read(array, offset, count);
if (Reading != null)
Reading(Position);
return readcount;
}
/// <summary>
/// 從自定義流讀取一個字節,但是不超過初始化時設定的長度。
/// </summary>
/// <returns> 讀取的字節,未找到則返回-1 </returns>
public override int ReadByte()
{
if (Position >= this._endPosition)
return - 1;
else
return base.ReadByte();
}
public override bool CanRead
{
get { return innerStream.CanRead; }
}
public override bool CanSeek
{
get { return false; }
}
public override bool CanWrite
{
get { return false; }
}
public override void Flush()
{
throw new NotImplementedException();
}
/// <summary>
/// 自定義流剩余長度。
/// </summary>
public override long Length
{
get { return _endPosition - innerStream.Position; }
}
/// <summary>
/// 自定義流位置,返回原始流的位置
/// </summary>
public override long Position
{
get
{
return innerStream.Position;
}
set
{
throw new NotImplementedException();
}
}
public override long Seek( long offset, SeekOrigin origin)
{
throw new NotImplementedException();
}
public override void SetLength( long value)
{
throw new NotImplementedException();
}
public override void Write( byte[] buffer, int offset, int count)
{
throw new NotImplementedException();
}
}
調用代碼
fs.Reading += (t) =>
{
// 限速代碼,實際使用時可以去掉,或者精確控制
Thread.Sleep( 300);
Console.WriteLine(t);
};
這里要特別說明的是這個Reading事件,非常重要。我們既可以利用它來限速,也可以用它來顯示傳輸的狀態,顯示當前傳了多少字節了。
二、異常處理 。
對於非法請求,必須返回錯誤異常,而WCF默認的錯誤異常處理和HTTP的不一樣,如果用HttpWebRequest來請求下載,錯誤異常是不能直接捕獲到的,查閱了相關資料后發現,要捕獲類型為WebException的異常,然后特殊處理。.NET4.0里面,可以直接拋出WebFaultException<T>類型的異常,這樣HttpWebRequest請求時可以很方便處理異常信息,但是這又帶來一個問題,如果同時支持ServiceModel里訪問,這個WebFaultException異常將獲取不到內容,只能得到錯誤代碼如(404),顯然這樣是不行的,反復調試后發現,服務端應該拋出FaultException異常,這樣ServiceModel里訪問可以直接捕獲到異常,HttpWebRequest也可以分析出錯誤信息,核心代碼如下:
WEB請求時的異常捕獲
{
}
catch (WebException ex)
{
var errResp = ex.Response as HttpWebResponse;
using ( var stream = errResp.GetResponseStream())
{
string message = null;
using ( var sr = new StreamReader(stream))
{
message = sr.ReadToEnd();
var match = Regex.Match(message, " (?is)(?<=<Text[^>]*>).*(?=</Text>) ");
if (match.Success)
{
message = match.Value;
}
}
MessageBox.Show(message);
}
}
其它的就不多說了,自己看代碼,雖然只是演示項目,但是涉及到的知識點很多。
