I have simple console TCP server app, and I am trying to make it async. For some reason I am getting deadlock. I run Test-NetConnection localhost -Port 12345
from PowerShell to trigger incoming connection.
I have an assumption: OnAcceptCompleted
method is getting called inside Thread
spawned by OS, and it is not part of ThreadPool
. But I don't know how to fix it.
using System;
using System.Net;
using System.Net.Sockets;
using System.Threading;
using System.Threading.Tasks;
class Program
{
private static CancellationTokenSource _shutdownSource = new CancellationTokenSource();
static async Task Main()
{
using var listener = new Socket(SocketType.Stream, ProtocolType.Tcp);
listener.Bind(new IPEndPoint(IPAddress.Any, 12345));
listener.Listen(10);
while (!_shutdownSource.IsCancellationRequested)
{
var socket = await AcceptClient(listener, _shutdownSource.Token).ConfigureAwait(false);
// execution never go this line
Console.WriteLine($"Client accepted {socket.RemoteEndPoint}");
}
}
private static async Task<Socket> AcceptClient(Socket listener, CancellationToken token)
{
var source = new TaskCompletionSource<Socket>();
var args = new SocketAsyncEventArgs();
args.UserToken = new TaskCompletionSource<Socket>();
args.Completed = OnAcceptCompleted;
if (!listener.AcceptAsync(args))
{
OnAcceptCompleted(listener, args);
}
using (token.Register(() => source.TrySetCanceled()))
{
// we don't need ConfigureAwait(false) here, but just to be sure
return await source.Task.ConfigureAwait(false);
}
}
private static void OnAcceptCompleted(object sender, SocketAsyncEventArgs args)
{
var source = (TaskCompletionSource<Socket>)args.UserToken;
if (args.SocketError == SocketError.Success)
{
// checked in debugger, TaskCompletionSource goes to RanToCompletion status here
source.TrySetResult(args.AcceptSocket);
}
else if (args.SocketError == SocketError.OperationAborted)
{
source.TrySetCanceled();
}
else
{
source.TrySetException(new InvalidOperationException("Socket error = " args.SocketError));
}
}
}
CodePudding user response:
I don't understand why you need the bottom two methods. Why is this not acceptable?
static async Task Main()
{
using var listener = new Socket(SocketType.Stream, ProtocolType.Tcp);
listener.Bind(new IPEndPoint(IPAddress.Any, 12345));
listener.Listen(10);
while (!_shutdownSource.IsCancellationRequested)
{
var socket = await listener.AcceptAsync(_shutdownSource.Token);
Console.WriteLine($"Client accepted {socket.RemoteEndPoint}");
}
}
CodePudding user response:
As you rightly noted, you are using two different TaskCompletionSource
objects, when they should be the same. So the source
object was never completing.
However, working with raw sockets is fraught with difficulties, and you should instead use TcpListener
, which handles all this for you
class Program
{
private static CancellationTokenSource _shutdownSource = new CancellationTokenSource();
static async Task Main()
{
var listener = new TcpListener(IPAddress.Any, 12345);
try
{
listener.Start(10);
while (!_shutdownSource.IsCancellationRequested)
{
var client = await listener.AcceptTcpClientAsync(_shutdownSource.Token).ConfigureAwait(false);
Console.WriteLine($"Client accepted {client.RemoteEndPoint}");
// hand off client to another function
// Task.Run(() => HandleYourClient(client, _shutdownSource.Token));
// otherwise `client` needs `using` above to dispose
}
}
catch (OperationCanceledException)
{ //
}
finally
{
if (listener.Active)
listener.Stop()
}
}
private async Task HandleClient(TcpClient client, CancellationToken token)
{
using client;
using var stream = client.GetStream();
// do stuff
}
}
CodePudding user response:
Sorry, my bad. I am creating 2 TaskCompletionSource<Socket>
in AcceptClient
method. Fixed code:
private static async Task<Socket> AcceptClient(Socket listener, CancellationToken token)
{
var source = new TaskCompletionSource<Socket>();
var args = new SocketAsyncEventArgs();
args.UserToken = source;
args.Completed = OnAcceptCompleted;
if (!listener.AcceptAsync(args))
{
OnAcceptCompleted(listener, args);
}
using (token.Register(() => source.TrySetCanceled()))
{
return await source.Task.ConfigureAwait(false);
}
}