Вопрос или проблема
private Stream PostData(WebRequest request, byte[] rawByteData)
{
Stream req = request.GetRequestStream();
req.Write(rawByteData, 0, rawByteData.Length);
return req;
}
public WebRequestResult RequestPostDataWebRequestResult(string url, string postData, IEnumerable<string> headers = null, string contentType = null, int timeout = 300)
{
WebRequestResult webRequestResult = new WebRequestResult();
try
{
byte[] rawByteData;
WebRequest request = PrepareWebRequest(url, postData, headers, out rawByteData, contentType, timeout:timeout);
Stream req = PostData(request, rawByteData);
var response = GetResponseStreamThrow(request);
req.Close();
webRequestResult.Message = response.Item1;
webRequestResult.StatusCode = response.Item2;
webRequestResult.Status = true;
return webRequestResult;
}
catch (Exception e)
{
webRequestResult.Message = e.ToString();
webRequestResult.Status = false;
if (e is WebException webException)
{
webRequestResult.WebExceptionStatus = webException.Status;
if (webException.Response is HttpWebResponse response)
{
webRequestResult.StatusCode = response.StatusCode;
}
}
return webRequestResult;
}
}
private async Task<WebResponse> TaskWithTimeout(Task<WebResponse> task, int duration)
{
if (duration == 0)
duration = 100 * 1000;
var retTask = await Task.WhenAny(task, Task.Delay(duration)).ConfigureAwait(false);
if (retTask is Task<WebResponse>) return task.Result;
return null;
}
Несмотря на то, что мы указываем тайм-аут в 100 секунд в PrepareWebRequest, HTTP-запросы выбрасывают System.IO.IOException: Невозможно прочитать данные из транспортного соединения: Попытка подключения не удалась, поскольку подключённая сторона не ответила должным образом в течение определённого времени, или установлено соединение не удалось, поскольку подключённый хост не ответил в конце 40 минут по какой-то причине, вместо того чтобы выдать ошибку тайм-аута.
Это не происходит каждый раз, случается очень редко.
В GetResponseStreamThrow мы вызывали один из них, но Task.Delay и WebRequest.Timeout не сработали.
WebResponse ws = await TaskWithTimeout(request.GetResponseAsync(), request.Timeout).ConfigureAwait(false);
WebResponse ws = TaskWithTimeout(Task.Run(request.GetResponse), request.Timeout).Result;
Когда я проверяю запрос в Fiddler, я вижу обмен ключами. Это действительно ошибка .net, так как как WebRequest.Timeout, так и Task.Delay для пользовательского тайм-аута, кажется, не сработали?
Также есть ли лучший способ реализовать тайм-аут на стороне клиента, поскольку WebRequest.Timeout не работает в этом сценарии?
Прежде всего, чтобы прояснить ситуацию, ошибка System.IO.IOException: Невозможно прочитать данные из транспортного соединения ...
не вызвана вашим методом или тем, как вы обрабатываете задачи в методе TaskWithTimeout
. Если бы ваши задачи занимали больше времени, чем тайм-аут в методе, просто было бы возвращено null
.
Если вас удивляет, что ваша задача все еще выполняется, несмотря на превышение тайм-аута, это связано с тем, что вы ничего не делаете для ее остановки. Чтобы остановить задачу, которую вы запускаете, передайте ей CancellationToken
.
Сравните небольшой пример, воспроизводящий ваш случай:
var r = await TaskWithTimeout(MethodAsync(default));
Console.WriteLine("Получили результат " + r);
await Task.Delay(1000);
async Task<int?> TaskWithTimeout(Task<int> task)
{
var retTask = await Task.WhenAny(task, Task.Delay(300)).ConfigureAwait(false);
if (retTask is Task<int>) return task.Result;
return null;
}
async Task<int> MethodAsync(CancellationToken cancellationToken)
{
try
{
await Task.Delay(500, cancellationToken);
Console.WriteLine("Асинхронный метод завершил выполнение");
return 1;
}
catch (Exception ex)
{
Console.WriteLine("Асинхронный метод поймал исключение: " + ex.Message);
return 0;
}
}
Вы увидите лог Асинхронный метод завершил выполнение
, даже если этот метод превысил тайм-аут.
Чтобы правильно остановить задачу, вам нужно использовать CancellationTokenSource
– ниже приведен ваш пример слегка изменённый для его использования:
async Task<int?> TaskWithTimeoutWithCancel()
{
var cts = new CancellationTokenSource();
var retTask = await Task.WhenAny(MethodAsync(cts.Token), Task.Delay(300)).ConfigureAwait(false);
if (retTask is Task<int> task)
{
return task.Result;
}
cts.Cancel();
return null;
}
Это правильно остановит задачу и выведет Асинхронный метод поймал исключение...
Ответ или решение
Сначала давайте разберемся с проблемой, описанной в вашем вопросе. Ошибка System.IO.IOException: Unable to read data from the transport connection ...
, скорее всего, не связана с тем, как вы обрабатываете асинхронные задачи в методе TaskWithTimeout
. Дело в том, что если ваша задача занимает больше времени, чем заданный таймаут, то возвращается null
, но сама задача продолжает выполняться в фоне, так как вы не останавливаете ее вручную.
Чтобы корректно остановить задачу, которая уже запущена, вы можете использовать CancellationToken
. Ниже приведен пример, который демонстрирует, как вы можете модифицировать ваш код для использования CancellationTokenSource
, что позволит вам отменять долг-running задачи:
async Task<WebResponse> TaskWithTimeoutWithCancel(Task<WebResponse> task, int duration)
{
var cts = new CancellationTokenSource();
var retTask = await Task.WhenAny(task, Task.Delay(duration, cts.Token)).ConfigureAwait(false);
if (retTask is Task<WebResponse> responseTask)
{
cts.Cancel(); // Отменяем задержку, если задача выполнена
return await responseTask; // Возвращаем результат задачи
}
cts.Cancel(); // Отменяем задачу, если возник таймаут
return null;
}
Объяснение изменений:
- Мы создали
CancellationTokenSource
для предоставления возможности отмены. - При вызове
Task.Delay
, мы передаемcts.Token
, что позволяет ему быть отмененным, если задача завершилась раньше. - Если задача (
task
) завершилась до окончания задержки, мы отменяем задержку с помощьюcts.Cancel()
.
Мысли о WebRequest.Timeout
:
Поведение для WebRequest.Timeout
может отличаться в зависимости от реализации платформы и фактического состояния сети. Если вы заметили, что таймаут не срабатывает, когда он должен, это может указывать на сетевые проблемы, которые не могут быть устранены через код вашего приложения.
Разработка клиентского таймаута:
Рекомендую всегда учитывать использование CancellationToken
для управления долгими задачами и их отмены, чтобы вы могли более эффективно обрабатывать случаи, когда что-то идет не так. Также вы можете рассмотреть возможность использования библиотеки HttpClient
, которая гораздо более современная и предоставляет больше возможностей по сравнению с WebRequest
.
Пример использования HttpClient
с таймаутом:
using (var client = new HttpClient { Timeout = TimeSpan.FromSeconds(100) })
{
try
{
var response = await client.PostAsync(url, new ByteArrayContent(rawByteData), cancellationToken);
response.EnsureSuccessStatusCode();
}
catch (TaskCanceledException)
{
// Обработка ситуации, когда запрос превышает время ожидания
}
}
Заключение:
Обязательно используйте CancellationToken
для управления остановкой задач, и рассмотрите возможность использования HttpClient
для более удобного управления запросами. Это поможет вам более эффективно работать с асинхронными вызовами и справляться с проблемами сетевой связи.