Asynchronous Server Socket in .Net Core - How do I return the result?

Viewed 3825

I have a requirement to send data asynchronously via TCP. It is a collection of strings ICollection<string>.

I searched and found a good starting example from Microsoft (see below). The sample seems to be under .NET Framework, but I assume it applies to .NET Core as well.

What I am doing:

  1. I am re-purposing the code as a non-static class

  2. I would like to send a collection of strings ICollection<string>. I know I can rewrite it to send the collection of strings in the main method. Not a problem.

  3. I would like to receive a response for each message sent and do something with it. The current response is stored statically in private static String response = String.Empty;. I don't want it to be static. I want a local method variable.

  4. My challenge begins from item 3.. How do I return back that response message that seems only accessible from within private static void ReceiveCallback( IAsyncResult ar )

    I do not think changing it to private static string ReceiveCallback( IAsyncResult ar ) would work. If so, how do I read it from client.BeginReceive( state.buffer, 0, StateObject.BufferSize, 0, new AsyncCallback(ReceiveCallback), state);?

I put out a 300point bounty on a very old post for a similar question I found: C# Asyn. Socket Programming. Happy to award anyone who answers here, then there.

An additional question is: Is it recommended practice to open a TCP connection, send the multiple messages, then close it? Or to open a TCP connection for each message being sent?

Microsoft Example

using System;  
using System.Net;  
using System.Net.Sockets;  
using System.Threading;  
using System.Text;  
  
// State object for receiving data from remote device.  
public class StateObject {  
    // Client socket.  
    public Socket workSocket = null;  
    // Size of receive buffer.  
    public const int BufferSize = 256;  
    // Receive buffer.  
    public byte[] buffer = new byte[BufferSize];  
    // Received data string.  
    public StringBuilder sb = new StringBuilder();  
}  
  
public class AsynchronousClient {  
    // The port number for the remote device.  
    private const int port = 11000;  
  
    // ManualResetEvent instances signal completion.  
    private static ManualResetEvent connectDone =
        new ManualResetEvent(false);  
    private static ManualResetEvent sendDone =
        new ManualResetEvent(false);  
    private static ManualResetEvent receiveDone =
        new ManualResetEvent(false);  
  
    // The response from the remote device.  <------ ### the response data that I want to access, non statically
    private static String response = String.Empty;  
  
    private static void StartClient() {  
        // Connect to a remote device.  
        try {  
            // Establish the remote endpoint for the socket.  
            // The name of the
            // remote device is "host.contoso.com".  
            IPHostEntry ipHostInfo = Dns.GetHostEntry("host.contoso.com");  
            IPAddress ipAddress = ipHostInfo.AddressList[0];  
            IPEndPoint remoteEP = new IPEndPoint(ipAddress, port);  
  
            // Create a TCP/IP socket.  
            Socket client = new Socket(ipAddress.AddressFamily,  
                SocketType.Stream, ProtocolType.Tcp);  
  
            // Connect to the remote endpoint.  
            client.BeginConnect( remoteEP,
                new AsyncCallback(ConnectCallback), client);  
            connectDone.WaitOne();  
  
            // Send test data to the remote device.  
            Send(client,"This is a test<EOF>");  
            sendDone.WaitOne();  
  
            // Receive the response from the remote device.  
            Receive(client);  
            receiveDone.WaitOne();  
  
            // Write the response to the console.  
            Console.WriteLine("Response received : {0}", response);  
  
            // Release the socket.  
            client.Shutdown(SocketShutdown.Both);  
            client.Close();  
  
        } catch (Exception e) {  
            Console.WriteLine(e.ToString());  
        }  
    }  
  
    private static void ConnectCallback(IAsyncResult ar) {  
        try {  
            // Retrieve the socket from the state object.  
            Socket client = (Socket) ar.AsyncState;  
  
            // Complete the connection.  
            client.EndConnect(ar);  
  
            Console.WriteLine("Socket connected to {0}",  
                client.RemoteEndPoint.ToString());  
  
            // Signal that the connection has been made.  
            connectDone.Set();  
        } catch (Exception e) {  
            Console.WriteLine(e.ToString());  
        }  
    }  
  
