WebSocket Application Messaging Protocol (WAMP)
// 16 мая, 2013 // Асинхронное программирование, Веб-разработка
В этом посте я обозрею, так сказать, протокол 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
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. Схематично это выглядит вот так:
Доступно 4 вида сообщений:
- SUBSCRIBE
- UNSUBSCRIBE
- PUBLISH
- EVENT
После отправки сообщение SUBSCRIBE клиент будет асинхронно получать от сервера сообщения EVENT, в ответ на оптравку другими клиентами сообщений PUBLISH. Подписка длится всё Wamp(Websocket) сессию, и может быть отключена с помощью отправки сообщения USUBSCRIBE.
В протоколе 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
Спасибо!
Если вам помогла статья, или вы хотите поддержать мои исследования и блог - вот лучший способ сделать это: