.Net Core 中的异步服务器套接字 - 如何 return 结果?

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

我需要通过 TCP 异步发送数据。它是字符串的集合 ICollection<string>.

我搜索并找到了一个很好的 starting example from Microsoft(见下文)。该示例似乎在 .NET Framework 下,但我认为它也适用于 .NET Core

我在做什么:

  1. 我将代码重新用作 非静态 class

  2. 我要发送一组字符串ICollection<string>。我知道我可以重写它以在 main 方法中发送字符串集合。没问题。

  3. 我希望收到每条发送的消息的回复并对其进行处理。当前响应静态存储在 private static String response = String.Empty; 中。我不希望它是静态的。我想要一个本地方法变量。

  4. 我的挑战从项目3.开始。我如何 return 返回似乎只能从 private static void ReceiveCallback( IAsyncResult ar )

    内部访问的响应消息

    我不认为将其更改为 private static string ReceiveCallback( IAsyncResult ar ) 会起作用。如果是这样,我如何从 client.BeginReceive( state.buffer, 0, StateObject.BufferSize, 0, new AsyncCallback(ReceiveCallback), state); 读取它?

我在一个非常古老的 post 上悬赏了 300 点,因为我发现了一个类似的问题:C# Asyn. Socket Programming。很高兴奖励在这里和那里回答的人。

另一个问题是:打开一个 TCP 连接,发送多条消息,然后关闭它是推荐的做法吗?或者为每条发送的消息打开一个 TCP 连接?

Microsoft 示例

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;  
    }  
}

您可以创建一个 class(非静态的,我将其称为 AsynchronousClient),它直接从 Microsoft 示例中实现套接字通信的所有逻辑。相关的添加是 3 个事件(更多关于 handling and raising events):

1) ConnectionComplete,异步连接操作完成时触发;

2) SendComplete,当数据(本例中为字符串)发送成功时触发;

3) DataReceived,当有来自远程端点的传入数据时触发。

基本上,class 公开了 3 个 public 方法:AsyncConnect、AsyncSend 和 AsyncReceive。在 3 个私有回调中,上面列表中的相应事件被触发,并通知使用 AsynchronousClient 的 class 操作终止。

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;
        }
    }
}

为了说明如何使用 class,我刚刚创建了一个带有两个按钮(BtnConnect 和 BtnSendString)的简单表单,当然它可以在不同的上下文中使用。 我使用 Microsoft 的 Asynchronous Server Socket Example 测试了连接。请注意,在此示例中,在发送回响应后,服务器始终关闭套接字连接,如果您需要发送一组字符串而不必为每个字符串创建新连接,则可能需要避免这种情况。

        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>");
        }

我从 .NET Core 团队找到了答案。据他们说:

关于 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

推荐的做法如下:

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();

添加的注释:

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

对于我的问题: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