Pull to refresh
0
Star.Comet
SaaS comet сервис

Реализация обмена сообщениями между вкладками браузера

Reading time 6 min
Views 17K
Это первая статья в нашем корпоративном блоге. На этот раз я расскажу о нашем решении задачи обмена сообщениями между вкладками браузера.

К примеру, мне потребовалось решить эту задачу при реализации JavaScript API к Comet сервису. Эта задача встречается достаточно часто и её уже рассматривали на хабре раньше здесь и здесь, но я решил написать своё решение задачи исходя из следующих требований к коду:

  • Кросбраузерность
  • Отсутствие зависимостей
  • Минимальный размер кода
  • Простота и удобство


Свою мини библиотеку я реализовал в стиле сигналов и слотов.

Эта очень удобная модель и мне кажется она в данном примере как нельзя лучше подходит. Достоинством этого подхода является слабая связность взаимодействующих между собой компонентов. Если кратко то модель сигналов и слотов нам даёт следующие возможности:
  • Код который излучает сигнал может ничего не знать о коде который этот сигнал обрабатывает. Он вообще не знает есть ли этот код или он вещает в пустоту;
  • Код принимающий сигнал не знает не чего об отправителе;
  • Единственное что является общим это формат сообщения.

Вот на пример нам надо оповестить все подписавшиеся функции о каком то событии.
Для этого выполняем:

tabSignal().emitAll('ИмяСобытия', "Данные") // Для уведомления всех открытых вкладок
tabSignal().emit( 'ИмяСобытия', "Данные" )  // Для работы в пределах одной вкладки
Всё код отработал и если был кто то подписан на это событие он получит данные.

Для подписки на событие надо передать имя события, на которое подписываемся и callBack для вызова на тот случай если событие произойдёт.

tabSignal().connect('ИмяСобытия',  function(param, signal_name){ });

Можно также передать ещё и имя слота, оно может понадобится если вы вдруг решили отписаться от уведомлений о событии.

tabSignal().connect("ИмяСобытия",'ИмяСлота', function(param, signal_name){} );

Здесь param будет содержать само сообщение. А signal_name имя сигнала, оно полезно на тот случай, если вы подписали один callBack на несколько разных сигналов

Вот код на тот случай если вам надо отписаться от события.

tabSignal().disconnect("ИмяСобытия", 'ИмяСлота');

Для передачи данных на другую вкладу библиотека просто пишет их в local storage браузера. Для того, чтобы получать данные, библиотека подписывается на событие onstorage, оно происходит во всех вкладках, когда кто-то пишет что-нибудь в local storage.

Я не стал обременять саму библиотеку функцией выбора мастер-вкладки, поэтому приведу её здесь. Заодно разберём алгоритм её работы. Но для начала расскажу, для чего вообще понадобилось искать мастер вкладку. Как уже говорил, я занимался разработкой JavaScript API к comet сервису.

Технология Comet позволяет отправлять сообщения в браузер по инициативе сервера. Это имеет множество применений, наиболее очевидным является создание чата между пользователями или пользователем и техподдержкой. Или, к примеру, динамическая подгруздка новых твитов в твиттере по мере их появления.

Для отправки push уведомлений в браузер необходимо иметь постоянно открытое соединение между браузером и комет сервером. Но многие люди открывают сайт более чем в одной вкладке и было бы полезно, если бы только одна из открытых вкладок держала реальное соединение с комет сервером, а пользовались этим соединением все отрытые вкладки. Этот подход не просто экономит ресурсы сервера, а ещё решает весьма важную проблему — ограничение на количество открытых одновременно соединений.

К примеру, chrome открывает не более 6 запросов к одному домену и не более 255 запросов в сумме на все открытые вкладки — не важно, к какому из доменов. Соответственно, если поддерживать отдельное соединение с комет сервером на каждой вкладке, то сможете открыть не более 6 вкладок, а потом всё.

Соответственно, исходя из этой задачи я решил, что мастер вкладкой будет первая из открытых вкладок, а если её закроют, то мастером станет случайная из оставшихся. Для этого мастер вкладка отправляет сообщение всем вкладкам каждые 150 миллисекунд о том, что она вообще есть.

При открытии вкладки подписываемся на получение уведомлений от мастер вкладки.

Затем ставим таймер хотя бы на 300мс, и если за это время не получаем уведомления от мастера, то считаем, что мастера нет, и мы за него. В таком случаи начинаем рассылать уведомления о том, что мы мастер вкладка каждые 50мс, а если получили уведомление от мастер вкладки, то отменяем поставленный таймер и сразу ставим его обратно — и так до тех пор, пока мастер вкладка успевает напомнить о своём существовании за время, меньшее 300мс.