    private static void Receive(Socket client) {  
        try {  
            // Create the state object.  
            StateObject state = new StateObject();  
            state.workSocket = client;  
  
            // Begin receiving the data from the remote device.  
            client.BeginReceive( state.buffer, 0, StateObject.BufferSize, 0,  
                new AsyncCallback(ReceiveCallback), state);  //<------ The receive callback is here, how do I return the result to the caller?
        } catch (Exception e) {  
            Console.WriteLine(e.ToString());  
        }  
    }  
  
    private static void ReceiveCallback( IAsyncResult ar ) {  
        try {  
            // Retrieve the state object and the client socket
            // from the asynchronous state object.  
            StateObject state = (StateObject) ar.AsyncState;  
            Socket client = state.workSocket;  
  
            // Read data from the remote device.  
            int bytesRead = client.EndReceive(ar);  
  
            if (bytesRead > 0) {  
                // There might be more data, so store the data received so far.  
            state.sb.Append(Encoding.ASCII.GetString(state.buffer,0,bytesRead));  
  
                // Get the rest of the data.  
                client.BeginReceive(state.buffer,0,StateObject.BufferSize,0,  
                    new AsyncCallback(ReceiveCallback), state);  
            } else {  
                // All the data has arrived; put it in response.  
                if (state.sb.Length > 1) {  
                    response = state.sb.ToString();  //<--------- ### Where it is assigned, I want it returned
                }  
                // Signal that all bytes have been received.  
                receiveDone.Set();  
            }  
        } catch (Exception e) {  
            Console.WriteLine(e.ToString());  
        }  
    }  
  
    private static void Send(Socket client, String data) {  
        // Convert the string data to byte data using ASCII encoding.  
        byte[] byteData = Encoding.ASCII.GetBytes(data);  
  
        // Begin sending the data to the remote device.  
        client.BeginSend(byteData, 0, byteData.Length, 0,  
            new AsyncCallback(SendCallback), client);  
    }  
  
    private static void SendCallback(IAsyncResult ar) {  
        try {  
            // Retrieve the socket from the state object.  
            Socket client = (Socket) ar.AsyncState;  
  
            // Complete sending the data to the remote device.  
            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());  
        }  
    }  
  
    public static int Main(String[] args) {  
        StartClient();  
        return 0;  
    }  
}
2 Answers

I found my answer from the .NET Core team. According to them:

With regards to the Microsoft Example:

That's actually not a good example, because it uses the outdated Begin*/End* pattern (also known as APM). Instead, you should use using async-await.

And if you switched to that, it would make changing the code the way you want much easier, because there aren't any callbacks anymore, instead you do e.g. await client.ReceiveAsync(…) and, after processing the response, return the result.

https://github.com/dotnet/core/issues/4828#issuecomment-643619106

The recommended way to do it is as follows:

ICollection<string> strings = ...;
using Socket socket = ...;
using var stream = new NetworkStream(socket);
using var writer = new StreamWriter(stream);

foreach(string s in strings)
{
    await writer.WriteLineAsync(s);
}
await writer.FlushAsync();

Added notes:

If your strings contain newlines, you will want to length-prefix your messages or escape the newlines prior to writing them.

For my question: Is it recommended practice to open a TCP connection, send the multiple messages, then close it? Or to open a TCP connection for each message being sent?

Establishing a TCP connection is generally a lot more expensive than using an existing one. But, this is ultimately scenario-dependent and you will want to do some more learning and prototyping here to see what is right for you.

https://github.com/dotnet/core/issues/4828#issuecomment-643694377

You can create a class (non-static, I called it AsynchronousClient) that implements all the logic of the socket communication straight from the Microsoft example. The relevant additions are the 3 events (more on handling and raising events):

1) ConnectionComplete, fired when an asynchronous connection operation is completed;

2) SendComplete, fired when data (a string, in this example) is successfully sent;

3) DataReceived, fired when there is incoming data from the remote endpoint.

Basically, the class exposes 3 public methods: AsyncConnect, AsyncSend and AsyncReceive. On the 3 private callbacks the corresponding event in the list above is fired and the class using AsynchronousClient is notified of the termination of the operation.

public class AsynchronousClient
{
    /// <summary>
    /// The client's socket instance.
    /// </summary>
    private Socket _clientSocket;

