WebSocket Application Messaging Protocol (WAMP)

// Май 16th, 2013 // Асинхронное программирование, Веб-разработка

wamp-logo-smallВ этом посте я обозрею, так сказать, протокол WebSocket Application Messaging Protocol или WAMP (не путать с аббривеатурой стека Windows Apache Mysql Php). Что же это такой за зверь, для чего он нужен, и вообще, с чего вдруг я начал им заниматься вы узнаете в этом посте.
Понадобилось мне для одного сервиса наладить реал-тайм взаимодействие, да не просто реал-тайм, а с поддержкой асинхронного паттерна Publisher-Subscriber (или Pub-Sub). Некторое время я изучал материалы, смотрел реализации и выбирал подходящий протокол, т.к. писать самому его не хотелось (да и времени не было). И тут ну глаза мне попался сайтик от авторов протокола (wamp.ws). Я был приятно удивлён, но это оказалось то, что мне нужно.

Что за зверь WAMP?

WAMP — это протокол прикладного уровня модели OSI для разработки интероперабельных приложений. Работает поверх протокола TCP-IP, и базируется на следующих технологиях.

  • Websockets
  • JSON
  • URI

Сейчас актуальной является версия 1 протокола WAMP и существуют множество её реализаций на JavaScript, Java, Python, PHP.

Поддерживаемые паттерны асинхронной обработки

  • Асинхронные сообщения
  • Publisher-Subscriber
  • Remote Procedure Calls (RPC)

Транспорт

В качестве транспорта для протокола используется Websocket соединение. Имхо самое прогрессивное на данный момент. А уже в качестве транспорта для вебсокетов можно использовать как нативные ws в браузерах, так и флеш-сокеты и другие извращения типа long-polling (правда эти извращения плохо работают с проксями). Например, библиотечка socket.io позволяет использтвать вот такие транспорты для веб-сокетов:

  • WebSocket (нативные)
  • Adobe® Flash® Socket
  • AJAX long polling
  • AJAX multipart streaming
  • Forever Iframe
  • JSONP Polling

Имхо, кроме первых двух, остальное — костыли.

Сериализация

Полезная нагрузка протокола (payload) сериализуется в формат JSON и поддерживает соответственно сериализацию чисел, строк, массивов и всего того, чему можно сделать json_encode. Тут есть важное замечание и, так сказать, дружеский совет. Перед сериализацией, пропускайте ваши строки через

$string = mb_convert_encoding($string,'UTF-8', 'auto')

иначе вы рискуете получить ошибочку

json_encode(): Invalid UTF-8 sequence in argument

Уникальные идентификаторы (или IDшки)

В протоколе используется реалиация GUID (Globally Unique Identifier) c помощью URI. Ну и правильно, нечего велосипеды изобретать.

Формат полезной нагрузки (payload)

Все сообщения, передающиеся по WebSocket, должны быть текстовые (ну или приведенные к тексту). Т.е. теоретически можно даже  BinaryJS впилить. Кодировка — UTF-8! см. предыдущий абзац про сериализацию. А формат — наш любимый JSON.

Типы сообщений

Протокол определяет сообщения, передающиеся между двумя точками (клиентом и сервером), всего есть 9 типов сообщений.

Тип N типа Направление Категория
WELCOME 0 Server-to-client Служебное
PREFIX 1 Client-to-server Служебное
CALL 2 Client-to-server RPC
CALLRESULT 3 Server-to-client RPC
CALLERROR 4 Server-to-client RPC
SUBSCRIBE 5 Client-to-server PubSub
UNSUBSCRIBE 6 Client-to-server PubSub
PUBLISH 7 Client-to-server PubSub
EVENT 8 Server-to-client PubSub

URI и CURIE

В протоколе есть поддержка не только нормального URI, но и формата CURIE (как в википедии). В общем-то смысл использовать этот формат только один — сократить сетевой трафик.

Служебные сообщения

К служебным сообщениям относятся сообщения WELCOME и PREFIX.

WELCOME

Это первое сообщение, которое получает клиент при коннекте к серверу.

[ TYPE_ID_WELCOME , sessionId , protocolVersion, serverIdent ]

sessionId — номер wamp-сессии. Случайная строка, уникальная в пределах сервера. Номер сессии может быть использован в нескольких случаях.

  • для безопасности при аутентификации и авторизации
  • для составления черных (exclude) или белых (eligible) списков.

protocolVersion — версия протокола wamp (сейчас = 1).

serverIdent — строка-идентификатор сервера (аналог UserAgent).

Пример сообщения:

WELCOME message: session ID = "v59mbCGDXZ7WTyxB"
[0, "v59mbCGDXZ7WTyxB", 1, "Autobahn/0.5.1"]

PREFIX

