Я предполагаю, что переход на что-то вроде SignalR не является приемлемым решением. Это было бы моей первой рекомендацией.
Пользовательские сокеты сервера - это сценарий, в котором допускается какое-то «увольнение». Я рассматриваю возможность добавления типа AsyncEx типа «Диспетчер задач», чтобы упростить решение такого рода, но пока не сделал этого.
Суть в том, что вам нужно управлять списком соединений сами. Объект «соединения» может включать Task
, который представляет цикл обработки; хорошо. Также полезно (особенно для целей отладки или управления) иметь и другие свойства, такие как удаленный IP-адрес.
Так что я бы подошел к этому примерно так:
private readonly object _mutex = new object();
private readonly List<State> _connections = new List<State>();
private void Add(State state)
{
lock (_mutex)
_connections.Add(state);
}
private void Remove(State state)
{
lock (_mutex)
_connections.Remove(state);
}
public async Task RunAsync(CancellationToken cancellationToken)
{
while (true)
{
var connection = await listener.AcceptAsync(cancellationToken);
Add(new State(this, connection));
}
}
private sealed class State
{
private readonly Parent _parent;
public State(Parent parent, Connection connection, CancellationToken cancellationToken)
{
_parent = parent;
Task = ExecuteAsync(connection, cancellationToken);
}
private static async Task ExecuteAsync(Connection connection, CancellationToken cancellationToken)
{
try { await HandleConnectionAsync(connection, cancellationToken); }
finally { _parent.Remove(this); }
}
public Task Task { get; }
// other properties as desired, e.g., RemoteAddress
}
Теперь у вас есть коллекция соединений. Вы можете либо игнорировать задачи в объектах State
(как это делается в приведенном выше коде), что похоже на запуск и забывание. Или вы можете await
их всех в какой-то момент. Например:
public async Task RunAsync(CancellationToken cancellationToken)
{
try
{
while (true)
{
var connection = await listener.AcceptAsync(cancellationToken);
Add(new State(this, connection));
}
}
catch (OperationCanceledException)
{
// Wait for all connections to cancel.
// I'm not really sure why you would *want* to do this, though.
List<State> connections;
lock (_mutex) { connections = _connections.ToList(); }
await Task.WhenAll(connections.Select(x => x.Task));
}
}
Затем можно легко расширить объект State
, чтобы вы могли выполнять действия, которые иногда полезны для серверного приложения, например:
- Список всех удаленные адреса, к которым подключен этот сервер.
- Подождите, пока не будет установлено указанное c соединение.
- ...
Примечания:
- Используйте один шаблон для отмены. Передача токена приведет к
OperationCanceledException
, что является нормальным шаблоном отмены. Код также ранее делал while (!IsCancellationRequested)
, что приводило к успешному завершению при отмене, что не является нормальным шаблоном отмены. Поэтому я удалил это, чтобы код больше не использовал два шаблона отмены. - При работе с необработанными сокетами, в общем случае, вы должны постоянно читать (даже когда вы запись) и периодически запись (даже если у вас нет данных для отправки). Таким образом, ваш
HandleConnectionAsync
должен запустить асинхронный считыватель и записывающее устройство, а затем использовать Task.WhenAll
. - Я удалил вызов на
HandleException
, потому что (вероятно) все, что он делает, должно обрабатываться State.ExecuteAsync
. Нетрудно добавить его обратно в случае необходимости.