NetworkStream.Read()和NetworkStream.BeginRead()有什么区别?

8
我需要从NetworkStream中读取数据,这些数据大小和发送时间都是随机的。我正在实现一个多线程应用程序,每个线程都有自己的流来读取数据。如果流上没有数据,则应用程序应该等待数据到达。但是,如果服务器已经完成了数据传输并终止了会话,则应退出。
最初,我使用Read方法从流中获取数据,但它会阻塞线程并一直等待,直到数据出现在流中。
MSDN文档建议:
如果没有可供读取的数据,则Read方法返回0。如果远程主机关闭连接,并且所有可用数据都已接收,则Read方法立即完成并返回零字节。
但在我的情况下,我从未让Read方法返回0并正常退出。它只是无限期地等待。
在进一步调查中,我发现了BeginRead,它会异步地监视流并调用回调方法,一旦接收到数据就会触发。我也尝试了使用这种方法的各种实现,但我无法确定何时使用BeginReadRead更有益。
据我所知,BeginRead仅具有异步调用的优点,这不会阻塞当前线程。但在我的应用程序中,我已经有一个单独的线程来读取和处理流中的数据,因此这不会有太大的区别。
以下是需要回答的问题:
1. 请问有谁能帮我理解BeginRead的等待和退出机制以及它与Read的区别?
2. 实现所需功能的最佳方法是什么?

你确定远程端是否正确关闭了连接?我从来没有遇到过Read不返回的问题。否则,你的方法似乎是正确的方向。 - user203570
@Matthew,老实说,我不能确定。我们正在读取第三方服务。他们已经指定了关闭时间,我们假设它按照规定的时间发生。在向他们发出警报之前,我想先在我的端口进行检查和重新检查。 - Danish Khan
4个回答

12

我使用BeginRead,但继续使用一个WaitHandle阻塞线程:

byte[] readBuffer = new byte[32];
var asyncReader = stream.BeginRead(readBuffer, 0, readBuffer.Length, 
    null, null);

WaitHandle handle = asyncReader.AsyncWaitHandle;

// Give the reader 2seconds to respond with a value
bool completed = handle.WaitOne(2000, false);
if (completed)
{
    int bytesRead = stream.EndRead(asyncReader);

    StringBuilder message = new StringBuilder();
    message.Append(Encoding.ASCII.GetString(readBuffer, 0, bytesRead));
}

基本上,它允许使用WaitHandle进行异步读取的超时,并在设置的时间(此处为2000)内完成读取时给出布尔值completed

这是我从我的一个Windows Mobile项目中复制和粘贴的完整流读取代码:

private static bool GetResponse(NetworkStream stream, out string response)
{
    byte[] readBuffer = new byte[32];
    var asyncReader = stream.BeginRead(readBuffer, 0, readBuffer.Length, null, null);
    WaitHandle handle = asyncReader.AsyncWaitHandle;

    // Give the reader 2seconds to respond with a value
    bool completed = handle.WaitOne(2000, false);
    if (completed)
    {
        int bytesRead = stream.EndRead(asyncReader);

        StringBuilder message = new StringBuilder();
        message.Append(Encoding.ASCII.GetString(readBuffer, 0, bytesRead));

        if (bytesRead == readBuffer.Length)
        {
            // There's possibly more than 32 bytes to read, so get the next 
            // section of the response
            string continuedResponse;
            if (GetResponse(stream, out continuedResponse))
            {
                message.Append(continuedResponse);
            }
        }

        response = message.ToString();
        return true;
    }
    else
    {
        int bytesRead = stream.EndRead(asyncReader);
        if (bytesRead == 0)
        {
            // 0 bytes were returned, so the read has finished
            response = string.Empty;
            return true;
        }
        else
        {
            throw new TimeoutException(
                "The device failed to read in an appropriate amount of time.");
        }
    }
}

感谢@GenericTypeTea,这看起来是一种有趣的方法,但我无法确定正确的超时时间。尽管如此,我认为可以稍作修改以解决我的问题。如果成功的话,我会试一下并报告。顺便说一句,你的用户名让我笑了.. :) - Danish Khan
1
如果我没记错的话,每次调用BeginABC都必须调用EndABC。在上面的代码中,如果出现超时,End就不会被调用。应该向上述代码添加一个回调函数来调用End(并处理任何可能的异常)。 - SpeksETC
@SpeksETC - 我认为你错了。我总是调用结束:int bytesRead = stream.EndRead(asyncReader);,但也许我应该改变代码,直接在 bool completed = handle.WaitOne(2000, false); 这一行之后调用那段代码,以消除重复。 - djdd87

5

异步 I/O 可以在更少的线程中实现相同数量的 I/O。

正如您所注意到的,现在您的应用程序每个流都有一个线程。虽然小规模连接可以承受,但如果需要支持 10000 个同时连接呢?使用异步 I/O,不再需要这样,因为读取完成回调允许传递标识相关流的上下文。您的读取不再阻塞,因此您不需要每个流一个线程。

无论您使用同步还是异步 I/O,都有一种方式来检测和处理相关 API 返回代码上的流关闭。当套接字已经关闭时,BeginRead 应该失败并引发 IOException。异步读取等待期间关闭操作将触发回调,EndRead 然后通知您当前状态。

当您的应用程序调用 BeginRead 时, 系统将等待数据接收或出现错误,然后 使用单独的线程执行指定的回调方法,并阻止 EndRead 直到提供的 NetworkStream 读取数据或引发异常。


谢谢@Steve。每个流一个线程是由于很多其他设计考虑,并不是问题。我们最多会有10个同时线程。 - Danish Khan

0
BeginRead是一个异步过程,这意味着您的主线程将在另一个进程中开始执行Read。现在我们有两个并行进程。如果您想获取结果,必须调用EndRead,它将返回结果。
一些伪代码。
BeginRead()
//...do something in main object while result is fetching in another thread
var result = EndRead();

但是,如果你的主线程没有其他事情要做,并且需要结果,那么应该调用Read。


2
这里涉及到多个进程的暗示是完全错误的。 - Steve Townsend
@ Steve,我认为@ Bonshington说“进程”时实际上是指“线程”。 - Danish Khan

0

你试过使用server.ReceiveTimeout了吗?你可以设置Read()函数在返回零之前等待传入数据的时间。在你的情况下,这个属性可能在某个地方被设置为无限。


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接