Протокол Handlersocket в деталях

Всем здрасьте. Решил опубликовать русскую версию своей же статьи «HandlerSocket protocol explained», опубликованной по адресу http://wk-photo.ru/en/events/view/handlersocket-protocol-explained/.

image

Итак, вы шли-шли и пришли к HandlerSocket. Чистый мёд. Это дьявольски быстрый вуду. А используемый протокол реально простой, как две копейки. Ну и если уж начистоту, кому важны детали протокола, если все равно будет использоваться какая-то библиотека, которая обо всем позаботится? Если, несмотря ни на что, вы все-таки хотите знать, что за неонка там внутре, можете нагуглить эту страницу. Несколько часов — и вы эксперт. Ну или вы хотите все и за 15 минут. Тогда добро поржаловать под кат!

Предполагается, что вы уже хотя бы отдаленно знакомы с Handlersocket, имеете в наличии MySQL/MariaDB/Percona server с включенным плагином. Я использовал MariaDB, там Handlersocket имеется по умолчанию, нужно всего лишь включить плагин, приправив my.cnf небольшим кол-вом строчек по вкусу.

На правах рекламы: https://github.com/crocodile2u/zhandlersocket — мое расширение для PHP для работы с Handlersocket.

Поехали. Залогинюсь в консоль MariaDB и создам таблицу:

CREATE TABLE movie (
  id int not null auto_increment primary key, 
  genre varchar(20) not null, 
  title varchar(100) not null, 
  view_count int default 0, 
  key(genre)
) engine innodb;

Теперь подключусь к Handlersocket. Порт 9999, то бишь дефолтный порт для записи: с таким соединением я смогу и читать и писать. Поскольку обычно все же читать нужно чаще, чем писать, то эффективнее будет думать заранее, какие действия понадобятся в приложении, и открывать коннект к порту на чтение или на чтение/запись соответственно. Для справки: по умолчанию, Handlersocket стартует 16 воркеров для обработки соединений «только на чтение» и один воркер для обработки соединений «на чтение/запись».

~/ telnet localhost 9999
Trying 127.0.0.1...
Connected to localhost.
Escape character is '^]'.

Класс. Теперь попробуем поговорить с этой штукой. Первое, что нужно сделать, когда используешь Handlersocket — это открыть индекс. Для любых запросов, которые вы отправляете в HS, чтобы найти или изменить данные, вам нужен идентификатор индекса. Паасмотрим, что говорит документация:

Запрос »open_index» имеет следующий синтаксис:

P /indexid/ /dbname/ /tablename/ /indexname/ /columns/ [/fcolumns/]
  • /indexid/ is a number in decimal.
  • /dbname/, /tablename/, and /indexname/ are strings. To open the primary key, use PRIMARY as /indexname/.
  • /columns/ is a comma-separated list of column names.
  • /fcolumns/ is a comma-separated list of column names. This parameter is optional.

Ну, все понятно… Ой, а чьи это штанишки с моторчиком что это за /indexid/? Это идентификатор индекса, который должно предоставить ваше приложение. Он не генерируется Handlersocket’ом. По факту, приложение может отправить в HS запрос, состоящий из ряда строк, одномоментно:

Open index 1
Find row(s) in index 1
Find/Modify row(s) in index 1
...

Это возможно только в том случае, если ID индекса назначается приложением, иначе пришлось бы сначала отправить в HS запрос на то, чтобы открыть индекс, получить от HS идентификатор, и только потом использовать этот идентификатор в своих запросах.

Продолжим наши экзерсисы в консоли HS (telnet). Забыл сказать, части запроса в HS разделяются TAB’ами. В нашем примере, я использую необязательный параметр /fcolumns/ для первого индекса. Он нам еще понадобится.

~/ telnet localhost 9999
...
P   1   test    movie   id,genre,title,view_count   genre
2   1   idxnum

Што такоя? Дока утверждает, что в ответ мы должны получить »0 1»! A. Ну да, сам виноват, забыл параметр /indexname/.

P   1   test    movie   PRIMARY id,genre,title,view_count   genre
0   1

Теперь порядок. Мы собираемся стучаться к таблице по первичному ключу, так что /indexname/ у нас будет PRIMARY. Откроем еще один HS-индекс, для MySQL-индекса по имени genre:

P   2   test    movie   genre   id,genre,title,view_count
0   1

Ишь ты подишь ты. И этот открылся. Давайте теперь добавим в табличку строк? Что там у нас в документации?

The 'insert' request has the following syntax.

