В конце лета мы добавили в наше облако Voximplant поддержку месседжинга. Теперь с помощью него и россыпи SDK под разные платформы можно делать собственные мобильные или веб-мессенджеры: голосовые звонки в любых комбинациях между телефонными сетями и SDK — есть, видеозвонки между SDK — есть, месседжинг — есть. А еще у текстовых сообщений есть ключевое отличие от голосовых и видеозвонков: их контент должен оставаться. Voximplant может записать голосовой и видеозвонок на стороне облака и отдать URL с получившимся файлом, но это «медленная» история для CRM, систем управления заказами и колл-центров. А сообщения — это быстрая история. Пользователь очень огорчается, когда клик по «старому» чату в Skype вызывает зависание мобильного или веб-приложения, которое пытается выкачать хоть сколько-нибудь истории с нагруженных серверов по неустойчивому 3G. В наших SDK мы предусмотрели несколько механизмов для максимально быстрой работы с историей сообщений, о которых под катом.
В чем, собственно, проблема?
Новый пользователь мессенджера начинает с единственным объектом Messenger, который дает доступ к API и позволяет получать эвенты. Общение между пользователями начинается, когда один из них создает объект Conversation («беседа» или «чат» на двоих и более) и они начинают обмениваться сообщениями с помощью метода этого объекта sendMessage. О происходящих событиях клиенты узнают с помощью эвентов. Например, если пользователь «А» хочет отправить пользователю «Б» сообщение в первый раз, то он создает conversation на двоих, после чего им обоим приходит эвент CreateConversation, по которому пользователь «Б» узнает, что с ним хотят общаться. Также эвенты сигнализируют о новых сообщениях, присоединяющихся к conversations и покидающим их пользователям, смене админского статуса или о том, что пользователь печатает текст.
Вся эта идиллия длится ровно до того момента, пока один из пользователей не закрывает вкладку с чатом. И не открывает ее снова через день. Или через месяц. Или через год. Что нужно сделать разработчику, чтобы пользователь увидел все то новое, что произошло за время его отсутствия? И желательно так, чтобы не подвесить браузер.
Сериализация и нумерация — два кита истории сообщений
Главная деталь механизма — это последовательная нумерация всех сообщений в conversation. В эвенте SendMessage есть поле seq, которое содержит уникальный идентификатор сообщения. Идентификатор уникален в рамках conversation и постоянно увеличивается. Соответственно, если мы закрыли страницу браузера, открыли ее через год и хотим узнать, какие новые сообщения за это время пришли, все что нужно сделать – это хранить где-нибудь sequence id последнего полученного сообщения, а после открытия страницы запросить у облака недостающие сообщения. Или, например, последние несколько десятков, и подгружать остальные, только если пользователь решил посмотреть лог.
Вспомогательная деталь — это сериализация. SDK высокоуровневый и работает с объектами. Например, если мы хотим получить новые сообщения для conversation, то вначале нужно получить объект для этого conversation с помощью getConversation, а затем — сообщения с помощью метода этого объекта, retransmitEvents
Но если мы только загрузили страницу, то откуда у нас объекты? У нас кучка id'шек, предусмотрительно сохраненных в localStorage. А объекты придется создавать, и каждое такое создание объекта — это запрос к облаку для получения нужной информации.
Решение — встроенный механизм сериализации объектов с помощью методов toCache и create...FromCache, которые создают JSON-представление внутренностей объекта и могут восстановить объект из такого JSON без обращения к серверу. А JSON можно хранить в localStorage, мгновенно восстанавливая при загрузке страницы сотню каналов и миллион сообщений.
Миллион сообщений — а JavaScript или localStorage не лопнут?
С веб-страницами, в отличии от desktop и мобильных приложений, все сложно. Когда пользователь командует «закрыть вкладку» или «закрыть браузер», срабатывает эвент «beforeunload», на который можно подписаться. Сообщения «у вас есть несохраненные данные» в google docs — это строка, которую разработчик вернул из обработчика. Раньше в нем можно было делать даже alert'ы, но скам-страницы «ваш браузер заблокирован, дайте денег» мягко намекнули разработчикам браузеров, что многое позволять в обработчике «beforeunload» не стоит.
Тем не менее, современные браузеры дают нашему коду несколько секунд, прежде чем покажут такое сообщение и пользователь начнет беспокоиться:
А за несколько секунд вполне можно сериализовать в localstorage несколько сотен conversations с миллионом сообщений. Но тут важно помнить, что по умолчанию localStorage ограничен 5-10 мегабайтами, и даже меньше для мобильных браузеров или если пользователь копался в настройках.
Лучшие практики, чтобы ничего не лопнуло
Если вы делаете новый «Skype for Web» и планируете действительно большое количество сообщений у ваших пользователей, то для хранения сериализованных объектов лучше использовать indexedDB, которое сейчас поддерживают все популярные браузеры. Квоты там по умолчанию намного больше и можно явно попросить у пользователя еще с помощью «Quota Management API» и специфичных браузерных штук.
Второй момент — если в каком-то conversation с последнего посещения накопилось много сообщений, то будет разумно запросить у сервера последние несколько десятков, а остальные подгрузить, только если пользователь поскроллил лог. Получается разновидность «обратного бесконечного скролла» — новые элементы будут возникать не снизу, как при скролле страницы фейсбука, а сверху.
В этом году мы планируем существенно расширить наш messaging, добавив управление через HTTP и вебхуки. Это позволит разработчикам делать интеграцию с другими мессенджерами, программное управление сообщениями вроде «чата с операторами» и другие интересные штуки.
Комментарии (23)
Slysar7
09.01.2018 17:01Скайп очень часто подвисает при использовании, как и все продукты майрософт. Да и в целом он уже больше подходит для видеозвонков и отживает свое. В плане оперативной беседы с пользователем и интеграцией с CRM я бы посоветовал интегрировать сервис типа chat2desk.com — насколько я помню он поддерживает в районе 5 мессенджеров
darklog
09.01.2018 19:50А что произойдет если _seq превзойдет MAX_SAFE_INTEGER?
eyeofhell Автор
09.01.2018 19:51А на сервере ограничение на количество обновлений от одного клиента в минуту :) За 100 лет не произойдет, не переживайте. Они же не на всю систему, а только для conversation уникальны.
meremin
10.01.2018 21:56Почему не timeuuid? Если причина не в оптимизации трафика? Кто счётчик увеличивает, приложение или база?
eyeofhell Автор
10.01.2018 21:56В эвенте уже есть timestamp — зачем две разные сущности в одну запихивать? Увеличивает, конечно же, сервер.
meremin
10.01.2018 22:39Прост как будете поддерживать консистентность счетчика, если у вас будет больше 1 сервера?
На уровне балансировщика, привязывать чат к серверу?eyeofhell Автор
11.01.2018 07:24У нас намного больше одного сервера :) Это не самая сложная техническая проблема.
meremin
11.01.2018 09:33Согласен. И все же как ее решили, если не секрет? Очень интересен ваш опыт.
Мы вот именно по причине распределённости системы выбрали uuidv1. А вернее ему подобную реализацию с сортировкой. Плюсов много: уникальность в рамках всей системы, содержит метку времени, сортировка. Но вот есть существенный для нас минус: существенно увеличивают размер пакета при обмене данными с клиентом.
Как идея — отдельный микрометрами, который будет заняться увеличением счетчика.eyeofhell Автор
11.01.2018 10:02Непосредственно у нас — шардинг. Но, как я уже говорил, есть множество разных способов и нужные выбирается под архитектуру, требования итд. У нас, к примеру, sequence id уникальны только в рамках conversation.
У timeuuid есть вопросы к синхронизации времени между серверами и задержками между сообщениями. Эвенты разные бывают, и если быстро случилась последовательность, к примеру, «отправил сообщение а затем вышел из канала» то хочется чтобы они были именно в такой последовательности, иначе возможно странное :)
makcums
Ваш текст so cute. Write ещё :)
eyeofhell Автор
Англицизмы? ИМХО, разработчикам с ними проще читать.
makcums
Я разработчик, мне не проще (имхо).
Приведу пример из вашего же текста.
Нормально: «В конце лета мы добавили в наше облако Voximplant поддержку месседжинга».
Странно: «В этом году мы планируем существенно расширить наш messaging».
Ну, и в целом, одно дело, если это название технологии, другое, — писать «conversation».
eyeofhell Автор
Я долго думал над адекватным переводом «Conversation». «Чат»? Но если на двоих — это нифига не чат. «Беседа»? «Канал»? Нет хорошего перевода.
beatleboy
Диалог?
eyeofhell Автор
Между двумя. А в conversation можно тыщу запихнуть и сделать аналог «Channel» или «Group» в телеграме.
inoyakaigor
Проблема отпадёт, если вы признаете где-то у себя внутри, что на двоих это тоже чат. :)
makcums
Беседа — самое оно :)
Germanets
И беседа, и канал, и диалог — вполне нормальные варианты, каждый из которых используется в существующих мессенджерах\соц сетях… Другой вопрос, что «Conversation» тоже вполне себе удобоварим для человека, который знает перевод слова и привык обсуждать код с коллегами, используя имена классов. Разве что для тех, кто перевода не знает, можно было в пером случае указать и перевод в скобках, чтоб и ежу понятно было)
eyeofhell Автор
Легко:
c4boomb
Выдуманная проблема.
В среде разработчиков большинство привыкло общаться с использованием названий классов.
В 99% случае названия классов на английском.
В чем проблема?
c4boomb
Посмотрел историю ваших комментариев там и "экзампловые", и "непофикшенный", и "бранч"