C# 异步套接字 - 线程逻辑

Luc*_*mes 5 c# sockets multithreading asynchronous

Socket.BeginSend、Socket.BeginReceive、Socket.BeginAccept 等背后的线程创建逻辑如何工作?

是否会为连接到我的服务器的每个客户端创建一个新线程来处理代码,或者是否只会为每个功能(接受、接收、发送...)创建一个线程,无论有多少客户端连接到服务器?这样,只有在客户端 1 接受代码完成后才执行客户端 2 接受代码,依此类推。

这是我编写的代码,我试图更好地理解其背后的逻辑:

public class SocketServer
{
    Socket _serverSocket;
    List<Socket> _clientSocket = new List<Socket>();
    byte[] _globalBuffer = new byte[1024];

    public SocketServer()
    {
        _serverSocket = new Socket(AddressFamily.InterNetwork, SocketType.Stream, ProtocolType.Tcp);
    }

    public void Bind(int Port)
    {
        Console.WriteLine("Setting up server...");
        _serverSocket.Bind(new IPEndPoint(IPAddress.Loopback, Port));
    }

    public void Listen(int BackLog)
    {
        _serverSocket.Listen(BackLog);
    }

    public void Accept()
    {
        _serverSocket.BeginAccept(AcceptCallback, null);
    }

    private void AcceptCallback(IAsyncResult AR)
    {
        Socket socket = _serverSocket.EndAccept(AR);
        _clientSocket.Add(socket);
        Console.WriteLine("Client Connected");
        socket.BeginReceive(_globalBuffer, 0, _globalBuffer.Length, SocketFlags.None, ReceiveCallback, socket);
        Accept();
    }

    private void ReceiveCallback(IAsyncResult AR)
    {
        Socket socket = AR.AsyncState as Socket;
        int bufferSize = socket.EndReceive(AR);

        string text = Encoding.ASCII.GetString(_globalBuffer, 0, bufferSize);
        Console.WriteLine("Text Received: {0}", text);

        string response = string.Empty;

        if (text.ToLower() != "get time")
            response = $"\"{text}\" is a Invalid Request";
        else
            response = DateTime.Now.ToLongTimeString();

        byte[] data = Encoding.ASCII.GetBytes(response);
        socket.BeginSend(data, 0, data.Length, SocketFlags.None, SendCallback, socket);

        socket.BeginReceive(_globalBuffer, 0, _globalBuffer.Length, SocketFlags.None, ReceiveCallback, socket);
    }

    private void SendCallback(IAsyncResult AR)
    {
        (AR.AsyncState as Socket).EndSend(AR);
    }
}
Run Code Online (Sandbox Code Playgroud)

ant*_*duh 6

一旦发生底层事件(无论是什么),这些类型的异步方法就会使用线程池中的线程来调用回调。在您的情况下,底层事件可能是建立了连接,或者您收到了一些数据。

当您将套接字设置为“接受”时,不需要存在任何线程。旧的同步处理方式是让一个线程一直阻塞,socket.Accept()直到连接进入,但这些Begin..()方法的目的是消除这种情况。

这里有一个技巧,.Net 使用的一个和您使用的一个:您可以向线程池注册任何 WaitHandle 对象(一个锁,例如 Semaphore、SemaphoreSlim、Mutex 等)和一个回调方法,这样当 WaitHandle 被设置时,线程池将选择一个线程,运行您的回调,并将线程返回到线程池。看ThreadPool.RegisterWaitForSingleObject()

事实证明,这些Begin..()方法中的许多基本上都做同样的事情。BeginAccept()使用 WaitHandle 来了解套接字何时收到连接 - 它向 ThreadPool 注册 WaitHandle,然后在发生连接时在 ThreadPool 线程上调用回调。

每次调用Begin...()并提供回调时,您都应该假设您的回调方法可以在新线程上调用,同时与Begin...()您所做的所有其他尚未完成的调用同时调用。

在 50 个不同的套接字上调用BeginReceive()50 次?您应该假设 50 个线程可以尝试同时调用您的回调方法。BeginReceive()调用50个和方法的混合BeginAccept()?50 个线程。

实际上,回调的同时调用数量将受到 ThreadPool 中设置的策略的限制,例如,它创建新线程的速度、它保持活动状态的线程数量等。

这样,您应该明白,调用BeginReceive()50 个不同的套接字,但传入相同的缓冲区 - _globalBuffer- 意味着 50 个套接字将写入同一个缓冲区,并将其弄乱,从而导致任意/损坏的数据。

相反,您应该为每个同时调用使用唯一的缓冲区BeginReceive()。我建议做的是创建一个新类来存储单个连接的上下文 - 连接的套接字、用于读取的缓冲区、其状态等。每个新连接都会获得一个新的上下文实例。

...

仅供参考,在 C# 中执行异步编程的现代方法是使用API 中的async/await关键字和匹配方法。async该设计比这些方法复杂得多,并且与执行环境的集成更加深入Begin...(),并且回答了诸如“何时调用我的回调”、“我的回调在哪些线程上被调用”以及“如何”等问题的答案。许多回调可能同时运行”完全取决于 C# / .Net 中的 async/await 设计所导致的程序执行环境。