/indexid/ + /vlen/ /v1/ ... /vn/
  • /vlen/ indicates the length of the trailing parameters /v1/… /vn/. This must be smaller than or equal to the length of /columns/ specified by the corresponding 'open_index' request.
  • /v1/… /vn/ specify the column values to set. For columns not in /columns/, the default values for each column are set.


… и мы должны получить ответ типа такого:

If 'insert' is succeeded, HanderSocket returns a line of the following syntax.

0 1

Попробуем…

1   +   3   0   Sci-Fi  Star wars
0   1   1

Упс. Ответ не совсем как в доке. Дополнительная единичка. Посмотрим, что происходит с таблицей, в консоли MariaDB:

SELECT * FROM movie;
+----+--------+-----------+
| id | genre  | title     |
+----+--------+-----------+
|  1 | Sci-Fi | Star wars |
+----+--------+-----------+

Сработало! Ну-ка, давай еще…

1   +   3   0   Comedy  Dumb & Dumber
0   1   2
1   +   3   0   Thriller    The Silence of the Lambs
0   1   3

Снова смотрим в MariaDB:

SELECT * FROM movie;
+----+----------+--------------------------+
| id | genre    | title                    |
+----+----------+--------------------------+
|  1 | Sci-Fi   | Star wars                |
|  2 | Comedy   | Dumb & Dumber            |
|  3 | Thriller | The Silence of the Lambs |
+----+----------+--------------------------+

Опа-на! Неизвестная третья цифирь, которую возвращает HS, точно совпадает с AUTO_INCREMENT-полем id!
А теперь попробуем указать значение для id вручную…

1   +   3   1   Sci-Fi  Star Trek
1   1   121

ОК.»1» в первой позиции ответа говорит нам о том, что случилась ошибка. HS, однако, не слишком многословен. Из дополнительной информации у нас есть только волшебное число »121», которое означает… Да черт его знает, собственно, но думаю, что таким нехитрым способом нам намекают на ошибку типа «DUPLICATE KEY». Еще разок…

1   +   3   4   Sci-Fi  Star Trek
0   1   0

Смотрим в MariaDB, наша новая запись добавлена с ID=4:

SELECT * FROM movie;
+----+----------+--------------------------+
| id | genre    | title                    |
+----+----------+--------------------------+
|              ...                         |
|  4 | Sci-Fi   | Star Trek                |
+----+----------+--------------------------+

Воот как-то так мы и вставляем записи в таблицы через HS и получаем значения AUTO_INCREMENT поля. Документация об этом молчит. Возможно, потому, что в былые времена HS не поддерживал AUTO_INCREMENT.

Будьте осторожны с колонками, которые имеют динамические значения по умолчанию. Такими как «created_at DATETIME DEFAULT CURRENT_TIMESTAMP» или «updated_at DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP». При инсертах и апдейтах необходимо задавать их значения вручную! Иначе, вместо текущего таймштампа при вставке в поле created_at вы получите »0000–00–00 00:00:00». А если вы не укажете значение для updated_at при апдейте, там будет прежнее значение.

С INSERT’ами закончили, уфф… Теперь попробуем выбрать что-то из HS. Смотрим в ман…

The 'find' request has the following syntax.

/indexid/ /op/ /vlen/ /v1/… /vn/ [LIM] [IN] [FILTER …]

LIM is a sequence of the following parameters.

/limit/ /offset/

IN is a sequence of the following parameters.

@ /icol/ /ivlen/ /iv1/ ... /ivn/

FILETER is a sequence of the following parameters.

/ftyp/ /fop/ /fcol/ /fval/
  • /indexid/ is a number. This number must be an /indexid/ specified by a 'open_index' request executed previously on the same connection.
  • /op/ specifies the comparison operation to use. The current version of HandlerSocket supports '=', '/', '/=', '/', and '/='.
  • /vlen/ indicates the length of the trailing parameters /v1/… /vn/. This must be smaller than or equal to the number of index columns specified by the /indexname/ parameter of the corresponding 'open_index' request.
  • /v1/… /vn/ specify the index column values to fetch.
  • LIM is optional. /limit/ and /offset/ are numbers. When omitted, it works as if 1 and 0 are specified. These parameter works like LIMIT of SQL. These values don’t include the number of records skipped by a filter.
  • IN is optional. It works like WHERE… IN syntax of SQL. /icol/ must be smaller than the number of index columns specified by the /indexname/ parameter of the corresponding 'open_index' request. If IN is specified in a find request, the /icol/-th parameter value of /v1/… /vn/ is ignored.
  • FILTERs are optional. A FILTER specifies a filter. /ftyp/ is either 'F' (filter) or 'W' (while). /fop/ specifies the comparison operation to use. /fcol/ must be smaller than the number of columns specified by the /fcolumns/ parameter of the corresponding 'open_index' request. Multiple filters can be specified, and work as the logical AND of them. The difference of 'F' and 'W' is that, when a record does not meet the specified condition, 'F' simply skips the record, and 'W' stops the loop.

