并发客户端在C#套接字中发布

Mir*_*lal 0 c# sockets client-server winsock

我正在研究客户端服务器应用程序,Windows Server和Linux Client.我正在使用多个并发客户端测试我的服务器.我尝试了来自客户端的20个并发连接,我注意到尽管所有20个请求都是相同的,但仍未处理一些请求.他们进入队列,由于某种原因轮到他们客户端关闭(客户端连接超时为5秒).

然后我添加了一个Thread.Sleep(1000),以检查它是否真的是异步但后来我意识到它不会处理其他请求,直到超时.尽管如此

  1. 它是异步的
  2. 在进入睡眠状态之前设置了ManualResetEvent.

现在我想知道我在这里失踪了什么,因为这主要发生在并发连接上?

public static void StartServer(IPAddress ipAddr, int port)
{
    //IPEndPoint serverEndPoint = new IPEndPoint(ipAddr, port);
    IPEndPoint serverEndPoint = new IPEndPoint(IPAddress.Any, port);
    Socket clientListener = new Socket(AddressFamily.InterNetwork, SocketType.Stream, ProtocolType.Tcp);
    try
    {
        clientListener.Bind(serverEndPoint);
        clientListener.Listen(500);
        Console.WriteLine("-- Server Listening: {0}:{1}",ipAddr,port);
        while (true)
        {
            resetEvent.Reset();
            Console.WriteLine("|| Waiting for connection");
            clientListener.BeginAccept(new AsyncCallback(AcceptConnection), clientListener);
            resetEvent.WaitOne();
        }
    }
    catch (Exception e)
    {
        Console.WriteLine(e.Message);
    }
}


public static void AcceptConnection(IAsyncResult ar)
{
    // Get the socket that handles the client request.
    Socket listener = (Socket)ar.AsyncState;
    Socket handler = listener.EndAccept(ar);
    // Signal the main thread to continue.
    resetEvent.Set();
    // Create the state object.
    JSStateObject state = new JSStateObject();
    state.workSocket = handler;
    if (handler.Connected)
    {
        Console.WriteLine("** Connected to: {0}", handler.RemoteEndPoint.ToString());
        state.workingDirectory = JSUtilityClass.CreatetTemporaryDirectry();
        try
        {
            Thread.Sleep(1000);
            Receive(state);
        }
        catch (Exception e)
        {
            handler.Shutdown(SocketShutdown.Both);
            handler.Close();
            Console.WriteLine(e.Message);
        }
    }
}
Run Code Online (Sandbox Code Playgroud)

Hac*_*ack 5

我创建了一个测试,发送了100个连接尝试,发现了一些减慢它的速度.

为什么这么慢?

我在AcceptConnection中设置了一个断点来查看callstack,就是这样

ConsoleApplication1.exe!ConsoleApplication1.Program.AcceptConnection(System.IAsyncResult ar) Line 62    C#
        System.dll!System.Net.LazyAsyncResult.Complete(System.IntPtr userToken) + 0x69 bytes    
        System.dll!System.Net.ContextAwareResult.CaptureOrComplete(ref System.Threading.ExecutionContext cachedContext, bool returnContext) + 0xab bytes    
        System.dll!System.Net.ContextAwareResult.FinishPostingAsyncOp(ref System.Net.CallbackClosure closure) + 0x3c bytes  
        System.dll!System.Net.Sockets.Socket.BeginAccept(System.AsyncCallback callback, object state) + 0xe3 bytes  
        ConsoleApplication1.exe!ConsoleApplication1.Program.StartServer(System.Net.IPAddress ipAddr, int port) Line 48 + 0x32 bytes C#
Run Code Online (Sandbox Code Playgroud)

所以回调AcceptConnection是从调用BeginAccept的同一个线程运行的.我看了一下FinishPostingAsyncOp反射器并使用异步模式,如果队列中已经有一个等待处理的套接字操作,那么它将在当前线程上执行,否则如果没有任何待处理的话,那么'稍后将在不同的线程中处理,例如

SocketAsyncEventArgs sae = new SocketAsyncEventArgs();
sae.Completed += new EventHandler<SocketAsyncEventArgs>(SocketOperation_Completed);
if (!clientListener.AcceptAsync(sae))
    AcceptConnection(clientListener, sae); // operation completed synchronously, process the result
else
    // operation will complete on a IO completion port (different thread) which we'll handle in the Completed event
Run Code Online (Sandbox Code Playgroud)

因此,当您观察到程序在这种情况下实际上是完全同步的,并且使用1秒Thread.Sleep它将花费至少100秒来接受所有连接,此时大多数连接将超时.

解决方案

尽管BeginAccept方法摘要说明了

开始异步操作以接受传入连接尝试.

事实证明,故事还有更多内容

来自MSDN http://msdn.microsoft.com/en-AU/library/system.net.sockets.socket.beginaccept.aspx

BeginAccept(Int32,AsyncCallback,Object)开始异步操作以接受传入连接尝试并接收客户端应用程序发送的第一个数据块.

因此,在触发回调之前,它正在执行具有短暂超时的读取操作.您可以通过指定receiveSize0.更改来禁用此功能

clientListener.BeginAccept(new AsyncCallback(AcceptConnection), clientListener);
Run Code Online (Sandbox Code Playgroud)

clientListener.BeginAccept(0, new AsyncCallback(AcceptConnection), clientListener);
Run Code Online (Sandbox Code Playgroud)

这加速了起来,如果我们去掉Thread.Sleep(1000)AcceptConnection那么所有的连接都接受的真快.

如果你Thread.Sleep(1000)留在那里模拟工作负载或只是为了测试那么你可能想要准备服务器来处理这样的负载

int minWorkerThreads = 0;
int minCompletionPortThreads = 0;
ThreadPool.GetMinThreads(out minWorkerThreads, out minCompletionPortThreads);
ThreadPool.SetMinThreads(minWorkerThreads, 100);
Run Code Online (Sandbox Code Playgroud)

其中100是您希望随时可用于处理套接字操作的线程数.

另外一件事,这是个人偏好的问题,但只是因为你知道你可能想BeginAccept从AcceptConnection中调用,这就不需要那个while循环了.即改变这一点

while (true)
{
    resetEvent.Reset();
    Console.WriteLine("|| Waiting for connection");
    clientListener.BeginAccept(new AsyncCallback(AcceptConnection), clientListener);
    resetEvent.WaitOne();
}
Run Code Online (Sandbox Code Playgroud)

对此

Console.WriteLine("|| Waiting for connection");
clientListener.BeginAccept(new AsyncCallback(AcceptConnection), clientListener);
Run Code Online (Sandbox Code Playgroud)

并把另一个BeginAcceptAcceptConnection

public static void AcceptConnection(IAsyncResult ar)
{
    // Get the socket that handles the client request.
    Socket listener = (Socket)ar.AsyncState;
    // start another listening operation
    listener.BeginAccept(new AsyncCallback(AcceptConnection), listener);
    ... the rest of the method
}
Run Code Online (Sandbox Code Playgroud)