Ознакомившись с основами HS в предыдущем посте, продолжим погружение в устройство этого NoSQL-протокола.
Поскольку информация по устройству этого пока достаточно экзотического протокола очень скудная, есть смысл остановится хотя бы на его базовом устройстве, тем более он достаточно прост и понятен. При этом будем стараться всю теорию демонстрировать на практике, и в силу простоты протокола, наиболее наглядно и просто это можно сделать через стандартный telnet
.
Итак, HandlerSocket — это простой протокол типа «запрос-ответ». Что касается ответа, то дополнительно нужно учесть, что он может быть пакетным (что-то вроде multi_get в memcache). Это значит, что сервер может принимать запросы в конвейерном режиме (каждый запрос — 1 строка) накапливая их какой-то целесообразный промежуток времени, а затем выстреливая единым ответным пакетом на все предшествовавшие запросы данного клиента (одной общей строкой). Все параметры в этом протоколе разделяются между собой символом TAB
.
Хотя набор команд у HS невелик, для краткости приведем в качестве примера синтаксис лишь его трех главных команд.
P <indexid> <dbname> <tablename> <indexname> <columns>
indexid
— это уникальный номер индекса (открытого канала) в десятичном виде. Для закрытия канала нужно просто повторно выполнить «open_index
» для требуемого индекса, ранее открытого этой же командой. Что, кстати, лишний раз говорит о необходимости внимательного выбора и учета используемых значений indexid
, чтобы избежать ненужных ошибок в программировании; dbname, tablename
и indexname
— строки, обозначающие соответственно имя БД, имя таблицы и имя индекса. Для открытия индекса на первичный ключ, нужно применять PRIMARY к indexname
; columns
— список колонок через запятую. Если однажды запрос «open_index
» был успешно выполнен, плагин будет держать этот канал (идентифицируемый целым числом indexid
) открытым, пока он не будет закрыт или не истечет время установленного таймаута. Допускается параллельно открывать любое количество каналов с аналогичным (или любым другим) сочетанием dbname, tablename
и indexname
, единственно, рекомендуется при этом выбирать наименьшее значение indexid
из доступных.
Если индекс был открыт успешно, HandlerSocket возвращает строку — «0 1».
<indexid> <op> <vlen> <v1> … <vn> <limit> <offset> <mop> <m1> … <mk>
indexid
— номер индекса (канала), по которому необходимо производить поиск. Первоначально он должен быть выделен через комманду open_index
(см. выше);op
— это операция сравнения, которая будет производиться при поиске. Доступны следующие стандартные операции сравнения: ‘=’, ‘>’, ‘>=’, ‘<’, ‘<=’. Отдельно подчеркну, что при выборке по символьным индексам необходимо обязательно учитывать кодировку, так как это может приводить к некоторым неожиданностям; vlen
— длина строки параметров (v1 ... vn
— смотрите описание ниже). Их количество должно быть меньше или равно количеству колонок в индексе, определённых при операции open_index
; v1... vn
— определяет непосредственно сами значения колонок, которые и нужно искать; limit, offset
— числа. По своему смыслу они полностью аналогичны параметрам LIMIT
и OFFSET
в стандартном SQL-запросе. Это необязательные параметры (по умолчанию используются значения 1 и 0); mop
— это код операции, возможно значение ‘U’ (при обновлении) или ‘D’ (при удалении); m1... mk
— перечисление значений для колонок, которые надо установить (если код mop
равен ‘D’, эти параметры игнорируются). Если запрос ‘find_modify
’ выполнен успешно, HandlerSocket возвратит строку такого вида: 0 1 numfound
, где numfound
— это количество строк, удовлетворяющих запросу.
<indexid> ‘+’ <vlen> <v1> … <vn>
indexid
— номер индекса (канала), по которому необходимо производить поиск;vlen
— определяет длину строки параметров v1 ... vn
;v1...vn
— определяет значения колонок, которые нужно вставить. Если INSERT был выполнен успешно, возвращается уже стандартная ответ-строка — «0 1
».
Первое число в ответе является значением errorcode
— и если оно отлично от нуля — операция завершилась с ошибкой, а само значение errorcode
в этом случае будет обозначать код ошибки. Второе число — numcolumns
— всегда возвращает количество колонок в результирующей выборке. В некоторых случаях возвращаются также и значения выборки (если это предполагается операцией) — r1 ... rn
, — при этом их количество в этом случае, очевидно, должно совпадать со значением numcolumns
. Таким образом, формат любого ответа в рамках HandlerSocket в общем случае будет иметь вид:
<errorcode> <numcolumns> <r1> ... <rn>
Все ответные данные, также как и параметры самих запросов, разделяются символом TAB (\t)
, каждая строка запроса (ответа) оканчивается BK (\n)
.
В качестве примера: типичный ответ на успешное открытие (закрытие) индекса будет иметь вид «0 1
». Этот же ответ будет возвращаться чаще всего и как подтверждение успешного завершения операций и у других вызовов. Описание всех 6 команд HandlerSocket и тонкостей их применения можно найти в официальной документации проекта.
Теперь, вооружившись этими базовыми теоретическими сведениями, мы посмотрим на механику работы протокола на простейших примерах. Для чего создадим в БД тестовую табличку, над которой далее и будем проводить наши эксперименты:
CREАTE TАBLE `test`.`test` ( `keyid` varchar(30), `value` varchar(30), PRIMARY KЕY (`keyid`) )
После чего сразу рвемся в бой и пробуем наши силы, выполняя совместный набор описанных выше команд:
telnet 192.168.1.10 9999 Trying 192.168.1.10... Connected to 192.168.1.10. Escape character is ’^]’. P 0 test test PRIMARY keyid,value 0 1 0 = 1 5555 1 0 D 0 1 1 0 + 2 1111 2222 3333 0 1
В качестве первой помощи, давайте я прокомментирую этот «поисковый ассемблер» построчно, чтобы лучше уловить устройство протокола HandlerSocket:
//Инициализируем канал, подключились к модифицирующему потоку (порт 9999) P 0 test test PRIMARY keyid,value //0 1 — открытие канала с данными параметрами прошло успешно 0 1 // Используем вызов FIND_MODIFY для канала 0 0 = 1 5555 1 0 D // здесь по-порядку идут: номер канала(0), тип операции (=), кол-во ключей (1), значение, которое ищем (в нашем случае — 5555), код операции — D (то есть удаление всего найденного). 0 1 1 // запрос успешно выполнен (0 1), количество обработанных строк — 1 0 + 2 1111 2222 // для открытого канала (0) делаем запрос на вставку данных (vlen=2, значения колонок для вставки: 1111, 2222) 0 2 // запрос успешно выполнен (формат любого ответа — <errorcode> <numcolumns>)
В качестве примеров демонстрации возникновения ошибок, приведу пример попытки модификации канала при открытии его в режиме чтения (через порт 9998), а также открытия несуществующего индекса:
telnet 192.168.1.10 9998 Trying 192.168.1.10... Connected 192.168.1.10. Escape character is ’^]’. P 0 test test PRIMARY keyid,value 0 1 0 = 1 77777 0 0 D 2 1 readonly P 1 test notest PRIMARY nokey,noval 3 0
Кроме этого приведу пример доступа к запароленному потоку, сначала приводящий к ошибке доступа (3 1 unauth
), а затем я показываю пример успешной авторизации (недокументированная команда A, пароль в данном случае — samag
).
Здесь сервер возвращает статус «0 1», что, согласно документации, эквивалентно сообщению об успешном завершении операции open_index
(в нашем контексте это значит скорее «авторизация прошла успешно»):
P 0 test test PRIMARY keyid,value 3 1 unauth 0 = 1 1 3 1 unauth A 1 samag 0 1
~
Читать этот материал дальше. Оглавление этой серии статей — здесь.