    /// <summary>
    /// Define the signature of the handler of the ConnectionComplete event.
    /// </summary>
    public delegate void ConnectionCompleteEventDelegate(AsynchronousClient sender, Socket clientSocket);

    /// <summary>
    /// Define the signature of the handler of the SendComplete event.
    /// </summary>
    public delegate void SendCompleteEventDelegate(AsynchronousClient sender, Socket clientSocket);

    /// <summary>
    ///  Define the signature of the handler of the DataReceived event.
    /// </summary>
    public delegate void DataReceivedEventDelegate(AsynchronousClient sender, Socket clientSocket, string data);

    /// <summary>
    /// ConnectionComplete event the client class can subscribe to.
    /// </summary>
    public event ConnectionCompleteEventDelegate ConnectionComplete;

    /// <summary>
    /// SendComplete event a class using an AsynchronousClient instance can subscribe to.
    /// </summary>
    public event SendCompleteEventDelegate SendComplete;

    /// <summary>
    /// DataReceived event a class using an AsynchronousClient instance can subscribe to.
    /// </summary>
    public event DataReceivedEventDelegate DataReceived;

    /// <summary>
    /// The remote endpoint the socket is going to communicate to. 
    /// </summary>
    public IPEndPoint RemoteEndpoint { get; private set; }

    /// <summary>
    /// Class initializer.
    /// </summary>
    /// <param name="remoteEndpoint">The remote endpoint to connect to.</param>
    public AsynchronousClient(IPEndPoint remoteEndpoint)
    {
        RemoteEndpoint = remoteEndpoint;
        // Create a TCP/IP socket.  
        _clientSocket = new Socket(
            RemoteEndpoint.AddressFamily, 
            SocketType.Stream, 
            ProtocolType.Tcp);
    }

    /// <summary>
    /// Asynchronous connection request.
    /// </summary>
    public void AsyncConnect()
    {
        try
        {
            // Initiate the connection procedure to the remote endpoint.  
            _clientSocket.BeginConnect(
                RemoteEndpoint,
                new AsyncCallback(AsyncConnectCallback), _clientSocket);
        }
        catch (Exception ex)
        {
            // TODO: manage exception.
            throw;
        }
    }

    /// <summary>
    /// Called after the connection to the remote endpoint is established.
    /// </summary>
    private void AsyncConnectCallback(IAsyncResult ar)
    {
        try
        {
            // Retrieve the socket from the state object.  
            Socket client = (Socket)ar.AsyncState;
            // Complete the connection.  
            client.EndConnect(ar);
            // If a client class is subscribed to the event, invoke the delegate.
            if (!(ConnectionComplete is null))
                ConnectionComplete.Invoke(this, client);
        }
        catch (Exception ex)
        {
            // TODO: manage exception.
            throw;
        }
    }

    /// <summary>
    /// Asynchronously sends a string to the remote endpoint.
    /// </summary>
    public void AsyncSend(string data)
    {
        try
        {
            // Convert the string data to byte data using ASCII encoding.  
            byte[] byteData = Encoding.ASCII.GetBytes(data);
            // Begin sending the data to the remote device.  
            _clientSocket.BeginSend(byteData, 0, byteData.Length, 0,
                new AsyncCallback(AsyncSendCallback), _clientSocket);
        }
        catch(Exception ex)
        {
            // TODO: manage exception.
            throw;
        }
    }

    /// <summary>
    /// Called after the send operation is complete.
    /// </summary>
    private void AsyncSendCallback(IAsyncResult ar)
    {
        try
        {
            // Retrieve the socket from the state object.  
            Socket client = (Socket)ar.AsyncState;
            // Complete sending the data to the remote device.  
            int bytesSent = client.EndSend(ar);
            // If a client class is subscribed to the event, invoke the delegate.
            if (!(SendComplete is null))
                SendComplete(this, client);
        }
        catch (Exception ex)
        {
            // TODO: manage exception.
            throw;
        }
    }

    /// <summary>
    /// Asynchronously waits for a response from the remote endpoint.
    /// </summary>
    public void AsyncReceive(Socket client)
    {
        try
        {
            // Create the state object.  
            StateObject state = new StateObject();
            state.workSocket = client;
            // Begin receiving the data from the remote device.  
            client.BeginReceive(state.buffer, 0, StateObject.BufferSize, 0,
                new AsyncCallback(AsyncReceiveCallback), state);
        }
        catch (Exception ex)
        {
            // TODO: manage exception.
            throw;
        }
    }

