Search code examples
c#socketsasynchronousasync-awaittcplistener

Ho to create an async TCP server in a better way than this?


I have a server application to accept TCP connections from a set of about 10000 clients (in fact field devices with GPRS interface).

The part accepting connections is as such:

public async System.Threading.Tasks.Task AcceptConnections()
{
    _listenerProxy.StartListen();
    var taskList = new List<System.Threading.Tasks.Task>();
    var application = _configManager.Configuration.GetSection("application");
    var maxTasks = int.Parse(application["MaxTasksPerListener"]);
    while (true)
    {
        if (taskList.Count > maxTasks)
        {
            _logger.Info($"Waiting for handling task to be completed {_listenerProxy.Port}.");
            await System.Threading.Tasks.Task.WhenAny(taskList.ToArray()).ConfigureAwait(false);
            taskList.RemoveAll(t => t.IsCompleted); // at least one task affected, but maybe more
        }
        _logger.Info($"Waiting for client to be accepted on port {_listenerProxy.Port}.");
        (var readStream, var writeStream, var sourceIP, var sourcePort) = await _listenerProxy.AcceptClient().ConfigureAwait(false);
        _logger.Info($"Client accepted on port {_listenerProxy.Port}: IP:{sourceIP}, Port:{sourcePort}");

        var clientContext = new ClientContext<TSendMessage, TReceiveMessage>
        {
            SourceAddress = sourceIP,
            SourcePort = sourcePort,
            DataAdapter = DataAdapterFactory<TSendMessage, TReceiveMessage>.Create(readStream, writeStream)
        };
        taskList.Add(HandleClient(clientContext));
    }
}

The HandleClient method is defined as:

public async System.Threading.Tasks.Task HandleClient(ClientContext<TSendMessage, TReceiveMessage> clientContext);

I want to be able to handle up to a predefined number of requests in parallel (handler function is HandleClient). Clients will connect, send some small amount of data and close connection afterwards. Since the whole project is async, I was tempted to try an async approach also for that part.

I feel quite sure that this solution is not recommended, but I have no idea how it could be done in a better way. I found a very closely related topic here: How to run a Task on a new thread and immediately return to the caller?

Stephen Cleary made a comment there:

Well, the very first thing I'd recommend is to try a simpler exercise. Seriously, an asynchronous TCP server is one of the most complex applications you can choose.

So, well, what would be the "right" approach in this case? I know, that my approach "works", but I have a bad feeling, especially that HandleClient is more or less "fire and forget". The only reason why I'm interested in the resulting task is to "throttle" the throughput (originally it was a async void). In fact I have the exactly same question as the TO in the link. However, I'm still not aware about what could be the biggest problem in such approach.

I would appreciate constructive hints...Thanks.


Solution

  • In a word: Kestrel. Offload all of these concerns to that and get a lot of other bonuses at the same time, such as advanced buffer lifetime management, and a design geared towards async. The key API here is UseConnectionHandler<T>, for example:

    public static IWebHostBuilder CreateHostBuilder(string[] args) =>
        WebHost.CreateDefaultBuilder(args).UseKestrel(options =>
        {   // listen on port 1000, using YourHandlerHere for the handler
            options.ListenLocalhost(1000, o => o.UseConnectionHandler<YourHandlerHere>());
        }).UseStartup<Startup>();
    

    You can see a trivial but runnable example here (heh, if you call a mostly-functional redis-like server "trivial"), or I have other examples in private github repos that I could probably ship to you, including a sample where all you need to do is implement the frame parser (the library code deals with everything else, and works on both TCP and UDP).

    As for the "throttle" - that sounds like an async semaphore; SemaphoreSlim has WaitAsync.