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);
}
}
最佳答案
这些类型的异步方法使用线程池中的线程来调用您的回调,一旦底层事件发生,无论它是什么。在您的情况下,基础事件可能是建立了连接,或者您收到了一些数据。
当您将套接字设置为“接受”时,不需要存在任何线程。旧的同步处理方式是让一个线程阻塞 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...()
方法更复杂,并且与执行环境的集成更深入,并且对诸如“我的回调何时被调用”、“什么线程”等问题的回答(s) 是我调用的回调”,以及“可能同时运行多少个回调”完全取决于 C#/.Net 中的异步/等待设计导致的程序执行环境。
关于C# 异步套接字 - 线程逻辑,我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/46724792/