Процедуры RPC (как и возврат ошибок) в RPC паттерне и PubSub идентифицируются через URI/CURIE. Когда требуется сократить URI, то можно использовать этот самый CURIE. Это сообщение как раз и задаёт соответствие между URI -> CURIE перед его использованием. Например, такой адрес:

http://example.com/simple/calc#square

преобразуется в такой

calc:square

если предварительно было задан префикс для calc:

http://example.com/simple/calc#

с помощью этого сообщения.

[ TYPE_ID_PREFIX , prefix , URI ]

prefix — префикс :-)

URI — URI. Ваш К.О.

Примеры:

[1, "calc", "http://example.com/simple/calc#"]
[1, "keyvalue", "http://example.com/simple/keyvalue#"]

Важно заметить, что соглашение по CURIE действуют в рамках коннекта. Т.е. если например сервер отвалился и произошел реконнект, то PREFIX придётся задать заново.

RPC паттерн

Доступны три типа сообщений.

  • CALL
  • CALLRESULT
  • CALLERROR

rpc

CALL

Инициация вызова удалённой процедуры на сервере.

[ TYPE_ID_CALL , callID , procURI , ... ]

callID — рандомный ID процедуры, генерируемый на клиенте. Потом возвращается в сообщении CALLRESULT/CALLERROR, т.е. один клиент может параллельно инициировать много разных процедур.

procURI — адрес процедуры (название) на сервере URI/CURIE.

Примеры:

CALL вызов RPC без аргументов

            [2, "7DK6TdN4wLiUJgNM", "http://example.com/api#howdy"]

CALL вызов RPC с двумя аргументами, с использованием CURIE

           [2, "Yp9EFZt9DFkuKndg", "api:add2", 23, 99]

CALL вызов RPC с одним аргументом в виде объекта (имхо лучший способ)

            [2, "J5DkZJgByutvaDWc", "http://example.com/api#storeMeal",
               {
                  "category": "dinner",
                  "calories": 2309
               }]

CALL вызов RPC с одинм аргументом NULL

            [2, "Dns3wuQo0ipOX1Xc", "http://example.com/api#woooat", null]

CALL вызов RPC с одним аргументом в виде массива, используя CURIE

            [2, "M0nncaH0ywCSYzRv", "api:sum", [9, 1, 3, 4]]

CALL вызов RPC with 1 argument, value being a list of integers, using CURIE

           [2, "ujL7WKGXCn8bkvFV", "keyvalue:set",
               "foobar",
               {
                  "value1": "23",
                  "value2": "singsing",
                  "value3": true,
                  "modified": "2012-03-29T10:29:16.625Z"
               }]

Когда выполнение RPC-процедуры завершилось, сервер уведомляет клиента при помощи сообщений CALLRESULT или CALLERROR, Важно помнить, что выполнение процедур — асинхронное, и не стоит ждать возврата немедленно после CALL.

CALLRESULT

Если процедура выполнилась успешно, то сервер возвращает это сообщение клиенту, инициировавшему CALL.

[ TYPE_ID_CALLRESULT , callID , result ]

callID — уникальный ID процедуры (клиент должен запомниьт её в момент отправки CALL).
result — результат, строка или JSON.

Примеры:

Результат — NULL

            [3, "CcDnuI2bl2oLGBzO", null]

Результат — строковой

           [3, "otZom9UsJhrnzvLa", "Awesome result .."]

Результат — объект

           [3, "CcDnuI2bl2oLGBzO",
               {
                  "value3": true,
                  "value2": "singsing",
                  "value1": "23",
                  "modified": "2012-03-29T10:29:16.625Z"
               }]

CALLERROR

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

[ TYPE_ID_CALLERROR , callID , errorURI , errorDesc ]

или

[ TYPE_ID_CALLERROR , callID , errorURI , errorDesc , errorDetails ]

callID — уникальный номер процедуры

errorURI — адрес процедуры на сервере

errorDesc — сообщение об ошибке. Присутствует всегда, но может быть пустой строкой (хотя это и не желательно).

errorDetails — более подробное описание ошибки (нпример стек). Если есть, то должно быть не NULL.

Примеры:

CALLERROR c ошибкой общего вида

            [4, "gwbN3EDtFv6JvNV5",
               "http://autobahn.tavendo.de/error#generic",
               "math domain error"]

CALLERROR с ошибкой и аргументом в errorDetails

            [4, "7bVW5pv8r60ZeL6u",
               "http://example.com/error#number_too_big",
               "1001 too big for me, max is 1000",
               1000]

CALLERROR с ошибкой и списком аргументов в errorDetails

            [4, "AStPd8RS60pfYP8c",
               "http://example.com/error#invalid_numbers",
               "one or more numbers are multiples of 3",
               [0, 3]]

