از سرگیری مجدد، لغو درخواست و سعی مجدد دریافت فایل‌های حجیم توسط HttpClient
اندازه‌ی قلم متن
تخمین مدت زمان مطالعه‌ی مطلب: پنج دقیقه

پس از آشنایی با «نکات دریافت فایل‌های حجیم توسط HttpClient»، در ادامه می‌توان سه قابلیت مهم از سرگیری مجدد، لغو درخواست و سعی مجدد دریافت فایل‌های حجیم را با HttpClient، همانند برنامه‌های download manager نیز پیاده سازی کرد.


از سرگیری مجدد درخواست ارسالی توسط HttpClient

یک نمونه از سرگیری مجدد درخواست را در مطلب «اضافه کردن قابلیت از سرگیری مجدد (resume) به HttpWebRequest» پیشتر در این سایت مطالعه کرده‌اید. اصول کلی آن نیز در اینجا صادق است. HTTP 1.1 از مفهوم range headers‌، برای دریافت پاسخ‌های جزئی پشتیبانی می‌کند. به این ترتیب در صورت پیاده سازی چنین قابلیتی در برنامه‌ی سمت سرور، می‌توان دریافت بازه‌ای از بایت‌ها را بجای دریافت فایل از ابتدا، از سرور درخواست کرد. به یک چنین قابلیتی Resume و یا از سرگیری مجدد گرفته می‌شود و درحین دریافت فایل‌های حجیم بسیار حائز اهمیت است.
var fileInfo = new FileInfo(outputFilePath);
long resumeOffset = 0;
if (fileInfo.Exists)
{
    resumeOffset = fileInfo.Length;
}
if (resumeOffset > 0)
{
    _client.DefaultRequestHeaders.Range = new RangeHeaderValue(resumeOffset, null);
}
در اینجا نحوه‌ی تنظیم یک RangeHeader را مشاهده می‌کنید. ابتدا نیاز است بررسی کنیم آیا فایل دریافتی از پیش موجود است؟ آیا قسمتی از این درخواست پیشتر دریافت شده و محتوای آن هم اکنون به صورت ذخیره شده وجود دارد؟ اگر بله، درخواست دریافت این فایل را بر اساس اندازه‌ی دریافتی فعلی آن، به سرور ارائه می‌کنیم.

یک نکته: تمام وب سرورها و یا برنامه‌های وب از یک چنین قابلیتی پشتیبانی نمی‌کنند.
روش تشخیص آن نیز به صورت زیر است:
var response = await client.GetAsync(url, HttpCompletionOption.ResponseHeadersRead);
if (response.Headers.AcceptRanges == null && resumeOffset > 0)
{
    // resume not supported, starting over
}
پس از خواندن هدر درخواست، اگر خاصیت AcceptRanges آن نال بود، یعنی قابلیت از سرگیری مجدد را ندارد. در این حالت باید فایل موجود فعلی را حذف و یا از نو (FileMode.CreateNew) بازنویسی کرد (بجای حالت FileMode.Append).


لغو درخواست ارسالی توسط HttpClient

