Как сделать UdpClient.ReceiveAsync() сократима?

у меня есть интерфейс INetwork С способ:

Task<bool> SendAsync(string messageToSend, CancellationToken ct)

одна реализация интерфейса имеет такой код:

public async Task<bool> SendAsync(string messageToSend, CancellationToken ct)
{
  var udpClient = new UdpClient();
  var data = Encoding.UTF8.GetBytes (messageToSend);
  var sentBytes = await udpClient.SendAsync(data);
  return sentBytes == data.Length; 
}

к сожалению, SendAsync() на UdpClient класс не воспринимает CancellationToken.

Итак, я начал менять его на:

public Task<bool> SendAsync(string messageToSend, CancellationToken ct)
{
  var udpClient = new UdpClient();
  var data = Encoding.UTF8.GetBytes (messageToSend);
  var sendTask = udpClient.SendAsync(data);
  sendTask.Wait(ct);

  if(sendTask.Status == RanToCompletion)
  {
    return sendTask.Result == data.Length;
  }
}

очевидно, что это не сработает, потому что нет Task возвращается. Однако, если я возвращаю задачу, подписи больше не совпадают. SendAsync() возвращает Task<int>, но мне нужно а Task<bool>.

и теперь я в замешательстве. :-) Как это решить?

2 ответов


Я знаю, что это немного поздно, но недавно мне пришлось отменить UdpClient ReceiveAsync/SendAsync.

ваш первый блок кода отправляет без отмены (ваш заголовок говорит, что, кстати, получите...).

ваш второй блок кода-это явно не способ сделать это. Вы вызываете *Async, а затем Task.Подождите, который блокирует, пока вызов не будет завершен. Это делает вызов эффективно синхронным, и нет смысла вызывать версию *Async. Этот лучшее решение-использовать Async следующим образом:

...
var sendTask = udpClient.SendAsync(data);
var tcs = new TaskCompletionSource<bool>();
using( ct.Register( s => tcs.TrySetResult(true), null) )
{
    if( sendTask != await Task.WhenAny( task, tcs.Task) )
        // ct.Cancel() called
    else
        // sendTask completed first, so .Result will not block
}
...

нет встроенного способа отменить UdpClient (ни одна из функций не принимает CancellationToken), но вы можете воспользоваться возможностью ожидания нескольких задач с Task.WhenAny. Это вернется с первой задачей, которая завершится (это также простой способ использовать Task.Delay() для реализации таймаутов). Затем нам просто нужно создать задачу, которая будет завершена, когда CancellationToken отменено, что мы можем сделать, создав TaskCompletionSource и настройка он с CancellationToken'ы вызова.

после отмены мы можем закрыть сокет, чтобы фактически "отменить" базовое чтение/запись.

оригинальная идея для этого пришла из другого ответа SO, касающегося дескрипторов файлов, но он также работает с сокетами. Обычно я заворачиваю его в метод расширения следующим образом:

public static class AsyncExtensions
{
    public static async Task<T> WithCancellation<T>( this Task<T> task, CancellationToken cancellationToken )
    {
        var tcs = new TaskCompletionSource<bool>();
        using( cancellationToken.Register( s => ( (TaskCompletionSource<bool>)s ).TrySetResult( true ), tcs ) )
        {
            if( task != await Task.WhenAny( task, tcs.Task ) )
            {
                throw new OperationCanceledException( cancellationToken );
            }
        }

        return task.Result;
    }
}

тогда используйте его так:

try
{
    var data = await client.ReceiveAsync().WithCancellation(cts.Token);
    await client.SendAsync(data.Buffer, data.Buffer.Length, toep).WithCancellation(cts.Token);
}
catch(OperationCanceledException)
{
    client.Close();
}

прежде всего, если вы хотите вернуться Task<bool>, вы можете просто сделать это с помощью Task.FromResult(). Но вы, вероятно, не должны этого делать, не имеет смысла иметь асинхронный метод, который на самом деле синхронен.

SendAsync(), но это все.

если вы действительно хотите притвориться, что метод был отменен как можно скорее, вы можете использовать ContinueWith() с гашением:

var sentBytes = await sendTask.ContinueWith(t => t.Result, ct);