Реализация в коде
function tryStartMasterTab(masterCallback, slaveCallback)
{ 
     var time_id = false;
     var start_timer = 2000;
     
     if( window.InTryStartMasterTab !== undefined )
     {
        console.log("Уеже запущено");
        return InTryStartMasterTab;
     }
    console.log("Запуск tryStartMasterTab");
     
    InTryStartMasterTab = 0;

    var setAsMaster = function(){
        // Отписываемся от уведомлений о наличии мастер вкладки
        tabSignal().disconnect("comet_msg_connect", 'comet_msg_master_signal');

        // Испускаем сигнал для уведомления всех остальных вкладок о своём превосходстве
        tabSignal().emitAll('comet_msg_master_signal');

        // Поставим таймер для уведомления всех остальных вкладок о своём превосходстве
        setInterval(function()
        {
            tabSignal().emitAll('comet_msg_master_signal');
            console.log("Мы мастер!");
            $("#consultantHolder").html("Мы мастер!");
        }, start_timer/8);

        InTryStartMasterTab = 1;
        if(masterCallback)
        {
            masterCallback();
        }
    };


     // Подключаемся на уведомления от других вкладок о том что уже есть мастер вкладка,
     // если за start_timer милисекунд уведомление произойдёт то отменим поставленый ранее таймер
     tabSignal().connect("comet_msg_connect",'comet_msg_master_signal', function()
     {
        if(time_id !== false) //  отменим поставленый ранее таймер если это ещё не сделано
        {
            console.log("Мы slave!, clearTimeout(time_id="+time_id+")");
            $("#consultantHolder").html("Мы slave!");
            clearTimeout( time_id );
            time_id = setTimeout(setAsMaster, start_timer );
        }

         if(InTryStartMasterTab === 0)
         {
             if(slaveCallback) slaveCallback();
         }
         InTryStartMasterTab = -1;
     });

     // Создадим таймер, если этот таймер не будет отменён за start_timer милисекунд то считаем себя мастер вкладкой
     time_id = setTimeout(setAsMaster, start_timer );
}



Но как заметил beliyadm этот подход иногда таки даёт сбой при большом количестве вкладок, и воспользовавшись советом от MarcusAurelius я ввёл в процесс выбора мастер вкладки систему приоритетов.
Приоритет это время открытия вкладки с милисекундами + случайное число от 0 до 10000 и чем меньше результат тем выше получается приоритет.

Улучшенная реализация с системой приоритетов
function tryStartMasterTab(masterCallback, slaveCallback)
{ 
     var time_id = false;
     var interval_id = false;
     var start_timer = 2000;
     
     if( window.InTryStartMasterTab !== undefined )
     {
        console.log("Уеже запущено");
        return InTryStartMasterTab;
     }
    console.log("Запуск tryStartMasterTab");
     
    InTryStartMasterTab = 0;

    var Today = new Date();
    var TabId =  (Today.getTime() *1000 + Today.getMilliseconds())*10000 + Math.floor( Math.random()*10000);

    var slaveloop = function(EventData)
    {
         if(time_id !== false) //  отменим поставленый ранее таймер если это ещё не сделано
         {
             console.log("Мы slave!, clearTimeout(time_id="+time_id+")"); 
             clearTimeout( time_id );
             time_id = setTimeout(setAsMaster, start_timer );
         }

         if(InTryStartMasterTab === 0)
         {
             if(slaveCallback) slaveCallback();
         }
         InTryStartMasterTab = -1;
    };

    var setAsMaster = function(){

        // Иницируем выбор нового мастера, отправляем свой TabId
        tabSignal().emitAll('comet_msg_new_master', TabId);

        time_id = setTimeout(function()
        {
            // Отписываемся от уведомлений о наличии мастер вкладки
            tabSignal().disconnect("comet_msg_connect", 'comet_msg_master_signal');

            // Испускаем сигнал для уведомления всех остальных вкладок о своём превосходстве
            tabSignal().emitAll('comet_msg_master_signal', TabId);

            // Поставим таймер для уведомления всех остальных вкладок о своём превосходстве
            interval_id = setInterval(function()
            {
                tabSignal().emitAll('comet_msg_master_signal', TabId);
                console.log("Мы мастер!");
            }, start_timer/8);

            InTryStartMasterTab = 1;
            if(masterCallback)
            {
                masterCallback();
            }
        }, start_timer);
    };


    // Подписываемся на голосование о выборе нового мастера
    tabSignal().connect('comet_msg_new_master', function(EventTabId)
    { 
        if(EventTabId == TabId)
        {
            // Если ventTabId == TabId то пришло сообщение от нас самих.
            return;
        }

        if(EventTabId > TabId)
        {
            // Если наш TabId меньше чем у той вкладки которая отправила сообщение считаем себя куче и нечего не делаем
            return;
        }

        // В остальных случаях EventTabId < TabId а значит это сообщение от кандидата в масер вкладки с большим приоритетом
        // Значит есть покрайней мере одна вкладка с приоритетом выше нашего. Следовательно снимаем свою кандидатуру с выборов мастер вкладки.
        if(time_id !== false) //  отменим поставленый ранее таймер если это ещё не сделано
        {
            console.log("Мы slave!, clearTimeout(time_id="+time_id+")"); 
            clearTimeout( time_id );
            time_id = setTimeout(setAsMaster, start_timer );
        }

        if(interval_id !== false) //  Перейдём в режим slave если до этого считали себя мастером
        { 
            clearTimeout( interval_id ); 
            // Подключаемся на уведомления от других вкладок о том что уже есть мастер вкладка,
            // если за start_timer милисекунд уведомление произойдёт то отменим поставленый ранее таймер
            tabSignal().connect("comet_msg_connect",'comet_msg_master_signal', slaveloop);
        }

        slaveCallback(); 
    });

     // Подключаемся на уведомления от других вкладок о том что уже есть мастер вкладка,
     // если за start_timer милисекунд уведомление произойдёт то отменим поставленый ранее таймер
     tabSignal().connect("comet_msg_connect",'comet_msg_master_signal', slaveloop);

     // Создадим таймер, если этот таймер не будет отменён за start_timer милисекунд то считаем себя мастер вкладкой
     time_id = setTimeout(setAsMaster, start_timer );
}



В конце привожу online demo.
Репозиторий TabSignal.js.
Tags:
Hubs:
+14
Comments 21
Comments Comments 21

Articles

Information

Website
comet-server.ru
Registered
Founded
Employees
1 employee (me only)
Location
Россия