订购反应性扩展事件

时间:2014-10-19 12:28:24

标签: udp system.reactive reactive-programming

我在多线程中接收UDP上的消息。每次接待后,我都会MessageReceived.OnNext(message)

因为我使用多个线程,所以消息无序,这是一个问题。

如何通过消息计数器命令加注消息? (假设有一个message.counter属性)

必须记住一条消息可能会在通信中丢失(假设我们在X消息之后有一个计数器漏洞,那个漏洞没有填满我提出下一条消息)

必须尽快提出消息(如果收到下一个计数器)

2 个答案:

答案 0 :(得分:6)

在说明检测丢失消息的要求时,您没有考虑到最后一条消息未到达的可能性;我添加了一个timeoutDuration,如果在给定时间内没有任何内容消息,则刷新缓冲的消息 - 您可能希望将此视为错误,请参阅有关如何执行此操作的注释。

我将通过使用以下签名定义扩展方法来解决此问题:

public static IObservable<TSource> Sort<TSource>(
    this IObservable<TSource> source,
    Func<TSource, int> keySelector,
    TimeSpan timeoutDuration = new TimeSpan(),
    int gapTolerance = 0)
  • source是未分类邮件的流
  • keySelector是一个从消息中提取int密钥的函数。我假设第一把关键是0;必要时修改。
  • 上面讨论了
  • timeoutDuration,如果省略,则没有超时
  • tolerance是等待无序消息时保留的最大消息数。通过0保留任意数量的消息
  • scheduler是用于超时的调度程序,用于测试目的,如果没有给出,则使用默认值。

操作实例

我将在此处提供逐行演练。下面将重复完整的实施。

分配默认计划程序

首先,如果没有提供默认调度程序,我们必须分配默认调度程序:

scheduler = scheduler ?? Scheduler.Default;

安排超时

现在,如果请求超时,我们将使用只会终止的副本替换来源,如果邮件未到达OnCompleted,则会发送timeoutDuration

if(timeoutDuration != TimeSpan.Zero)
    source = source.Timeout(
        timeoutDuration,
        Observable.Empty<TSource>(),
        scheduler);

如果您希望发送TimeoutException,只需将第二个参数删除到Timeout - 空流,以选择执行此操作的重载。请注意,我们可以安全地与所有订阅者共享此内容,因此它位于Observable.Create的调用之外。

创建订阅处理程序

我们使用Observable.Create来构建我们的流。每当订阅发生时,都会调用作为Create的参数的lambda函数,并传递调用观察者(o)。 Create会返回IObservable<T>,因此我们会将其返回。