    /// <summary>
    /// Called after the receive operation is complete.
    /// </summary>
    private void AsyncReceiveCallback(IAsyncResult ar)
    {
        try
        {
            // Retrieve the state object and the client socket
            // from the asynchronous state object.  
            StateObject state = (StateObject)ar.AsyncState;
            Socket client = state.workSocket;
            // Read data from the remote device.  
            int bytesRead = client.EndReceive(ar);

            if (bytesRead > 0)
            {
                // There might be more data, so store the data received so far.  
                state.sb.Append(Encoding.ASCII.GetString(state.buffer, 0, bytesRead));
                // Get the rest of the data.  
                client.BeginReceive(state.buffer, 0, StateObject.BufferSize, 0,
                    new AsyncCallback(AsyncReceiveCallback), state);
            }
            else
            {
                // All the data has arrived; put it in response.  
                if (state.sb.Length > 1)
                {
                    var response = state.sb.ToString();  //<--------- ### Where it is assigned, I want it returned
                    // If a client class is subscribed to the event, invoke the delegate.
                    // Here the client class is notified, and the response is passed as parameter to the delegate.
                    if (!(DataReceived is null))
                        DataReceived.Invoke(this, client, response);
                }
            }
        }
        catch (Exception ex)
        {
            // TODO: manage exception.
            throw;
        }
    }
}

To illustrate how to use the class, I just created a simple Form with two buttons (BtnConnect and BtnSendString) but of course it can be used in different contexts. I tested the connection using the Asynchronous Server Socket Example by Microsoft. Note that in this example the socket connection is always closed by the server after the response is sent back and this is probably something you want to avoid if you need to send a collection of strings without having to create a new connection for each of them.

        private AsynchronousClient _asyncClient;

        private void Form1_Load(object sender, EventArgs e)
        {
            // I'm testing on the loopback interface.
            var remoteIp = IPAddress.Parse("127.0.0.1");

            // Create a new remote endpoint.
            var remoteEndpoint = new IPEndPoint(remoteIp, 11000);

            // Create a new instance of the AsynchronousClient client, 
            // passing the remote endpoint as parameter.
            _asyncClient = new AsynchronousClient(remoteEndpoint);

            // Subscription to the ConnectionComplete event.
            _asyncClient.ConnectionComplete += AsyncClient_ConnectionComplete;

            // Subscription to the SendComplete event.
            _asyncClient.SendComplete += AsyncClient_SendComplete;

            // Subscription to the DataReceived event.
            _asyncClient.DataReceived += AsyncClient_DataReceived;
        }

        /// <summary>
        /// Handler of the DataReceived event.
        /// </summary>
        private void AsyncClient_DataReceived(AsynchronousClient sender, Socket clientSocket, string data)
        {
            // Here I manage the data received by the remote endpoint.
            MessageBox.Show(string.Format("Data received: {0}", data));
        }

        /// <summary>
        /// Handler of the SendComplete event.
        /// </summary>
        private void AsyncClient_SendComplete(AsynchronousClient sender, Socket clientSocket)
        {
            // Here I'm starting an async receive operation, as I expect the remote endpoint
            // to send back some data.
            _asyncClient.AsyncReceive(clientSocket);
        }

        /// <summary>
        /// Handler of the ConnectionComplete event.
        /// </summary>
        private void AsyncClient_ConnectionComplete(AsynchronousClient sender, Socket clientSocket)
        {
            // Here I just want to warn the user the connection is set.
            MessageBox.Show("Successfully connected to the remote endpoint.");
        }

        /// <summary>
        /// Handler of the connect button.
        /// </summary>
        private void BtnConnect_Click(object sender, EventArgs e)
        {
            _asyncClient.AsyncConnect();
        }

        /// <summary>
        /// Handler of the SendString button.
        /// </summary>
        private void BtnSendString_Click(object sender, EventArgs e)
        {
            _asyncClient.AsyncSend("TEST DATA<EOF>");
        }
Related