В конце лета мы добавили в наше облако 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)


  1. makcums
    09.01.2018 14:30
    +2

    Ваш текст so cute. Write ещё :)


    1. eyeofhell Автор
      09.01.2018 14:31

      Англицизмы? ИМХО, разработчикам с ними проще читать.


      1. makcums
        09.01.2018 14:37
        +1

        Я разработчик, мне не проще (имхо).
        Приведу пример из вашего же текста.
        Нормально: «В конце лета мы добавили в наше облако Voximplant поддержку месседжинга».
        Странно: «В этом году мы планируем существенно расширить наш messaging».
        Ну, и в целом, одно дело, если это название технологии, другое, — писать «conversation».


        1. eyeofhell Автор
          09.01.2018 14:49

          Я долго думал над адекватным переводом «Conversation». «Чат»? Но если на двоих — это нифига не чат. «Беседа»? «Канал»? Нет хорошего перевода.


          1. beatleboy
            09.01.2018 15:11

            Диалог?


            1. eyeofhell Автор
              09.01.2018 15:14

              Между двумя. А в conversation можно тыщу запихнуть и сделать аналог «Channel» или «Group» в телеграме.


          1. inoyakaigor
            09.01.2018 15:17

            Проблема отпадёт, если вы признаете где-то у себя внутри, что на двоих это тоже чат. :)


          1. makcums
            09.01.2018 15:20

            Беседа — самое оно :)


          1. Germanets
            09.01.2018 15:20

            И беседа, и канал, и диалог — вполне нормальные варианты, каждый из которых используется в существующих мессенджерах\соц сетях… Другой вопрос, что «Conversation» тоже вполне себе удобоварим для человека, который знает перевод слова и привык обсуждать код с коллегами, используя имена классов. Разве что для тех, кто перевода не знает, можно было в пером случае указать и перевод в скобках, чтоб и ежу понятно было)


            1. eyeofhell Автор
              09.01.2018 15:27
              +1

              Легко:

              image


        1. c4boomb
          10.01.2018 13:11

          Выдуманная проблема.
          В среде разработчиков большинство привыкло общаться с использованием названий классов.
          В 99% случае названия классов на английском.
          В чем проблема?


    1. c4boomb
      10.01.2018 13:16

      Посмотрел историю ваших комментариев там и "экзампловые", и "непофикшенный", и "бранч"


  1. Slysar7
    09.01.2018 17:01

    Скайп очень часто подвисает при использовании, как и все продукты майрософт. Да и в целом он уже больше подходит для видеозвонков и отживает свое. В плане оперативной беседы с пользователем и интеграцией с CRM я бы посоветовал интегрировать сервис типа chat2desk.com — насколько я помню он поддерживает в районе 5 мессенджеров


    1. c4boomb
      10.01.2018 13:10
      +1

      Вы бы перед тем как оставлять коммент посмотрели о Skype ли статья


  1. darklog
    09.01.2018 19:50

    А что произойдет если _seq превзойдет MAX_SAFE_INTEGER?


    1. eyeofhell Автор
      09.01.2018 19:51

      А на сервере ограничение на количество обновлений от одного клиента в минуту :) За 100 лет не произойдет, не переживайте. Они же не на всю систему, а только для conversation уникальны.


    1. meremin
      10.01.2018 21:56

      Почему не timeuuid? Если причина не в оптимизации трафика? Кто счётчик увеличивает, приложение или база?


      1. eyeofhell Автор
        10.01.2018 21:56

        В эвенте уже есть timestamp — зачем две разные сущности в одну запихивать? Увеличивает, конечно же, сервер.


        1. meremin
          10.01.2018 22:39

          Прост как будете поддерживать консистентность счетчика, если у вас будет больше 1 сервера?
          На уровне балансировщика, привязывать чат к серверу?


          1. eyeofhell Автор
            11.01.2018 07:24

            У нас намного больше одного сервера :) Это не самая сложная техническая проблема.


            1. meremin
              11.01.2018 09:33

              Согласен. И все же как ее решили, если не секрет? Очень интересен ваш опыт.
              Мы вот именно по причине распределённости системы выбрали uuidv1. А вернее ему подобную реализацию с сортировкой. Плюсов много: уникальность в рамках всей системы, содержит метку времени, сортировка. Но вот есть существенный для нас минус: существенно увеличивают размер пакета при обмене данными с клиентом.
              Как идея — отдельный микрометрами, который будет заняться увеличением счетчика.


              1. meremin
                11.01.2018 09:35

                • микросервис


              1. eyeofhell Автор
                11.01.2018 10:02

                Непосредственно у нас — шардинг. Но, как я уже говорил, есть множество разных способов и нужные выбирается под архитектуру, требования итд. У нас, к примеру, sequence id уникальны только в рамках conversation.

                У timeuuid есть вопросы к синхронизации времени между серверами и задержками между сообщениями. Эвенты разные бывают, и если быстро случилась последовательность, к примеру, «отправил сообщение а затем вышел из канала» то хочется чтобы они были именно в такой последовательности, иначе возможно странное :)