Протокол WebSocket, как и любые другие протоколы, имеет свои преимущества и свои недостатки. Именно из-за последних появляются новые версии протоколов, новые протоколы и новые подходы к реализации всего, что вокруг них, а конкретно — клиентских и серверных приложений.
Хороший сервер — это такой сервер, который учитывает особенности протокола. Хороший клиент — то же самое. Здесь под «сервер» и «клиент» подразумевается именно имплементация протокола в виде конкретного софта или библиотеки.
Изучив WebSocket с разных сторон, образовался следующий список проблем, которые в WebSocket есть, а в других популярных и используемых местах их не имеется, либо не несут важных или критичных проблем. Но давайте сперва изучим другую сторону WebSocket, ту, которая лежит в его основе: TCP-keepalive.
TCP KeepAlive
Что такое KeepAlive? Это способ оставлять TCP-соединение открытым долгое время.
Как это делается? Раз в определенный период происходит обмен специальными пакетами, которые озаглавлены в документации «keepalive probes». Выполняется это с помощью PSH- и RST-пакетов.
Как долго может жить KeepAlive соединение? Существует максимальный временной интервал между пакетами с данными, в течение которого соединение может продолжать жить. Если обмен данными происходит в этот период, то следующий период начинается сначала, т.е. KeepAlive-соединение периодически (пусть и редко), обменивающееся внутри себя данными, может жить довольно долго.
В ядре Linux вокруг этого есть три настройки:
root@net-stage:~# sysctl -a | grep tcp_keepalive_ net.ipv4.tcp_keepalive_intvl = 75 net.ipv4.tcp_keepalive_probes = 9 net.ipv4.tcp_keepalive_time = 7200
Здесь показаны их значения по умолчанию. net.ipv4.tcp_keepalive_time — это как раз максимальное время между пакетами с данными.
net.ipv4.tcp_keepalive_intvl — интервал обмена пакетами «keepalive probes» по умолчанию 75 секунд. «net.ipv4.tcp_keepalive_probes» — это количество возможных неотвеченных «keepalive probes» пакетов — по сути попыток возобновить соединение.
Как переводится соединение в режим KeepAlive? На языке C очень просто:
optval = 1; optlen = sizeof(optval); setsockopt(sock, SOL_SOCKET, SO_KEEPALIVE, &optval, optlen);
Так же можно использовать для своих сокетов свои величины intvl и probes:
int keepcnt = 5; int keepintvl = 120; setsockopt(sock, IPPROTO_TCP, TCP_KEEPCNT, &keepcnt, sizeof(int)); //эквивалентно tcp_keepalive_probes setsockopt(sock, IPPROTO_TCP, TCP_KEEPINTVL, &keepintvl, sizeof(int)); //эквивалентно tcp_keepalive_intvl
В комментариях к этим опциям в документации отмечено: «This option should not be used in code intended to be portable.» Эти опции требуется применять очень аккуратно: это настройки, которые ДОЛЖНЫ быть одинаковы и на клиенте, и на сервере.
Если величины, например, tcp_keepalive_intvl разойдутся, то клиент, работающий по умолчанию, и сервер, работающий в режиме tcp_keepalive_intvl=25, будут иметь разную информацию о соединении. В этом примере 75/25 — клиент может долго думать, что соединение еще открыто, когда сервер будет уже знать о том, что оно закрылось. Это будет приводить к отправке пакетов в уже закрытое на том конце соединение и потере пакетов.
Но, тем не менее, если вы контролируете и клиентский код, и серверный, то эти величины для вас доступны.
Например, величина 75 секунд может быть слишком велика в определенных ситуациях, например, когда данные требуется передавать быстро, и при их недоставке так же быстро требуется получить ответ об ошибке.
WebSocket
Websocket использует TCP-KeepAlive соединения. В конкретном применении это дает как плюсы, так и минусы. Плюсы очевидны:
- Постоянное соединение, которое можно просунуть в Web-браузер, от которого наступает счастье и на FrontEnd, и на BackEnd в Web-приложении или мобильном приложении, работающем с сервером.
- Кратковременное отсутвие связи вообще не обрывает такое соединение.
- Позволяет работать асинхронно, вместо привычной для web-а работы в режиме запрос-ответ.
Проблемы WebSocket в современном использовании менее очевидны:
Молчаливый отвал соединения. При отправке пакета в WebSocket вы не узнаете о том, доставлен он или нет, пока не пройдет 75 секунд таймаута. Сам протокол ничего не расскажет об этом, а величина по умолчанию 75 секунд велика для быстрого взаимодействия. Например, тот же чат, работающий поверх реальной сети, где клиенты могут «выпадать из сети», усложняется архитектурно: сервер должен внутри себя иметь очередь, он должен иметь unack-буфер и логику переотправки сообщений. Часто по WebSocket разработчики запускают «свои самодельные ping-и» с целью понять, жив он или нет.
Смена сети клиентом. В сетях мобильной связи часто бывает так, что ваш внешний IP, NAT и вообще сеть, в которой присутствует мобильное устройство, меняются. Это даже не зависит от оператора: вы пришли домой, подключился Wi-Fi и весь websocket рухнул очень забавным образом:
- Сервер ничего не знает о вашей смене адреса, если клиент не закрыл соединение при переподключении к другой сети.
- Сервер продолжает отправлять ваши приватные данные на старый IP, а вас там уже нет. Там уже кто-то другой их получает и это утечка (только не говорите мне, что SSL решает эту проблему, к сожалению, он решает её на уровне криптографии, но не на уровне доступа. Иногда сам факт передачи Вам какой-либо информации от известного получателя, без деталей, являтеся ценным и это не редкость, а ежедневный кейс при борьбе с утечками информации)
Хотите пример такой утечки? Легко:
- Боб говорит Алисе что не пользуется услугами ТТТ-Банка и не может перевести ей денег.
- Боб покинул 4G сеть и ушел в Wi-Fi.
- Алиса знала, что на этом IP минуту назад был Боб.
- Сейчас IP Боба достался Алисе. (это не маловероятная ситуация, а вполне подстраиваемая, если сделать для этого некоторые усилия)
- Алиса получает пакет от сервера его банка «ТТТ-банк». Алиса знает, что IP отправителя принадлежит TTT-Банку (через whois, путем запросов на 443/80 порт и визуальное наблюдение API и т.п.). Теперь Алиса знает, что Боб пользуется мобильным приложением ТТТ-Банка и, скорее всего, у него есть карта.
- Итог:
- Боб пойман на вранье, чего Боб никак не хотел.
- Личная информация о том, каким банком пользуется Боб, известна Алисе, отчасти это уже может быть утечкой тайны (например, банковской)
- SSL не спас, что и ожидалось, т.к. ssl существует выше уровня IP, а IP отправителя (банка) известен Алисе.
Повышенная нагрузка на серверы. Клиент ничего не знает о своей смене внешнего IP и сует данные в отпавшее соединение — сервер получает пакеты старого соединения уже с нового IP и отбрасывает их. Если у вас популярное мобильное приложение и, как результат, нагруженный сервер, то маленький чих в сети оператора мобильной связи устроит DDoS атаку на ваш сервер и вам прилетят старые пакеты, реконнекты, данные, вместо того, чтобы просто передать данные 1 раз. Т.е. вам требуется иметь многократный запас пропускной способности на серверах.
Способы решения проблем WebSocket
На данный момент решения для отработки обрыва соединения костыльно-ориентированные:
- Решать на L7 задачи L4 — гонять «свои пинги» по websocket-у
- Тюнить сеть и на клиентах, и на серверах путем установки tcp_keepalive_intvl в коде или настройках сервера:
- Очень чреватый путь, особенно в настройках сервера, т.к. затронет работу всех приложений.
- Недоступен для мобильных клиентов на Android (если я не прав и на Android есть способ установить setsockopt — поправьте меня, плиз, в комментариях в FB или VK).
- Требует грамотных админов, разработчиков, разбирающихся в сети, и усложняет продукт на ровном месте.
- Использовать готовые решения, которые уже гоняют «свои пинги» по L7 и т.п.
Универсальных способов решения проблем утечки информации об отправляемых пакетах при смене мобильным клиентом сети до сих пор нет.