Диспетчер довільних повідомлень на базі google protocol buffers - kildekode

Пісочниця →

З'явився вільний день, і я вирішив погратися з бібліотекою google :: protobuf. Дана бібліотека надає можливість кодування і декодування структурованих даних. На базі цієї бібліотеки я побудую простенький диспетчер, який може обробляти будь-які повідомлення. Незвичайність цього диспетчера полягає в тому, що він не буде знати типи переданих повідомлень, і буде обробляти повідомлення тільки за допомогою зареєстрованих обробників.

Короткий опис бібліотеки protobuf


Отже, спочатку коротко розглянемо бібліотеку google :: protobuf, вона поставляється у вигляді двох компонентів:
власне, сама бібліотека + заголовні файли
компілятор файлів * .proto - генерує з опису повідомлення C ++ клас (також є можливість генерації для інших мов програмування: Java, Python і т.д.)
В окремому файлі створюється опис повідомлення, з якого буде згенеровано клас, синтаксис дуже простий:
Тут ми описуємо повідомлення ServerStatusAnswer, яке має два необов'язкових поля:
  • threadCount - необов'язковий цілочисельний параметр
  • listeners - це необов'язковий ланцюжок, яка може кілька разів повторюватися
Даному опису задовольняє, наприклад, наступне повідомлення:
Насправді формат protobuf - бінарний, тут я привів повідомлення в Новомосковскемом форматі тільки для зручності сприйняття

Компілятор автоматично генерує C ++ код для сериализации і десеріалізациі подібних повідомлень. Бібліотека protobuf також надає додаткові можливості: сериализация в файл, в потік, в буфер.

Я використовую CMake як системи збирання, і в ньому вже є підтримка protobuf:
PROTOBUF_GENERATE_CPP - даний макрос викликає компілятор protoc для кожного * .proto файлу, і генерує відповідні cpp і h файли, які додаються до збірки.
Все робиться автоматично, і ніяких додаткових присідань робити не треба (Під * nix може знадобитися додатковий пакет Threads і відповідний прапор лінковщік).

опис диспетчера


Я вирішив спробувати написати диспетчер повідомлень, який приймає якесь повідомлення, викликає відповідний обробник і відправляє відповідь на отримане повідомлення. При цьому диспетчер не повинен знати типи переданих йому повідомлень. Це може бути необхідно в разі, якщо диспетчер додає або видаляє відповідні обробники в процесі роботи (наприклад, довантажити відповідний модель розширення, * .dll, * .so).

Для того щоб обробляти довільні повідомлення, у нас повинен бути клас, який обробляє абстрактне повідомлення. Очевидно, якщо у нас будуть опису повідомлень в * .proto файлі, то компілятор нам згенерує відповідні класи, але на жаль всі вони будуть успадковані від google :: protobuf :: Message. У даного класу проблематично витягти всі дані з повідомлення (зробити це в принципі можна, але тоді ми будемо робити купу зайвої роботи), до того ж ми не будемо знати, як нам сформувати відповідь.
На допомогу приходить вислів: «Будь-яку проблему можна вирішити шляхом введення додаткового рівня абстракції, крім проблеми занадто великої кількості рівнів абстракції».
Нам треба відокремити визначення типу повідомлення від самого повідомлення, ми це можемо зробити наступним способом:
Ми запакуємо наше повідомлення всередину ще одного повідомлення:
  • обов'язкове поле id містить унікальний ідентифікатор повідомлення
  • необов'язкове поле data містить наше повідомлення
Таким чином, наш диспетчер буде по полю id шукати відповідний обробник повідомлення:
Але тепер ми отримуємо, що кожен обробник повинен проводити розпакування повідомлення sample :: proto :: Message в своє власне повідомлення. А цей процес буде дублюватися для кожного такого обробника. Ми хочемо уникнути дублювання коду, тому візьмемо патерн Type Erasure. Даний патерн дозволяє приховати тип оброблюваної суті за загальним інтерфейсом, проте кожен обробник буде працювати з конкретним типом, відомим тільки йому.

Отже, реалізація дуже проста:
Ми визначаємо віртуальну функцію process. але також додаємо віртуальну функцію doProcess. яка вже працює з нашими конкретними повідомленнями. Даний прийом заснований на механізмі інстанцірованія шаблонів: типи підставляються в момент реального використання шаблону, а не в момент декларації. А так як даний клас успадковується від MessageProcessorBase, то ми сміливо можемо передавати спадкоємців даного класу в наш диспетчер. Також необхідно зауважити, що даний клас здійснює сериализацию і десеріалізацію наших конкретних повідомлень і кидає виключення в разі виникнення помилок.

Ну і наостанок наведу приклад використання даного диспетчера, припустимо у нас є два види повідомлень:

Як видно з опису - дані повідомлення запитують у сервера його внутрішній стан (ServerStatus), і просто повертає отриманий запит (Echo). Реалізація самих оброблювачів тривіальна, я приведу реалізацію тільки ServerStatus:
Сама реалізація:
Ось як це працює:

P.S. Для написання даної статті використовувалися:
  • gcc-4.4.5-linux
  • cmake-2.8.2
  • boost-1.42
  • protobuf-2.3.0

Приклад викладений на github