使用Observable通过FromAsyncPattern从Stream读取,如何正确关闭/取消

时间:2015-08-18 17:48:13

标签: c# .net asynchronous network-programming rx.net

需要:具有TCP连接的长时间运行程序

C#4.0(VS1010,XP)程序需要使用TCP连接到主机,发送和接收字节,有时会正确关闭连接并稍后重新打开。使用Rx.Net Observable样式编写周围代码。数据量很小但程序应该连续运行(通过妥善处理资源来避免内存泄漏)。

下面的文字很长,因为我解释了我搜索和发现的内容。它现在似乎有效。

总体问题是:由于Rx有时不直观,解决方案是否良好?这是否可靠(比如说,它可能会运行多年没有问题)?

到目前为止的解决方案

发送

该程序获得NetworkStream,如下所示:

TcpClient tcpClient = new TcpClient();
LingerOption lingerOption = new LingerOption(false, 0); // Make sure that on call to Close(), connection is closed immediately even if some data is pending.
tcpClient.LingerState = lingerOption;

tcpClient.Connect(remoteHostPort);
return tcpClient.GetStream();

异步发送很简单。与传统解决方案相比,Rx.Net允许使用更短更简洁的代码来处理此问题。我创建了一个EventLoopScheduler的专用线程。需要发送的操作使用IObservable表示。使用ObserveOn(sendRecvThreadScheduler)保证所有发送操作都在该线程上完成。

sendRecvThreadScheduler = new EventLoopScheduler(
    ts =>
    {
        var thread = new System.Threading.Thread(ts) { Name = "my send+receive thread", IsBackground = true };
        return thread;
    });
        // Loop code for sending not shown (too long and off-topic).

到目前为止,这是优秀和完美的。

接收

似乎要接收数据,Rx.Net还应该允许传统解决方案更短更清晰的代码。 在阅读了几个资源(例如http://www.introtorx.com/)和stackoverflow之后,似乎一个非常简单的解决方案是将异步编程桥接到Rx.Net,就像在https://stackoverflow.com/a/14464068/1429390中一样:

public static class Ext
{
    public static IObservable<byte[]> ReadObservable(this Stream stream, int bufferSize)
    {
        // to hold read data
        var buffer = new byte[bufferSize];
        // Step 1: async signature => observable factory
        var asyncRead = Observable.FromAsyncPattern<byte[], int, int, int>(
            stream.BeginRead,
            stream.EndRead);
        return Observable.While(
            // while there is data to be read
            () => stream.CanRead,
            // iteratively invoke the observable factory, which will
            // "recreate" it such that it will start from the current
            // stream position - hence "0" for offset
            Observable.Defer(() => asyncRead(buffer, 0, bufferSize))
                .Select(readBytes => buffer.Take(readBytes).ToArray()));
    }
}

它主要起作用。我可以发送和接收字节。

关闭时间

这是事情开始出错的时候。

有时我需要关闭流并保持清洁。基本上这意味着:停止读取,结束字节接收observable,用新的连接打开新连接。

首先,当远程主机强制关闭连接时,BeginRead()/EndRead()立即循环使用所有CPU返回零字节。我让更高级别的代码注意到这一点(在高级元素可用的上下文中使用Subscribe()ReadObservable)并清理(包括关闭和处理流)。这也很有效,我负责处理Subscribe()返回的对象。

    someobject.readOneStreamObservableSubscription = myobject.readOneStreamObservable.Subscribe(buf =>
    {
        if (buf.Length == 0)
        {
                MyLoggerLog("Read explicitly returned zero bytes.  Closing stream.");
                this.pscDestroyIfAny();
        }
    });

有时,我只需要关闭流。但显然这必须导致在异步读取中抛出异常。 c# - Proper way to prematurely abort BeginRead and BeginWrite? - Stack Overflow

我添加了CancellationToken,导致Observable.While()结束序列。这对于避免这些异常没有多大帮助,因为BeginRead()可以长时间睡眠。

observable中的未处理异常导致程序退出。正在搜索提供的.net - Continue using subscription after exception - Stack Overflow,其中建议添加Catch,以便有效地恢复已损坏的Observable

代码如下所示:

public static IObservable<byte[]> ReadObservable(this Stream stream, int bufferSize, CancellationToken token)
{
    // to hold read data
    var buffer = new byte[bufferSize];
    // Step 1: async signature => observable factory
    var asyncRead = Observable.FromAsyncPattern<byte[], int, int, int>(
        stream.BeginRead,
        stream.EndRead);
    return Observable.While(
        // while there is data to be read
        () =>
        {
            return (!token.IsCancellationRequested) && stream.CanRead;
        },
        // iteratively invoke the observable factory, which will
        // "recreate" it such that it will start from the current
        // stream position - hence "0" for offset
        Observable.Defer(() =>
            {
                if ((!token.IsCancellationRequested) && stream.CanRead)
                {
                    return asyncRead(buffer, 0, bufferSize);
                }
                else
                {
                    return Observable.Empty<int>();
                }
            })
            .Catch(Observable.Empty<int>()) // When BeginRead() or EndRead() causes an exception, don't choke but just end the Observable.
        .Select(readBytes => buffer.Take(readBytes).ToArray()));
}

现在怎么办?问题

这似乎运作良好。检测到远程主机强制关闭连接或者不再可访问的条件,导致更高级别的代码关闭连接并重试。到目前为止一切都很好。

我不确定事情是否正确。

首先,那一行:

.Catch(Observable.Empty<int>()) // When BeginRead() or EndRead() causes an exception, don't choke but just end the Observable.

感觉就像命令式代码中空捕获块的坏习惯。实际代码会记录异常,而更高级别的代码会检测到没有回复并正确处理,所以它应该被认为是相当正常的(见下文)?

.Catch((Func<Exception, IObservable<int>>)(ex =>
{
    MyLoggerLogException("On asynchronous read from network.", ex);
    return Observable.Empty<int>();
})) // When BeginRead() or EndRead() causes an exception, don't choke but just end the Observable.

此外,这确实比大多数传统解决方案更短。

解决方案是正确的还是我错过了一些更简单/更清洁的方法?

对于Reactive Extensions的向导,是否存在一些显而易见的可怕问题?

感谢您的关注。

0 个答案:

没有答案