Ну, все ясно, как на ладони. Пойдем дальше.
Ладно, ладно, шучу. Попробуем найти запись по ID. Сначала идет /indexid/ — в нашем случае это 1 (тот индекс, что мы открыли для первичного ключа). Затем /op/: мы хотим достать запись с id=1, так что это будет »=». Потом, /vlen/. Это число колонок в индексе. Индекс может состоять из нескольких колонок. В нашем случае, однако, это 1. Затем идет часть /v1/… /vn/. Это значения колонок, в нашем примере одна колонка и значение 1:

1   =   1   1
0   3   1   Sci-Fi  Star wars

Ура, сработало! Результат можно интерпретировать так:

0 - no errors;
3 - number of columns
[1  Sci-Fi  Star wars] - the row that we've got from InnoDB.

Что если мы попробуем другую операцию (/op/)? Например, получить записи с id > 1:

1   >   1   1
0   3   2   Comedy  Dumb & Dumber

Как так? Только одна запись? У нас же было больше, зуб даю! Оок, это есть в документации: «LIM is optional. /limit/ and /offset/ are numbers. When omitted, it works as if 1 and 0 are specified». Попробуем указать достаточный LIMIT, чтобы достать все ряды, которые нас интересуют.

1   >   1   1   10  0
0   3   2   Comedy  Dumb & Dumber   3   Thriller    The Silence of the Lambs    6   Sci-Fi  Star Trek

Все результаты возвращаются в одной строке:

0 - no errors;
3 - number of columns
[2  Comedy  Dumb & Dumber] - row 1
[3  Thriller    The Silence of the Lambs] - row 2
[6  Sci-Fi  Star Trek] - row 3

Все это надо будет разобрать в клиентском приложении (мы знаем кол-во колонок, знаем порядок, в котором мы их перечисляли при открытии индекса, так что нет проблем). Может быть, вы задаете себе вопрос:, а что будет, если в наших данных будет символ табуляции? Все же посыпется! Протокол описывает передачу символов от 0×00 до 0×0f. Клиентские библиотеки должны кодировать эти символы с помощью простого алгоритма (к ним добавляется префикс 0×01 и сдвигаются на 0×40). Об этом должна позаботиться клиентская библиотека. Для простоты, мы в наших примерах не будем использовать символы, которые требуют кодирования.

Кроме этого, у нас есть еще запрос IN — то есть можно получить данные сразу кучи записей по их ключам, в одном запросе! Я провел несколько опытов. Для начала, я не знал, что делать с /op/, /vlen/ и /v1/… /vn/. Ведь мы же собираемся указать все id, которые хотим получить, в части IN. Что ж, попробуем так же, как мы пробовали с простым поиском по id.

Условие IN — »@ 0 1 2», то есть колонка номер 0, одно значение, и это значение — 2.

1   =   1   1   @   0   1   2
2   1   modop

«modop». Спасибо, HandlerSocket! Теперь-то все понятно! Попробуем немного поменять…

1   =   1   0   @   0   1   2
2   1   modop

Опять. Чего же ты хочешь? А что если я укажу LIMIT?

1       =   1   0   1   0   @   0   1   2
0   3   2   Comedy  Dumb & Dumber

Вот оно что! Похоже, для запросов IN нужно обязательно указывать LIMIT и OFFSET. /op/, /vlen/ и /v1/… /vn/ также нужно указывать, хотя это только вводит в замешательство. В примере ниже, я указываю значение 1 вместо 0 в секции /vn/ — и получаю точно такой же ответ!

1   =   1   1   1   0   @   0   1   2
0   3   2   Comedy  Dumb & Dumber

Попробую изменить /op/:

1   >   1   1   1   0   @   0   1   2
0   3   3   Thriller    The Silence of the Lambs

А это еще откуда? Ну ладно, попробуем получить больше одного id:

1   >   1   1   1   0   @   0   2   2   3
0   3   3   Thriller    The Silence of the Lambs

Черт, забыл про LIMIT. Укажем, что нам нужно 2 две записи:

1   >   1   1   2   0   @   0   2   2   3
0   3   3   Thriller    The Silence of the Lambs    6   Sci-Fi  Star Trek

