C#實現上傳文件分割,斷點續傳上傳文件


一 介紹

斷點續傳搜索大部分都是下載的斷點續傳,涉及到HTTP協議1.1的Range和Content-Range頭。

來個簡單的介紹

所謂斷點續傳,也就是要從文件已經下載的地方開始繼續下載。在以前版本的 HTTP 協議是不支持斷點的,HTTP/1.1 開始就支持了。一般斷點下載時才用到 Range 和 Content-Range 實體頭。

Range

用於請求頭中,指定第一個字節的位置和最后一個字節的位置,一般格式:

Range:(unit=first byte pos)-[last byte pos]

Content-Range

用於響應頭,指定整個實體中的一部分的插入位置,他也指示了整個實體的長度。在服務器向客戶返回一個部分響應,它必須描述響應覆蓋的范圍和整個實體長度。一般格式:

Content-Range: bytes (unit first byte pos) – [last byte pos]/[entity legth]

請求下載整個文件:

  1. GET /test.rar HTTP/1.1
  2. Connection: close
  3. Host: 116.1.219.219
  4. Range: bytes=0-801 //一般請求下載整個文件是bytes=0- 或不用這個頭

一般正常回應

  1. HTTP/1.1 200 OK
  2. Content-Length: 801
  3. Content-Type: application/octet-stream
  4. Content-Range: bytes 0-800/801 //801:文件總大小

 

而今天要說的是上傳的斷點續傳,用到了Content-Range頭

上傳的續傳原理跟下載的續傳同理。

就是在上傳前把文件拆分后上傳。服務器端接收合並,即使上傳斷了。下次上傳依然從服務器端的文件現有字節后合並文件。最終上傳完成。

二 實現

 服務器端
服務端是webapi實現。或是mvc,webform皆可。

服務端的原理就是接收上傳數據流。保存文件。如果此文件已存在。就是合並現有文件。

這里文件的文件名是采用客戶端傳過來的數據。

文件名稱是文件的MD5,保證文件的唯一性。