پس از شروع غیرهمزمان client.GetAsync می‌توان متد CancelPendingRequests آن‌را فراخوانی کرد تا کلیه درخواست‌های مرتبط با این client لغو شوند. اما این متد صرفا برای حالت پیش‌فرض client.GetAsync که دریافت هدر + محتوا است کار می‌کند (یعنی حالت HttpCompletionOption.ResponseContentRead). اگر همانند نکات بررسی شده‌ی در مطلب «دریافت فایل‌های حجیم توسط HttpClient» صرفا درخواست خواندن هدر را بدهیم (HttpCompletionOption.ResponseHeadersRead)، چون کنترل ادامه‌ی بحث را خودمان بر عهده گرفته‌ایم، لغو آن نیز به عهده‌ی خودمان است و متد CancelPendingRequests بر روی آن تاثیر نخواهد داشت.
این نکته در مورد تنظیم خاصیت TimeOut نیز صادق است. این خاصیت فقط زمانیکه دریافت کل هدر + محتوا توسط متد GetAsync مدیریت شوند، تاثیر گذار است.
بنابراین درحالتیکه نیاز به کنترل بیشتر است، هرچند فراخوانی متد CancelPendingRequests ضرری ندارد، اما الزاما سبب قطع کل درخواست نمی‌شود و باید این لغو را به صورت ذیل پیاده سازی کرد:
ابتدا یک منبع توکن لغو عملیات را به صورت ذیل ایجاد می‌کنیم:
private readonly CancellationTokenSource _cts = new CancellationTokenSource();
سپس، متد لغو برنامه، تنها کافی است متد Cancel این cts را فراخوانی کند؛ تا عملیات دریافت فایل خاتمه یابد.
پس از این فراخوانی (()cts.Cancel)، نحوه‌ی واکنش به آن به صورت ذیل خواهد بود:
var result = await client.GetAsync(url, HttpCompletionOption.ResponseHeadersRead, _cts.Token);
using(var stream = await result.Content.ReadAsStreamAsync())
{
   byte[] buffer = new byte[80000];
   int bytesRead;
   while((bytesRead = await stream.ReadAsync(buffer, 0, buffer.Length)) > 0 &&
         !_cts.IsCancellationRequested)
   {
      outputStream.Write(buffer, 0, bytesRead);
   }
}
در اینجا از cts.Token به عنوان پارامتر سوم متد GetAsync استفاده شده‌است. همچنین قسمت ثبت اطلاعات دریافتی، در استریم خروجی نیز به صورت یک حلقه درآمده‌است تا بتوان خاصیت IsCancellationRequested این توکن لغو را بررسی کرد و نسبت به آن واکنش نشان داد.


سعی مجدد درخواست ارسالی توسط HttpClient

یک روش پیاده سازی سعی مجدد درخواست شکست خورده، توسط کتابخانه‌ی Polly است. روش دیگر آن نیز به صورت ذیل است:
public async Task DownloadFileAsync(string url, string outputFilePath, int maxRequestAutoRetries)
{
            var exceptions = new List<Exception>();

            do
            {
                --maxRequestAutoRetries;
                try
                {
                    await doDownloadFileAsync(url, outputFilePath);
                }
                catch (TaskCanceledException ex)
                {
                    exceptions.Add(ex);
                }
                catch (HttpRequestException ex)
                {
                    exceptions.Add(ex);
                }
                catch (Exception ex) when (isNetworkError(ex))
                {
                    exceptions.Add(ex);
                }

                // Wait a bit and try again later
               if (exceptions.Any())  await Task.Delay(2000, _cts.Token);
            } while (maxRequestAutoRetries > 0 &&
                     !_cts.IsCancellationRequested);

            var uniqueExceptions = exceptions.Distinct().ToList();
            if (uniqueExceptions.Any())
            {
                if (uniqueExceptions.Count() == 1)
                    throw uniqueExceptions.First();
                throw new AggregateException("Could not process the request.", uniqueExceptions);
            }
}

