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.
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
.