ASP.NET Web API編程——文件下載

斷點續傳基本原理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);
        }

 

---------------------------------------------------------------------

轉載與引用請註明出處。

時間倉促,水平有限,若有不當之處,歡迎指正。

相關文章
相關標籤/搜索