WebSockets.NET принудительно закрыты, несмотря на поддержание активности и активность соединения

Мы написали простой клиент WebSocket с использованием System.Net.WebSockets. KeepAliveInterval на ClientWebSocket установлен на 30 секунд.

Соединение успешно установлено, и трафик проходит, как и ожидалось, в обоих направлениях, или, если соединение не используется, клиент отправляет запросы Pong каждые 30 секунд на сервер (отображается в Wireshark).

Но через 100 секунд соединение внезапно разрывается из-за закрытия сокета TCP на стороне клиента (в Wireshark мы видим, что клиент отправляет FIN). Сервер отвечает 1001 Going Away перед закрытием сокета.

После долгих раскопок мы нашли причину и нашли довольно сложный обходной путь. Несмотря на большое количество поисков в Google и Stack Overflow, мы видели только пару других примеров, когда люди писали о проблеме, но никто не дал ответа, поэтому я публикую это, чтобы избавить других от боли и в надежде, что кто-то сможет предложить лучший обходной путь.

Источником 100-секундного тайм-аута является то, что WebSocket использует System.Net.ServicePoint, который имеет свойство MaxIdleTime, позволяющее закрывать незанятые сокеты. При открытии WebSocket, если существует существующая ServicePoint для Uri, он будет использовать ее, независимо от того, какое свойство MaxIdleTime было установлено при создании. Если нет, то будет создан новый экземпляр ServicePoint, в котором MaxIdleTime будет установлен из текущего значения свойства System.Net.ServicePointManager MaxServicePointIdleTime (по умолчанию 100000 миллисекунд).

Проблема заключается в том, что ни трафик WebSocket, ни служебные сообщения WebSocket (Ping/Pong), по-видимому, не регистрируются как трафик в отношении таймера простоя ServicePoint. Таким образом, ровно через 100 секунд после открытия WebSocket он просто разрушается, несмотря на трафик или keep-alive.

Мы предполагаем, что это может быть потому, что WebSocket начинает жизнь как HTTP-запрос, который затем обновляется до websocket. Похоже, что таймер простоя ищет только HTTP-трафик. Если это действительно так, то это похоже на серьезную ошибку в реализации System.Net.WebSockets.

Обходное решение, которое мы используем, - установить для MaxIdleTime в ServicePoint значение int.MaxValue. Это позволяет WebSocket оставаться открытым неограниченно долго. Но недостатком является то, что это значение применяется к любым другим соединениям для этой ServicePoint. В нашем контексте (который является нагрузочным тестом с использованием Visual Studio Web и нагрузочным тестированием) у нас есть другие (HTTP) соединения, открытые для той же ServicePoint, и фактически к тому времени, когда мы открываем наш WebSocket, уже существует активный экземпляр ServicePoint. Это означает, что после того, как мы обновим MaxIdleTime, все HTTP-соединения для теста загрузки не будут иметь времени простоя. Это не совсем удобно, хотя на практике веб-сервер все равно должен закрывать незанятые соединения.

Мы также кратко рассмотрим, можем ли мы создать новый экземпляр ServicePoint, зарезервированный только для нашего соединения с WebSocket, но не смогли найти чистый способ сделать это.

Еще один маленький поворот, который усложнил отслеживание, заключается в том, что хотя свойство System.Net.ServicePointManager MaxServicePointIdleTime по умолчанию равно 100 секундам, Visual Studio переопределяет это значение и задает его равным 120 секундам, что затрудняет его поиск.

3 ответа

Я столкнулся с этим вопросом на этой неделе. Ваш обходной путь указал мне правильное направление, но я считаю, что сузил основную причину.

Если заголовок "Content-Length: 0" включен в ответ "101 Switching Protocols" от сервера WebSocket, WebSocketClient запутывается и планирует подключение для очистки через 100 секунд.

Вот оскорбительный код из .Net Reference Source:

//if the returned contentlength is zero, preemptively invoke calldone on the stream.
//this will wake up any pending reads.
if (m_ContentLength == 0 && m_ConnectStream is ConnectStream) {
    ((ConnectStream)m_ConnectStream).CallDone();
}

Согласно RFC 7230, раздел 3.3.2, Content-Length запрещен в 1xx (информационных) сообщениях, но я обнаружил, что он ошибочно включен в некоторые реализации сервера.

Дополнительные сведения, включая пример кода для диагностики проблем с ServicePoint, см. В этой ветке: https://github.com/ably/ably-dotnet/issues/107

Я установил KeepAliveInterval для сокета на 0 следующим образом:

theSocket.Options.KeepAliveInterval = TimeSpan.Zero;

Это устранило проблему отключения веб-сокета по истечении времени ожидания. Но опять же, он также, вероятно, полностью отключает отправку сообщений ping.

Я изучал эту проблему на днях, сравнивал пакеты захвата в Wireshark(webclient-client для python и WebSocketClient для .Net) и нашел, что произошло. В WebSocketClient «Options.KeepAliveInterval» отправляет на сервер только один пакет, если за этот период с сервера не получено ни одного сообщения. Но некоторые серверы судят только о том, есть ли активное сообщение от клиента. Таким образом, мы должны вручную отправлять произвольные пакеты (не обязательно пакеты ping, а WebSocketMessageType не имеет типа ping) на сервер через равные промежутки времени, даже если сторона сервера постоянно отправляет пакеты. Это решение.

Другие вопросы по тегам