[{"data":1,"prerenderedAt":1474},["ShallowReactive",2],{"page-\u002Fcsharp\u002Fc的流传输文件和断点下载":3},{"id":4,"title":5,"body":6,"description":15,"extension":1468,"meta":1469,"navigation":62,"path":1470,"seo":1471,"stem":1472,"__hash__":1473},"content\u002Fcsharp\u002Fc#的流传输文件和断点下载.md","C#的流传输文件和断点下载",{"type":7,"value":8,"toc":1465},"minimark",[9,664,669,698,702,732,744,777,783,814,824,827,834,878,883,935,941,972,977,1006,1020,1031,1047,1065,1070,1089,1098,1120,1127,1144,1173,1193,1257,1278,1328,1335,1377,1398,1405,1412,1419,1461],[10,11,16],"pre",{"className":12,"code":13,"language":14,"meta":15,"style":15},"language-csharp shiki shiki-themes github-light github-dark","using System.IO;\nusing System.Net.Http;\nusing System.Net.Http.Headers;\nusing System.Threading.Tasks;\nusing System;\nusing Microsoft.Extensions.Logging;\n\nnamespace qqbot.Services \u002F\u002F 请确保命名空间正确\n{\n    public record DownloadProgress(long BytesDownloaded, long TotalBytes);\n\n    public class FileCacheHttpService : IDisposable\n    {\n        private readonly HttpClient _httpClient;\n        private readonly ILogger\u003CFileCacheHttpService> _logger;\n        public int Retries { get; set; } = 5;\n\n        public FileCacheHttpService(HttpClient httpClient, ILogger\u003CFileCacheHttpService> logger)\n        {\n            _logger = logger;\n            _httpClient = httpClient;\n            _httpClient.Timeout = TimeSpan.FromHours(1); \u002F\u002F 保持一个较长的超时\n        }\n\n        public async Task DownlaodFileAsync(string url, string destinationPath,\n            IProgress\u003CDownloadProgress>? progress = null,\n            CancellationToken cancellationToken = default\n            )\n        {\n            for (int i = 0; i \u003C Retries; i++)\n            {\n                try\n                {\n                    _logger.LogInformation(\"开始缓存文件 {Url}\", url);\n                    await PerformDownloadAsync(url, destinationPath, progress, cancellationToken);\n                    _logger.LogInformation(\"文件 {DestinationPath} 下载成功。\", destinationPath);\n                    \n                    \u002F\u002F ✅ 关键修正 1: 成功后立即返回，不再继续循环。\n                    return; \n                }\n                catch (Exception ex)\n                {\n                    _logger.LogWarning(ex, \"第 {Attempt} 次下载失败。将在5秒后重试...\", i + 1);\n                    if (i == Retries - 1)\n                    {\n                        _logger.LogError(ex, \"已达到最大重试次数，下载文件 {Url} 失败。\", url);\n                        throw; \u002F\u002F 达到最大次数后，将异常抛出\n                    }\n                    await Task.Delay(5000, cancellationToken);\n                }\n            }\n        }\n\n        private async Task PerformDownloadAsync(\n            string url, string destinationPath, \u002F\u002F 修正了参数名的拼写错误\n            IProgress\u003CDownloadProgress>? progress,\n            CancellationToken cancellationToken\n            )\n        {\n            long existingFileSize = 0;\n            if (File.Exists(destinationPath))\n            {\n                existingFileSize = new FileInfo(destinationPath).Length;\n            }\n\n            var request = new HttpRequestMessage(HttpMethod.Get, url);\n            if (existingFileSize > 0)\n            {\n                request.Headers.Range = new RangeHeaderValue(existingFileSize, null);\n                _logger.LogInformation(\"文件已存在，从 {Bytes} 字节处继续下载。\", existingFileSize);\n            }\n\n            using var response = await _httpClient.SendAsync(request, HttpCompletionOption.ResponseHeadersRead, cancellationToken);\n            \n            if (response.StatusCode == System.Net.HttpStatusCode.OK)\n            {\n                existingFileSize = 0;\n            }\n            else if (response.StatusCode != System.Net.HttpStatusCode.PartialContent && existingFileSize > 0)\n            {\n                throw new InvalidOperationException($\"服务器不支持断点续传 (状态码: {response.StatusCode})\");\n            }\n            response.EnsureSuccessStatusCode();\n\n            long totalBytes = response.Content.Headers.ContentLength ?? 0;\n            if (existingFileSize > 0 && response.Content.Headers.ContentRange != null)\n            {\n                totalBytes = response.Content.Headers.ContentRange.Length ?? 0;\n            }\n            _logger.LogInformation(\"开始下载，总大小 {TotalBytes} 字节。\", totalBytes);\n\n            using var contentStream = await response.Content.ReadAsStreamAsync(cancellationToken);\n            using var fileStream = new FileStream(destinationPath, existingFileSize == 0 ? FileMode.Create : FileMode.Append, FileAccess.Write, FileShare.None);\n            \n            var buffer = new byte[81920];\n            long totalBytesRead = existingFileSize;\n            int bytesRead;\n            while ((bytesRead = await contentStream.ReadAsync(buffer, 0, buffer.Length, cancellationToken)) > 0)\n            {\n                await fileStream.WriteAsync(buffer, 0, bytesRead, cancellationToken);\n                totalBytesRead += bytesRead;\n                progress?.Report(new DownloadProgress(totalBytesRead, totalBytes));\n            }\n        }\n        \n        public void Dispose()\n        {\n            \u002F\u002F ✅ 关键修正 2: 正确实现 Dispose\n            _httpClient?.Dispose();\n        }\n    }\n}\n","csharp","",[17,18,19,27,33,39,45,51,57,64,70,76,82,87,93,99,105,111,117,122,128,134,140,146,152,158,163,169,175,181,187,192,198,204,210,216,222,228,234,240,246,252,258,264,269,275,281,287,293,299,305,311,316,322,327,332,338,344,350,356,361,366,372,378,383,389,394,399,405,411,416,422,428,433,438,444,450,456,461,467,472,478,483,489,494,500,505,511,517,522,528,533,539,544,550,556,561,567,573,579,585,590,596,602,608,613,618,624,630,635,641,647,652,658],"code",{"__ignoreMap":15},[20,21,24],"span",{"class":22,"line":23},"line",1,[20,25,26],{},"using System.IO;\n",[20,28,30],{"class":22,"line":29},2,[20,31,32],{},"using System.Net.Http;\n",[20,34,36],{"class":22,"line":35},3,[20,37,38],{},"using System.Net.Http.Headers;\n",[20,40,42],{"class":22,"line":41},4,[20,43,44],{},"using System.Threading.Tasks;\n",[20,46,48],{"class":22,"line":47},5,[20,49,50],{},"using System;\n",[20,52,54],{"class":22,"line":53},6,[20,55,56],{},"using Microsoft.Extensions.Logging;\n",[20,58,60],{"class":22,"line":59},7,[20,61,63],{"emptyLinePlaceholder":62},true,"\n",[20,65,67],{"class":22,"line":66},8,[20,68,69],{},"namespace qqbot.Services \u002F\u002F 请确保命名空间正确\n",[20,71,73],{"class":22,"line":72},9,[20,74,75],{},"{\n",[20,77,79],{"class":22,"line":78},10,[20,80,81],{},"    public record DownloadProgress(long BytesDownloaded, long TotalBytes);\n",[20,83,85],{"class":22,"line":84},11,[20,86,63],{"emptyLinePlaceholder":62},[20,88,90],{"class":22,"line":89},12,[20,91,92],{},"    public class FileCacheHttpService : IDisposable\n",[20,94,96],{"class":22,"line":95},13,[20,97,98],{},"    {\n",[20,100,102],{"class":22,"line":101},14,[20,103,104],{},"        private readonly HttpClient _httpClient;\n",[20,106,108],{"class":22,"line":107},15,[20,109,110],{},"        private readonly ILogger\u003CFileCacheHttpService> _logger;\n",[20,112,114],{"class":22,"line":113},16,[20,115,116],{},"        public int Retries { get; set; } = 5;\n",[20,118,120],{"class":22,"line":119},17,[20,121,63],{"emptyLinePlaceholder":62},[20,123,125],{"class":22,"line":124},18,[20,126,127],{},"        public FileCacheHttpService(HttpClient httpClient, ILogger\u003CFileCacheHttpService> logger)\n",[20,129,131],{"class":22,"line":130},19,[20,132,133],{},"        {\n",[20,135,137],{"class":22,"line":136},20,[20,138,139],{},"            _logger = logger;\n",[20,141,143],{"class":22,"line":142},21,[20,144,145],{},"            _httpClient = httpClient;\n",[20,147,149],{"class":22,"line":148},22,[20,150,151],{},"            _httpClient.Timeout = TimeSpan.FromHours(1); \u002F\u002F 保持一个较长的超时\n",[20,153,155],{"class":22,"line":154},23,[20,156,157],{},"        }\n",[20,159,161],{"class":22,"line":160},24,[20,162,63],{"emptyLinePlaceholder":62},[20,164,166],{"class":22,"line":165},25,[20,167,168],{},"        public async Task DownlaodFileAsync(string url, string destinationPath,\n",[20,170,172],{"class":22,"line":171},26,[20,173,174],{},"            IProgress\u003CDownloadProgress>? progress = null,\n",[20,176,178],{"class":22,"line":177},27,[20,179,180],{},"            CancellationToken cancellationToken = default\n",[20,182,184],{"class":22,"line":183},28,[20,185,186],{},"            )\n",[20,188,190],{"class":22,"line":189},29,[20,191,133],{},[20,193,195],{"class":22,"line":194},30,[20,196,197],{},"            for (int i = 0; i \u003C Retries; i++)\n",[20,199,201],{"class":22,"line":200},31,[20,202,203],{},"            {\n",[20,205,207],{"class":22,"line":206},32,[20,208,209],{},"                try\n",[20,211,213],{"class":22,"line":212},33,[20,214,215],{},"                {\n",[20,217,219],{"class":22,"line":218},34,[20,220,221],{},"                    _logger.LogInformation(\"开始缓存文件 {Url}\", url);\n",[20,223,225],{"class":22,"line":224},35,[20,226,227],{},"                    await PerformDownloadAsync(url, destinationPath, progress, cancellationToken);\n",[20,229,231],{"class":22,"line":230},36,[20,232,233],{},"                    _logger.LogInformation(\"文件 {DestinationPath} 下载成功。\", destinationPath);\n",[20,235,237],{"class":22,"line":236},37,[20,238,239],{},"                    \n",[20,241,243],{"class":22,"line":242},38,[20,244,245],{},"                    \u002F\u002F ✅ 关键修正 1: 成功后立即返回，不再继续循环。\n",[20,247,249],{"class":22,"line":248},39,[20,250,251],{},"                    return; \n",[20,253,255],{"class":22,"line":254},40,[20,256,257],{},"                }\n",[20,259,261],{"class":22,"line":260},41,[20,262,263],{},"                catch (Exception ex)\n",[20,265,267],{"class":22,"line":266},42,[20,268,215],{},[20,270,272],{"class":22,"line":271},43,[20,273,274],{},"                    _logger.LogWarning(ex, \"第 {Attempt} 次下载失败。将在5秒后重试...\", i + 1);\n",[20,276,278],{"class":22,"line":277},44,[20,279,280],{},"                    if (i == Retries - 1)\n",[20,282,284],{"class":22,"line":283},45,[20,285,286],{},"                    {\n",[20,288,290],{"class":22,"line":289},46,[20,291,292],{},"                        _logger.LogError(ex, \"已达到最大重试次数，下载文件 {Url} 失败。\", url);\n",[20,294,296],{"class":22,"line":295},47,[20,297,298],{},"                        throw; \u002F\u002F 达到最大次数后，将异常抛出\n",[20,300,302],{"class":22,"line":301},48,[20,303,304],{},"                    }\n",[20,306,308],{"class":22,"line":307},49,[20,309,310],{},"                    await Task.Delay(5000, cancellationToken);\n",[20,312,314],{"class":22,"line":313},50,[20,315,257],{},[20,317,319],{"class":22,"line":318},51,[20,320,321],{},"            }\n",[20,323,325],{"class":22,"line":324},52,[20,326,157],{},[20,328,330],{"class":22,"line":329},53,[20,331,63],{"emptyLinePlaceholder":62},[20,333,335],{"class":22,"line":334},54,[20,336,337],{},"        private async Task PerformDownloadAsync(\n",[20,339,341],{"class":22,"line":340},55,[20,342,343],{},"            string url, string destinationPath, \u002F\u002F 修正了参数名的拼写错误\n",[20,345,347],{"class":22,"line":346},56,[20,348,349],{},"            IProgress\u003CDownloadProgress>? progress,\n",[20,351,353],{"class":22,"line":352},57,[20,354,355],{},"            CancellationToken cancellationToken\n",[20,357,359],{"class":22,"line":358},58,[20,360,186],{},[20,362,364],{"class":22,"line":363},59,[20,365,133],{},[20,367,369],{"class":22,"line":368},60,[20,370,371],{},"            long existingFileSize = 0;\n",[20,373,375],{"class":22,"line":374},61,[20,376,377],{},"            if (File.Exists(destinationPath))\n",[20,379,381],{"class":22,"line":380},62,[20,382,203],{},[20,384,386],{"class":22,"line":385},63,[20,387,388],{},"                existingFileSize = new FileInfo(destinationPath).Length;\n",[20,390,392],{"class":22,"line":391},64,[20,393,321],{},[20,395,397],{"class":22,"line":396},65,[20,398,63],{"emptyLinePlaceholder":62},[20,400,402],{"class":22,"line":401},66,[20,403,404],{},"            var request = new HttpRequestMessage(HttpMethod.Get, url);\n",[20,406,408],{"class":22,"line":407},67,[20,409,410],{},"            if (existingFileSize > 0)\n",[20,412,414],{"class":22,"line":413},68,[20,415,203],{},[20,417,419],{"class":22,"line":418},69,[20,420,421],{},"                request.Headers.Range = new RangeHeaderValue(existingFileSize, null);\n",[20,423,425],{"class":22,"line":424},70,[20,426,427],{},"                _logger.LogInformation(\"文件已存在，从 {Bytes} 字节处继续下载。\", existingFileSize);\n",[20,429,431],{"class":22,"line":430},71,[20,432,321],{},[20,434,436],{"class":22,"line":435},72,[20,437,63],{"emptyLinePlaceholder":62},[20,439,441],{"class":22,"line":440},73,[20,442,443],{},"            using var response = await _httpClient.SendAsync(request, HttpCompletionOption.ResponseHeadersRead, cancellationToken);\n",[20,445,447],{"class":22,"line":446},74,[20,448,449],{},"            \n",[20,451,453],{"class":22,"line":452},75,[20,454,455],{},"            if (response.StatusCode == System.Net.HttpStatusCode.OK)\n",[20,457,459],{"class":22,"line":458},76,[20,460,203],{},[20,462,464],{"class":22,"line":463},77,[20,465,466],{},"                existingFileSize = 0;\n",[20,468,470],{"class":22,"line":469},78,[20,471,321],{},[20,473,475],{"class":22,"line":474},79,[20,476,477],{},"            else if (response.StatusCode != System.Net.HttpStatusCode.PartialContent && existingFileSize > 0)\n",[20,479,481],{"class":22,"line":480},80,[20,482,203],{},[20,484,486],{"class":22,"line":485},81,[20,487,488],{},"                throw new InvalidOperationException($\"服务器不支持断点续传 (状态码: {response.StatusCode})\");\n",[20,490,492],{"class":22,"line":491},82,[20,493,321],{},[20,495,497],{"class":22,"line":496},83,[20,498,499],{},"            response.EnsureSuccessStatusCode();\n",[20,501,503],{"class":22,"line":502},84,[20,504,63],{"emptyLinePlaceholder":62},[20,506,508],{"class":22,"line":507},85,[20,509,510],{},"            long totalBytes = response.Content.Headers.ContentLength ?? 0;\n",[20,512,514],{"class":22,"line":513},86,[20,515,516],{},"            if (existingFileSize > 0 && response.Content.Headers.ContentRange != null)\n",[20,518,520],{"class":22,"line":519},87,[20,521,203],{},[20,523,525],{"class":22,"line":524},88,[20,526,527],{},"                totalBytes = response.Content.Headers.ContentRange.Length ?? 0;\n",[20,529,531],{"class":22,"line":530},89,[20,532,321],{},[20,534,536],{"class":22,"line":535},90,[20,537,538],{},"            _logger.LogInformation(\"开始下载，总大小 {TotalBytes} 字节。\", totalBytes);\n",[20,540,542],{"class":22,"line":541},91,[20,543,63],{"emptyLinePlaceholder":62},[20,545,547],{"class":22,"line":546},92,[20,548,549],{},"            using var contentStream = await response.Content.ReadAsStreamAsync(cancellationToken);\n",[20,551,553],{"class":22,"line":552},93,[20,554,555],{},"            using var fileStream = new FileStream(destinationPath, existingFileSize == 0 ? FileMode.Create : FileMode.Append, FileAccess.Write, FileShare.None);\n",[20,557,559],{"class":22,"line":558},94,[20,560,449],{},[20,562,564],{"class":22,"line":563},95,[20,565,566],{},"            var buffer = new byte[81920];\n",[20,568,570],{"class":22,"line":569},96,[20,571,572],{},"            long totalBytesRead = existingFileSize;\n",[20,574,576],{"class":22,"line":575},97,[20,577,578],{},"            int bytesRead;\n",[20,580,582],{"class":22,"line":581},98,[20,583,584],{},"            while ((bytesRead = await contentStream.ReadAsync(buffer, 0, buffer.Length, cancellationToken)) > 0)\n",[20,586,588],{"class":22,"line":587},99,[20,589,203],{},[20,591,593],{"class":22,"line":592},100,[20,594,595],{},"                await fileStream.WriteAsync(buffer, 0, bytesRead, cancellationToken);\n",[20,597,599],{"class":22,"line":598},101,[20,600,601],{},"                totalBytesRead += bytesRead;\n",[20,603,605],{"class":22,"line":604},102,[20,606,607],{},"                progress?.Report(new DownloadProgress(totalBytesRead, totalBytes));\n",[20,609,611],{"class":22,"line":610},103,[20,612,321],{},[20,614,616],{"class":22,"line":615},104,[20,617,157],{},[20,619,621],{"class":22,"line":620},105,[20,622,623],{},"        \n",[20,625,627],{"class":22,"line":626},106,[20,628,629],{},"        public void Dispose()\n",[20,631,633],{"class":22,"line":632},107,[20,634,133],{},[20,636,638],{"class":22,"line":637},108,[20,639,640],{},"            \u002F\u002F ✅ 关键修正 2: 正确实现 Dispose\n",[20,642,644],{"class":22,"line":643},109,[20,645,646],{},"            _httpClient?.Dispose();\n",[20,648,650],{"class":22,"line":649},110,[20,651,157],{},[20,653,655],{"class":22,"line":654},111,[20,656,657],{},"    }\n",[20,659,661],{"class":22,"line":660},112,[20,662,663],{},"}\n",[665,666,668],"h3",{"id":667},"检测文件的下载点是否存在文件","检测文件的下载点，是否存在文件",[10,670,672],{"className":12,"code":671,"language":14,"meta":15,"style":15},"        long existingFileSize = 0;\n        \u002F\u002F 检查下载点\n        if (File.Exists(desinationPath)){\n            existingFileSize = new FileInfo(desinationPath).Length;\n        }\n",[17,673,674,679,684,689,694],{"__ignoreMap":15},[20,675,676],{"class":22,"line":23},[20,677,678],{},"        long existingFileSize = 0;\n",[20,680,681],{"class":22,"line":29},[20,682,683],{},"        \u002F\u002F 检查下载点\n",[20,685,686],{"class":22,"line":35},[20,687,688],{},"        if (File.Exists(desinationPath)){\n",[20,690,691],{"class":22,"line":41},[20,692,693],{},"            existingFileSize = new FileInfo(desinationPath).Length;\n",[20,695,696],{"class":22,"line":47},[20,697,157],{},[699,700,701],"p",{},"​\t在检查完是否已经存在了之后，开始建立一个请求，尝试从断点开始下载：",[10,703,705],{"className":12,"code":704,"language":14,"meta":15,"style":15}," var request = new HttpRequestMessage(HttpMethod.Get, url); \u002F\u002F 创建Url的http请求\n if (existingFileSize > 0){\n     request.Headers.Range = new RangeHeaderValue(existingFileSize, null);\n     _logger.LogInformation(\"文件已经存在，开始从{Bytes字节下载}\", existingFileSize);\n }\n",[17,706,707,712,717,722,727],{"__ignoreMap":15},[20,708,709],{"class":22,"line":23},[20,710,711],{}," var request = new HttpRequestMessage(HttpMethod.Get, url); \u002F\u002F 创建Url的http请求\n",[20,713,714],{"class":22,"line":29},[20,715,716],{}," if (existingFileSize > 0){\n",[20,718,719],{"class":22,"line":35},[20,720,721],{},"     request.Headers.Range = new RangeHeaderValue(existingFileSize, null);\n",[20,723,724],{"class":22,"line":41},[20,725,726],{},"     _logger.LogInformation(\"文件已经存在，开始从{Bytes字节下载}\", existingFileSize);\n",[20,728,729],{"class":22,"line":47},[20,730,731],{}," }\n",[699,733,734,735,739,740,743],{},"​\t之后获取响应头：为了",[736,737,738],"strong",{},"避免内存爆炸","和实现",[736,741,742],{},"真正的流式处理","。",[10,745,747],{"className":12,"code":746,"language":14,"meta":15,"style":15},"        \u002F\u002F 确保先获取响应头，而不会缓冲整个响应体\n        using var response = await _httpClient.SendAsync(request, HttpCompletionOption.ResponseHeadersRead, cancellationToken);\n        if (response.StatusCode == System.Net.HttpStatusCode.OK){ \u002F\u002F 不支持范围请求的情况\n\n            existingFileSize = 0; \u002F\u002F\n        }\n",[17,748,749,754,759,764,768,773],{"__ignoreMap":15},[20,750,751],{"class":22,"line":23},[20,752,753],{},"        \u002F\u002F 确保先获取响应头，而不会缓冲整个响应体\n",[20,755,756],{"class":22,"line":29},[20,757,758],{},"        using var response = await _httpClient.SendAsync(request, HttpCompletionOption.ResponseHeadersRead, cancellationToken);\n",[20,760,761],{"class":22,"line":35},[20,762,763],{},"        if (response.StatusCode == System.Net.HttpStatusCode.OK){ \u002F\u002F 不支持范围请求的情况\n",[20,765,766],{"class":22,"line":41},[20,767,63],{"emptyLinePlaceholder":62},[20,769,770],{"class":22,"line":47},[20,771,772],{},"            existingFileSize = 0; \u002F\u002F\n",[20,774,775],{"class":22,"line":53},[20,776,157],{},[699,778,779,782],{},[17,780,781],{},"_httpClient"," 在后台会执行以下操作：",[784,785,786,790,796,803],"ol",{},[787,788,789],"li",{},"发送 GET 请求。",[787,791,792,793,743],{},"接收到",[736,794,795],{},"响应头",[787,797,798,799,802],{},"开始接收响应体（文件内容），并将其",[736,800,801],{},"全部缓冲","到一个内部的内存流中。",[787,804,805,806,809,810,813],{},"当",[736,807,808],{},"整个文件","都下载到内存中之后，",[17,811,812],{},"await"," 操作才会完成，您的代码才会继续执行。",[699,815,816,819,820,823],{},[736,817,818],{},"后果",": 如果下载一个 1GB 的文件，程序会",[736,821,822],{},"瞬间消耗 1GB 的内存","。这对于服务器应用或同时进行多个下载的客户端来说是致命的。",[699,825,826],{},"​",[699,828,829,830,833],{},"​\t因此我们使用",[17,831,832],{},"ResponseHeadersRead"," 模式",[10,835,837],{"className":12,"code":836,"language":14,"meta":15,"style":15},"        using var response = await _httpClient.SendAsync(request, HttpCompletionOption.ResponseHeadersRead, cancellationToken);\n        if (response.StatusCode == System.Net.HttpStatusCode.OK){ \u002F\u002F 不支持范围请求的情况\n\n            existingFileSize = 0; \u002F\u002F\n        }\n        else if (response.StatusCode != System.Net.HttpStatusCode.PartialContent && existingFileSize > 0) {\n            throw new InvalidOperationException($\"服务器不支持断点续传 (状态码: {response.StatusCode})\");\n        }\n        response.EnsureSuccessStatusCode();\n",[17,838,839,843,847,851,855,859,864,869,873],{"__ignoreMap":15},[20,840,841],{"class":22,"line":23},[20,842,758],{},[20,844,845],{"class":22,"line":29},[20,846,763],{},[20,848,849],{"class":22,"line":35},[20,850,63],{"emptyLinePlaceholder":62},[20,852,853],{"class":22,"line":41},[20,854,772],{},[20,856,857],{"class":22,"line":47},[20,858,157],{},[20,860,861],{"class":22,"line":53},[20,862,863],{},"        else if (response.StatusCode != System.Net.HttpStatusCode.PartialContent && existingFileSize > 0) {\n",[20,865,866],{"class":22,"line":59},[20,867,868],{},"            throw new InvalidOperationException($\"服务器不支持断点续传 (状态码: {response.StatusCode})\");\n",[20,870,871],{"class":22,"line":66},[20,872,157],{},[20,874,875],{"class":22,"line":72},[20,876,877],{},"        response.EnsureSuccessStatusCode();\n",[699,879,880,882],{},[17,881,781],{}," 的行为会完全改变：",[784,884,885,888,901,911,925],{},[787,886,887],{},"发送请求。",[787,889,890,893,894,896,897,900],{},[736,891,892],{},"一旦接收到响应头","，",[17,895,812],{}," 操作",[736,898,899],{},"立即完成","，您的代码就可以继续执行。",[787,902,903,904,907,908,743],{},"此时，响应体（文件内容）还",[736,905,906],{},"停留在底层的网络套接字缓冲区","中，",[736,909,910],{},"几乎没有占用您的应用程序内存",[787,912,913,914,917,918,743],{},"然后，您可以从 ",[17,915,916],{},"response.Content"," 中获取一个",[736,919,920,921,924],{},"网络流 (",[17,922,923],{},"Stream",")",[787,926,927,928,931,932],{},"接下来，您就可以在一个循环中，用一个小小的缓冲区（例如 80KB），",[736,929,930],{},"一块一块地","从这个网络流中读取数据，",[736,933,934],{},"并直接写入到磁盘的文件流中。",[699,936,937,940],{},[736,938,939],{},"好处",":",[942,943,944,950],"ul",{},[787,945,946,949],{},[736,947,948],{},"极低的内存占用",": 无论您下载的文件是 1GB 还是 10GB，您的程序内存占用始终都非常低（只有那个小缓冲区的大小）。",[787,951,952,955,956,959,960,963,964,967,968,971],{},[736,953,954],{},"更快的“首字节”响应",": 您的代码可以非常快地拿到响应头，从而可以立即检查服务器的状态码 (",[17,957,958],{},"response.StatusCode",")、文件总大小 (",[17,961,962],{},"response.Content.Headers.ContentLength",") 等信息，并根据这些信息来决定下一步怎么做（例如，我们代码中的断点续传逻辑就需要根据状态码是 ",[17,965,966],{},"200 OK"," 还是 ",[17,969,970],{},"206 PartialContent"," 来做不同处理）。",[699,973,974],{},[736,975,976],{},"断点续传的进度条实现",[10,978,980],{"className":12,"code":979,"language":14,"meta":15,"style":15},"        long totalBytes = response.Content.Headers.ContentLength ?? 0;\n        if ( existingFileSize >0 && response.Content.Headers.ContentRange != null){\n            totalBytes = response.Content.Headers.ContentRange.Length ?? 0;\n        }\n        _logger.LogInformation(\"开始下载，总大小{TotalBytes}字节\", totalBytes);\n",[17,981,982,987,992,997,1001],{"__ignoreMap":15},[20,983,984],{"class":22,"line":23},[20,985,986],{},"        long totalBytes = response.Content.Headers.ContentLength ?? 0;\n",[20,988,989],{"class":22,"line":29},[20,990,991],{},"        if ( existingFileSize >0 && response.Content.Headers.ContentRange != null){\n",[20,993,994],{"class":22,"line":35},[20,995,996],{},"            totalBytes = response.Content.Headers.ContentRange.Length ?? 0;\n",[20,998,999],{"class":22,"line":41},[20,1000,157],{},[20,1002,1003],{"class":22,"line":47},[20,1004,1005],{},"        _logger.LogInformation(\"开始下载，总大小{TotalBytes}字节\", totalBytes);\n",[699,1007,1008,1009,1012,1013,1016,1017,743],{},"​\t为了计算下载进度百分比，我们需要两个数：",[17,1010,1011],{},"已下载字节数"," 和 ",[17,1014,1015],{},"文件总字节数","。这段代码",[736,1018,1019],{},"就是为了准确地获得第二个值",[699,1021,1022,1023,1026,1027,1030],{},"​\t",[736,1024,1025],{},"含义",": “首先，我假设这是一次全新的下载。请尝试从服务器响应的 ",[17,1028,1029],{},"Content-Length"," 头部信息中，获取文件的总大小。”",[699,1032,1022,1033,1036,1037,1040,1041,1043,1044,1046],{},[736,1034,1035],{},"工作场景",": 当我们发起一个",[736,1038,1039],{},"普通的、从头开始的下载请求时","，服务器会返回一个 ",[17,1042,966],{}," 状态码，并在响应头中包含一个 ",[17,1045,1029],{}," 字段，它的值就是文件的完整大小。",[699,1048,1022,1049,1054,1055,1058,1059,1061,1062],{},[736,1050,1051],{},[17,1052,1053],{},"?? 0",": 这是一个",[736,1056,1057],{},"空值合并运算符","，意思是如果服务器没有提供 ",[17,1060,1029],{},"，就",[736,1063,1064],{},"默认总大小为 0。",[699,1066,1067],{},[736,1068,1069],{},"特殊情况 (断点续传)",[10,1071,1073],{"className":12,"code":1072,"language":14,"meta":15,"style":15},"if ( existingFileSize > 0 && response.Content.Headers.ContentRange != null){\n    totalBytes = response.Content.Headers.ContentRange.Length ?? 0;\n}\n",[17,1074,1075,1080,1085],{"__ignoreMap":15},[20,1076,1077],{"class":22,"line":23},[20,1078,1079],{},"if ( existingFileSize > 0 && response.Content.Headers.ContentRange != null){\n",[20,1081,1082],{"class":22,"line":29},[20,1083,1084],{},"    totalBytes = response.Content.Headers.ContentRange.Length ?? 0;\n",[20,1086,1087],{"class":22,"line":35},[20,1088,663],{},[699,1090,1091,1094,1095,743],{},[17,1092,1093],{},"existingFileSize > 0",": 这个条件确认了我们",[736,1096,1097],{},"本地确实已经存在一个未下载完的文件",[699,1099,1022,1100,1103,1104,1107,1108,1111,1112,1115,1116,1119],{},[17,1101,1102],{},"response.Content.Headers.ContentRange != null",": 这个条件",[736,1105,1106],{},"确认了服务器的回应","是一个 ",[17,1109,1110],{},"206 Partial Content","（部分内容）响应。这种响应",[736,1113,1114],{},"必须","包含一个 ",[17,1117,1118],{},"Content-Range"," 头部。",[699,1121,1122,940],{},[736,1123,1124],{},[17,1125,1126],{},"totalBytes = response.Content.Headers.ContentRange.Length ?? 0;",[699,1128,1022,1129,1132,1133,1135,1136,1139,1140,1143],{},[736,1130,1131],{},"这是最关键的一步！"," 当服务器响应一个“部分内容”时，它的 ",[17,1134,1029],{}," 头部只会包含",[736,1137,1138],{},"本次传输的数据块的大小","，而",[736,1141,1142],{},"不是","整个文件的总大小。",[699,1145,1146,1147,1149,1150,1153,1154,1157,1158,1161,1162,1165,1166,1169,1170,743],{},"​\t而 ",[17,1148,1118],{}," 头部则包含了更丰富的信息，它的格式通常像这样：",[17,1151,1152],{},"bytes 1048576-2097151\u002F5242880"," 这句话的意思是：“我正在向你发送从第 1048576 字节到第 2097151 字节的数据，这个文件的",[736,1155,1156],{},"完整总大小是 5242880 字节","。”",[17,1159,1160],{},"response.Content.Headers.ContentRange.Length"," 正是用来",[736,1163,1164],{},"精确地提取","出那个 ",[17,1167,1168],{},"\u002F"," 后面的",[736,1171,1172],{},"完整文件总大小",[699,1174,1022,1175,1178,1179,1181,1182,1185,1186,1189,1190,1192],{},[736,1176,1177],{},"为什么要覆盖",": 所以，如果这是一次断点续传，我们必须用 ",[17,1180,1118],{}," 中的",[736,1183,1184],{},"总大小","，来",[736,1187,1188],{},"覆盖","掉 ",[17,1191,1029],{}," 中那个不准确的“部分大小”。",[10,1194,1196],{"className":12,"code":1195,"language":14,"meta":15,"style":15}," \u002F\u002F 异步读写数据流\n using var contentStream = await response.Content.ReadAsStreamAsync(cancellationToken);\n \u002F\u002F append模式追踪数据\n using var fileStream = new FileStream(desinationPath,existingFileSize == 0 ? FileMode.Create : FileMode.Append , FileAccess.Write, FileShare.None);\n var buffer = new byte[81920]; \u002F\u002F 80KB 缓冲区\n long totalBytesRead = existingFileSize;\n int bytesRead;\n while ((bytesRead = await contentStream.ReadAsync(buffer, 0, buffer.Length, cancellationToken)) > 0) {\n     await fileStream.WriteAsync(buffer, 0, bytesRead, cancellationToken); \u002F\u002F \n     totalBytesRead += bytesRead;\n     progress?.Report(new DownloadProgress(totalBytesRead, totalBytes));\n }\n",[17,1197,1198,1203,1208,1213,1218,1223,1228,1233,1238,1243,1248,1253],{"__ignoreMap":15},[20,1199,1200],{"class":22,"line":23},[20,1201,1202],{}," \u002F\u002F 异步读写数据流\n",[20,1204,1205],{"class":22,"line":29},[20,1206,1207],{}," using var contentStream = await response.Content.ReadAsStreamAsync(cancellationToken);\n",[20,1209,1210],{"class":22,"line":35},[20,1211,1212],{}," \u002F\u002F append模式追踪数据\n",[20,1214,1215],{"class":22,"line":41},[20,1216,1217],{}," using var fileStream = new FileStream(desinationPath,existingFileSize == 0 ? FileMode.Create : FileMode.Append , FileAccess.Write, FileShare.None);\n",[20,1219,1220],{"class":22,"line":47},[20,1221,1222],{}," var buffer = new byte[81920]; \u002F\u002F 80KB 缓冲区\n",[20,1224,1225],{"class":22,"line":53},[20,1226,1227],{}," long totalBytesRead = existingFileSize;\n",[20,1229,1230],{"class":22,"line":59},[20,1231,1232],{}," int bytesRead;\n",[20,1234,1235],{"class":22,"line":66},[20,1236,1237],{}," while ((bytesRead = await contentStream.ReadAsync(buffer, 0, buffer.Length, cancellationToken)) > 0) {\n",[20,1239,1240],{"class":22,"line":72},[20,1241,1242],{},"     await fileStream.WriteAsync(buffer, 0, bytesRead, cancellationToken); \u002F\u002F \n",[20,1244,1245],{"class":22,"line":78},[20,1246,1247],{},"     totalBytesRead += bytesRead;\n",[20,1249,1250],{"class":22,"line":84},[20,1251,1252],{},"     progress?.Report(new DownloadProgress(totalBytesRead, totalBytes));\n",[20,1254,1255],{"class":22,"line":89},[20,1256,731],{},[699,1258,1259,1260,1262,1263,917,1265,1269,1270,1273,1274,1277],{},"​\t在我们通过 ",[17,1261,832],{}," 获取到响应头之后，这行代码会从 ",[17,1264,916],{},[736,1266,920,1267,924],{},[17,1268,923],{},"。此时，真正的",[736,1271,1272],{},"文件数据还在服务器端或网络缓冲区","中，并没有被下载到您的程序内存里。",[17,1275,1276],{},"contentStream"," 就是我们用来从网络上“抽水”的管子。",[699,1279,1280,1281,1287,1288,1291,1292,1295,1296,1299,1300,1303,1304,1306,1307,1312,1313,1316,1317,1320,1321,1324,1325,743],{},"​\tusing var fileStream = new FileStream(...);这行代码会创建一个指向本地磁盘文件的",[736,1282,1283,1284,924],{},"文件流 (",[17,1285,1286],{},"FileStream","。这是我们将要写入数据的目标。如果 ",[17,1289,1290],{},"existingFileSize"," 是 ",[17,1293,1294],{},"0","（表示这是一次全新的下载），就使用 ",[17,1297,1298],{},"FileMode.Create"," 来",[736,1301,1302],{},"创建","一个新文件（如果已存在则覆盖）。如果 ",[17,1305,1290],{}," ",[736,1308,1309,1310],{},"大于 ",[17,1311,1294],{},"（表示这是一次",[736,1314,1315],{},"断点续传","），就使用 ",[17,1318,1319],{},"FileMode.Append","，它会打开现有文件并将写入位置",[736,1322,1323],{},"自动定位到文件的末尾","，准备",[736,1326,1327],{},"追加新数据",[699,1329,1330,1331,1334],{},"**",[17,1332,1333],{},"while ((bytesRead = await contentStream.ReadAsync(...)) > 0)","**作用: 这是下载的核心循环，意思是“只要网络水管里还有水，就一直抽”。",[942,1336,1337,1361],{},[787,1338,1339,1342,1343,1345,1346,1349,1350,1353,1354,1357,1358,743],{},[17,1340,1341],{},"await contentStream.ReadAsync(buffer, 0, buffer.Length, cancellationToken)",": 这是一个异步操作。它会尝试从网络流 (",[17,1344,1276],{},") 中读取数据，最多读取 ",[17,1347,1348],{},"buffer.Length"," 那么多字节（在我们的例子中是 80KB），然后将数据放入 ",[17,1351,1352],{},"buffer"," 数组中。",[17,1355,1356],{},"bytesRead"," 变量会得到",[736,1359,1360],{},"实际读取到的字节数",[787,1362,1363,1366,1367,1370,1371,1373,1374,743],{},[17,1364,1365],{},"while (... > 0)",": 当服务器发送完所有数据后，下一次 ",[17,1368,1369],{},"ReadAsync"," 会返回 ",[17,1372,1294],{},"，这个",[736,1375,1376],{},"循环就会自然结束",[699,1378,1022,1379,940,1384,1387,1388,1390,1391,1394,1395,1397],{},[736,1380,1381],{},[17,1382,1383],{},"await fileStream.WriteAsync(buffer, 0, bytesRead, cancellationToken);",[736,1385,1386],{},"作用",": 将刚刚从网络“抽”到的一桶水（",[17,1389,1352],{}," 里的数据），",[736,1392,1393],{},"异步地","倒入“本地水桶”（写入磁盘文件）。我们只写入 ",[17,1396,1356],{}," 那么多，因为这才是本次实际读取到的数据量。",[699,1399,1400,940],{},[736,1401,1402],{},[17,1403,1404],{},"totalBytesRead += bytesRead;",[942,1406,1407],{},[787,1408,1409,1411],{},[736,1410,1386],{},": 更新我们的“已下载总量”计数器。",[699,1413,1414,940],{},[736,1415,1416],{},[17,1417,1418],{},"progress?.Report(new DownloadProgress(totalBytesRead, totalBytes));",[942,1420,1421,1430,1444],{},[787,1422,1423,1425,1426,1429],{},[736,1424,1386],{},": 向外界",[736,1427,1428],{},"报告","当前的下载进度。",[787,1431,1432,1435,1436,1439,1440,1443],{},[17,1433,1434],{},"progress?"," 是一种空条件运算符，意思是“如果 ",[17,1437,1438],{},"progress"," 对象不是 ",[17,1441,1442],{},"null","（即调用者关心进度），才执行后面的代码”。",[787,1445,1446,1449,1450,1453,1454,1457,1458,743],{},[17,1447,1448],{},"Report(...)",": 这个方法会触发您在调用 ",[17,1451,1452],{},"DownloadFileAsync"," 时传入的那个 ",[17,1455,1456],{},"IProgress\u003CT>"," 回调，将当前的“已下载量”和“总量”传递出去，",[736,1459,1460],{},"从而让 UI 界面可以更新进度条",[1462,1463,1464],"style",{},"html .default .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .dark .shiki span {color: var(--shiki-dark);background: var(--shiki-dark-bg);font-style: var(--shiki-dark-font-style);font-weight: var(--shiki-dark-font-weight);text-decoration: var(--shiki-dark-text-decoration);}html.dark .shiki span {color: var(--shiki-dark);background: var(--shiki-dark-bg);font-style: var(--shiki-dark-font-style);font-weight: var(--shiki-dark-font-weight);text-decoration: var(--shiki-dark-text-decoration);}",{"title":15,"searchDepth":29,"depth":29,"links":1466},[1467],{"id":667,"depth":35,"text":668},"md",{},"\u002Fcsharp\u002Fc的流传输文件和断点下载",{"description":15},"csharp\u002Fc#的流传输文件和断点下载","maC5SognSNg0hYoyT7k-4DfFLtETsYma9VXxzhTJlBk",1791042465144]