Опыт внедрения WebSocket в приложение на Flutter с использованием Socket.IO
В нашем блоге мы регулярно делимся реальными кейсами из практики — не только чтобы показать, как мы решаем задачи, но и чтобы помочь другим разработчикам избежать типичных подводных камней. Сегодня один из наших Flutter-инженеров, Матвей Крикунов, рассказывает о непростом, но показательном опыте интеграции WebSocket через Socket.IO в кроссплатформенное приложение.
Изначально проект задумывался как PWA, но необходимость добавить мобильную версию без изменений на бэкенде поставило перед командой ряд нетривиальных задач — от стабильности соединения при передаче медиа до восстановления сессии после обрыва. Этот опыт может пригодиться каждому, кто работает с реалтайм-коммуникацией в Flutter.
Передаю слово Матвею
В этой статье я хочу поделиться своим опытом внедрения WebSocket в приложение на Flutter, используя библиотеку Socket.IO для общения с сервером. Проект изначально задумывался как Progressive Web App (PWA), но в процессе заказчик решил, что нужно сделать и мобильное приложение, сохранив при этом PWA. Бэкенд не претерпел изменений, что впоследствии создало определенные сложности, поскольку архитектура изначально проектировалась исключительно для веб-сайта и поддержки ограниченного набора функций.
Расскажу, как я боролся с разрывами соединения из-за передачи большого объема картинок, решал проблему восстановления авторизации после реконнекта и написал собственный дебаг-сервис для логирования WebSocket-обращений, так как бесплатных готовых решений найти не удалось. Постараюсь рассказать все дружелюбно и с небольшим количеством воды, чтобы было интересно и полезно.
Немного предыстории
Изначально наш проект был задуман как PWA — веб-приложение, которое должно было работать в браузере и обеспечивать удобный интерфейс для пользователей. Мы выбрали Socket.IO для общения с сервером, так как он позволял реализовать обмен данными в реальном времени, например для обновления заказов и уведомлений.
Выбор Socket.IO вместо чистого WebSocket был обусловлен несколькими факторами:
- Во-первых, Socket.IO предоставляет встроенную поддержку автоматического восстановления соединения, обработки ошибок и механизма heartbeat, что упрощает управление стабильностью связи, особенно в условиях нестабильной сети.
- Во-вторых, библиотека поддерживает событийно-ориентированную модель, что удобно для структурирования кода: вместо низкоуровневой работы с сообщениями WebSocket мы могли отправлять и обрабатывать именованные события, что ускорило разработку.
- Кроме того, Socket.IO уже был знаком нашей серверной команде, что снизило порог вхождения.
Однако у этого выбора были и минусы: Socket.IO добавляет некоторый оверхед по сравнению с чистым WebSocket, а его зависимость от дополнительных протоколов (например, HTTP для начального рукопожатия) иногда усложняла отладку. Впоследствии выбор оправдался для PWA, где стабильность соединения и простота интеграции сыграли ключевую роль, но в случае с мобильным приложением мы столкнулись с рядом ограничений, особенно при работе в фоновом режиме, о чем расскажу ниже.
Переход от PWA к мобильному приложению
Однако в какой-то момент заказчик решил, что PWA недостаточно и нужно полноценное мобильное приложение на iOS и Android, которое бы работало на той же серверной инфраструктуре. Это решение изменило подход к разработке, так как мобильные приложения имеют свои особенности, прежде всего в плане управления соединением и фоновой активности.
Для мобильного приложения я использовал Flutter и пакет socket_io_client, который хорошо интегрируется с серверной частью Socket.IO. Настройка соединения прошла относительно гладко, но, как это часто бывает, проблемы начались, когда мы перешли к реальному тестированию.
Проблема 1: Разрывы соединения из-за большого объема картинок
Одной из главных трудностей стало то, что WebSocket-соединение обрывалось при попытке передать большое количество изображений. Когда несколько пользователей начинали отправлять картинки, особенно в высоком разрешении, канал связи перегружался, что приводило к разрывам соединения.
Решение: Совместная работа с бэкендом
Чтобы справиться с этой проблемой, мы с командой бэкенда решили пересмотреть подход к передаче изображений. Изначально я пытался отправлять их целиком через WebSocket, но это оказалось неэффективно. После обсуждения мы пришли к выводу, что мы будем сжимать изображения, и сделали отдельный метод, позволяющий грузить несколько картинок поочередно. Это позволило разгрузить WebSocket-канал и сделать его более стабильным.
Этот метод проверяет состояние соединения и, если оно отсутствует, добавляет событие в очередь для последующей обработки, минимизируя нагрузку на канал.
Проблема 2: Разрывы соединения в фоновом режиме
Еще одной сложностью стали разрывы WebSocket-соединения, когда приложение уходило в фоновый режим. На iOS и Android приложение могло засыпать, и соединение обрывалось. Когда пользователь возвращался к приложению, данные в реальном времени (например, новые сообщения) не поступали, пока соединение не восстанавливалось.
Почему стандартный механизм Socket.IO не сработал?
Socket.IO имеет встроенный механизм автоматического переподключения, который теоретически должен справляться с разрывами соединения. Однако в мобильных приложениях, особенно на iOS, из-за строгих ограничений на фоновую активность стандартные настройки Socket.IO оказались недостаточными. При уходе приложения в фон соединение часто разрывалось без уведомления, а попытки переподключения либо не запускались вовремя, либо завершались с ошибками из-за отсутствия активной сети или устаревших токенов авторизации. Кроме того, стандартный механизм не учитывал необходимость повторной авторизации и восстановления пользовательской роли после переподключения, что приводило к сбоям в обработке событий.
Решение: Настройка реконнекта и фонового режима
Для решения проблемы я реализовал кастомную логику быстрого восстановления соединения, которая учитывает особенности мобильных приложений. В основе решения лежит класс SocketService, который управляет WebSocket-соединением с использованием пакета socket_io_client для Flutter. Вот как это работает:
Отслеживание состояния соединения. Я настроил обработчики событий onConnect, onDisconnect, onConnectError и onError, чтобы в реальном времени отслеживать статус соединения. При разрыве соединения (например, когда приложение уходит в фон) запускается процесс переподключения, а пользователю показывается модальное окно с индикатором загрузки, чтобы он понимал, что соединение восстанавливается.
Экспоненциальная задержка для переподключения. Чтобы не перегружать сервер частыми попытками, я внедрил механизм экспоненциальной задержки (backoff): начальная задержка составляет 1 секунду и увеличивается с каждой попыткой, но не превышает 30 секунд. Это позволило балансировать между скоростью восстановления и нагрузкой на сервер. Максимальное количество попыток ограничено 15, чтобы избежать бесконечных циклов.
Проверка доступности сервера. Перед попыткой переподключения я добавил проверку доступности хоста через временное тестовое соединение. Это помогло избежать лишних попыток, если сеть недоступна, и улучшило пользовательский опыт.
Очередь событий. Если соединение отсутствует, все исходящие события (например, отправка сообщений) сохраняются в очередь (_eventQueue). После восстановления соединения очередь обрабатывается с приоритетом для критически важных событий, таких как повторная авторизация (users:sign_in_token) и установка роли (users:set_role).
Повторная авторизация. После восстановления соединения я проверяю валидность refresh_token и отправляю событие users:sign_in_token для получения новых токенов доступа. Если токен истек, пользователь перенаправляется на экран авторизации.
Проблема 3: Восстановление авторизации после реконнекта
После разрыва соединения и последующего переподключения сервер требовал повторной авторизации, так как токен, отправленный при первом подключении, становился невалидным. Это приводило к тому, что пользователи теряли доступ к данным до ручного ввода учетных данных.
Решение: Хранение и повторная отправка токена
Чтобы решить эту проблему, я сохранял JWT-токен в безопасном хранилище на устройстве с помощью пакета flutter_secure_storage. При переподключении клиент автоматически извлекал токен и отправлял его серверу, чтобы восстановить сессию. На сервере мы настроили обработку этого токена, что позволило сделать процесс переподключения незаметным для пользователя.
Проблема 4: Отсутствие подходящих инструментов для дебага
Когда я начал тестировать приложение, мне понадобился способ отслеживать все WebSocket-сообщения, чтобы находить ошибки (в том числе и соединения). Я искал бесплатные инструменты для логирования WebSocket-трафика, но ничего подходящего не нашел. Большинство решений были либо платными, либо не подходили для моего случая. В итоге я решил написать собственный дебаг-сервис.
Решение: Собственный дебаг-сервис
Я создал простой сервис, который записывал все входящие и исходящие сообщения WebSocket в текстовый файл на устройстве. Сервис добавлял временную метку и данные сообщения в файл, а в приложении я добавил возможность экспортировать этот файл для анализа. Это решение оказалось удобным и позволило быстро находить проблемные места в логике общения с сервером.
Итоги и уроки
Внедрение WebSocket в приложение на Flutter с использованием Socket.IO оказалось непростой задачей, особенно с учетом того, что проект изначально был PWA, а потом стал еще и мобильным приложением. Совместная работа с бэкендом помогла решить проблему с перегрузкой канала, настройка реконнекта сделала приложение надежнее, а собственный дебаг-сервис стал настоящим спасением.
Главный урок: переход от PWA к мобильному приложению требует тщательной адаптации, особенно если бэкенд остается неизменным. WebSocket — мощный инструмент, но его нужно грамотно настраивать, особенно для мобильных устройств с их ограничениями по сети и фоновой активности. Если вы планируете похожий проект, заранее подумайте о разделении загрузки тяжелых данных и WebSocket-сообщений, а также о логировании для отладки.
Надеюсь, мой опыт поможет вам избежать некоторых ошибок и сделать ваше приложение стабильным и удобным. Если у вас есть свои истории о работе с WebSocket или переходе от PWA к мобильным приложениям, делитесь в комментариях — будет интересно обсудить!
Если вам интересны подобные кейсы — заходите на наш сайт, где мы публикуем разборы архитектурных решений, инсайты из разработки и практики наших инженеров. А ещё лучше — подписывайтесь на наш Telegram-канал, чтобы не пропустить новые статьи и обсуждения!