很多人都有過使用網路螞蟻或網路快車軟體下載互連網檔案的經曆,這些軟體的使用可以大大加速互連網上檔案的傳輸速度,減少檔案傳輸的時間。這些軟體為什麼有如此大的魔力呢?其主要原因是這些軟體都採用了多線程下載和斷點續傳技術。如果我們自己來編寫一個類似這樣的程式,也能夠快速的在互連網上下載檔案,那一定是非常愉快的事情。下面我就講一講如何利用C#語言編寫一個支援多線程下載檔案的程式,你會看到利用C#語言編寫網路應程式是多麼的容易,從中也能體會到C#語言中強大的網路功能。
首先介紹一下HTTP協議,HTTP亦即Hpyer Text Transfer Protocal的縮寫,它是現代互連網上最重要的一種網路通訊協定,超文字傳輸通訊協定 (HTTP)位於TCP/IP協議的應用程式層,是一個面向無串連、簡單、快速的C/S結構的協議。HTTP的工作過程大體上分串連、請求、響應和中斷連線四個步驟。C#語言對HTTP協議提供了良好的支援,在.NET類庫中提供了WebRequest和WebResponse類,這兩個類都包含在System.Net命名空間中,利用這兩個類可以實現很多進階的網路功能,本文中多線程檔案下載就是利用這兩個類實現的。 WebRequest和WebResponse都是抽象基類,因此在程式中不能直接作為對象使用,必須被繼承,實際使用中,可根據URI參數中的URI首碼選用它們合適的子類,對於HTTP這類URI,HttpWebRequest和HttpWebResponse類可以用於處理客戶程式同WEB伺服器之間的HTTP通訊。
HttpWebRequest類實現了很多通過HTTP訪問WEB伺服器上檔案的進階功能。HttpWebRequest類對WebRequest中定義的屬性和方法提供支援,HttpWebRequest將發送到Internet資源的公用HTTP標題的值公開為屬性,由方法或系統設定,常用的由屬性或方法設定的HTTP標題為:接受, 由Accept屬性設定, 串連, 由Connection屬性和KeepAlive屬性設定, Content-Length, 由ContentLength屬性設定, Content-Type, 由ContentType屬性設定, 範圍, 由AddRange方法設定. 實際使用中是將標題資訊正確設定後,傳遞到WEB伺服器,WEB伺服器根據要求作出回應。
HttpWebResponse類繼承自WebResponse類,專門處理從WEB伺服器返回的HTTP響應,這個類實現了很多方法,具有很多屬性,可以全面處理接收到的互連網資訊。在HttpWebResponse類中,對於大多數通用的HTTP標題欄位,都有獨立的屬性與其對應,程式員可以通過這些屬性方便的訪問位於HTTP接收報文標題欄位中的資訊,本例中用到的HttpWebResponse類屬性為:ContentLength 既接收內容的長度。
有了以上的瞭解後,下面看看這兩個類的用法,要建立HttpWebRequest對象,不要直接使用HttpWebRequest的建構函式,而要使用WebRequest.Create方法初始化一個HttpWebRequest執行個體,如:
HttpWebRequest hwr=(HttpWebRequest)WebRequest.Create(http://www.163.com/);
建立了這個對象後,就可以通過HttpWebRequest屬性,設定很多HTTP標題欄位的內容,如hwr.AddRange(100,1000);設定接收對象的範圍為100-1000位元組。
HttpWebReques對象使用GetResponse()方法時,會返回一個HttpWebResponse對象,為提出HTTP返回報文資訊,需要使用HttpWebResponse的GetResponseStream()方法,該方法返回一個Stream對象,可以讀取HTTP返回的報文,如:首先定義一個Strean 對象 public System.IO.Stream ns; 然後 ns=hwr.GetResponse ().GetResponseStream ();即可建立Stream對象。有了以上的準備知識後下面開始設計我們的多線程互連網檔案的下載程式,首先開啟Visual Studio.Net整合式開發環境,選擇“檔案”、“建立”、“項目”,然後選擇“Visual C#項目”,在嚮導右邊列表框中選中“Windows應用程式”,輸入項目名稱,如本例為:httpftp,然後選擇“確定”按鈕,嚮導自動產生了一個Windows應用程式項目。首先開啟視窗設計器設計應用程式視窗,增加如下控制項:
一個列表框 listBox1 三個文字標籤 label1-label3 三個文字框 textBox1-textBox3 一個開始接收按鈕 button1 設計好的視窗如:
控制項定義代碼是:
public System.Windows.Forms.ListBox listBox1;
private System.Windows.Forms.Label label1;
private System.Windows.Forms.TextBox textBox1
private System.Windows.Forms.Button button1;
private System.Windows.Forms.Label label2;
private System.Windows.Forms.TextBox textBox2;
private System.Windows.Forms.Label label3;
private System.Windows.Forms.TextBox textBox3;
private System.Windows.Forms.Label label4;
private System.Windows.Forms.TextBox textBox4;
開啟Form1的代碼編輯器,增加如下的命名空間:
using System.Net;//網路功能
using System.IO;//流支援
using System.Threading ;//線程支援
增加如下的程式變數:
public bool[] threadw; //每個線程結束標誌
public string[] filenamew;//每個線程接收檔案的檔案名稱
public int[] filestartw;//每個線程接收檔案的起始位置
public int[] filesizew;//每個線程接收檔案的大小
public string strurl;//接受檔案的URL
public bool hb;//檔案合并標誌
public int thread;//進程數
定義一個HttpFile類,用於管理接收線程,其代碼如下:
public class HttpFile
{
public Form1 formm;
public int threadh;//線程代號
public string filename;//檔案名稱
public string strUrl;//接收檔案的URL
public FileStream fs;
public HttpWebRequest request;
public System.IO.Stream ns;
public byte[] nbytes;//接收緩衝區
public int nreadsize;//接收位元組數
public HttpFile(Form1 form,int thread)//構造方法
{
formm=form;
threadh=thread;
}
~HttpFile()//析構方法
{
formm.Dispose ();
}
public void receive()//接收線程
{
filename=formm.filenamew[threadh];
strUrl=formm.strurl;
ns=null;
nbytes= new byte[512];
nreadsize=0;
formm.listBox1 .Items .Add ("線程"+threadh.ToString ()+"開始接收");
fs=new FileStream (filename,System.IO.FileMode.Create);
try
{
request=(HttpWebRequest)HttpWebRequest.Create (strUrl);
//接收的起始位置及接收的長度
request.AddRange(formm.filestartw [threadh],
formm.filestartw [threadh]+formm.filesizew [threadh]);
ns=request.GetResponse ().GetResponseStream ();//獲得接收流
nreadsize=ns.Read (nbytes,0,512);
while (nreadsize>0)
{
fs.Write (nbytes,0,nreadsize);
nreadsize=ns.Read (nbytes,0,512);
formm.listBox1 .Items .Add ("線程"+threadh.ToString ()+"正在接收");
}
fs.Close();
ns.Close ();
}
catch (Exception er)
{
MessageBox.Show (er.Message );
fs.Close();
}
formm.listBox1 .Items.Add ("進程"+threadh.ToString ()+"接收完畢!");
formm.threadw[threadh]=true;
}
}
該類和Form1類處於統一命名空間,但不包含在Form1類中。下面定義“開始接收”按鈕控制項的事件響應函數:
private void button1_Click(object sender, System.EventArgs e)
{
DateTime dt=DateTime.Now;//開始接收時間
textBox1.Text =dt.ToString ();
strurl=textBox2.Text .Trim ().ToString ();
HttpWebRequest request;
long filesize=0;
try
{
request=(HttpWebRequest)HttpWebRequest.Create (strurl);
filesize=request.GetResponse ().ContentLength;//取得目標檔案的長度
request.Abort ();
}
catch (Exception er)
{
MessageBox.Show (er.Message );
}
// 接收線程數
thread=Convert.ToInt32 (textBox4.Text .Trim().ToString (),10);
//根據線程數初始化數組
threadw=new bool [thread];
filenamew=new string [thread];
filestartw=new int [thread];
filesizew=new int[thread];
//計算每個線程應該接收檔案的大小
int filethread=(int)filesize/thread;//平均分配
int filethreade=filethread+(int)filesize%thread;//剩餘部分由最後一個線程完成
//為數組賦值
for (int i=0;i<thread;i++)
{
threadw[i]=false;//每個線程狀態的初始值為假
filenamew[i]=i.ToString ()+".dat";//每個線程接收檔案的臨時檔案名稱
if (i<thread-1)
{
filestartw[i]=filethread*i;//每個線程接收檔案的起始點
filesizew[i]=filethread-1;//每個線程接收檔案的長度
}
else
{
filestartw[i]=filethread*i;
filesizew[i]=filethreade-1;
}
}
//定義線程數組,啟動接收線程
Thread[] threadk=new Thread [thread];
HttpFile[] httpfile=new HttpFile [thread];
for (int j=0;j<thread;j++)
{
httpfile[j]=new HttpFile(this,j);
threadk[j]=new Thread(new ThreadStart (httpfile[j].receive ));
threadk[j].Start ();
}
//啟動合并各線程接收的檔案線程
Thread hbth=new Thread (new ThreadStart (hbfile));
hbth.Start ();
}
合并檔案的線程hbfile定義在Form1類中,定義如下:
using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Data;
using System.Drawing;
using System.Linq;
using System.Text;
using System.Windows.Forms;
namespace HttpDownload
{
public partial class Form1 : Form
{
public int threadNum; // 進程
public bool[] threadStatus; // 每個線程結束標誌
public string[] fileNames; // 每個線程接收檔案的檔案名稱
public int[] fileStartPos; // 每個線程接收檔案的起始位置
public int[] fileSize; // 每個線程接收檔案的大小
public string url; // 接受檔案的URL
public bool isMerge; // 檔案合并標誌
public Form1()
{
InitializeComponent();
}
/// <summary>
///
/// </summary>
/// <param name="sender"></param>
/// <param name="e"></param>
private void btn_rec_Click(object sender, EventArgs e)
{
url = this.txt_url.Text.Trim().ToString();
System.Net.HttpWebRequest request;
long fileSizeAll = 0;
request = (System.Net.HttpWebRequest)System.Net.HttpWebRequest.Create(url);
fileSizeAll = request.GetResponse().ContentLength;
request.Abort();
threadNum = int.Parse(this.txt_threadNum.Text.Trim().ToString());
Init(fileSizeAll);
for (int i = 0; i < threadNum; i++)
{
this.lst_infos.Items.Add("線程" + i + ":" +
" 線程狀態:" + threadStatus[i].ToString() + "-開始位置:" + fileStartPos[i].ToString() + "-大小:" + fileSize[i].ToString());
}
this.lst_infos.Items.Add(" 檔案總大小:" + fileSizeAll);
// 定義並啟動線程數組
System.Threading.Thread[] threads = new System.Threading.Thread[threadNum];
HttpFile[] httpDownloads = new HttpFile[threadNum];
for (int i = 0; i < threadNum; i++)
{
httpDownloads[i] = new HttpFile(this, i);
threads[i] = new System.Threading.Thread(new System.Threading.ThreadStart(httpDownloads[i].receive));
threads[i].Start();
}
System.Threading.Thread merge = new System.Threading.Thread(new System.Threading.ThreadStart(MergeFile));
merge.Start();
this.txt_overTime.Text = DateTime.Now.ToString();
}
/// <summary>
/// 初始化
/// </summary>
/// <remarks>
/// 每個線程平均分配檔案大小,剩餘部分由最後一個線程完成
/// </remarks>
/// <param name="filesize"></param>
private void Init(long filesize)
{
threadStatus = new bool[threadNum];
fileNames = new string[threadNum];
fileStartPos = new int[threadNum];
fileSize = new int[threadNum];
int filethread = (int)filesize / threadNum;
int filethreade = filethread + (int)filesize % threadNum;
for (int i = 0; i < threadNum; i++)
{
threadStatus[i] = false;
fileNames[i] = i.ToString() + ".dat";
if (i < threadNum - 1)
{
fileStartPos[i] = filethread * i;
fileSize[i] = filethread - 1;
}
else
{
fileStartPos[i] = filethread * i;
fileSize[i] = filethreade - 1;
}
}
}
/// <summary>
/// 合并檔案
/// </summary>
public void MergeFile()
{
while (true)
{
isMerge = true;
for (int i = 0; i < threadNum; i++)
{
if (threadStatus[i] == false) // 若有未結束線程,則等待
{
isMerge = false;
System.Threading.Thread.Sleep(100);
break;
}
}
if (isMerge == true) // 否則,停止等待
{
break;
}
}
System.IO.FileStream fs;
System.IO.FileStream fstemp;
int readfile;
byte[] bytes = new byte[512];
fs = new System.IO.FileStream(txt_localFile.Text.Trim().ToString(), System.IO.FileMode.Create);
for (int k = 0; k < threadNum; k++)
{
fstemp = new System.IO.FileStream(fileNames[k], System.IO.FileMode.Open);
while (true)
{
readfile = fstemp.Read(bytes, 0, 512);
if (readfile > 0)
{
fs.Write(bytes, 0, readfile);
}
else
{
break;
}
}
fstemp.Close();
}
fs.Close();
System.Windows.Forms.MessageBox.Show("接收完畢!!!");
}
}
} 至此,一個多線程下載檔案的程式就大功告成了,注意在輸入本地檔案名稱時,應按如下格式輸入:“c:\\test\\httpftp\\bin\\d.htm”,因”\”後的字元在C#中是逸出字元,線程數並非越大越好,一般5個線程就可以了,該程式在Visual Studio.Net 2002開發環境及Windows xp 作業系統上通過。