0

Here's the entry point:

public class Program
{
    private static readonly CancellationTokenSource TokenSource = new CancellationTokenSource();

    public static void Main(string[] args)
    {
        // start the app
        new Bootstrap()
            .RunAsync(TokenSource.Token)
            .Wait();

        Console.CancelKeyPress += (sender, eventArgs) =>
        {
            TokenSource.CancelAfter(500);
        };
    }
}

Here's the bootstrap:

public class Bootstrap : IBootstrap
{
    private readonly IServer server;

    private readonly ILogger logger;

    public Bootstrap(
        IServer server,
        ILogger logger)
    {
        this.server = server;
        this.logger = logger;
    }

    public async Task RunAsync(CancellationToken token)
    {
        this.logger.Info("Application Starting...");

        await this.server.StartAsync(token);
    }
}

Here's the server:

public abstract class BaseServer : IServer
{
    private readonly IPAddress ipAddress;

    private readonly int port;

    private readonly ILogger logger;

    protected BaseServer(
        string ipAddress, 
        int port,
        ILogger logger)
    {
        this.ipAddress = IPAddress.Parse(ipAddress);
        this.port = port;
        this.logger = logger;
    }

    public async Task StartAsync(CancellationToken token)
    {
        this.logger.Debug("[{0}] Listening for connections using: {1}:{2}", this.GetType().Name, this.ipAddress.ToString(), this.port);
        var tcpListener = new TcpListener(this.ipAddress, this.port);

        tcpListener.Start();

        while (!token.IsCancellationRequested)
        {
            await this.ServerProcessorAsync(tcpListener, token);
        }

        tcpListener.Stop();
        Console.WriteLine("Stopped Listener");
    }

    public abstract Task ServerProcessorAsync(TcpListener listener, CancellationToken token);
}

Here's the Server Processor:

public class Server : BaseServer
{
    private readonly ILogger logger;

    public Server(
        IAppConfiguration configuration,
        ILogger logger) 
        : base(configuration.IpAddress, configuration.Port, logger)
    {
        this.logger = logger;
    }

    public override async Task ServerProcessorAsync(TcpListener listener, CancellationToken token)
    {
        this.logger.Debug("[{0}] Waiting for connection...", this.GetType().Name);
        var client = await listener.AcceptTcpClientAsync();

        this.logger.Debug("[{0}] Spawning Thread for Connection...", this.GetType().Name);
        Parallel.Invoke(new ParallelOptions
            {
                CancellationToken = token,
                MaxDegreeOfParallelism = 10000,
                TaskScheduler = TaskScheduler.Current
            }, () => this.ListenToClient(client));
    }

    private void ListenToClient(TcpClient client)
    {
        var threadName = Thread.CurrentThread.Name;

        var bytes = new byte[2048];

        var stream = client.GetStream();

        int i;
        while ((i = stream.Read(bytes, 0, bytes.Length)) != 0)
        {
            var timeString = DateTime.Now.ToLongTimeString();
            var currentRes = Encoding.UTF8.GetString(bytes);
            var received = $"Recieved [{threadName}] [{timeString}]:  {currentRes}";

            this.logger.Info(received);
            var responseData = Encoding.UTF8.GetBytes(received);
            stream.Write(responseData, 0, responseData.Length);

        }

        client.Close();
    }
}

Will this correctly shut the app down when ctrl+c is pressed?

Is there a way to debug this, or to know that the resources have been released properly.

I assume that the while (!token.IsCancellationRequested) will break when ctrl+c. I also assume that that if there are any threads created by Parallel.Invoke they will disposed of when the Cancel is called.

If in the case that:

Console.CancelKeyPress += (sender, eventArgs) =>
{
     TokenSource.CancelAfter(500);
};

doesn't wait for things to be cleared up, is there a better way than a timeout to make sure that everything is properly cleared up?

Callum Linington
  • 14,213
  • 12
  • 75
  • 154

1 Answers1

1

First, you Wait on RunAsync before subscribing to Console.CancelKeyPress event, so you will subscribe to it when it's too late.

Second, cancellation token won't work anyway in your case. This line:

var client = await listener.AcceptTcpClientAsync();

Will block until new client connects, and because AcceptTcpClientAsync does not has overload which accepts CancellationToken - usage of CancellationTokens in the whole program becomes not needed. What you should do instead is stop your listener instead of cancellation. This will throw exception on the line above, which you should catch and gracefully end the task.

If you really want to continue with CancellationToken, even if it's not really needed here, consider this approach to make it work with AcceptTcpClientAsync: https://stackoverflow.com/a/14524565/5311735. This might also be good idea if you use CancellationToken to cancel many different operations not shown in your question.

Community
  • 1
  • 1
Evk
  • 98,527
  • 8
  • 141
  • 191
  • Note that you also need to wait for your task to finish in CancelKeyPress event handler, otherwise your process will just get down before anything is cleaned up. – Evk May 01 '16 at 22:20
  • And more: you are passing _one_ action to Parallel.Invoke, meaning that Parallel.Invoke does nothing useful at all. Not sure what did you mean to do even. – Evk May 01 '16 at 22:23
  • I read a stackoverflow post answer that Jon Skeet wrote saying that you could use `Parallel.Invoke` to spawn off new threads each time a new client connected. – Callum Linington May 01 '16 at 22:39
  • Do you have a link to that answer? – Evk May 01 '16 at 22:42
  • He doesn't say how to do it explicitly but does say to use the TPL http://stackoverflow.com/questions/5339782/how-do-i-get-tcplistener-to-accept-multiple-connections-and-work-with-each-one-i – Callum Linington May 01 '16 at 23:13
  • To be honest, it doesn't make a whole lot of sense to me doing Parallel.Invoke. But i would be good to use something that properly managed the thread pool for you – Callum Linington May 01 '16 at 23:15
  • Well he means something like Task.Run in your case, not Parallel.Invoke. You can also make ListenToClient async and _not_ await it, will have the same result. – Evk May 02 '16 at 07:18
  • Yeah I was thinking more down those lines. – Callum Linington May 02 '16 at 09:53