我如何正确地写Socket/NetworkStream异步

本文关键字:NetworkStream 异步 Socket 正确地 | 更新日期: 2023-09-27 17:53:22

我想使用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

(我知道这意味着轮询,但它将在一个服务器,预计是活跃的大部分时间,我正在计划一些线程。)

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

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

另外,对于使用Socket还是NetworkStream,欢迎提出任何意见。

我如何正确地写Socket/NetworkStream异步

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

  • End*需要在操作完成时调用,因为这会清理异步操作使用的操作系统资源,并允许您获得操作的结果(即使该结果只是操作成功或失败)。MSDN对这种常见的异步模式有一个很好的概述。
  • 不要求传递任何特定对象作为状态参数。在编写该文档时,传递它是将对象保持在完成委托范围内的最简单方法。现在,对于lambda表达式,这种做法几乎不常见。
  • 您可以将写入排队到套接字,并且在大多数情况下这是可行的。但是,其中一个写入可能只完成部分(我从未见过这种情况,但理论上是可能的)。我这样做了很多年,但不再推荐了。
我建议使用完成委托而不是轮询。如果你想要一个更容易使用的包装器,你可以尝试一下Nito.Async。套接字,封装Begin/End操作,并通过单个线程(例如UI线程)序列化它们。

注:如果想要停止阻塞操作,可以从另一个线程关闭套接字,这将导致操作以错误完成。但是,我不建议使用阻塞套接字操作。

NetworkStream的文档。Read意味着它是非阻塞的("如果没有数据可用于读取,Read方法返回0"),所以我想我不需要异步回调读取…对吧?

我不明白这意味着一个非阻塞操作,除非你从来没有计划有任何数据可供读取。如果有可用的数据可读,那么只要读取数据,操作就会阻塞。


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

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

如果操作已经完成,为什么我们必须调用Socket.EndSend(IAsyncResult)?

来自Stream.BeginWrite方法的MSDN文档:

将当前方法返回的IAsyncResult传递给EndWrite,以确保写入完成并适当地释放资源。EndWrite必须在每次调用BeginWrite时调用一次。你可以通过使用与BeginWrite相同的代码或在传递给BeginWrite的回调中实现这一点。如果在异步写入过程中发生错误,则在调用EndWrite并使用此方法返回IAsyncResult之前不会抛出异常。

你永远不知道I/O操作会发生什么,而且你不能对异步操作进行正常的错误处理,因为执行是在线程池中的一个单独的线程上进行的。因此,调用EndWrite的要点是给你一个机会,通过将EndWrite放入AsyncCallback方法中的try/catch块来处理任何错误。


我不能把IAsyncResult存储在某个地方…并定期检查是否完成?

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


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

虽然我从来没有尝试过,我不知道它是否会工作,它真的取决于NetworkStream类的具体实现。第二个请求可能会等到第一个请求完成后才尝试执行,但我有一种感觉,它要么会抛出异常,要么会在另一端得到损坏的数据。如果你想同时发送2个独立的数据流,我认为最好的方法是使用2个独立的NetworkStreams。