Realtime-чат без WebSocket: long-polling, гонки переподключения и дубли пушей
vientooscuro 30 минут назад Realtime-чат без WebSocket: long-polling, гонки переподключения и дубли пушей Средний 8 мин 663 iOS * Swift * Разработка мобильных приложений * Кейс Бывает так, что realtime на проекте есть,...
Anthropic — What company has the best second artificial intelligence model at the end of June?
В сфере искусственного интеллекта произошло заметное событие. vientooscuro 30 минут назад Realtime-чат без WebSocket: long-polling, гонки переподключения и дубли пушей Средний 8 мин 663 iOS * Swift * Разработка мобильных приложений * Кейс Бывает так, что realtime на проекте есть, а WebSocket — нет. Сервер отдаёт сообщения через long-polling (он же Comet): клиент шлёт «висящий» HTTP-запрос, сервер держит его открытым, пока не появятся новые сообщения, отвечает — и клиент тут же открывает следующий. С виду всё просто, по сути — бесконечный цикл из одного запроса.
Просто это ровно до первого запуска на реальном устройстве где-нибудь в метро. Дальше начинается то, ради чего я и пишу эту статью: гонки при переподключении, дубли локальных пушей, два потока сообщений в одном ответе и куча мелких состояний, которые надо аккуратно разруливать. Ниже — как с этим жить, на примере iOS-сервиса (назову его LongPollChatService).
Технические детали
Одна оговорка на весь дальнейший код: в сниппетах я опускаю синхронизацию, чтобы не размазывать идею. В реальном сервисе всё изменяемое состояние long-poll цикла — currentRequestUUID, курсоры, счётчики, словарь отложенных задач — живёт на одном serial context (у меня это отдельная очередь; в другом проекте это мог бы быть actor или main thread). Иначе сам механизм защиты от гонок легко сам становится гонкой, что было бы немного обидно)Сам цикл: хвостовая рекурсия вместо whileWebSocket держит соединение, и события прилетают сами.
С long-polling ты сам себе event loop: получил ответ — запросил снова. В коде это не while, а хвостовая рекурсия — метод запроса в случае успеха вызывает сам себя:private func requestNewMessages(token: ChatToken, requestUUID: String) { apiManager. getMessages(token: token) { response in guard let self guard self.
currentRequestUUID == requestUUID // про это — ниже switch } } Полная цепочка старта чуть длиннее: сначала берём токен сессии, потом синхронизируем курсоры (про которые мы пока не знаем — об этом дальше), и только потом уходим в этот «висящий» запрос. Но сердце — вот эти две строки: handle + повторный вызов себя. Главная проблема: гонки при переподключенииВот где начинается интересное.
Отраслевые последствия
Long-polling-запрос живёт долго — секунды, иногда десятки секунд. За это время может случиться что угодно: пользователь свернул приложение, сменил аккаунт, потерял сеть. Нам надо перезапустить цикл.
Но старый-то запрос уже летит, и его колбэк прилетит — возможно, уже после того, как мы всё перезапустили. Если ничего не делать, получаем классику: два конкурирующих цикла, дубли сообщений, рассинхрон курсоров. Отменить сетевой запрос физически не всегда успеваешь — cancel() мог уже не догнать ответ.
Решение, которое мне зашло — токен поколения. У сервиса есть currentRequestUUID. Каждый старт цикла генерит новый UUID, и каждый колбэк первым делом сверяет «а я ещё актуален?
Событие, по словам экспертов, усилит конкуренцию в сфере ИИ.





