Библиотека сайта rus-linux.net
Электронные таблицы SocialCalc
Глава 19 из книги "Архитектура приложений с открытым исходным кодом", том 1.
Оригинал: SocialCalc,
глава из книги "The Architecture of Open Source Applications" том 1.
Автор: Audrey Tang, перевод: Н.Ромоданов
19.7. Совместные работы в режиме реального времени
Следующий пример, который мы изучим, является многопользовательской общедоступной электронной таблицей, редактируемой в режиме реального времени. Это на первый взгляд может показаться сложным, но благодаря модульной конструкции SocialCalc все, что нужно для каждого он-лайн пользователя, это транслировать свои команды другим участникам работы.
Чтобы различать локально-используемые команды и команды, используемые дистанционно, мы в метод ScheduleSheetCommands
добавляем параметр isRemote
:
SocialCalc.ScheduleSheetCommands = function(sheet, cmdstr, saveundo, isRemote) { if (SocialCalc.Callbacks.broadcast && !isRemote) { SocialCalc.Callbacks.broadcast('execute', { cmdstr: cmdstr, saveundo: saveundo }); } // … здесь собственно код ScheduleSheetCommands here… }
Теперь все, что нам нужно сделать, это определить подходящую функцию обратного вызова SocialCalc.Callbacks.broadcast
. Как только она появится, у всех пользователей, подключенных к той же самой таблице, будут выполняться одни и те же команды.
Когда эта функция была впервые реализована для OLPC (One Laptop Per Child — Проект «Каждому ребенку свой ноутбук» [2]) лабораторией Sugar Labs в Сита (Уганда) [3] в 2009 году, то функция broadcast была создана с использованием вызовов XPCOM в D-Bus/Telepathy - стандартного транспорта для сетей OLPC/Sugar (смотрите рис.19.16).
Рис.19.16: Реализация OLPC
Это работает достаточно хорошо, позволяя в одной и той же сети Sugar использовать экземпляры объектов XO для совместной работы над общей таблицей SocialCalc. Однако, есть особенности, касающиеся как браузерной платформы Mozilla/XPCOM, так и платформы обмена сообщениями D-Bus/Telepathy.
19.7.1. Кросс-браузерный транспорт
Чтобы можно было выполнять эту работу в разных браузерах и разных операционных системах, мы пользуемся фреймворком Web::Hippie
[4], высокоуровневой абстракции JSON-поверх-WebSocket с удобной привязкой к Jquery и с MXHR ( запроса XML HTTP, состоящего из нескольких частей [5]) в качестве резервного механизма транспорта на случай, если WebSocket недоступен.
Для браузеров с установленным плагином Adobe Flash, но без встроенной поддержки WebSocket, мы используем Flash-эмуляцию WebSocket из проекта web_socket.js
[6], которая работает даже быстрее и надежнее, чем MXHR. Поток операций показан на рис.19.17.
Рис.19.17: Кросс-браузерный поток операций
Функция клиентской стороны SocialCalc.Callbacks.broadcast
определяется следующим образом:
var hpipe = new Hippie.Pipe(); SocialCalc.Callbacks.broadcast = function(type, data) { hpipe.send({ type: type, data: data }); }; $(hpipe).bind("message.execute", function (e, d) { var sheet = SocialCalc.CurrentSpreadsheetControlObject.context.sheetobj; sheet.ScheduleSheetCommands( d.data.cmdstr, d.data.saveundo, true // isRemote = true ); break; });
Хотя это работает достаточно хорошо, есть еще два оставшихся вопроса, которые нужно решить.
19.7.2. Разрешение конфликтов
Первым вопросом является состояние гонки (race-condition) во время выполнения команд: Если пользователи А и В одновременно выполняют операции, влияющие на одни и те же ячейки, принимают и выполняют команды, транслируемые другими пользователями, то в итоге они могут попасть в различающиеся состояния так, как это показано на рис.19.18.
Рис.19.18: Конфликт, связанный с состоянием гонки (Race Condition)
Мы можем решить это с помощью встроенного механизма undo/redo, имеющегося в SocialCalc, так, как показано на рис.19.19.
Рис.19.19: Разрешение конфликта, связанного с состоянием гонки (Race Condition)
Процесс, используемый для разрешения конфликта заключается в следующем. Когда клиент рассылает команду, он добавляет команду в очередь ожидания команд Pending. Когда клиент получает команду, он проверяет, не противоречит ли полученная команда той команде, которая находится в очереди ожидания команд Pending.
Если очередь отложенных команд Pending пуста, то команда просто выполняется как дистанционно исполняемое действие. Если дистанционная команда находит в очереди Pending совпадающую команду, то локальный команда удаляется из очереди.
В противном случае, клиент проверяет, есть ли в очереди команды, которые противоречат принятой команде. Если есть противоречащие команды, клиент сначала отменяет эти команды (операция Undo
) и помечает их для повторного выполнения их впоследствии (операция Redo
). После отмены действий конфликтующих команд (если таковые имеются), дистанционная команда выполняется обычным образом.
Когда от сервера поступает команда, отмеченная для повторного выполнения (операция Redo
), клиент выполнит ее снова, а затем удалит ее из очереди.
19.7.3. Дистанционное управление курсором
Даже когда проблема с состоянием гонки решена, все еще есть шанс, что кто-нибудь случайно изменит содержимое ячейки, которое в настоящее время редактирует другой пользователь. Простым решением является рассылка каждым пользователем позиции своего курсора всем остальным пользователям с тем, чтобы все видели, в какой ячейке выполняется работа.
Чтобы реализовать эту идею, мы добавляем к событию MoveECellCallback
еще один обработчик broadcast
:
editor.MoveECellCallback.broadcast = function(e) { hpipe.send({ type: 'ecell', data: e.ecell.coord }); }; $(hpipe).bind("message.ecell", function (e, d) { var cr = SocialCalc.coordToCr(d.data); var cell = SocialCalc.GetEditorCellElement(editor, cr.row, cr.col); // … оформляем ячейку в стиле, соответствующему ситуации, когда ней работает другой пользователь … });
Чтобы пометить ячейку, которая получила фокус в таблицах, обычно используют цветные границы. Однако для ячейки может быть определено ее собственное свойство border
(граница), а так как границы окрашены одинаково, в одной о той же ячейке может находиться только один курсор.
Поэтому в браузерах, в которых поддерживается CSS3, мы для представления нескольких курсоров в одной и той же ячейке используем свойство box-shadow
:
/* Два курсора в в одной и той же ячейке */ box-shadow: inset 0 0 0 4px red, inset 0 0 0 2px green;
На рис.19.20 показано, как будет выглядеть экран, когда с одной и той же таблицей работаю четыре человека.
Рис.19.20: Четыре пользователя редактируют одну и ту же таблицу
Далее: Усвоенные уроки