Publisher & Subscribe

В протокол встроено описание реализации паттерна PubSub. Схематично это выглядит вот так:

pubsub_concept

Доступно 4 вида сообщений:

  • SUBSCRIBE
  • UNSUBSCRIBE
  • PUBLISH
  • EVENT

После отправки сообщение SUBSCRIBE клиент будет асинхронно получать от сервера сообщения EVENT, в ответ на оптравку другими клиентами сообщений PUBLISH. Подписка длится всё Wamp(Websocket) сессию, и может быть отключена с помощью отправки сообщения USUBSCRIBE.

warningВ протоколе Wamp v.1 нет такой важной вещи, как уведомление о статусе подписки после SUBSCRIBE. Т.е. например, в случае неудачно подписки или, когда подписка на определённый канал запрещена (а также в случае неверного адреса подписки) клиент об этом не узнает, и неудачный SUBSCRIBE просто игнорируется сервером. Думаю в версии Wamp v.2 это испрваят.

SUBSCRIBE

Собственно подписка на топик.

[ TYPE_ID_SUBSCRIBE , topicURI ]

topicURI — адрес топика (URI/CURIE).

Посе подписки клиент начнёт получать уведомления, отправленные в канал с именем topicURI. Запрос асинхронный, сервер ничего не возвращает в ответ.

Примеры:

SUBSCRIBE с URI

            [5, "http://example.com/simple"]

SUBSCRIBE с CURIE

            [5, "event:myevent1"]

UNSUBSCRIBE

Отписка от канала.

[ TYPE_ID_UNSUBSCRIBE , topicURI ]

topicURI — адрес топика (URI/CURIE).

Примеры:

UNSUBSCRIBE с полным URI

            [6, "http://example.com/simple"]

UNSUBSCRIBE с CURIE

            [6, "event:myevent1"]

PUBLISH

Отправка сообщения в канал.

[ TYPE_ID_PUBLISH , topicURI , event ]
[ TYPE_ID_PUBLISH , topicURI , event , excludeMe ]
[ TYPE_ID_PUBLISH , topicURI , event , exclude , eligible ]

topicURI — адрес топика (URI/CURIE).

event — строка (или JSON) с полезной нагрузкой. Сразу рекомендую вам придумать форматы event-ов для вашего приложения, и следовать им.

excludeMe — флаг, отправить сообщение всем подписчикам, кроме меня (если я подписан) если значение TRUE (в JSON)

exclude — черный список, номера wamp-сессий в массиве, которым не надо отправлять сообщение.

eligible — белый список, номера wamp-сессий, кому разрешено отправлять сообщения.

Примеры:

PUBLISH со payload в виде строки

            [7, "http://example.com/simple", "Hello, world!"]

PUBLISH с NULL

            [7, "http://example.com/simple", null]

PUBLISH с объектом в виде payload

            [7, "http://example.com/event#myevent2",
               {
                  "rand": 0.09187032734575862,
                  "flag": false,
                  "num": 23,
                  "name":
                  "Kross",
                  "created": "2012-03-29T10:41:09.864Z"
               }]

PUBLISH с черным списком

            [7, "event:myevent1",
               "hello",
               ["NwtXQ8rdfPsy-ewS", "dYqgDl0FthI6_hjb"]]

PUBLISH с белым списком

            [7, "event:myevent1",
               "hello",
               [],
               ["NwtXQ8rdfPsy-ewS"]]

  EVENT

Подписчики получают это сообщение при возникновении события в канале.

[ TYPE_ID_EVENT , topicURI , event ]

topicURI — адрес топика (URI).

event — строка (или JSON) с полезной нагрузкой. Всегда присутствует.

Примеры:

EVENT с payload в виде строки

            [8, "http://example.com/simple", "Hello, I am a simple event."]

EVENT с payload — null

            [8, "http://example.com/simple", null]

EVENT с  полезной нагрузкой в виде объекта

           [8, "http://example.com/event#myevent2",
               {
                  "rand": 0.09187032734575862,
                  "flag": false,
                  "num": 23,
                  "name": "Kross",
                  "created": "2012-03-29T10:41:09.864Z"
               }]

Вот как-то так. Имхо протокол очень перспективный, всем приятного кодинга.

Ссылки

WAMP v.1 Specification
The WebSocket Protocol
UTF-8, a transformation format of ISO 10646
The application/json Media Type for JavaScript Object Notation (JSON)
Uniform Resource Identifier (URI): Generic Syntax, RFC 3986
CURIE Syntax 1.0
Autobahn.JS client library
Ratchet WebSocket for PHP Documentation

 

Share

Спасибо!


Если вам помогла статья, или вы хотите поддержать мои исследования и блог - вот лучший способ сделать это:


Комментировать