C#怎樣實現(xiàn)文件下載斷點續(xù)傳
前言
老規(guī)矩,還是從最簡單粗暴的開始。那么多簡單算簡單?多粗暴算粗暴?我告訴你可以不寫一句代碼,你信嗎?直接把一個文件往IIS服務(wù)器上一扔,就支持下載。還TM么可以斷點續(xù)傳(IIS服務(wù)端默認(rèn)支持)。
在貼代碼之前先來了解下什么是斷點續(xù)傳(這里說的是下載斷點續(xù)傳)?怎么實現(xiàn)的斷點續(xù)傳?
斷點續(xù)傳就是下載了一半斷網(wǎng)或者暫停了,然后可以接著下載。不用從頭開始下載。
很神奇嗎,其實簡單得很,我們想想也是可以想到的。
首先客戶端向服務(wù)端發(fā)送一個請求(下載文件)。然后服務(wù)端響應(yīng)請求,信息包含文件總大小、文件流開始和結(jié)束位置、內(nèi)容大小等。那具體是怎么實現(xiàn)的呢?
HTTP/1.1有個頭屬性Range。比如你發(fā)送請求的時候帶上Range:0-199,等于你是請求0到199之間的數(shù)據(jù)。然后服務(wù)器響應(yīng)請求Content-Range: bytes 0-199/250 ,表示你獲取了0到199之間的數(shù)據(jù),總大小是250。(也就是告訴你還有數(shù)據(jù)沒有下載完)。
我們來畫個圖吧。
是不是很簡單?這么神奇的東西也就是個“約定”而已,也就是所謂的HTTP協(xié)議。
然而,協(xié)議這東西你遵守它就存在,不遵守它就不存在。就像民國時期的錢大家都信它,它就有用。如果大部分人不信它,也就沒卵用了。
這個斷點續(xù)傳也是這樣。你服務(wù)端遵守就支持,不遵守也就不支持?jǐn)帱c續(xù)傳。所以我們寫下載工具的時候需要判斷響應(yīng)報文里有沒有Content-Range,來確定是否支持?jǐn)帱c續(xù)傳。
廢話夠多了,下面擼起袖子開干。
文件下載-服務(wù)端
使用a標(biāo)簽提供文件下載
利用a標(biāo)簽來下載文件,也就是我們前面說的不寫代碼就可以實現(xiàn)下載。直接把文件往iis服務(wù)器上一扔,然后把鏈接貼到a標(biāo)簽上,完事。
<a href="/新建文件夾2.rar" rel="external nofollow" >下載</a>
簡單、粗暴不用說了。如真得這么好那大家也不會費力去寫其他下載邏輯了。這里有個致命的缺點。這種方式提供的下載不夠安全。誰都可以下載,沒有權(quán)限控制,說不定還會被人文件掃描(好像csdn就出過這檔子事)。
使用Response.TransmitFile提供文件下載
上面說直接a標(biāo)簽提供下載不夠安全。那我們怎么提供相對安全的下載呢。asp.net默認(rèn)App_Data
文件夾是不能被直接訪問的,那我們把下載文件放這里面。然后下載的時候我們讀取文件在返回到響應(yīng)流。
//文件下載 public void FileDownload5() { //前面可以做用戶登錄驗證、用戶權(quán)限驗證等。 string filename = "大數(shù)據(jù).rar"; //客戶端保存的文件名 string filePath = Server.MapPath("/App_Data/大數(shù)據(jù).rar");//要被下載的文件路徑 Response.ContentType = "application/octet-stream"; //二進(jìn)制流 Response.AddHeader("Content-Disposition", "attachment;filename=" + filename); Response.TransmitFile(filePath); //將指定文件寫入 HTTP 響應(yīng)輸出流 }
其他方式文件下載
在網(wǎng)上搜索C#文件下載一般都會搜到所謂的“四種方式”。其實那些代碼并不能拿來直接使用,有坑的。
第一種:(Response.BinaryWrite)
public void FileDownload2() { string fileName = "新建文件夾2.rar";//客戶端保存的文件名 string filePath = Server.MapPath("/App_Data/新建文件夾2.rar");//要被下載的文件路徑 Response.ContentType = "application/octet-stream";//二進(jìn)制流 //通知瀏覽器下載文件而不是打開 Response.AddHeader("Content-Disposition", "attachment; filename=" + HttpUtility.UrlEncode(fileName, System.Text.Encoding.UTF8)); //以字符流的形式下載文件 using (FileStream fs = new FileStream(filePath, FileMode.Open, FileAccess.Read)) { Response.AddHeader("Content-Length", fs.Length.ToString()); //這里容易內(nèi)存溢出 //理論上數(shù)組最大長度 int.MaxValue 2147483647 //(實際分不到這么多,不同的程序能分到值也不同,本人機器,winfrom( 2147483591 相差56)、iis(也差不多2G)、iis Express(只有100多MB)) byte[] bytes = new byte[(int)fs.Length]; fs.Read(bytes, 0, bytes.Length); Response.BinaryWrite(bytes); } Response.Flush(); Response.End(); }
首先數(shù)組最大長度為int.MaxValue,然后正常程序是不會分這么大內(nèi)存,很容易搞掛服務(wù)器。(也就是可以下載的文件,極限值最多也就2G不到。)【不推薦】
第二種:(Response.WriteFile)
public void FileDownload3() { string fileName = "新建文件夾2.rar";//客戶端保存的文件名 string filePath = Server.MapPath("/App_Data/新建文件夾2.rar");//要被下載的文件路徑 FileInfo fileInfo = new FileInfo(filePath); Response.Clear(); Response.ClearContent(); Response.ClearHeaders(); Response.AddHeader("Content-Disposition", "attachment;filename=\"" + HttpUtility.UrlEncode(fileName, System.Text.Encoding.UTF8) + "\""); Response.AddHeader("Content-Length", fileInfo.Length.ToString());//文件大小 Response.AddHeader("Content-Transfer-Encoding", "binary"); Response.ContentType = "application/octet-stream"; Response.WriteFile(fileInfo.FullName);//大小參數(shù)必須介于零和最大的 Int32 值之間(也就是最大2G,不過這個操作非常耗內(nèi)存) //這里容易內(nèi)存溢出 Response.Flush(); Response.End(); }
問題和第一種類似,也是不能下載大于2G的文件。然后下載差不多2G文件時,機器也是處在被掛的邊緣,相當(dāng)恐怖。【不推薦】
第三種:(Response.OutputStream.Write)
public void FileDownload4() { string fileName = "大數(shù)據(jù).rar";//客戶端保存的文件名 string filePath = Server.MapPath("/App_Data/大數(shù)據(jù).rar");//要被下載的文件路徑 if (System.IO.File.Exists(filePath)) { const long ChunkSize = 102400; //100K 每次讀取文件,只讀取100K,這樣可以緩解服務(wù)器的壓力 byte[] buffer = new byte[ChunkSize]; Response.Clear(); using (FileStream fileStream = System.IO.File.OpenRead(filePath)) { long fileSize = fileStream.Length; //文件大小 Response.ContentType = "application/octet-stream"; //二進(jìn)制流 Response.AddHeader("Content-Disposition", "attachment; filename=" + HttpUtility.UrlEncode(fileName, System.Text.Encoding.UTF8)); Response.AddHeader("Content-Length", fileStream.Length.ToString());//文件總大小 while (fileSize > 0 && Response.IsClientConnected)//判斷客戶端是否還連接了服務(wù)器 { //實際讀取的大小 int readSize = fileStream.Read(buffer, 0, Convert.ToInt32(ChunkSize)); Response.OutputStream.Write(buffer, 0, readSize); Response.Flush();//如果客戶端 暫停下載時,這里會阻塞。 fileSize = fileSize - readSize;//文件剩余大小 } } Response.Close(); } }
這里明顯看到了是在循環(huán)讀取輸出,比較機智。下載大文件時沒有壓力?!就扑]】
第四種:(Response.TransmitFile)
也就上開始舉例說的那種,下載大文件也沒有壓力?!就扑]】
public void FileDownload5() { //前面可以做用戶登錄驗證、用戶權(quán)限驗證等。 string filename = "大數(shù)據(jù).rar"; //客戶端保存的文件名 string filePath = Server.MapPath("/App_Data/大數(shù)據(jù).rar");//要被下載的文件路徑 Response.ContentType = "application/octet-stream"; //二進(jìn)制流 Response.AddHeader("Content-Disposition", "attachment;filename=" + filename); Response.TransmitFile(filePath); //將指定文件寫入 HTTP 響應(yīng)輸出流 }
文件下載-客戶端
上面實現(xiàn)了文件下載的服務(wù)端實現(xiàn),接下來我們實現(xiàn)文件下載的客戶端實現(xiàn)。客戶端的下載可以直接是瀏覽器提供的下載,也可以是迅雷或者我們自己寫的下載程序。這里為了更好的分析,我們來用winfrom程序自己寫個下載客戶端。
直接下載
private async void button1_ClickAsync(object sender, EventArgs e) { using (HttpClient http = new HttpClient()) { var httpResponseMessage = await http.GetAsync("http://localhost:813/新建文件夾2.rar");//發(fā)送請求 (鏈接是a標(biāo)簽提供的) var contentLength = httpResponseMessage.Content.Headers.ContentLength;//讀取文件大小 using (var stream = await httpResponseMessage.Content.ReadAsStreamAsync())//讀取文件流 { var readLength = 1024000;//1000K 每次讀取大小 byte[] bytes = new byte[readLength]; int writeLength; using (FileStream fs = new FileStream(Application.StartupPath + "/temp.rar", FileMode.Append, FileAccess.Write))//使用追加方式打開一個文件流 { while ((writeLength = stream.Read(bytes, 0, readLength)) > 0)//分塊讀取文件流 { fs.Write(bytes, 0, writeLength);//追加寫入文件 contentLength -= writeLength; if (contentLength == 0)//如果寫入完成 給出提示 MessageBox.Show("下載完成"); } } } } }
看著這么漂亮的代碼,好像沒問題。可現(xiàn)實往往事與愿違。
我們看到了一個異?!癝ystem.Net.Http.HttpRequestException:“不能向緩沖區(qū)寫入比所配置最大緩沖區(qū)大小 2147483647 更多的字節(jié)。”,什么鬼,又是2147483647這個數(shù)字。因為我們下載的文件大小超過了2G,無法緩沖下載。
可是“緩沖下載”下又是什么鬼。我也不知道。那我們試試可以關(guān)掉這個東東呢?答案是肯定的。
var httpResponseMessage = await http.GetAsync("http://localhost:813/新建文件夾2.rar");//發(fā)送請求
改成下面就可以了
var httpResponseMessage = await http.GetAsync("http://localhost:813/新建文件夾2.rar",HttpCompletionOption.ResponseHeadersRead);//響應(yīng)一可用且標(biāo)題可讀時即應(yīng)完成的操作。 (尚未讀取的內(nèi)容。)
我們看到枚舉HttpCompletionOption的兩個值。一個是響應(yīng)讀取內(nèi)容,一個是響應(yīng)讀取標(biāo)題(也就是Headers里的內(nèi)容)。
【注意】:using (FileStream fs = new FileStream
要放在 while ((writeLength =
的外面,不然可能出現(xiàn)寫入文件被占用的異常。
異步下載
我們發(fā)現(xiàn)在下載大文件的時候會造成界面假死。這是UI單線程程序的通病。當(dāng)然,這么差的用戶體驗是我們不能容忍的。下面我們?yōu)橄螺d開一個線程,避免造成UI線程的阻塞。
/// <summary> /// 異步下載 /// </summary> /// <param name="sender"></param> /// <param name="e"></param> private async void button2_ClickAsync(object sender, EventArgs e) { //開啟一個異步線程 await Task.Run(async () => { //異步操作UI元素 label1.Invoke((Action)(() => { label1.Text = "準(zhǔn)備下載..."; })); long downloadSize = 0;//已經(jīng)下載大小 long downloadSpeed = 0;//下載速度 using (HttpClient http = new HttpClient()) { var httpResponseMessage = await http.GetAsync("http://localhost:813/新建文件夾2.rar", HttpCompletionOption.ResponseHeadersRead);//發(fā)送請求 var contentLength = httpResponseMessage.Content.Headers.ContentLength; //文件大小 using (var stream = await httpResponseMessage.Content.ReadAsStreamAsync()) { var readLength = 1024000;//1000K byte[] bytes = new byte[readLength]; int writeLength; var beginSecond = DateTime.Now.Second;//當(dāng)前時間秒 //使用追加方式打開一個文件流 using (FileStream fs = new FileStream(Application.StartupPath + "/temp.rar", FileMode.Append, FileAccess.Write)) { while ((writeLength = stream.Read(bytes, 0, readLength)) > 0) { fs.Write(bytes, 0, writeLength); downloadSize += writeLength; downloadSpeed += writeLength; progressBar1.Invoke((Action)(() => { var endSecond = DateTime.Now.Second; if (beginSecond != endSecond)//計算速度 { downloadSpeed = downloadSpeed / (endSecond - beginSecond); label1.Text = "下載速度" + downloadSpeed / 1024 + "KB/S"; beginSecond = DateTime.Now.Second; downloadSpeed = 0;//清空 } progressBar1.Value = Math.Max((int)(downloadSize * 100 / contentLength), 1); })); } } label1.Invoke((Action)(() => { label1.Text = "下載完成"; })); } } }); }
效果圖:
斷點續(xù)傳
上面的方式我們發(fā)現(xiàn),如果下載到一個半斷網(wǎng)了下次會重頭開始下載。這和我們今天的主題明顯不符嘛。下面我們開始正式進(jìn)入主題文件下載之?dāng)帱c續(xù)傳
。把前面我們說到的頭屬性Range用起來。
var request = new HttpRequestMessage { RequestUri = new Uri(url) }; request.Headers.Range = new RangeHeaderValue(rangeBegin, null); //【關(guān)鍵點】全局變量記錄已經(jīng)下載了多少,然后下次從這個位置開始下載。 var httpResponseMessage = await http.SendAsync(request, HttpCompletionOption.ResponseHeadersRead);
完整代碼:
/// <summary> /// 是否暫停 /// </summary> static bool isPause = true; /// <summary> /// 下載開始位置(也就是已經(jīng)下載了的位置) /// </summary> static long rangeBegin = 0; //(當(dāng)然,這個值也可以存為持久化。如文本、數(shù)據(jù)庫等) private async void button3_ClickAsync(object sender, EventArgs e) { isPause = !isPause; if (!isPause)//點擊下載 { button3.Text = "暫停"; await Task.Run(async () => { //異步操作UI元素 label1.Invoke((Action)(() => { label1.Text = "準(zhǔn)備下載..."; })); long downloadSpeed = 0;//下載速度 using (HttpClient http = new HttpClient()) { var url = "http://localhost:813/新建文件夾2.rar"; var request = new HttpRequestMessage { RequestUri = new Uri(url) }; request.Headers.Range = new RangeHeaderValue(rangeBegin, null); //【關(guān)鍵點】全局變量記錄已經(jīng)下載了多少,然后下次從這個位置開始下載。 var httpResponseMessage = await http.SendAsync(request, HttpCompletionOption.ResponseHeadersRead); var contentLength = httpResponseMessage.Content.Headers.ContentLength;//本次請求的內(nèi)容大小 if (httpResponseMessage.Content.Headers.ContentRange != null) //如果為空,則說明服務(wù)器不支持?jǐn)帱c續(xù)傳 { contentLength = httpResponseMessage.Content.Headers.ContentRange.Length;//服務(wù)器上的文件大小 } using (var stream = await httpResponseMessage.Content.ReadAsStreamAsync()) { var readLength = 1024000;//1000K byte[] bytes = new byte[readLength]; int writeLength; var beginSecond = DateTime.Now.Second;//當(dāng)前時間秒 while ((writeLength = stream.Read(bytes, 0, readLength)) > 0 && !isPause) { //使用追加方式打開一個文件流 using (FileStream fs = new FileStream(Application.StartupPath + "/temp.rar", FileMode.Append, FileAccess.Write)) { fs.Write(bytes, 0, writeLength); } downloadSpeed += writeLength; rangeBegin += writeLength; progressBar1.Invoke((Action)(() => { var endSecond = DateTime.Now.Second; if (beginSecond != endSecond)//計算速度 { downloadSpeed = downloadSpeed / (endSecond - beginSecond); label1.Text = "下載速度" + downloadSpeed / 1024 + "KB/S"; beginSecond = DateTime.Now.Second; downloadSpeed = 0;//清空 } progressBar1.Value = Math.Max((int)((rangeBegin) * 100 / contentLength), 1); })); } if (rangeBegin == contentLength) { label1.Invoke((Action)(() => { label1.Text = "下載完成"; })); } } } }); } else//點擊暫停 { button3.Text = "繼續(xù)下載"; label1.Text = "暫停下載"; } }
效果圖:
到現(xiàn)在為止,你以為我們的斷點續(xù)傳就完成了嗎?
錯,你有沒有發(fā)現(xiàn)我們使用的下載鏈接是a標(biāo)簽的。也就是我們自己寫服務(wù)端提供的下載鏈接是不是也可以支持?jǐn)帱c續(xù)傳呢?下面我換個下載鏈接試試便知。
斷點續(xù)傳(服務(wù)端的支持)
測試結(jié)果如下:
發(fā)現(xiàn)并不支持?jǐn)帱c續(xù)傳。為什么a標(biāo)簽鏈接可以直接支持,我們寫的下載卻不支持呢。
a標(biāo)簽的鏈接指向的直接是iis上的文件(iis默認(rèn)支持),而我們寫的卻沒有做響應(yīng)報文表頭Range的處理。(沒想象中的那么智能嘛 >_<)
前面我們說過,斷線續(xù)傳是HTTP的一個協(xié)議。我們遵守它,它就存在,我們不遵守它也就不存在。
那下面我們修改前面的文件下載代碼(服務(wù)端):
public void FileDownload5() { //前面可以做用戶登錄驗證、用戶權(quán)限驗證等。 string filename = "大數(shù)據(jù).rar"; //客戶端保存的文件名 string filePath = Server.MapPath("/App_Data/大數(shù)據(jù).rar");//要被下載的文件路徑 var range = Request.Headers["Range"]; if (!string.IsNullOrWhiteSpace(range))//如果遵守協(xié)議,支持?jǐn)帱c續(xù)傳 { var fileLength = new FileInfo(filePath).Length;//文件的總大小 long begin;//文件的開始位置 long end;//文件的結(jié)束位置 long.TryParse(range.Split('=')[1].Split('-')[0], out begin); long.TryParse(range.Split('-')[1], out end); end = end - begin > 0 ? end : (fileLength - 1);// 如果沒有結(jié)束位置,那我們讀剩下的全部 //表頭 表明 下載文件的開始、結(jié)束位置 和文件總大小 Response.AddHeader("Content-Range", "bytes " + begin + "-" + end + "/" + fileLength); Response.ContentType = "application/octet-stream"; Response.AddHeader("Content-Disposition", "attachment;filename=" + filename); Response.TransmitFile(filePath, begin, (end - begin));//發(fā)送 文件開始位置讀取的大小 } else { Response.ContentType = "application/octet-stream"; Response.AddHeader("Content-Disposition", "attachment;filename=" + filename); Response.TransmitFile(filePath); } }
然后再測試斷點續(xù)傳,完美支持。
多線程同時下載(分片下載)
文件的斷點續(xù)傳已經(jīng)分析完了。不過中間有些細(xì)節(jié)的東西你可以根據(jù)實際需求去完善。如:文件命名、斷點續(xù)傳的文件是否發(fā)生了改變、下載完成后驗證文件和服務(wù)器上的是否一致。
還有我們可以根據(jù)表頭屬性Range來實現(xiàn)多線程下載,不過這里就不貼代碼了,貼個效果圖吧。和上一篇文件上傳里的多線程上傳同理。您也可以根據(jù)提供的demo代碼下載查看,內(nèi)有完整實現(xiàn)。
以上就是C#怎樣實現(xiàn)文件下載斷點續(xù)傳的詳細(xì)內(nèi)容,更多關(guān)于C#文件下載斷點續(xù)傳的資料請關(guān)注腳本之家其它相關(guān)文章!
相關(guān)文章
c#進(jìn)度條 progressBar 使用方法的小例子
1、創(chuàng)建進(jìn)度條窗口2、在其他窗口中調(diào)用方法ShowProcess(int percent,string message)2013-04-04使用C# CefSharp Python采集某網(wǎng)站簡歷并且自動發(fā)送邀請短信的方法
這篇文章主要給大家介紹了關(guān)于如何使用C# CefSharp Python采集某網(wǎng)站簡歷并且自動發(fā)送邀請短信的相關(guān)資料,文中通過示例代碼介紹的非常詳細(xì),對大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價值,需要的朋友們下面來一起看看吧2019-03-03WinForm實現(xiàn)為ComboBox綁定數(shù)據(jù)源并提供下拉提示功能
這篇文章主要介紹了WinForm實現(xiàn)為ComboBox綁定數(shù)據(jù)源并提供下拉提示功能,是非常實用的功能,需要的朋友可以參考下2014-08-08