return Observable.Create<TSource>(o => { ...

初始化一些变量

我们会在nextKey中跟踪下一个预期的键值,并创建一个SortedDictionary来保存无序消息,直到可以发送它们为止。

int nextKey = 0;  
var buffer = new SortedDictionary<int, TSource>();

订阅源,并处理消息

现在我们可以订阅消息流(可能已应用超时)。首先,我们介绍OnNext处理程序。下一条消息已分配给x

return source.Subscribe(x => { ...

我们调用keySelector函数从消息中提取密钥:

var key = keySelector(x);

如果邮件中有一个旧密钥(因为它超出了我们对无序邮件的容忍度),我们只是放弃它并完成此消息(您可能希望采取不同的行为):

// drop stale keys
if(key < nextKey) return;

否则,我们可能会有预期的密钥,在这种情况下我们可以增加nextKey发送消息:

if(key == nextKey)
{
    nextKey++;
    o.OnNext(x);                    
}

或者,我们可能有一个无序的未来消息,在这种情况下我们必须将它添加到我们的缓冲区。如果我们这样做,我们还必须确保我们的缓冲区没有超出我们存储乱序消息的容忍度 - 在这种情况下,我们也会将nextKey碰到缓冲区中的第一个键,因为它是{ {1}}是下一个最低的键:

SortedDictionary

现在无论上面的结果如何,我们都需要清空现在可以使用的任何键的缓冲区。我们使用辅助方法。请注意,它会调整else if(key > nextKey) { buffer.Add(key, x); if(gapTolerance != 0 && buffer.Count > gapTolerance) nextKey = buffer.First().Key; } ,因此我们必须小心通过引用传递它。我们只需循环缓冲区读取,删除和发送消息,只要密钥彼此相继,每次递增nextKey

nextKey

处理错误

接下来我们提供一个private static void SendNextConsecutiveKeys<TSource>( ref int nextKey, IObserver<TSource> observer, SortedDictionary<int, TSource> buffer) { TSource x; while(buffer.TryGetValue(nextKey, out x)) { buffer.Remove(nextKey); nextKey++; observer.OnNext(x); } } 处理程序 - 如果您选择这样做,这将只会传递任何错误,包括Timeout异常。

刷新缓冲区

最后,我们必须处理OnError。在这里,我选择清空缓冲区 - 如果无序消息阻止了消息并且从未到达,则这是必要的。这就是我们需要超时的原因:

OnCompleted

完全实施

这是完整的实施。

() => {
    // empty buffer on completion
    foreach(var item in buffer)
        o.OnNext(item.Value);                
    o.OnCompleted();
});

测试线束

如果您在控制台应用中包含nuget public static IObservable<TSource> Sort<TSource>( this IObservable<TSource> source, Func<TSource, int> keySelector, int gapTolerance = 0, TimeSpan timeoutDuration = new TimeSpan(), IScheduler scheduler = null) { scheduler = scheduler ?? Scheduler.Default; if(timeoutDuration != TimeSpan.Zero) source = source.Timeout( timeoutDuration, Observable.Empty<TSource>(), scheduler); return Observable.Create<TSource>(o => { int nextKey = 0; var buffer = new SortedDictionary<int, TSource>(); return source.Subscribe(x => { var key = keySelector(x); // drop stale keys if(key < nextKey) return; if(key == nextKey) { nextKey++; o.OnNext(x); } else if(key > nextKey) { buffer.Add(key, x); if(gapTolerance != 0 && buffer.Count > gapTolerance) nextKey = buffer.First().Key; } SendNextConsecutiveKeys(ref nextKey, o, buffer); }, o.OnError, () => { // empty buffer on completion foreach(var item in buffer) o.OnNext(item.Value); o.OnCompleted(); }); }); } private static void SendNextConsecutiveKeys<TSource>( ref int nextKey, IObserver<TSource> observer, SortedDictionary<int, TSource> buffer) { TSource x; while(buffer.TryGetValue(nextKey, out x)) { buffer.Remove(nextKey); nextKey++; observer.OnNext(x); } } ,则会为您提供以下测试工具:

rx-testing

结束评论

这里有各种有趣的替代方法。我采用这种主要的命令式方法,因为我认为最容易遵循 - 但可能会有一些花哨的分组诡计,你可以用来做到这一点。我知道有一件事一直都是关于Rx的 - 有很多方法可以让猫皮肤好看!

我对这里的超时想法也不太满意 - 在生产系统中,我想实现一些检查连接的方法,例如心跳或类似。我没有进入这个,因为显然它将是特定于应用程序的。此外,在这些委员会和其他地方(such as on my blog for example)之前已经讨论过心跳。

答案 1 :(得分:4)

如果您想要可靠的订购,请强烈考虑使用TCP - 这是它的用途;否则,你将被迫用UDP玩猜谜游戏,有时你会出错。

例如,假设您按此顺序收到以下数据报:[A,B,D]

当你收到D时,在推D之前你应该等多久C到达?

无论你选择哪种持续时间,你都可能错了:

  1. 如果C在传输过程中丢失了,那么它永远不会到达怎么办?
  2. 如果你选择的持续时间太短而你最终推D但接收C?
  3. 怎么办?

    也许您可以选择启发式最佳的持续时间,但为什么不使用TCP呢?

    旁注:

    MessageReceived.OnNext表示您正在使用Subject<T>,这可能是不必要的。考虑直接将异步UdpClient方法转换为observable,或者通过Observable.Create<T>(async (observer, cancel) => { ... })编写异步迭代器来转换它们。