Отмена блокировки вызова AcceptTcpClient
Как все, возможно, уже знают, самый простой способ принимать входящие TCP-соединения в C# - это циклическое выполнение по TcpListener.AcceptTcpClient(). Кроме того, этот способ будет блокировать выполнение кода, пока не будет получено соединение. Это чрезвычайно ограничивает GUI, поэтому я хочу прослушивать соединения в отдельном потоке или задаче.
Мне сказали, что у потоков есть несколько недостатков, однако никто не объяснил мне, что это такое. Поэтому вместо использования потоков я использовал задачи. Это прекрасно работает, однако, поскольку метод AcceptTcpClient блокирует выполнение, я не могу найти какой-либо способ обработки отмены задачи.
В настоящее время код выглядит следующим образом, но я понятия не имею, как бы я хотел отменить задачу, когда я хочу, чтобы программа перестала прослушивать соединения.
Сначала от функции, выполняемой в задаче:
static void Listen () {
// Create listener object
TcpListener serverSocket = new TcpListener ( serverAddr, serverPort );
// Begin listening for connections
while ( true ) {
try {
serverSocket.Start ();
} catch ( SocketException ) {
MessageBox.Show ( "Another server is currently listening at port " + serverPort );
}
// Block and wait for incoming connection
if ( serverSocket.Pending() ) {
TcpClient serverClient = serverSocket.AcceptTcpClient ();
// Retrieve data from network stream
NetworkStream serverStream = serverClient.GetStream ();
serverStream.Read ( data, 0, data.Length );
string serverMsg = ascii.GetString ( data );
MessageBox.Show ( "Message recieved: " + serverMsg );
// Close stream and TcpClient connection
serverClient.Close ();
serverStream.Close ();
// Empty buffer
data = new Byte[256];
serverMsg = null;
}
}
Во-вторых, функции запуска и остановки службы прослушивания:
private void btnListen_Click (object sender, EventArgs e) {
btnListen.Enabled = false;
btnStop.Enabled = true;
Task listenTask = new Task ( Listen );
listenTask.Start();
}
private void btnStop_Click ( object sender, EventArgs e ) {
btnListen.Enabled = true;
btnStop.Enabled = false;
//listenTask.Abort();
}
Мне просто нужно что-то для замены вызова listenTask.Abort() (который я закомментировал, потому что метод не существует)
5 ответов
Следующий код закроет / прервет AcceptTcpClient, когда переменная isRunning станет false
public static bool isRunning;
delegate void mThread(ref book isRunning);
delegate void AccptTcpClnt(ref TcpClient client, TcpListener listener);
public static main()
{
isRunning = true;
mThread t = new mThread(StartListening);
Thread masterThread = new Thread(() => t(this, ref isRunning));
masterThread.IsBackground = true; //better to run it as a background thread
masterThread.Start();
}
public static void AccptClnt(ref TcpClient client, TcpListener listener)
{
if(client == null)
client = listener.AcceptTcpClient();
}
public static void StartListening(ref bool isRunning)
{
TcpListener listener = new TcpListener(new IPEndPoint(IPAddress.Any, portNum));
try
{
listener.Start();
TcpClient handler = null;
while (isRunning)
{
AccptTcpClnt t = new AccptTcpClnt(AccptClnt);
Thread tt = new Thread(() => t(ref handler, listener));
tt.IsBackground = true;
// the AcceptTcpClient() is a blocking method, so we are invoking it
// in a separate dedicated thread
tt.Start();
while (isRunning && tt.IsAlive && handler == null)
Thread.Sleep(500); //change the time as you prefer
if (handler != null)
{
//handle the accepted connection here
}
// as was suggested in comments, aborting the thread this way
// is not a good practice. so we can omit the else if block
// else if (!isRunning && tt.IsAlive)
// {
// tt.Abort();
//}
}
// when isRunning is set to false, the code exits the while(isRunning)
// and listner.Stop() is called which throws SocketException
listener.Stop();
}
// catching the SocketException as was suggested by the most
// voted answer
catch (SocketException e)
{
}
}
Отмена AcceptTcpClient
Ваша лучшая ставка для отмены блокировки AcceptTcpClient
Операция заключается в вызове TcpListener.Stop, который генерирует исключение SocketException, которое вы можете перехватить, если хотите явно проверить, что операция была отменена.
TcpListener serverSocket = new TcpListener ( serverAddr, serverPort );
...
try
{
TcpClient serverClient = serverSocket.AcceptTcpClient ();
// do something
}
catch (SocketException e)
{
if ((e.SocketErrorCode == SocketError.Interrupted))
// a blocking listen has been cancelled
}
...
// somewhere else your code will stop the blocking listen:
serverSocket.Stop();
Что бы вы ни хотели вызвать Stop на вашем TcpListener, потребуется некоторый уровень доступа к нему, поэтому вы должны либо ограничить его за пределами вашего метода Listen, либо обернуть логику слушателя внутри объекта, который управляет TcpListener и предоставляет методы Start и Stop (с помощью Stop призвание TcpListener.Stop()
).
Асинхронное завершение
Потому что принятый ответ использует Thread.Abort()
чтобы завершить поток, было бы полезно отметить, что лучший способ завершить асинхронную операцию - это совместное отмена, а не принудительное прерывание.
В кооперативной модели целевая операция может отслеживать индикатор отмены, который сигнализируется терминатором. Это позволяет цели обнаруживать запрос отмены, очищать при необходимости, а затем в соответствующее время сообщать состояние завершения обратно терминатору. Без такого подхода внезапное завершение операции может привести к потере ресурсов потока и, возможно, даже хост-процесса или домена приложения в поврежденном состоянии.
Начиная с.NET 4.0 и далее лучший способ реализовать этот шаблон - использовать CancellationToken. При работе с потоками токен может быть передан в качестве параметра методу, выполняемому в потоке. В Tasks поддержка CancellationTokens встроена в несколько конструкторов Task. Токены отмены обсуждаются более подробно в этой статье MSDN.
Для полноты, асинхронный аналог ответа выше:
async Task<TcpClient> AcceptAsync(TcpListener listener, CancellationToken ct)
{
using (ct.Register(listener.Stop))
{
try
{
return await listener.AcceptTcpClientAsync();
}
catch (SocketException e)
{
if (e.SocketErrorCode == SocketError.Interrupted)
throw new OperationCanceledException();
throw;
}
}
}
Обновление: как @Mitch предлагает в комментариях (и как это обсуждение подтверждает), ожидая AcceptTcpClientAsync
может бросить ObjectDisposedException
после Stop
(который мы называем в любом случае), поэтому имеет смысл поймать ObjectDisposedException
тоже:
async Task<TcpClient> AcceptAsync(TcpListener listener, CancellationToken ct)
{
using (ct.Register(listener.Stop))
{
try
{
return await listener.AcceptTcpClientAsync();
}
catch (SocketException e) when (e.SocketErrorCode == SocketError.Interrupted)
{
throw new OperationCanceledException();
}
catch (ObjectDisposedException) when (ct.IsCancellationRequested)
{
throw new OperationCanceledException();
}
}
}
Ну, в старые времена, прежде чем правильно работать асинхронные сокеты (лучший способ сегодня IMO, BitMask говорит об этом), мы использовали простой трюк: установить isRunning
в false (опять же, в идеале, вы хотите использовать CancellationToken
вместо, public static bool isRunning;
не потокобезопасный способ прекратить фоновый работник:)) и начать новый TcpClient.Connect
к себе - это вернет вас из Accept
позвоните, и вы можете завершить грациозно.
Как уже сказал BitMask, Thread.Abort
наиболее определенно не безопасный подход в завершении. На самом деле, это не будет работать вообще, учитывая, что Accept
обрабатывается собственным кодом, где Thread.Abort
не имеет силы. Единственная причина, по которой это работает, заключается в том, что вы фактически не блокируете ввод-вывод, а скорее выполняете бесконечный цикл при проверке Pending
(неблокирующий вызов). Это выглядит как отличный способ использовать процессор на 100% на одном ядре:)
В вашем коде также есть много других проблем, которые не появляются у вас на глазах только потому, что вы делаете очень простые вещи, и из-за того, что.NET довольно хорош. Например, вы всегда делаете GetString
на весь буфер, в который вы читаете - но это неправильно. Фактически, это учебный пример переполнения буфера, например, в C++ - единственная причина, по которой он работает в C#, заключается в том, что он предварительно обнуляет буфер, поэтому GetString
игнорирует данные после "реальной" строки, которую вы прочитали. Вместо этого вам нужно принять возвращаемое значение Read
call - это говорит вам, сколько байтов вы прочитали, и, как таковые, сколько вам нужно декодировать.
Еще одним очень важным преимуществом этого является то, что вам больше не нужно воссоздавать byte[]
после каждого чтения - вы можете просто использовать буфер снова и снова.
Не работайте с GUI из другого потока, кроме потока GUI (да, ваш Task
выполняется в отдельном потоке пула потоков). MessageBox.Show
это грязный хак, который на самом деле работает из других потоков, но на самом деле это не то, что вы хотите. Вам необходимо вызвать действия GUI в потоке GUI (например, с помощью Form.Invoke или с помощью задачи, которая имеет контекст синхронизации в потоке GUI). Это будет означать, что окно сообщения будет правильным диалогом, который вы ожидаете.
Есть много проблем с фрагментом, который вы опубликовали, но, учитывая, что это не Code Review, и что это старая тема, я не собираюсь делать это больше:)
Вот как я это преодолел. Надеюсь, это поможет. Может быть не самый чистый, но работает на меня
public class consoleService {
private CancellationTokenSource cts;
private TcpListener listener;
private frmMain main;
public bool started = false;
public bool stopped = false;
public void start() {
try {
if (started) {
stop();
}
cts = new CancellationTokenSource();
listener = new TcpListener(IPAddress.Any, CFDPInstanceData.Settings.RemoteConsolePort);
listener.Start();
Task.Run(() => {
AcceptClientsTask(listener, cts.Token);
});
started = true;
stopped = false;
functions.Logger.log("Started Remote Console on port " + CFDPInstanceData.Settings.RemoteConsolePort, "RemoteConsole", "General", LOGLEVEL.INFO);
} catch (Exception E) {
functions.Logger.log("Error starting remote console socket: " + E.Message, "RemoteConsole", "General", LOGLEVEL.ERROR);
}
}
public void stop() {
try {
if (!started) { return; }
stopped = false;
cts.Cancel();
listener.Stop();
int attempt = 0;
while (!stopped && attempt < GlobalSettings.ConsoleStopAttempts) {
attempt++;
Thread.Sleep(GlobalSettings.ConsoleStopAttemptsDelayMS);
}
} catch (Exception E) {
functions.Logger.log("Error stopping remote console socket: " + E.Message, "RemoteConsole", "General", LOGLEVEL.ERROR);
} finally {
started = false;
}
}
void AcceptClientsTask(TcpListener listener, CancellationToken ct) {
try {
while (!ct.IsCancellationRequested) {
try {
TcpClient client = listener.AcceptTcpClient();
if (!ct.IsCancellationRequested) {
functions.Logger.log("Client connected from " + client.Client.RemoteEndPoint.ToString(), "RemoteConsole", "General", LOGLEVEL.DEBUG);
ParseAndReply(client, ct);
}
} catch (SocketException e) {
if (e.SocketErrorCode == SocketError.Interrupted) {
break;
} else {
throw e;
}
} catch (Exception E) {
functions.Logger.log("Error in Remote Console Loop: " + E.Message, "RemoteConsole", "General", LOGLEVEL.ERROR);
}
}
functions.Logger.log("Stopping Remote Console Loop", "RemoteConsole", "General", LOGLEVEL.DEBUG);
} catch (Exception E) {
functions.Logger.log("Error in Remote Console: " + E.Message, "RemoteConsole", "General", LOGLEVEL.ERROR);
} finally {
stopped = true;
}
functions.Logger.log("Stopping Remote Console", "RemoteConsole", "General", LOGLEVEL.INFO);
}
}