2

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.

MichaelW
  • 1,328
  • 1
  • 15
  • 32

1 Answers1

8

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.

Marc Gravell
  • 1,026,079
  • 266
  • 2,566
  • 2,900
  • 3
    TIL! I had no idea Kestrel could handle raw TCP. – Stephen Cleary Apr 21 '21 at 12:15
  • 2
    @StephenCleary yep; and because it exposes everything as "pipelines" (i.e. `PipeReader`, `PipeWriter`), you don't need to deal with any of the back-buffer management etc - just take what you want from the inbound pipe – Marc Gravell Apr 21 '21 at 12:19
  • @Marc This is the sort of solution I'm looking for. But unfortunately Kestrel is extremely overwhelming and it sounds like to take years to be comfortable with it. What kind of documentation you can recommend to start with? – MichaelW Apr 21 '21 at 13:27
  • @MichaelW high capacity socket servers are fundamentally complex - this is inescapable. IMO Kestrel makes it a lot simpler than any other API I'm aware of. That said, I do have another server host in Pipelines.Sockets.Unofficial that works similarly but without Kestrel. I've linked to a working server; I can also point you to my 3-part series here that covers most of the pipelines details: https://blog.marcgravell.com/2018/07/pipe-dreams-part-1.html, and which discusses the non-Kestrel host – Marc Gravell Apr 21 '21 at 17:08
  • 1
    @Marc: I integrated Kestrel as TCP socket server according to you example. It works. I use Stream based on pipe and was able to re-use my old code nearly untouched. We use Kestrel already for other things (gRPC, Metrics, ...) and I didn't know, that we can use it for TCP socket communication too. Thanks a lot!!! – MichaelW Apr 21 '21 at 19:18
  • @Marc: Kestrel is my solution; but was was in fact wrong with my naïve solution above? Where could be problems which I do not have with Kestrel? – MichaelW Apr 21 '21 at 19:25