WebSocket Application Messaging Protocol (WAMP)
В этом посте я обозрею, так сказать, протокол 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. Тут есть важное замечание и, так сказать, дружеский совет. Перед сериализацией, пропускайте ваши строки через
1 |
$string = mb_convert_encoding($string,'UTF-8', 'auto') |
иначе вы рискуете получить ошибочку
1 |
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
Это первое сообщение, которое получает клиент при коннекте к серверу.
1 |
[ TYPE_ID_WELCOME , sessionId , protocolVersion, serverIdent ] |
sessionId – номер wamp-сессии. Случайная строка, уникальная в пределах сервера. Номер сессии может быть использован в нескольких случаях.
- для безопасности при аутентификации и авторизации
- для составления черных (exclude) или белых (eligible) списков.
protocolVersion – версия протокола wamp (сейчас = 1).
serverIdent – строка-идентификатор сервера (аналог UserAgent).
Пример сообщения:
1 2 |
WELCOME message: session ID = "v59mbCGDXZ7WTyxB" [0, "v59mbCGDXZ7WTyxB", 1, "Autobahn/0.5.1"] |
PREFIX
Процедуры RPC (как и возврат ошибок) в RPC паттерне и PubSub идентифицируются через URI/CURIE. Когда требуется сократить URI, то можно использовать этот самый CURIE. Это сообщение как раз и задаёт соответствие между URI -> CURIE перед его использованием. Например, такой адрес:
1 |
http://example.com/simple/calc#square |
преобразуется в такой
1 |
calc:square |
если предварительно было задан префикс для calc:
1 |
http://example.com/simple/calc# |
с помощью этого сообщения.
1 |
[ TYPE_ID_PREFIX , prefix , URI ] |
prefix – префикс 🙂
URI – URI. Ваш К.О.
Примеры:
1 |
[1, "calc", "http://example.com/simple/calc#"] |
1 |
[1, "keyvalue", "http://example.com/simple/keyvalue#"] |
Важно заметить, что соглашение по CURIE действуют в рамках коннекта. Т.е. если например сервер отвалился и произошел реконнект, то PREFIX придётся задать заново.
RPC паттерн
Доступны три типа сообщений.
- CALL
- CALLRESULT
- CALLERROR
CALL
Инициация вызова удалённой процедуры на сервере.
1 |
[ TYPE_ID_CALL , callID , procURI , ... ] |
callID – рандомный ID процедуры, генерируемый на клиенте. Потом возвращается в сообщении CALLRESULT/CALLERROR, т.е. один клиент может параллельно инициировать много разных процедур.
procURI – адрес процедуры (название) на сервере URI/CURIE.
Примеры:
CALL вызов RPC без аргументов
1 |
[2, "7DK6TdN4wLiUJgNM", "http://example.com/api#howdy"] |
CALL вызов RPC с двумя аргументами, с использованием CURIE
1 |
[2, "Yp9EFZt9DFkuKndg", "api:add2", 23, 99] |
CALL вызов RPC с одним аргументом в виде объекта (имхо лучший способ)
1 2 3 4 5 |
[2, "J5DkZJgByutvaDWc", "http://example.com/api#storeMeal", { "category": "dinner", "calories": 2309 }] |
CALL вызов RPC с одинм аргументом NULL
1 |
[2, "Dns3wuQo0ipOX1Xc", "http://example.com/api#woooat", null] |
CALL вызов RPC с одним аргументом в виде массива, используя CURIE
1 |
[2, "M0nncaH0ywCSYzRv", "api:sum", [9, 1, 3, 4]] |
CALL вызов RPC with 1 argument, value being a list of integers, using CURIE
1 2 3 4 5 6 7 8 |
[2, "ujL7WKGXCn8bkvFV", "keyvalue:set", "foobar", { "value1": "23", "value2": "singsing", "value3": true, "modified": "2012-03-29T10:29:16.625Z" }] |
Когда выполнение RPC-процедуры завершилось, сервер уведомляет клиента при помощи сообщений CALLRESULT или CALLERROR, Важно помнить, что выполнение процедур – асинхронное, и не стоит ждать возврата немедленно после CALL.
CALLRESULT
Если процедура выполнилась успешно, то сервер возвращает это сообщение клиенту, инициировавшему CALL.
1 |
[ TYPE_ID_CALLRESULT , callID , result ] |
callID – уникальный ID процедуры (клиент должен запомниьт её в момент отправки CALL).
result – результат, строка или JSON.
Примеры:
Результат – NULL
1 |
[3, "CcDnuI2bl2oLGBzO", null] |
Результат – строковой
1 |
[3, "otZom9UsJhrnzvLa", "Awesome result .."] |
Результат – объект
1 2 3 4 5 6 7 |
[3, "CcDnuI2bl2oLGBzO", { "value3": true, "value2": "singsing", "value1": "23", "modified": "2012-03-29T10:29:16.625Z" }] |
CALLERROR
Когда выполнение удалённой процедуры завершилось ошибкой или процедуру невозможно выполнить, то сервер возвращает сообщение этого типа.
1 |
[ TYPE_ID_CALLERROR , callID , errorURI , errorDesc ] |
или
1 |
[ TYPE_ID_CALLERROR , callID , errorURI , errorDesc , errorDetails ] |
callID – уникальный номер процедуры
errorURI – адрес процедуры на сервере
errorDesc – сообщение об ошибке. Присутствует всегда, но может быть пустой строкой (хотя это и не желательно).
errorDetails – более подробное описание ошибки (нпример стек). Если есть, то должно быть не NULL.
Примеры:
CALLERROR c ошибкой общего вида
1 2 3 |
[4, "gwbN3EDtFv6JvNV5", "http://autobahn.tavendo.de/error#generic", "math domain error"] |
CALLERROR с ошибкой и аргументом в errorDetails
1 2 3 4 |
[4, "7bVW5pv8r60ZeL6u", "http://example.com/error#number_too_big", "1001 too big for me, max is 1000", 1000] |
CALLERROR с ошибкой и списком аргументов в errorDetails
1 2 3 4 |
[4, "AStPd8RS60pfYP8c", "http://example.com/error#invalid_numbers", "one or more numbers are multiples of 3", [0, 3]] |
Publisher & Subscribe
В протокол встроено описание реализации паттерна PubSub. Схематично это выглядит вот так:
Доступно 4 вида сообщений:
- SUBSCRIBE
- UNSUBSCRIBE
- PUBLISH
- EVENT
После отправки сообщение SUBSCRIBE клиент будет асинхронно получать от сервера сообщения EVENT, в ответ на оптравку другими клиентами сообщений PUBLISH. Подписка длится всё Wamp(Websocket) сессию, и может быть отключена с помощью отправки сообщения USUBSCRIBE.
В протоколе Wamp v.1 нет такой важной вещи, как уведомление о статусе подписки после SUBSCRIBE. Т.е. например, в случае неудачно подписки или, когда подписка на определённый канал запрещена (а также в случае неверного адреса подписки) клиент об этом не узнает, и неудачный SUBSCRIBE просто игнорируется сервером. Думаю в версии Wamp v.2 это испрваят.
SUBSCRIBE
Собственно подписка на топик.
1 |
[ TYPE_ID_SUBSCRIBE , topicURI ] |
topicURI – адрес топика (URI/CURIE).
Посе подписки клиент начнёт получать уведомления, отправленные в канал с именем topicURI. Запрос асинхронный, сервер ничего не возвращает в ответ.
Примеры:
SUBSCRIBE с URI
1 |
[5, "http://example.com/simple"] |
SUBSCRIBE с CURIE
1 |
[5, "event:myevent1"] |
UNSUBSCRIBE
Отписка от канала.
1 |
[ TYPE_ID_UNSUBSCRIBE , topicURI ] |
topicURI – адрес топика (URI/CURIE).
Примеры:
UNSUBSCRIBE с полным URI
1 |
[6, "http://example.com/simple"] |
UNSUBSCRIBE с CURIE
1 |
[6, "event:myevent1"] |
PUBLISH
Отправка сообщения в канал.
1 2 3 |
[ 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 в виде строки
1 |
[7, "http://example.com/simple", "Hello, world!"] |
PUBLISH с NULL
1 |
[7, "http://example.com/simple", null] |
PUBLISH с объектом в виде payload
1 2 3 4 5 6 7 8 9 |
[7, "http://example.com/event#myevent2", { "rand": 0.09187032734575862, "flag": false, "num": 23, "name": "Kross", "created": "2012-03-29T10:41:09.864Z" }] |
PUBLISH с черным списком
1 2 3 |
[7, "event:myevent1", "hello", ["NwtXQ8rdfPsy-ewS", "dYqgDl0FthI6_hjb"]] |
PUBLISH с белым списком
1 2 3 4 |
[7, "event:myevent1", "hello", [], ["NwtXQ8rdfPsy-ewS"]] |
EVENT
Подписчики получают это сообщение при возникновении события в канале.
1 |
[ TYPE_ID_EVENT , topicURI , event ] |
topicURI – адрес топика (URI).
event – строка (или JSON) с полезной нагрузкой. Всегда присутствует.
Примеры:
EVENT с payload в виде строки
1 |
[8, "http://example.com/simple", "Hello, I am a simple event."] |
EVENT с payload – null
1 |
[8, "http://example.com/simple", null] |
EVENT с полезной нагрузкой в виде объекта
1 2 3 4 5 6 7 8 |
[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