private static bool isNetworkError(Exception ex)
{
    if (ex is SocketException || ex is WebException)
        return true;
    if (ex.InnerException != null)
        return isNetworkError(ex.InnerException);
    return false;
}
در اینجا متد doDownloadFileAsync، پیاده سازی همان متدی است که در قسمت «لغو درخواست ارسالی توسط HttpClient» در مورد آن بحث شد. این قسمت دریافت فایل را در یک حلقه که حداقل یکبار اجرا می‌شود، قرار می‌دهیم. متد GetAsync استثناءهایی مانند TaskCanceledException (در حین TimeOut و یا فراخوانی متد CancelPendingRequests که البته همانطور که توضیح داده شد، بر روی روش کنترل Response تاثیری ندارند)، HttpRequestException پس از فراخوانی متد response.EnsureSuccessStatusCode (جهت اطمینان حاصل کردن از دریافت پاسخی بدون مشکل از طرف سرور) و یا SocketException و WebException را درصورت بروز مشکلی در شبکه، صادر می‌کند. نیازی به بررسی سایر استثناءها در اینجا نیست.
اگر یکی از این استثناءهای یاد شده رخ‌دادند، اندکی صبر کرده و مجددا درخواست را از ابتدا صادر می‌کنیم.
در پایان این سعی‌های مجدد، اگر استثنایی ثبت شده بود و همچنین عملیات نیز با موفقیت به پایان نرسیده بود، آن‌را به فراخوان صادر می‌کنیم.
  • #
    ‫۶ سال و ۸ ماه قبل، چهارشنبه ۲۷ دی ۱۳۹۶، ساعت ۱۳:۲۵
    یک نکته‌ی تکمیلی: پشتیبانی توکار ASP.NET Core 2.0 از Range headers

    فرض کنید برای آزمایش قسمت «از سرگیری مجدد» دریافت یک فایل حجیم مطلب جاری، یک چنین کدی را در یک برنامه‌ی ASP.NET Core 2.0 تهیه کرده‌اید:
        public class HomeController : Controller
        {
            public IActionResult GetFile()
            {
                return PhysicalFile(@"C:\path\file.pdf", "application/pdf");
            }
    در مورد PhysicalFile در مطلب «تغییرات متدهای بازگشت فایل‌ها به سمت کلاینت در ASP.NET Core» بیشتر بحث شده‌است.
    اجرای این کد به همراه هدر مخصوص «Accept-Ranges: bytes » که در مطلب جاری در مورد آن بحث شد نیز هست:


    یعنی دریافت فایل‌ها در ASP.NET Core 2.0 به صورت توکار از ویژگی «از سرگیری مجدد» پشتیبانی می‌کند. قابلیتی که در نگارش‌های پیشین ASP.NET (تمام نگارش‌های آن‌)، به صورت پیش‌فرض و توکار وجود نداشت و برای پیاده سازی آن می‌بایستی مقدار زیادی کد نوشته می‌شد.
    در اینجا return File، FileStreamResult و VirtualFileResult نیز از ویژگی partial range requests پشتیبانی می‌کنند. همچنین حتی اگر از static files middleware آن نیز استفاده کنید، یک چنین قابلیتی را پیاده سازی کرده‌است.
    به علاوه تمام متدهای بازگشت فایل، پارامتر enableRangeProcessing را نیز به همراه دارند:
    var result = new FileStreamResult(readStream, contentType)
    {
                    LastModified = lastModified,
                    EntityTag = entityTag,
                    EnableRangeProcessing = true,
    };
    
     return PhysicalFile(path, "text/plain", "downloadName.txt", lastModified, entityTag, true);
    
     return File(data, "text/plain", "downloadName.txt", lastModified: null, entityTag: entityTag,
        enableRangeProcessing: true);
    • #
      ‫۵ سال و ۹ ماه قبل، پنجشنبه ۱ آذر ۱۳۹۷، ساعت ۲۲:۳۷
      همانطور که در نکته تکمیلی بالا عنوان شده‌است، تمام متدهای بازگشت فایل، پارامتر enableRangeProcessing را نیز به همراه دارند. در صورت استفاده از FileStreamResult، در Response Header، هدر Accept-Ranges وجود ندارد. ولی در صورت استفاده از PhysicalFile  این هدر در Response Header وجود دارد، دلیل چیست؟ 
      public async Task<IActionResult> Get () {
           var url = "https://anthonygiretti.blob.core.windows.net/videos/earth.mp4";
            return new FileStreamResult (await this._httpClient.GetStreamAsync (url), "video/mp4") {
               EnableRangeProcessing = true
            };
      }

      • #
        ‫۵ سال و ۹ ماه قبل، پنجشنبه ۱ آذر ۱۳۹۷، ساعت ۲۳:۰۹
        دلیل آن مرتبط است به روشی که از آن استفاده کردید. این قابلیت برای اینکه کار کند، نیاز به بافر کردن اطلاعات دارد، در حالیکه شما در حال دانلود یک فایل از یک سایت دیگر هستید. ترکیب این‌ها با هم، برای ارائه‌ی resume کار نمی‌کنند. زمانیکه قرار است قابلیت resume وجود داشته باشد، یعنی مثلا کاربر درخواست دریافت اطلاعات را از بایت 1000 تا 1500، می‌دهد. File Stream Result چطور باید این درخواست را برای httpClient.GetStreamAsync که چنین قابلیتی را ندارد، ترجمه کند؟ اگر می‌خواهید آن‌را برای حالت resume آزمایش کنید، از استریمی از نوع System.IO.File.OpenRead و یا new FileStream استفاده کنید:
        public IActionResult FileStreamActionResult()
        {
          var fileStream = System.IO.File.OpenRead(@"D:\path\Controllers\HomeController.cs");
          return new FileStreamResult(fileStream, "text/plain") { FileDownloadName = "HomeController.cs" };
        }

  • #
    ‫۵ سال و ۱۰ ماه قبل، دوشنبه ۲۸ آبان ۱۳۹۷، ساعت ۱۵:۵۰
    سلام؛ اگر قصد داشته باشیم از طریق Angular 2  دانلود رو فراخوانی کنیم ، به شکلی که یک فایل حجیم بر روی مرورگر کاربر دانلود بشه ، پیاده سازی به چه صورت خواهد بود ، الان شما در سمت سرور یک حلقه while دارید که محتوی رو می‌خونه ، این یعنی در هر اجرای حلقه من باید محتوی خوانده شده رو return کنم به کلاینت و دوباره با آفست جدید فراخوانی کنم؟
    نحوه فراخوانی من به شکل زیر هست که با FileStreamResult برگشتی از سمت سرور به درستی کار میکنه ولی خب همه فایل رو یکجا برمی گردونه :
     downloadOrder(orderId: number , userId : string) {
    
        this._http.get(this._config.getApiURI() + '/Download/productfile/' + orderId + '/' + userId, { observe: 'blob'})
          .subscribe(
            (data) => {
              if(this._functionService.isNullOrEmpty(data.body)){
                this._snackBarService.error('فایل پیدا نشد');
                return;
              }
              var contentType = data.body.type || "application/octet-stream";
    
              var fileInfo = JSON.parse(data.headers.get('FileInfo'));          
              var blob = new Blob([data.body], { type: contentType });
              var url = window.URL.createObjectURL(blob);
              var anchor = document.createElement("a");
              anchor.setAttribute("href", url)
              anchor.setAttribute("download", fileInfo.fileName + fileInfo.fileType);
              anchor.setAttribute("target", "_blank");
              document.body.appendChild(anchor);
              anchor.click();
              setTimeout(function () {
                document.body.removeChild(anchor);
                window.URL.revokeObjectURL(url);
              }, 200);
            },
            error => {
              this._snackBarService.error(error);
            });
      }
    ولی حالا که قراره فایل رو در چندین مرحله و با حلقه while برگردونیم ، با این سناریو یعنی باید از طرف کلاینت چندین بار فراخوانی باشه ، مگر اینکه کلا یک window جدید باز کنیم  (که احتمالا با مشکل popup blocker مواجه میشیم) و ... آیا باید در سمت سرور بایت‌های خوانده شده رو تو Response بنویسیم ...
    نحوه پیاده سازی کد پایین به چه شکل میشه
                 while ((bytesRead = await stream.ReadAsync(buffer, 0, buffer.Length)) > 0)  
                        {                        
                            await Response.Body.WriteAsync(buffer);                        
                            //return File(buffer, GetContentType(fullPath), newFileName, true);                        
                            //return new FileStreamResult(stream,"application/octet-stream");
                        }
  • #
    ‫۲ سال و ۴ ماه قبل، سه‌شنبه ۲۳ فروردین ۱۴۰۱، ساعت ۰۵:۰۷
    یکی از صفحات وب سایتم که با mvc5  نوشته شده است مخصوص دانلود فایل اپلیکیشن هست و حدودا روزانه یک میلیون بازدید دارد کانفیگ سرور عالی ولی متاسفانه بعضی از مواقع هنگامی که کاربر دکمه دانلود فایل را کلیک میکند فایل شروع به دانلود و سپس متوقف میشود و مجددا پس از چند ثانیه  فایل جدید شروع به دانلود مشود و متوقف میشود و این مسئله چند بار تکرار میشود و در نهایت یک فایل دانلود میشود.یکی از راه حلهایی که به ذهنمان رسید ایجاد سرورهای دانلود فایل  بود که متاسفانه در حال حاضر برایمان مقدور نیست.ایا راه حلی وجود دارد که    مشکل را حل کند؟