Как правильно закрыть постоянное соединение System.Net.WebSockets.ClientWebSocket? - PullRequest
2 голосов
/ 21 июня 2019

Я использую System.Net.WebSockets.ClientWebSocket для создания клиента C # WebSocket для подключения к серверу, которому не принадлежит исходный код. Пока все работает хорошо, но я хочу «правильно» отключить моего клиента. Вот сокращенная версия моего исходного кода:

public class Client : IDisposable
{
    private ClientWebSocket socket;
    private string endpoint;
    private Task receiveTask;

    public Client(string endpoint)
    {
        this.endpoint = endpoint;
        this.socket = new ClientWebSocket();
    }

    public async Task Initialize()
    {
        byte[] contentBuffer = Encoding.UTF8.GetBytes("notify message");

        // Connect to the server, and send a message to notify
        // it of the client's availability to receive data.
        await OpenConnection();
        await socket.SendAsync(new ArraySegment<byte>(contentBuffer), WebSocketMessageType.Text, true, CancellationToken.None);
    }

    private async Task OpenConnection()
    {
        if (socket.State != WebSocketState.Open)
        {
            await socket.ConnectAsync(new Uri(endpoint), CancellationToken.None);
            receiveTask = Task.Run(async () => await Receive());
        }
    }

    private async Task Receive()
    {
        while (socket.State == WebSocketState.Open)
        {
            byte[] buffer = new byte[1024];
            var result = await m_sessionSocket.ReceiveAsync(new ArraySegment<byte>(buffer), cancellationToken);

            if (result.MessageType == WebSocketMessageType.Close)
            {
                break;
            }
            else
            {
                using (var stream = new MemoryStream())
                {
                    stream.Write(buffer, 0, result.Count);
                    while (!result.EndOfMessage)
                    {
                        result = await socket.ReceiveAsync(new ArraySegment<byte>(buffer), CancellationToken.None);
                        stream.Write(buffer, 0, result.Count);
                    }

                    stream.Seek(0, SeekOrigin.Begin);
                    using (var reader = new StreamReader(stream, Encoding.UTF8))
                    {
                        string message = reader.ReadToEnd();
                        // Do stuff with received message
                    }
                }
            }
        }
    }

    public void Dispose()
    {
        // NOTE: This is a gross oversimplification. Assume in the
        // actual project that a proper implementation of the Dispose
        // pattern has been created.
        if (socket.State == WebSocketState.Open)
        {
            // How do I notify the server of disconnection?
            // The below has some sort of race condition, whereby
            // it hangs the client.
            Task.Run(async () => await socket.CloseAsync(WebSocketCloseStatus.NormalClosure, string.Empty, CancellationToken.None)).Wait();
        }

        if (receiveTask != null)
        {
            receiveTask.Dispose();
        }

        socket.Dispose()
    }
}

Теперь за вызов, который у меня есть. Я не могу избавиться от Task, выполняющего получение данных, пока оно не закончится. Эта задача заблокирована в ожидании получения от сервера. Попытка дозвониться до CloseAsync изнутри Dispose, похоже, зашла в тупик, поэтому это может быть неправильный подход.

Очевидно, я мог бы использовать CancellationTokenSource, чтобы передать CancellationToken методу Receive, передав его, в свою очередь, методу ReceiveAsync WebSocket. Если я сделаю это и скажу источнику токена пометить токен, он освободит блок, и я смогу проверить токен на наличие флага IsCancellationRequested внутри метода receive, который завершится, когда его увидят. Однако, если я это сделаю, это вызовет исключение. Мне действительно нужно поймать исключение (yuck!) В моем методе Dispose только для того, чтобы отменить задачу? Кроме того, если я отменяю задачу, это правильно уведомляет сервер об отключении? Или я просто делаю это неправильно, в первую очередь из-за того, что долгосрочный (не ожидаемый) Task метод ReceiveAsync работает?

1 Ответ

0 голосов
/ 22 июня 2019

Вы не должны использовать IDisposable в этом случае.Изящное закрытие соединения через веб-сокет требует асинхронного ввода-вывода.Требуется чистое «рукопожатие», когда отправитель отправляет кадр ЗАКРЫТЬ, а получатель отправляет обратно кадр ЗАКРЫТЬ.Только тогда соединение перейдет в чистое закрытие сокета (с FIN, а не с RST).

Если вам нужна семантика «IDisposable», вы можете посмотреть на новый шаблон IAsyncDisposable, представленный в последней версии .NET Core.Но в целом использование синхронного шаблона Dispose () не очень хорошая идея.

...