【问题标题】:C# Async Sockets - Thread LogicC# 异步套接字 - 线程逻辑
【发布时间】:2018-03-25 06:54:07
【问题描述】:

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

【问题讨论】:

    标签: c# multithreading sockets asynchronous


    【解决方案1】:

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

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

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

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

    每次您调用 Begin...() 并提供回调时,您应该假设您的回调方法可以在新线程上调用,同时您曾经进行过的所有其他 Begin...() 调用仍然未完成。

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

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

    有了这个,你应该明白,在 50 个不同的套接字上调用 BeginReceive(),但传入同一个缓冲区 - _globalBuffer - 意味着 50 个套接字将写入同一个缓冲区并把它弄得一团糟,导致任意/损坏的数据。

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

    ...

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

    【讨论】:

    • 我很清楚 _globalBuffer 的问题,我认为这是因为多个并发尝试访问同一个变量而发生的。我决定在尝试找到解决方案之前,我需要知道如何创建线程。我在想什么会更便宜,在每个函数上放置一个唯一的缓冲区,或者在全局缓冲区上放置一个锁定/等待?最后我只是迷失了你的建议,我是编程新手,如果可能的话,你能尝试用不同的方式解释吗?非常感谢您的解释。
    • 此设计中没有任何部分允许您在 globalBuffer 上使用锁定来防止多个线程同时对其进行写入 - 缓冲区由您无法控制的代码写入,该代码一次执行你不控制。您调用 BeginReceive 一段时间后,操作系统会写入您的缓冲区并通知您。假设一个套接字一次只能有一个未完成的 BeginReceive 调用,那么您可以为每个套接字使用一个缓冲区。如果你真的希望你的整个程序只有一个缓冲区,那么你必须确保你只有一个 BeginReceive 挂起,句号。
    • 另外,如果您是编程新手,请暂时忽略 c# 中的 async/await 功能。
    • 我认为最重要的问题... Socket API 什么时候支持 async/await ? @antiduh
    • 我通常不使用 C#,所以当我查看这个 BeginSend 和 BeginReceive 的东西时,我不确定我是否阅读正确。从文档看来,在 BeginSend 之后将回调的线程与为 SAME 套接字执行 BeginReceive 的线程不同。这意味着您想要在发送和接收调用之间访问的任何应用程序数据结构都不会受到保护,您仍然需要使用某种锁来保护发送和接收之间的应用程序数据?
    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 2010-11-28
    • 1970-01-01
    • 2021-01-25
    • 2012-04-23
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多