C#套接字服务器如何取消卡在socket.accept()中的线程

时间:2018-09-11 20:30:04

标签: c# multithreading sockets

我正在使用C#和套接字为类分配编写一个非常简单的RPC库。我的发送功能正常工作,但我正在测试KillServer函数的工作方式。我已经在服务器库中实现了很多线程,但是我不确定是否掌握了取消和软终止线程的正确方法。

我的StartServerThread方法:

public void StartServer()
        {
            ServerThread = new Task(()=> 
            {
                _Socket.Listen(100);
                // Enter a loop waiting for connection requests
                // Or because I am RDM I don't need connections
                while (!_TokenSource.IsCancellationRequested)
                {
                    try
                    {
                        var newClient = _Socket.Accept();

                        _Clients.Add(newClient, ClientThread(newClient));
                    }
                    catch (OperationCanceledException)
                    {
                        Debug.WriteLine("Canceled");
                    }
                }


                foreach (var client in _Clients)
                {
                    client.Key.Disconnect(false);
                }

            },_TokenSource.Token, TaskCreationOptions.LongRunning);

            ServerThread.Start();
        }

我的问题是,一旦线程撞到_Socket.Accept()行,它就会阻塞。因此,即使我在TokenSource上调用Cancel,该线程仍然停留在该行,并且不会很好地结束。我在设计这种库时遇到了麻烦,该方法使用线程并在连接多个客户端时允许良好的性能。

2 个答案:

答案 0 :(得分:0)

有几种方法可以解决此问题。您可以使用已经提到的非阻塞套接字,也可以使用诸如Socket.​Begin​Accept之类的异步函数。这是来自MSDN的example

重要代码如下。

while (true)
{  
    // Set the event to nonsignaled state.  
    allDone.Reset();  

    listener.BeginAccept(new AsyncCallback(AcceptCallback), listener);

    // Wait until a connection is made before continuing.  
    allDone.WaitOne();  
}  

当接受新的TCP连接时,在回调函数中设置了手动重置事件allDone,该连接在上一个代码循环以再次调用Socket.​Begin​Accept时唤醒。

public static void AcceptCallback(IAsyncResult ar)    
{  
    // Signal the main thread to continue.  
    allDone.Set();  
    // Get the socket that handles the client request.  
    Socket listener = (Socket) ar.AsyncState;  
    Socket handler = listener.EndAccept(ar);
    ...

现在,如果要停止线程接受新的调用,您可以“滥用”事件allDone,并将其设置为唤醒等待事件allDone的“ BeginAccept”线程。然后,“ BeginAccept”线程可以检查TokenSource.IsCancellationRequested循环周期是否应该继续或退出。

// MODIFY while contition.
while (!_TokenSource.IsCancellationRequested)
{  
    // Set the event to nonsignaled state.  
    allDone.Reset();  

    listener.BeginAccept(new AsyncCallback(AcceptCallback), listener);

    // Wait until a connection is made before continuing.  
    allDone.WaitOne();  
}  

这是您要退出“ BeginAccept”线程时调用的代码。

 // Don't continue with the while cycle.
 _TokenSource.Cancel();

 // Wake up thread calling BeginAccept().
 allDone.Set()

另一种可能性是使用2个事件实例。像这样的东西。

// Declared somewhere to be accessible from both threads.
ManualResetEvent exitThread;


while (true)
{  
    // Set the event to nonsignaled state.  
    allDone.Reset();  

    listener.BeginAccept(new AsyncCallback(AcceptCallback), listener);

    // Wait until a connection is made or thread is exiting 
    if (WaitHandle.WaitAny(new WaitHandle[]{exitThread,allDone})==0)
    {
        break;      
    }
}  

现在可以通过执行停止第二个线程的循环。

exitThread.Set();

代码WaitHandle.WaitAny()等待直到发出2个事件中的任何一个信号,并且当发出exitThread信号(WaitHandle.Wait()返回0)时,它跳出循环。

但是现在出现了另一个问题。因为您在调用Socket.​Begin​Accept时已经调用了Socket.​Close-触发了回调AcceptCallback。而且由于套接字已经关闭,所以方法Socket.EndAccept将抛出ObjectDisposedException。因此,您必须在回调方法AcceptCallback中捕获并忽略此异常。关于Socket.​Begin​Receive的更多details也适用于Socket.​Begin​Accept

随着.NET Task库的到来,通过使用扩展方法Accept​Async,还有另一个更优雅的解决方案,但是我在Internet上找不到一些很好的示例。而且我个人更喜欢“老式”的BeginXYZ()-EndXYZ()异步模型-也许是因为我太老了...

答案 1 :(得分:0)

在同一应用程序中创建本地连接以解锁此呼叫:

Socket handler = listener.Accept();

代码如下:

Socket clientToUnlockAccept = new Socket(AddressFamily.InterNetwork,
    SocketType.Stream, ProtocolType.Tcp);

clientToUnlockAccept.Connect(localEndPoint)