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

5

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

它会为每个连接到我的服务器的客户端创建一个新线程来处理代码吗?还是无论有多少客户端连接到服务器,它只会为每个函数(accept、receive、send...)创建一个线程?这样,仅在完成客户端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);
    }
}
1个回答

6
这些异步方法会使用线程池中的线程调用您的回调函数,一旦发生底层事件(可能是连接建立或接收到某些数据)。当您将套接字设置为“接受”时,不需要存在任何线程。以前同步的做法是有一个线程仅在 socket.Accept() 上阻塞,直到有连接进来,但这些 Begin..() 方法的目的就是摆脱这种方式。这里有一个技巧,.Net 和您都使用它:您可以向线程池注册任何 WaitHandle 对象(如 Semaphore、SemaphoreSlim、Mutex 等锁),以及回调方法,这样当 WaitHandle 被设置时,线程池会选择一个线程,运行您的回调函数,然后返回该线程到线程池中。请参见 ThreadPool.RegisterWaitForSingleObject()
事实上,许多这些 Begin..() 方法基本上做着相同的事情。例如,BeginAccept() 使用 WaitHandle 以知道 socket 已经接收到连接--它向线程池注册 WaitHandle,然后在连接发生时在 ThreadPool 线程上调用您的回调函数。
每次调用 Begin...() 并提供回调函数时,都应该假定您的回调函数可能会在新线程上被调用,并且与您尚未完成调用的每个其他 Begin...() 调用同时发生。
例如,对 50 个不同的套接字调用 BeginReceive()?您应该假设 50 个线程可能同时尝试调用您的回调函数。调用 50 个混合使用 BeginReceive()BeginAccept() 方法?就是 50 个线程。
实际上,您的回调函数同时被调用的次数将受到 ThreadPool 中设置的策略的限制,例如它可以生成新线程的速度、保持准备好的活动线程数量等等。
因此,您应该理解,虽然在 50 个不同的套接字上调用 BeginReceive() 并传递相同的缓冲区 - _globalBuffer - 可以让 50 个套接字都写入同一个缓冲区,从而使得数据变得杂乱无章/损坏。相反,您应该为每个同时的 BeginReceive() 调用使用唯一的缓冲区。我建议创建一个新的类来存储单个连接的上下文,包括连接的套接字、读取的缓冲区、它的状态等。每个新连接都会获得一个新的上下文实例。

值得注意的是,在C#中执行异步编程的现代方式是使用async/await关键字和匹配的API中的async方法。这种设计比Begin...()方法更加复杂,与执行环境深度集成。当你的回调函数被调用,它们在哪个线程上被调用,以及可能同时运行多少个回调函数等问题完全取决于你的程序在C# / .Net的异步/等待设计下的执行环境。


我非常清楚_globalBuffer的问题,我认为这可能是由于多个并发尝试访问同一变量引起的。我决定在尝试解决它之前需要知道线程将如何创建。我在思考做什么更便宜,是在每个函数上放置一个唯一的缓冲区,还是在全局缓冲区上放置锁/等待?但是我在你最后的建议处迷失了,我是新手,如果可能的话,您能否尝试以不同的方式解释一下?顺便说一句,非常感谢您的解释。 - Lucas Gomes
该设计没有任何部分允许您在全局缓冲区上使用锁来防止多个线程同时写入它 - 缓冲区由您无法控制的代码编写,该代码在您无法控制的时间执行。您调用BeginReceive,一段时间后操作系统会写入您的缓冲区并通知您。假设一个套接字一次只会有一个BeginReceive调用未完成,则可以为每个套接字使用一个缓冲区。如果您真的希望您的整个程序只有一个缓冲区,则必须确保您仅有一个待处理的BeginReceive。 - antiduh
此外,如果您是编程新手,请暂时忽略C#中的async/await功能。 - antiduh
我认为最重要的问题是... Socket API 何时支持 async/await?@antiduh - Altiano Gerung
我通常不使用C#,所以当我看到这个BeginSend和BeginReceive的东西时,我不确定我是否理解正确。从文档中看来,调用BeginSend后回调的线程与执行同一套接字的BeginReceive的线程不同。这意味着在你的发送和接收调用之间想要访问的任何应用程序数据结构都没有受到保护,你仍然需要使用某种锁来保护你的应用程序数据。 - Ammo Goettsch

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接