3

我想使用NetworkStream(或者也许Socket)来读/写 TCP 连接。我想使用非阻塞操作,这样我就不必处理多个线程,或者处理如果某些线程可能因阻塞网络操作而停止时如何停止程序的问题。

NetworkStream.Read的文档暗示它是非阻塞的(“如果没有数据可用于读取,则 Read 方法返回 0”),所以我想我不需要异步回调来读取......对吗?

但是写作呢?好吧,微软有一个关于这个主题的通常的低质量教程,他们建议AsyncCallback为每个操作编写一个繁琐的方法。这个回调中的代码有什么意义?

client.BeginSend(byteData, 0, byteData.Length, SocketFlags.None,
                 new AsyncCallback(SendCallback), client);
    ...
private static void SendCallback(IAsyncResult ar)
{
    try {
        Socket client = (Socket)ar.AsyncState;

        int bytesSent = client.EndSend(ar);

        Console.WriteLine("Sent {0} bytes to server.", bytesSent);
        // Signal that all bytes have been sent.

        sendDone.Set();
    }
    catch (Exception e)
    {
        Console.WriteLine(e.ToString());
    }
}

的文档AsyncCallback说它是“异步操作完成时调用的回调方法”。如果操作已经完成,为什么还要调用Socket.EndSend(IAsyncResult)?同时,文档NetworkStream.BeginWrite说“您必须创建一个实现AsyncCallback委托的回调方法并将其名称传递给该BeginWrite方法。您的状态参数至少必须包含NetworkStream.”

但为什么“必须”我呢?我不能只是存储在IAsyncResult某个地方...

_sendOp = client.BeginSend(message, 0, message.Length, SocketFlags.None, null, null);

...并定期检查是否完成?

if (_sendOp.IsCompleted)
    // start sending the next message in the queue

(我知道这意味着轮询,但它将在预计大部分时间处于活动状态的服务器中,并且我计划在空闲时进行一些 Thread.Sleeps。)

另一个问题。我发送的消息被分解为一个头数组和一个正文数组。我可以连续发出两个 BeginWrites 吗?

IAsyncResult ar1 = _stream.BeginWrite(header, 0, header.Length, null, null);
IAsyncResult ar2 = _stream.BeginWrite(msgBody, 0, msgBody.Length, null, null);

此外,欢迎任何关于是否使用 Socket 或 NetworkStream 的意见。

4

2 回答 2

3

我没有任何经验NetworkStream,但我可以这样说异步Socket操作:

  • End*需要在操作完成时调用,因为这会清理异步操作使用的操作系统资源并允许您获取操作的结果(即使该结果只是成功或失败)。MSDN很好地概述了这种常见的异步模式。
  • 不需要将任何特定对象作为状态参数传递。在编写该文档时,传递它是将对象保持在完成委托范围内的最简单方法。如今,使用 lambda 表达式,这种做法几乎不再常见。
  • 您可以排队写入套接字,并且在大多数情况下这将起作用。但是,其中一个写入可能只会部分完成(我从未见过这种情况发生,但理论上是可能的)。我这样做了多年,但不再推荐它。

我确实建议使用完成委托而不是轮询。如果您想要一个更易于使用的包装器,您可以尝试Nito.Async.Sockets,它包装Begin/End操作并通过单个线程(例如,UI 线程)将它们全部序列化。

PS如果要停止一个阻塞操作,可以从另一个线程关闭socket,这样会导致操作完成并报错。但是,我不建议使用阻塞套接字操作。

于 2011-04-27T00:53:55.133 回答
1

NetworkStream.Read 的文档暗示它是非阻塞的(“如果没有数据可用于读取,则 Read 方法返回 0”),所以我想我不需要异步回调来读取......对吗?

我不明白这将如何暗示非阻塞操作,除非您从未计划有任何数据可供读取。如果有数据可供读取,则操作将在读取数据所需的时间内阻塞。


但是写作呢?嗯,微软有一个关于这个主题的通常的低质量教程,他们建议为每个操作编写一个繁琐的 AsyncCallback 方法。这个回调中的代码有什么意义?

关键是要避免为自己编写更繁琐的线程管理代码。随着多线程的发展,AsyncCallback 非常简单,尽管这个例子相当愚蠢。在示例中,回调例程发出ManualResetEvent( sendDone.Set()) 信号,这是避免轮询的一种方法;您将有一个单独的线程等待它ManualResetEvent,这会导致它阻塞,直到其他东西调用它的Set()方法。但是等等,首先使用 AsyncCallback 以避免编写自己的线程管理的主要目的不是吗?那么为什么有一个线程正在等待ManualResetEvent? 一个更好的示例将显示在 AsyncCallback 方法中实际执行的操作,但请注意,如果您正在与任何 UI 元素(例如 Forms 控件)进行交互,则需要使用控件的Invoke方法,而不是直接操作它。


如果操作已经完成,为什么还要调用 Socket.EndSend(IAsyncResult)?

来自Method的 MSDN 文档Stream.BeginWrite

将当前方法返回的 IAsyncResult 传递给 EndWrite 以确保写入完成并适当地释放资源。每次调用 BeginWrite 都必须调用一次 EndWrite。您可以通过使用调用 BeginWrite 的相同代码或在传递给 BeginWrite 的回调中执行此操作。如果在异步写入期间发生错误,则在使用此方法返回的 IAsyncResult 调用 EndWrite 之前不会引发异常。

您永远不知道 I/O 操作会发生什么,并且您无法使用异步操作进行正常的错误处理,因为执行发生在线程池中的单独线程上。因此,调用EndWrite的目的是让您有机会通过EndWrite在 AsyncCallback 方法中放入 try/catch 块来处理任何错误。


我不能将 IAsyncResult 存储在某处......并定期检查它是否完成?

如果您想处理所有线程管理和安全问题,以确保不同的线程永远不会尝试同时处理相同的 IAsyncResult,您可以这样做,但这对我来说似乎比编写 AsyncCallback 麻烦得多。同样,使用异步方法的目的是使您必须尽可能少地处理线程问题。


另一个问题。我发送的消息被分解为一个头数组和一个正文数组。我可以连续发出两个 BeginWrites 吗?

这个虽然没试过,但是不知道能不能用,还得看NetworkStream类的具体实现。第二个请求可能会等到第一个请求完成后再尝试执行,但我感觉它要么会引发异常,要么会在另一端得到损坏的数据。如果您想同时发送 2 个单独的数据流,我认为最好的方法是使用 2 个单独的 NetworkStreams。

于 2011-04-27T00:19:07.483 回答