Source code    
[HttpGet] public HttpResponseMessage GetResumFile() { //用於獲取當前文件是否是續傳。和續傳的字節數開始點。 var md5str = HttpContext.Current.Request.QueryString["md5str"]; var saveFilePath = HttpContext.Current.Server.MapPath("~/Images/") + md5str; if(System.IO.File.Exists(saveFilePath)) { var fs = System.IO.File.OpenWrite(saveFilePath); var fslength = fs.Length.ToString(); fs.Close(); return new HttpResponseMessage { Content = new StringContent(fslength, System.Text.Encoding.UTF8, "text/plain") }; } return new HttpResponseMessage(HttpStatusCode.OK); } [HttpPost] public HttpResponseMessage Rsume() {   var file = HttpContext.Current.Request.InputStream; var filename = HttpContext.Current.Request.QueryString["filename"];   this.SaveAs(HttpContext.Current.Server.MapPath("~/Images/") + filename, file);     HttpContext.Current.Response.StatusCode = 200;   // For compatibility with IE's "done" event we need to return a result as well as setting the context.response return new HttpResponseMessage(HttpStatusCode.OK); }     private void SaveAs(string saveFilePath,System.IO.Stream stream) { long lStartPos = 0; int startPosition = 0; int endPosition = 0; var contentRange = HttpContext.Current.Request.Headers["Content-Range"]; //bytes 10000-19999/1157632 if (!string.IsNullOrEmpty(contentRange)) { contentRange = contentRange.Replace("bytes", "").Trim(); contentRange = contentRange.Substring(0, contentRange.IndexOf("/")); string[] ranges = contentRange.Split('-'); startPosition = int.Parse(ranges[0]); endPosition = int.Parse(ranges[1]); } System.IO.FileStream fs; if (System.IO.File.Exists(saveFilePath)) { fs = System.IO.File.OpenWrite(saveFilePath); lStartPos = fs.Length;   } else { fs = new System.IO.FileStream(saveFilePath, System.IO.FileMode.Create); lStartPos = 0; } if (lStartPos > endPosition) { fs.Close(); return; } else if (lStartPos < startPosition) { lStartPos = startPosition; } else if (lStartPos > startPosition && lStartPos < endPosition) { lStartPos = startPosition; } fs.Seek(lStartPos, System.IO.SeekOrigin.Current); byte[] nbytes = new byte[512]; int nReadSize = 0; nReadSize = stream.Read(nbytes, 0, 512); while (nReadSize > 0) { fs.Write(nbytes, 0, nReadSize); nReadSize = stream.Read(nbytes, 0, 512); } fs.Close(); }

客戶端

這里的客戶端是winform,功能就是選擇文件后即刻上傳。如果中途網絡,斷點等因素沒有傳成功。

可以再次選擇此文件上傳。服務器會合並之前傳送的文件字節。實現斷點續傳。

private void btnSelectFile_Click(object sender, EventArgs e) { OpenFileDialog openFileDialog = new OpenFileDialog(); openFileDialog.InitialDirectory = "c:\\"; openFileDialog.RestoreDirectory = true; openFileDialog.FilterIndex = 1; if (openFileDialog.ShowDialog() == DialogResult.OK) { var fName = openFileDialog.FileName; FileStream fStream = new FileStream(fName, FileMode.Open, FileAccess.Read); var mdfstr = GetStreamMd5(fStream); fStream.Close(); var startpoint = isResume(mdfstr, Path.GetExtension(fName)); MessageBox.Show(UpLoadFile(fName, url, 64, startpoint,mdfstr)); } }   /// <summary> /// 根據文件名獲取是否是續傳和續傳的下次開始節點 /// </summary> /// <param name="md5str"></param> /// <param name="fileextname"></param> /// <returns></returns> private int isResume(string md5str, string fileextname) { System.Net.WebClient WebClientObj = new System.Net.WebClient(); var url = "http://localhost:13174/api/file/GetResumFile?md5str="+md5str+fileextname; byte[] byRemoteInfo = WebClientObj.DownloadData(url); string result = System.Text.Encoding.UTF8.GetString(byRemoteInfo); if(string.IsNullOrEmpty(result)) { return 0; } return Convert.ToInt32(result);   } #region /// <summary> /// 上傳文件(自動分割) /// </summary> /// <param name="filePath">待上傳的文件全路徑名稱</param> /// <param name="hostURL">服務器的地址</param> /// <param name="byteCount">分割的字節大小</param> /// <param name="cruuent">當前字節指針</param> /// <returns>成功返回"";失敗則返回錯誤信息</returns> public string UpLoadFile(string filePath, string hostURL, int byteCount, long cruuent, string mdfstr) { string tmpURL = hostURL; byteCount = byteCount * 1024;     System.Net.WebClient WebClientObj = new System.Net.WebClient(); FileStream fStream = new FileStream(filePath, FileMode.Open, FileAccess.Read);     BinaryReader bReader = new BinaryReader(fStream); long length = fStream.Length; string sMsg = "上傳成功"; string fileName = filePath.Substring(filePath.LastIndexOf('\\') + 1); try {   #region 續傳處理 byte[] data; if (cruuent > 0) { fStream.Seek(cruuent, SeekOrigin.Current); } #endregion   #region 分割文件上傳 for (; cruuent <= length; cruuent = cruuent + byteCount) { if (cruuent + byteCount > length) { data = new byte[Convert.ToInt64((length - cruuent))]; bReader.Read(data, 0, Convert.ToInt32((length - cruuent))); } else { data = new byte[byteCount]; bReader.Read(data, 0, byteCount); }   try {     //*** bytes 21010-47021/47022 WebClientObj.Headers.Remove(HttpRequestHeader.ContentRange); WebClientObj.Headers.Add(HttpRequestHeader.ContentRange, "bytes " + cruuent + "-" + (cruuent + byteCount) + "/" + fStream.Length);   hostURL = tmpURL + "?filename=" + mdfstr + Path.GetExtension(fileName); byte[] byRemoteInfo = WebClientObj.UploadData(hostURL, "POST", data); string sRemoteInfo = System.Text.Encoding.Default.GetString(byRemoteInfo);   // 獲取返回信息 if (sRemoteInfo.Trim() != "") { sMsg = sRemoteInfo; break;   } } catch (Exception ex) { sMsg = ex.ToString(); break; } #endregion   } } catch (Exception ex) { sMsg = sMsg + ex.ToString(); } try { bReader.Close(); fStream.Close(); } catch (Exception exMsg) { sMsg = exMsg.ToString(); }   GC.Collect(); return sMsg; } public static string GetStreamMd5(Stream stream) { var oMd5Hasher = new MD5CryptoServiceProvider(); byte[] arrbytHashValue = oMd5Hasher.ComputeHash(stream); //由以連字符分隔的十六進制對構成的String,其中每一對表示value 中對應的元素;例如“F-2C-4A” string strHashData = BitConverter.ToString(arrbytHashValue); //替換- strHashData = strHashData.Replace("-", ""); string strResult = strHashData; return strResult; }

  好久沒登錄博客園了,今天來一發分享。

  最近項目里有個需求,上傳文件(好吧,這種需求很常見,這也不是第一次遇到了)。當時第一想法就是直接用form表單提交(原諒我以前就是這么干的),不過表單里不僅有文件還有別的信息需要交互,跟后端商量后決定文件單獨上傳,獲取到服務器端返回的文件地址在和表單一起提交。這里就需要異步上傳文件。

  在網上扒了扒相關的內容,發現還真不少,阮一峰老師的這篇文章(文件上傳的漸進式增強)就介紹的很具體,下面就談談自己在實戰中遇到的一些問題的感受吧。

  先看看效果,實現了哪些功能

  (好吧,就一個按鈕而已,搞得神神秘秘,嘿嘿)

  

1
< button  type="button" class="btn" @click="upload">點擊上傳文件</ button >

  給按鈕綁定了一個點擊事件,下面看看點擊事件方法里做了什么

復制代碼
methods: {
        upload: function(){
            myUpload({
                url: window.location.protocol + '//' + window.location.host + '/crm/upload',
                maxSize: 10,
                beforeSend: function(file){

                },
                callback: function(res){
                    var data = JSON.parse(res);
                    pageCont.attachmentUrl = data.url;
                },
                uploading: function(pre){
                    pageCont.uploadCont.display = 'block';
                    pageCont.uploadStyle.width = pre * 2 + 'px';
                    pageCont.pre = pre;
                }
            });
        }
}
復制代碼

  按鈕綁定的點擊事件執行了upload方法,在upload方法里調用了一下myUpload方法,並傳遞了一些配置信息進去,稍后說下這些配置信息。先看看myUpload的具體實現:

  初始化了一個FormData對象和一個XMHttpResquest對象,創建一個type為file的input,並觸發一次該input的click,如下

復制代碼
var fd = new FormData(),
        xhr = new XMLHttpRequest(),
        input;
input = document.createElement('input');
input.setAttribute('id', 'myUploadInput');
input.setAttribute('type', 'file');
input.setAttribute('name', 'file');
document.body.appendChild(input);
input.style.display = 'none';
input.click();
復制代碼

  監聽剛才創建的input的change事件,並作在里面做相應處理

復制代碼
input.onchange = function(){
        if(!input.value){return;}
        if(option.maxSize &&  input.files[0].size > option.maxSize * 1024 * 1024){
            dialog({
                title: '提示',
                content: '請上傳小於'+option.maxSize+'M的文件',
                okValue: '確定',
                ok: function () {}
            }).showModal();
            return;
        }
        if(option.beforeSend instanceof Function){
            if(option.beforeSend(input) === false){
                return false;
            }
        }
        fd.append('file', input.files[0]);
        xhr.open('post', option.url);
        xhr.onreadystatechange = function(){
            if(xhr.status == 200){
                if(xhr.readyState == 4){
                    if(option.callback instanceof Function){
                        option.callback(xhr.responseText);
                    }
                }
            }else{
                if(!(dialog.get('uploadfail'))){
                    dialog({
                        id: 'uploadfail',
                        title: '提示',
                        content: '上傳失敗',
                        okValue: '確定',
                        ok: function () {}
                    }).showModal();
                }
            }
        }
        xhr.upload.onprogress = function(event){
            var pre = Math.floor(100 * event.loaded / event.total);
            if(option.uploading instanceof Function){
                option.uploading(pre);
            }
        }
        xhr.send(fd);
    }
復制代碼

  解釋下上面的代碼。input的change事件觸發后,首先判斷了下當前是否選擇了文件

if(!input.value){return;}

  已開始我是沒做這個判斷的,在后來的測試過程中發現,當上傳一次文件后,再次點擊按鈕上傳,打開文件選擇框,然后不選擇文件,而是點擊取消按鈕,change事件也觸發了,導致后面的代碼也會執行,顯然這不合理,故加了這個判斷。

  然后限制了下上傳文件的大小(這樣的事能夠前端處理就不要交給服務端來驗證了),當文件大小超過最大限制,就會彈框提示

復制代碼
if(option.maxSize &&  input.files[0].size > option.maxSize * 1024 * 1024){
            dialog({
                title: '提示',
                content: '請上傳小於'+option.maxSize+'M的文件',
                okValue: '確定',
                ok: function () {}
            }).showModal();
            return;
        }
復制代碼

  然后加了一個文件上傳前的操作,可以在文件上傳前做一些處理,如進度條的顯示,圖片預覽等等

       if(option.beforeSend instanceof Function){
            if(option.beforeSend(input) === false){
                return false;
            }
        }   

 

  接下來將文件append到formData對象里,使用字段名‘file’,該字段名是服務端接收文件時使用的字段名

fd.append('file', input.files[0]);

  然后就是使用XMLHttpRequest對象向服務端發送數據了

復制代碼
        xhr.open('post', option.url);
        xhr.onreadystatechange = function(){
            if(xhr.status == 200){
                if(xhr.readyState == 4){
                    if(option.callback instanceof Function){
                        option.callback(xhr.responseText);
                    }
                }
            }else{
                if(!(dialog.get('uploadfail'))){
                    dialog({
                        id: 'uploadfail',
                        title: '提示',
                        content: '上傳失敗',
                        okValue: '確定',
                        ok: function () {}
                    }).showModal();
                }
            }
        }
        xhr.upload.onprogress = function(event){
            var pre = Math.floor(100 * event.loaded / event.total);
            if(option.uploading instanceof Function){
                option.uploading(pre);
            }
        }
        xhr.send(fd);    
復制代碼

  在向服務端發送數據時,使用了監聽了一下progress事件,主要是為了進行上傳進度的顯示,上述代碼中,

var pre = Math.floor(100 * event.loaded / event.total);

  獲取上傳的百分比,能夠拿到這個值,頁面上就可以展示各種各樣的上傳進度效果了。

 

  差不多介紹完了,下面補充一下使用中遇到的問題:

  問題一:文件在上傳的過程中,使用JSON.parse()序列化服務端返回的json字符串報錯(傻啊,文件還在上傳,服務端怎么會返回數據啊)。

事情是這樣的,一開始,我在readystatechange里只監聽了狀態碼是否是200,如果是就說明通了,然后執行回調,在回調里處理服務端返回的數據,但是通了不一定代表服務端已經返回了數據,所以就出現了上面的錯誤,所以后來在判斷了status是否為200后,還判斷了readyState,以確保服務端已處理完畢並返回數據在執行回調

            if(xhr.status == 200){
                    if(option.callback instanceof Function){
                        option.callback(xhr.responseText);
                    }
            }    

  問題二:重復創建input。每次點擊按鈕上傳文件后,頁面都會多一個type=file的input感覺不是很好(個人癖好吧),所以對最開始的初始化代碼做了下優化,判斷當前頁面是否存在剛才創建的input,存在就直接使用,不存在就創建,如下

復制代碼
    if(document.getElementById('myUploadInput')){
        input = document.getElementById('myUploadInput');
    }else{
        input = document.createElement('input');
        input.setAttribute('id', 'myUploadInput');
        input.setAttribute('type', 'file');
        input.setAttribute('name', 'file');
        document.body.appendChild(input);
        input.style.display = 'none';
    }
復制代碼

 

好了,就這么多了。看看效果

 

 

Resumable.js是一個JavaScript庫,通過HTML5 File API來為應用加入多文件同步上傳、穩定傳輸和斷點續傳功能。

 http://www.resumablejs.com/

 


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM