【问题标题】:UDP protocol with TaskCompletionSource and async/await带有 TaskCompletionSource 和 async/await 的 UDP 协议
【发布时间】:2017-07-03 13:46:59
【问题描述】:

我有一个使用 UDP 与硬件设备通信的库。 对话是这样的:

|------------000E------------>|
|                             |
|<-----------000F-------------|
|                             |
|------------DC23------------>|
|                             |
|<-----------DC24-------------|

首先我发送操作码 000E 并期望得到 000F 作为响应。收到 000F 后,我会发送一个 DC23 并期待一个 DC24 作为响应。 (响应中包含附加信息以及操作码。)将来,可能需要在此对话中添加更多步骤。

负责与设备通信的对象有如下接口:

public class Communication : ICommunication
{
    public Communication();
    public bool Send_LAN(byte subnetID, byte deviceID, int operateCode, ref byte[] addtional);
    public event DataArrivalHandler DataArrival;
    public delegate void DataArrivalHandler(byte subnetID, byte deviceID, int deviceType, int operateCode, int lengthOfAddtional, ref byte[] addtional);
}

当我尝试天真地编写此代码时,我会在 DataArrival 事件处理程序中得到一个 switch 语句,该语句根据响应代码执行不同的操作,如下所示:

    private void _com_DataArrival(byte subnetID, byte deviceID, int deviceTypeCode, int operateCode, int lengthOfAddtional, ref byte[] addtional)
    {
        Debug.WriteLine($"OpCode: 0x{operateCode:X4}");
        switch (operateCode)
        {
        case 0x000F: // Response to scan
             // Process the response...
             _com.Send_LAN(subnet, device, 0xDC23, ...);
             break;
        case 0xDC24:
             // Continue processing...
             break;
        }
    }

它开始看起来像是要变成状态机了。我认为必须有更好的方法来使用TaskCompletionSourceasync/await

我该怎么做?

【问题讨论】:

  • 别忘了你可能会误报 udp 的数据报,这可能会挂起你的状态机
  • 这是我不想使用状态机的原因之一。
  • 这个问题不取决于你是否使用状态机。在这两种解决方案中,您都必须检查是否发生超时。我认为使用状态机更容易。 (回复超时重传)

标签: c# asynchronous taskcompletionsource


【解决方案1】:

如果你只是想知道如何在这里使用TaskCompletionSource - 你可以这样做:

public Task<Response> RequestAsync(byte subnetID, byte deviceID, int deviceType, int operateCode, ref byte[] addtional, int expectedResponseCode, CancellationToken ct = default(CancellationToken)) {
    var tcs = new TaskCompletionSource<Response>();           
    DataArrivalHandler handler = null;
    handler = (byte sub, byte device, int type, int opCode, int length, ref byte[] additional) => {
        // got something, check if that is what we are waiting for
        if (opCode == expectedResponseCode) {
            DataArrival -= handler;
            // construct response here
            Response res = null; // = new Response(subnetID, deviceID, etc)
            tcs.TrySetResult(res);
        }
    };
    DataArrival += handler;
    // you can use cancellation for timeouts also
    ct.Register(() =>
    {
        DataArrival -= handler;
        tcs.TrySetCanceled(ct);
    });
    if (!Send_LAN(subnetID, deviceID, operateCode, ref addtional)) {
        DataArrival -= handler;                
        // throw here, or set exception on task completion source, or set result to null
        tcs.TrySetException(new Exception("Send_LAN returned false"));
    }
    return tcs.Task;
}

public class Response {
    public byte SubnetID { get; set; }
    // etc
}

那么你就可以以请求-响应的方式使用它了:

var response = await communication.RequestAsync(...);

【讨论】:

  • 当一个 000E 请求可以得到多个 000F 响应并且每个 000F 后面都必须跟着自己的 DC23 时,你认为仍然可以使用 async/await 吗?
  • @RonInbar 如果我理解正确,您可以连续提出多个请求。首先发送 000E 并期望 000F。然后发送 DC23 并根据需要再次发送 000F。
  • 发生的情况是 000E 被发送到 广播地址,总线上的每个设备都以 000F 及其信息进行响应。然后必须向每个设备发送 DC23 请求,要求该设备提供更具体的信息。当响应到达时,您的代码会分离事件处理程序,因此它将无法处理对单个请求的多个响应。如果不分离事件处理程序会发生什么?
  • 那是非常不同的情况。当响应到达时,您不应该分离处理程序,而是调用一些将与该客户端一起使用的回调函数。而不是在第一次响应时返回成功 - 保持处理程序连接一段时间(总是)。但不要修改此方法 - 单独创建一个。如果您对此感到困扰-我可以提供一些示例。
  • 您认为添加具有以下签名 Task&lt;List&lt;Response&gt;&gt; RequestAsync(byte subnetID, byte deviceID, int opCode, int expectedResponseCode, int timeoutInMilliseconds) 的重载是否有意义,该重载会一直等待到超时到期并返回到目前为止得到的所有响应?
【解决方案2】:

您可以像使用同步 IO 一样编写它,而且这通常比您拥有的基于事件的代码要容易得多。

例如你可以说:

await SendAsync("000E");
var received = await ReceiveAsync();
if (received != "000F") AbortConnection();

await 使得使用异步 IO 和同步模式成为可能。

【讨论】:

  • 好的,但是考虑到上面基于事件的接口,我该如何实现等待的 SendAsync 和 ReceiveAsync?
  • 有一些标准模式。这总是可能的。 stackoverflow.com/questions/12858501/… 你只需要薄包装。所有逻辑都进入调用包装器的基于等待的代码。为了说明这一点,您可以等待 GUI 中的按钮单击。
猜你喜欢
  • 1970-01-01
  • 2017-11-07
  • 1970-01-01
  • 1970-01-01
  • 2017-01-05
  • 1970-01-01
  • 2017-09-12
  • 2023-03-10
  • 1970-01-01
相关资源
最近更新 更多