Очевидно, наши id проигнорировались, и HS выдает записи с id > 1. В конце концов, я выяснил, что в запросе IN можно смело указывать »=» в качестве /op/, и 0 в качестве значения /v1/. Можно заметить, что результаты мы получили ровно в том порядке, как мы их указывали в условии IN. Однако, я не знаю, совпадение это или нет. Я бы не стал на это полагаться.

Перейдем к фильтрам? Что если взять наш последний запрос и попросить HS применить фильтр по genre = 'Sci-Fi'? Помните, когда мы открывали индекс, мы указывали /fcolumns/? Вот теперь-то они и пригодятся, чтобы применять добавочные фильтры. Если бы мы не указали /fcolumns/, любой запрос с фильтрами возвращал бы »2 1 filterfld».

1   =   1   0   3   0   @   0   3   2   3   1   F   =   0   Sci-Fi
0   3   1   Sci-Fi  Star wars

Сработало! ОК, это, пожалуй, все насчет получения записей из HS. Теперь перейдем к UPDATE’ам. Что насчет документации?

The 'find_modify' request has the following syntax.

/indexid/ /op/ /vlen/ /v1/ ... /vn/ [LIM] [IN] [FILTER ...] MOD

MOD is a sequence of the following parameters.

/mop/ /m1/ ... /mk/
  • /mop/ is 'U' (update), '+' (increment), '-' (decrement), 'D' (delete), 'U?', '+?', '-?', or 'D?'. If the '?' suffix is specified, it returns the contents of the records before modification (as if it’s a 'find' request), instead of the number of modified records.
  • /m1/… /mk/ specifies the column values to set. The length of /m1/… /mk/ must be smaller than or equal to the length of /columns/ specified by the corresponding 'open_index' request. If /mop/ is 'D', these parameters are ignored. If /mop/ is '+' or '-', values must be numeric. If /mop/ is '-' and it attempts to change a column value from negative to positive or positive to negative, the column value is not modified.

Первая часть выглядит знакомо. Все это мы только видели, когда рассматривали FIND-запросы, и похоже, что это условия WHERE для нашего UDPATE. Обновим ряд с id=1 и поставим view_count=100:

1   =   1   1   U   1   Sci-Fi  Star Wars   100
2   1   modop

modop. Как приятно. Как понятно! Что если указать LIMIT?

1   =   1   1   1   0   U   1   Sci-Fi  Star Wars   100
0   1   1

Таак. Посмотрим в MariaDB:

SELECT * FROM movie WHERE id = 1;
+----+--------+-----------+------------+
| id | genre  | title     | view_count |
+----+--------+-----------+------------+
|  1 | Sci-Fi | Star Wars |        100 |
+----+--------+-----------+------------+

Попробуем кое-что посложнее. Увеличис счетчик view_count для всех комедий на 10.

1   /   1   0   1000    0   F   =   0   Comedy  +   0   0   0   10
0   1   1

Похоже, норм! Посмотрим в MariaDB:

SELECT * FROM movie;
+----+----------+--------------------------+------------+
| id | genre    | title                    | view_count |
+----+----------+--------------------------+------------+
|  1 | Sci-Fi   | Star Wars                |        100 |
|  2 | 0        | 0                        |         10 |
|  3 | Thriller | The Silence of the Lambs |          0 |
|  6 | Sci-Fi   | Star Trek                |          0 |
+----+----------+--------------------------+------------+

Нет, не заработало. По крайней мере, не заработало хорошо. Счетчик и правда увеличился на 10, но также проапдейтились и все другие колонки. Поскольку id у нас числовой, и мы сказали увеличить его на 0, он остался нетронутым. Верну все обратно:

1   =   1   2   1   0   U   2   Comedy  Dumb & Dumber   10

Могу сказать сразу, что указывать значение для колонок «genre» и «title» в запросе на инкремент не сработает. Они получат значение »0», так же как произошло при нашей первой попытке. Чтобы инкремент заработал как надо, нужно открыть отдельный индекс, который будет содержать только колонки «id» и «view_count»:

P   3   test    movie   PRIMARY id,view_count   genre
0   1
3   /   1   0   1000    0   F   =   0   Comedy  +   0   10
0   1   1

Ну вот, другое дело:

SELECT * FROM movie;
+----+----------+--------------------------+------------+
| id | genre    | title                    | view_count |
+----+----------+--------------------------+------------+
|  1 | Sci-Fi   | Star Wars                |        100 |
|  2 | Comedy   | Dumb & Dumber            |         30 |
|  3 | Thriller | The Silence of the Lambs |          0 |
|  6 | Sci-Fi   | Star Trek                |          0 |
+----+----------+--------------------------+------------+

На этом все. Счастливого хэндлерсокетинга!

© Habrahabr.ru