如何异步正确编写Socket / NetworkStream?

时间:2011-04-26 23:02:25

标签: c# .net sockets asynchronous networkstream

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

documentation of NetworkStream.Read意味着它是非阻塞的(“如果没有数据可供读取,Read方法返回0”),所以我想我不需要异步回调来读...对吗?

但写作怎么样?好吧,微软在这个问题上有一个常见的低质量tutorials,他们建议为每个操作编写一个繁琐的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方法。至少,你的state参数必须包含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.Sleep。)

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

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

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

2 个答案:

答案 0 :(得分:3)

我对NetworkStream没有任何经验,但我可以说有关异步Socket操作的信息:

    操作完成时需要调用
  • End*,因为这会清除异步操作使用的操作系统资源,并允许您获取操作的结果(即使该结果只是成功还是失败) 。 MSDN具有a good overview这种常见的异步模式。
  • 您不是必需将任何特定对象作为 state 参数传递。编写该文档时,传递它是将对象保留在完成委托范围内的最简单方法。现在,使用lambda表达式,这种做法并不常见。
  • 您可以将写入排队到套接字,在大多数情况下,这都可以。但是,有可能其中一个写入只会部分完成(我从未见过这种情况,但理论上可行)。我这样做多年但不再推荐它了。

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

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

答案 1 :(得分:1)

  

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

除非您从未计划有任何可用于阅读的数据,否则我不会看到这意味着非阻塞操作。如果有可读取的数据,则只要读取数据,操作就会阻塞。


  

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

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


  

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

来自the MSDN documentation for the Stream.BeginWrite Method

  

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

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


  

我不能只将IAsyncResult存储在某个地方......并定期检查它是否已完成?

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


  

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

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