非同期メソッドが着信接続を待機している間にTcpListenerを適切に閉じる方法がわかりません。私はSOでこのコードを見つけました、ここにコード:
public class Server
{
private TcpListener _Server;
private bool _Active;
public Server()
{
_Server = new TcpListener(IPAddress.Any, 5555);
}
public async void StartListening()
{
_Active = true;
_Server.Start();
await AcceptConnections();
}
public void StopListening()
{
_Active = false;
_Server.Stop();
}
private async Task AcceptConnections()
{
while (_Active)
{
var client = await _Server.AcceptTcpClientAsync();
DoStuffWithClient(client);
}
}
private void DoStuffWithClient(TcpClient client)
{
// ...
}
}
そしてメイン:
static void Main(string[] args)
{
var server = new Server();
server.StartListening();
Thread.Sleep(5000);
server.StopListening();
Console.Read();
}
この行に例外がスローされます
await AcceptConnections();
server.StopListening()を呼び出すと、オブジェクトが削除されます。
だから私の質問は、TcpListenerを正しく閉じるためにAcceptTcpClientAsync()をキャンセルするにはどうすればよいですか?.
Stephen Toubによるブログ投稿 に基づくかなり 複雑なソリューション がありますが、組み込みの.NETAPIを使用したはるかに簡単なソリューションがあります。
var cancellation = new CancellationTokenSource();
await Task.Run(() => listener.AcceptTcpClientAsync(), cancellation.Token);
// somewhere in another thread
cancellation.Cancel();
このソリューションは、保留中の受け入れ呼び出しを強制終了しません。しかし、他のソリューションもそれを行わず、このソリューションは少なくとも短いです。
更新:キャンセルが通知された後に何が起こるかを示すより完全な例:
var cancellation = new CancellationTokenSource();
var listener = new TcpListener(IPAddress.Any, 5555);
listener.Start();
try
{
while (true)
{
var client = await Task.Run(
() => listener.AcceptTcpClientAsync(),
cancellation.Token);
// use the client, pass CancellationToken to other blocking methods too
}
}
finally
{
listener.Stop();
}
// somewhere in another thread
cancellation.Cancel();
更新2:Task.Run
は、タスクの開始時にキャンセルトークンのみをチェックします。受け入れループの終了を高速化するには、キャンセルアクションを登録することをお勧めします。
cancellation.Token.Register(() => listener.Stop());
ここには適切な実例がないため、次の例を示します。
スコープ内にcancellationToken
とtcpListener
の両方があるとすると、次のことができます。
using (cancellationToken.Register(() => tcpListener.Stop()))
{
try
{
var tcpClient = await tcpListener.AcceptTcpClientAsync();
// … carry on …
}
catch (InvalidOperationException)
{
// Either tcpListener.Start wasn't called (a bug!)
// or the CancellationToken was cancelled before
// we started accepting (giving an InvalidOperationException),
// or the CancellationToken was cancelled after
// we started accepting (giving an ObjectDisposedException).
//
// In the latter two cases we should surface the cancellation
// exception, or otherwise rethrow the original exception.
cancellationToken.ThrowIfCancellationRequested();
throw;
}
}
私のために働いた:リスナーに接続するためのローカルダミークライアントを作成し、接続が受け入れられた後、別の非同期受け入れを行わないでください(アクティブフラグを使用してください)。
// This is so the accept callback knows to not
_Active = false;
TcpClient dummyClient = new TcpClient();
dummyClient.Connect(m_listener.LocalEndpoint as IPEndPoint);
dummyClient.Close();
これはハックかもしれませんが、ここの他のオプションよりもきれいに見えます:)
この拡張メソッドを定義します。
public static class Extensions
{
public static async Task<TcpClient> AcceptTcpClientAsync(this TcpListener listener, CancellationToken token)
{
try
{
return await listener.AcceptTcpClientAsync();
}
catch (Exception ex) when (token.IsCancellationRequested)
{
throw new OperationCanceledException("Cancellation was requested while awaiting TCP client connection.", ex);
}
}
}
拡張メソッドを使用してクライアント接続を受け入れる前に、次のようにします。
token.Register(() => listener.Stop());
StopListening
(ソケットを破棄する)の呼び出しは正しいです。その特定のエラーを飲み込むだけです。とにかく保留中の通話を停止する必要があるため、これを回避することはできません。そうでない場合は、ソケットと保留中の非同期IOをリークし、ポートは使用されたままになります。
新しい接続クライアントを継続的にリッスンするときに、次のソリューションを使用しました。
_public async Task ListenAsync(IPEndPoint endPoint, CancellationToken cancellationToken)
{
TcpListener listener = new TcpListener(endPoint);
listener.Start();
// Stop() typically makes AcceptSocketAsync() throw an ObjectDisposedException.
cancellationToken.Register(() => listener.Stop());
// Continually listen for new clients connecting.
try
{
while (true)
{
cancellationToken.ThrowIfCancellationRequested();
Socket clientSocket = await listener.AcceptSocketAsync();
}
}
catch (OperationCanceledException) { throw; }
catch (Exception) { cancellationToken.ThrowIfCancellationRequested(); }
}
_
TcpListener
がキャンセルされたときに、CancellationToken
インスタンスでStop()
を呼び出すコールバックを登録します。AcceptSocketAsync
は通常、すぐにObjectDisposedException
をスローします。Exception
以外のOperationCanceledException
をキャッチしましたが、外部の呼び出し元に「正気の」OperationCanceledException
をスローしました。私はasync
プログラミングにかなり慣れていないので、このアプローチに問題がある場合はすみません。それから学ぶことが指摘されているのを見てうれしいです。
キャンセルトークンには、サーバーを停止するために使用できるデリゲートがあります。サーバーが停止すると、リスニング接続呼び出しはソケット例外をスローします。
次のコードを参照してください。
public class TcpListenerWrapper
{
// helper class would not be necessary if base.Active was public, c'mon Microsoft...
private class TcpListenerActive : TcpListener, IDisposable
{
public TcpListenerActive(IPEndPoint localEP) : base(localEP) {}
public TcpListenerActive(IPAddress localaddr, int port) : base(localaddr, port) {}
public void Dispose() { Stop(); }
public new bool Active => base.Active;
}
private TcpListenerActive server
public async Task StartAsync(int port, CancellationToken token)
{
if (server != null)
{
server.Stop();
}
server = new TcpListenerActive(IPAddress.Any, port);
server.Start(maxConnectionCount);
token.Register(() => server.Stop());
while (server.Active)
{
try
{
await ProcessConnection();
}
catch (Exception ex)
{
Console.WriteLine(ex);
}
}
}
private async Task ProcessConnection()
{
using (TcpClient client = await server.AcceptTcpClientAsync())
{
// handle connection
}
}
}