How do I properly write Socket/NetworkStream asynchronously?
I want to use NetworkStream
(or maybe Socket
) to read/write TCP connections. I want to use non-blocking operations so that I don't have to deal with several threads, or deal with the issue of how to stop the program if some threads might be halted on blocking network operations.
The documentation of NetworkStream.Read implies it is non-blocking ("If no data is available for reading, the Read method returns 0"), so I guess I don't need async callbacks for reading... right?
But what about writing? Well, Microsoft has one of its usual low-quality tutorials on this subject, and they propose writing a cumbersome AsyncCallback
method for every operation. What's the point of the code inside this callback?
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());
}
}
The documentation of AsyncCallback
says it's "a callback method that is called when the asynchronous operation completes". If the operation is completed already, why do we have to call Socket.EndSend(IAsyncResult)
? Meanwhile, the documentation of NetworkStream.BeginWrite
says "You must create a callback method that implements the AsyncCallback
delegate and pass its name to the BeginWrite
method. At the very minimum, your state parameter must contain the NetworkStream
."
But why "must" I? Can't I just store the IAsyncResult
somewhere...
_sendOp = client.BeginSend(message, 0, message.Length, SocketFlags.None, null, null);
...and periodically check whether it's finished?
if (_sendOp.IsCompleted)
// start sending the next message in the queue
(I know this implies polling, but it will be in a server that is expected to be active most of the time, and I'm planning some Thread.Sleeps when idle.)
Another issue. The messages I'm sending are broken up into a header array and a body array. Can I c开发者_如何转开发an issue two BeginWrites in a row?
IAsyncResult ar1 = _stream.BeginWrite(header, 0, header.Length, null, null);
IAsyncResult ar2 = _stream.BeginWrite(msgBody, 0, msgBody.Length, null, null);
Also, any opinions are welcome about whether to use Socket or NetworkStream.
I don't have any experience with NetworkStream
, but I can say this about asynchronous Socket
operations:
End*
needs to be called when the operation completes because this cleans up OS resources used by the asynchronous operation and allows you to get the result of the operation (even if that result is just whether it succeeded or failed). MSDN has a good overview of this common asynchronous pattern.- You are not required to pass any particular object as the state parameter. When that documentation was written, passing it was the easiest way to keep the object in scope for the completion delegate. These days, with lambda expressions, that practice is not nearly as common.
- You can queue up writes to the socket, and under most conditions this will work. It is possible, though, that one of the writes will only partially complete (I've never seen this happen, but it is theoretically possible). I did this for years but no longer recommend it.
I do recommend using completion delegates instead of polling. If you want an easier-to-use wrapper, you could try Nito.Async.Sockets, which wraps the Begin
/End
operations and serializes them all through a single thread (e.g., a UI thread).
P.S. If you want to stop a blocking operation, you can close the socket from another thread, which will cause the operation to complete with an error. However, I don't recommend using blocking socket operations.
The documentation of NetworkStream.Read implies it is non-blocking ("If no data is available for reading, the Read method returns 0"), so I guess I don't need async callbacks for reading... right?
I don't see how that would imply a non-blocking operation, unless you never plan for there to be any data available for reading. If there's data available to be read, the operation will block for as long as it takes to read the data.
But what about writing? Well, Microsoft has one of its usual low-quality tutorials on this subject, and they propose writing a cumbersome AsyncCallback method for every operation. What's the point of the code inside this callback?
The point is to avoid writing even more-cumbersome thread management code for yourself. As multi-threading goes, an AsyncCallback is about as straightforward as it gets, although this example is rather silly. In the example, the callback routine signals a ManualResetEvent
(sendDone.Set()
), which would be one way to avoid polling; you would have a separate thread that waits on that ManualResetEvent
, which causes it to block until something else calls its Set()
method. But wait, wasn't the main point of using an AsyncCallback in the first place to avoid writing your own thread management? So why have a thread that's waiting on a ManualResetEvent
? A better example would show actually doing something in your AsyncCallback method, but be aware that if you're interacting with any UI elements such as Forms controls, you'll need to use the control's Invoke
method, not manipulate it directly.
If the operation is completed already, why do we have to call Socket.EndSend(IAsyncResult)?
From the MSDN documentation for the Stream.BeginWrite
Method:
Pass the IAsyncResult returned by the current method to EndWrite to ensure that the write completes and frees resources appropriately. EndWrite must be called once for every call to BeginWrite. You can do this either by using the same code that called BeginWrite or in a callback passed to BeginWrite. If an error occurs during an asynchronous write, an exception will not be thrown until EndWrite is called with the IAsyncResult returned by this method.
You never know what's going to happen with I/O operations, and you can't do normal error handling with an asynchronous operation because execution is taking place on a separate thread in the thread pool. So the point of calling EndWrite
is to give you a chance to handle any errors by putting EndWrite
inside a try/catch block within your AsyncCallback method.
Can't I just store the IAsyncResult somewhere... and periodically check whether it's finished?
You could, if you want to handle all the thread management and safety concerns to ensure that different threads would never try to handle the same IAsyncResult at the same time, but that seems like a lot more trouble to me than writing an AsyncCallback. Again, the point of using asynchronous methods is to make it so you have to deal with threading concerns as little as possible.
Another issue. The messages I'm sending are broken up into a header array and a body array. Can I can issue two BeginWrites in a row?
Although I've never tried this, I don't know if it would work, and it really depends on the specific implementation of the NetworkStream class. It's possible the 2nd request would wait until the 1st request completes before attempting to execute, but I have a feeling it would either throw an exception or you would get corrupted data on the other end. If you want to send 2 separate data streams at the same time, I think the best way to do this would be to use 2 separate NetworkStreams.
精彩评论