斷點續傳基本原理web
HTTP協議中與斷點續傳相關的HTTP頭爲:Range和Content-Range標頭,斷點續傳實現流程:
1)客戶端請求下載一個文件,文件的總長度爲n;已經下載了一部分文件,長度爲m(單位KB)
2) 客戶端主動暫停下載或網絡中斷,客戶端請求繼續下載,HTTP請求標頭設置爲:windows
Range:bytes=m-
3) 服務端收到斷點續傳請求,從文件的m位置開始傳輸,HTTP響應頭設置爲:
Content-Range:bytes m-n/n,服務端返回的HTTP狀態碼是206。api
HTTP請求與響應實例(使用wireshark抓取HTTP報文):瀏覽器
第一次請求的請求頭:緩存
暫停後,再次請求的請求頭:服務器
某次暫停後再次發起的請求和返回的響應頭:網絡
Web API提供了對上述標頭的支持:app
HttpRequestMessage.Headers.Range:設置請求頭的Range標頭,Range的類型是RangeHeaderValue,RangeHeaderValue有一個類型爲ICollection<RangeItemHeaderValue>的屬性Ranges,RangeItemHeaderValue有兩個類型爲long的屬性From和To,這兩個屬性分別表達了請求數據的開始和結束位置。async
HttpResponseHeaders.AcceptRanges屬性設置Accept-Ranges標頭,HttpResponseMessage.Content屬性的Headers屬性設置響應內容標頭,q其類型爲HttpContentHeaders,HttpContentHeaders.ContentDisposition屬性設置Content-Disposition標頭值,ContentDisposition屬性類型爲ContentDispositionHeaderValue,可以使用ContentDispositionHeaderValue.FileName設置文件名。HttpContentHeaders.ContentTypes屬性設置Content-Type標頭。HttpContentHeaders.ContentRangese設置響應的消息體的數據範圍。ide
2、示例
Get請求,調用url:http://localhost/webApi_test/api/download?filecode=KBase[V11.0%2020140828]&filetype=exe
1使用StreamContent向消息體中寫數據
使用StreamContent適合將磁盤文件流直接「掛」到響應流,對於那種數據源是另外一個服務,或者數據來自本地磁盤,可是沒法將文件流直接掛到響應流(可能對文件要進行編碼轉換或加密解密等操做)的情形不適合使用StreamContent,由於直接將流「掛」到響應流,能夠實現對服務器緩存的控制,已實如今服務器和客戶端之間創建一個管道,一點一點地,源源不斷將數據傳送給客戶端,而沒必要一次將數據都讀入內存,這樣極大的節省了內存,同時也使得傳輸大文件成爲了可能。
控制器及操做:
public class DownloadController : ApiController { public HttpResponseMessage Get([FromUri]Input input) { string filePath = string.Format(@"D:\工具軟件\{0}.{1}", input.FileCode, input.FileType); string fileName = Path.GetFileName(filePath); DiskFileProvider fileProvider = new DiskFileProvider(filePath); long entireLength = fileProvider.GetLength(); ContentInfo contentInfo = GetContentInfoFromRequest(entireLength, this.Request); Stream partialStream = fileProvider.GetPartialStream(contentInfo.From); HttpContent content = new StreamContent(partialStream, 1024); return SetResponse(content, contentInfo, entireLength,fileName); } }
得到請求信息,包括:文件的總長度,請求數據的額範圍,是否支持多個範圍。
private ContentInfo GetContentInfoFromRequest(long entireLength, HttpRequestMessage request) { var contentInfo = new ContentInfo { From = 0, To = entireLength - 1, IsPartial = false, Length = entireLength }; RangeHeaderValue rangeHeader = request.Headers.Range; if (rangeHeader != null && rangeHeader.Ranges.Count != 0) { //僅支持一個range if (rangeHeader.Ranges.Count > 1) { throw new HttpResponseException(HttpStatusCode.BadRequest); } RangeItemHeaderValue range = rangeHeader.Ranges.First(); if (range.From.HasValue && range.From < 0 || range.To.HasValue && range.To > entireLength - 1) { throw new HttpResponseException(HttpStatusCode.BadRequest); } contentInfo.From = range.From ?? 0; contentInfo.To = range.To ?? entireLength - 1; contentInfo.IsPartial = true; contentInfo.Length = entireLength; if (range.From.HasValue && range.To.HasValue) { contentInfo.Length = range.To.Value - range.From.Value + 1; } else if (range.From.HasValue) { contentInfo.Length = entireLength - range.From.Value; } else if (range.To.HasValue) { contentInfo.Length = range.To.Value + 1; } } return contentInfo; }
設置響應,對上述介紹的響應內容標頭字段進行合理的設置。
private HttpResponseMessage SetResponse(HttpContent content, ContentInfo contentInfo, long entireLength,string fileName) { HttpResponseMessage response = new HttpResponseMessage(); //設置Accept-Ranges:bytes response.Headers.AcceptRanges.Add("bytes"); //設置傳輸部分數據時,若是成功,那麼狀態碼爲206 response.StatusCode = contentInfo.IsPartial ? HttpStatusCode.PartialContent : HttpStatusCode.OK; //設置響應內容 response.Content = content; //Content-Disposition設置爲attachment,指示瀏覽器客戶端彈出下載框。 response.Content.Headers.ContentDisposition = new ContentDispositionHeaderValue("attachment"); //設置下載文件的文件名 response.Content.Headers.ContentDisposition.FileName = fileName; //設置Content-Type:application/octet-stream response.Content.Headers.ContentType = new MediaTypeHeaderValue("application/octet-stream"); //設置響應消息內容長度 response.Content.Headers.ContentLength = contentInfo.Length; if (contentInfo.IsPartial) { //設置響應內容的起始位置 response.Content.Headers.ContentRange = new ContentRangeHeaderValue(contentInfo.From, contentInfo.To, entireLength); } return response; }
數據源訪問接口:
public interface IFileProvider { bool Exists(); Stream GetPartialStream(long offset); long GetLength(); }
數據源接口實現
public class DiskFileProvider : IFileProvider,IDisposable { private Stream fileStream; private string filePath; public DiskFileProvider(string filePath) { try { this.filePath = filePath; this.fileStream = new FileStream(filePath, FileMode.Open,FileAccess.Read,FileShare.Read); } catch (Exception ex) { } } public bool Exists() { return File.Exists(filePath); } public Stream GetPartialStream(long offset) { if (offset > 0) { fileStream.Seek(offset, SeekOrigin.Begin); } return fileStream; } public long GetLength() { return fileStream.Length; } public void Dispose() { if(fileStream!=null)fileStream.Close(); } }
數據模型:請求參數模型和請求數據信息模型
public class Input { public string FileCode { set; get; } public string FileType { set; get; } } public class ContentInfo { public long From {set;get;} public long To { set; get; } public bool IsPartial { set; get; } public long Length { set; get; } }
2使用PushStreamContent
爲了使用PushStreamContent須要對IFileProvider進行改造,以下:
public interface IFileProvider { long Offset{set;get;} bool Exists(); Stream GetPartialStream(long offset); Task WriteToStream(Stream outputStream, HttpContent content, TransportContext context); long GetLength(); }
能夠發現與原來的接口相比較多了Offset屬性和WriteToStream方法。
下面是IFileProvider接口的實現,爲了使用PushStreamContent,實現接口的WriteToStream方法,這裏須要注意:
PushStreamContent構造函數有幾個重載的方法,他們的共同特色是含有委託類型的參數。而本文采用了有返回值的參數,經實踐發現採用無返回值的參數,會隨機地生成一條windows警告日誌。另外調用FileStream.Read函數時,其參數都是int類型的,可是FileStream.Length倒是long類型的,在使用時就須要轉型,不要將FileStream.Length,而應在(int)Math.Min(length, (long)buffer.Length)這部分執行轉型,這樣若是FileStream.Length真的比int類型的最大值還大,那麼也不會由於轉型而出現錯誤。
public class ByteToStream : IFileProvider { private string filePath; public long Offset{set;get;} public ByteToStream(string filePath) { try { this.filePath = filePath; } catch (Exception ex) { } } public bool Exists() { return File.Exists(filePath); } public Stream GetPartialStream(long offset) { throw new NotImplementedException(); } public long GetLength() { using (FileStream fileStream = new FileStream(filePath, FileMode.Open, FileAccess.Read)) { return fileStream.Length; } } public async Task WriteToStream(Stream outputStream, HttpContent content, TransportContext context) { try { var buffer = new byte[1024000]; using (FileStream fileStream = new FileStream(filePath, FileMode.Open, FileAccess.Read)) { fileStream.Seek(Offset, SeekOrigin.Begin); long length = fileStream.Length; var bytesRead = 1; while (length > 0 && bytesRead > 0) { bytesRead = fileStream.Read(buffer, 0, (int)Math.Min(length, (long)buffer.Length)); await outputStream.WriteAsync(buffer, 0, bytesRead); length -= bytesRead; } } } catch (HttpException ex) { return; } finally { outputStream.Close(); } }
}
控制器操做相應地變爲:
public HttpResponseMessage Get([FromUri]Input input) { string filePath = string.Format(@"D:\工具軟件\{0}.{1}", input.FileCode, input.FileType); string fileName = Path.GetFileName(filePath); IFileProvider fileProvider = new ByteToStream(filePath); long entireLength = fileProvider.GetLength(); ContentInfo contentInfo = GetContentInfoFromRequest(entireLength, this.Request); Func<Stream, HttpContent, TransportContext, Task> onStreamAvailable = fileProvider.WriteToStream; HttpContent content = new PushStreamContent(onStreamAvailable); return SetResponse(content, contentInfo, entireLength,fileName); }
---------------------------------------------------------------------
轉載與引用請註明出處。
時間倉促,水平有限,若有不當之處,歡迎指正。