Бинарный протокол | Tarantool
Документация на русском языке
поддерживается сообществом
Справочники Детали реализации Бинарный протокол

Бинарный протокол

Бинарный протокол передает данные по схеме «запрос-ответ», то есть он предназначен для отправки запросов на сервер Tarantool и получения ответов. Протокол предоставляет полный доступ к функциям Tarantool, включая:

  • мультиплексирование запросов, т.е. возможность асинхронной отправки множества запросов по одному соединению;
  • формат ответа, который поддерживает запись в режиме без копирования (zero-copy).

Протокол можно назвать «бинарным», поскольку наиболее частые обращения к базе данных выполняются с помощью бинарного кода, а не текста запроса на языке Lua. Специалисты по Tarantool используют его, чтобы писать собственные коннекторы, понимать сетевые сообщения, поддерживать новые функции, которые их любимый коннектор еще не поддерживает, или чтобы избежать повторного разбора на сервере.

Если слово начинается с MP_, это указывает на тип MessagePack или ряд типов MessagePack, включая сигнал и, возможно, включая значение, с небольшими изменениями:

  • MP_NIL nil
  • MP_UINT unsigned integer
  • MP_INT integer или unsigned integer
  • MP_STR string
  • MP_BIN binary string
  • MP_ARRAY array
  • MP_MAP map
  • MP_BOOL boolean
  • MP_FLOAT float
  • MP_DOUBLE double
  • MP_EXT extension (включая тип DECIMAL и тип UUID)
  • MP_OBJECT любой объект формата MessagePack

Краткое описание приводится на странице «спецификации» MessagePack.

Если же слово начинается с IPROTO_, это указывает на константу Tarantool, которая либо определена, либо упоминается в файле iproto_constants.h.

В этом разделе мы будем упоминать следующие константы IPROTO, которые идентифицируют запросы:

IPROTO_SELECT=0x01
IPROTO_INSERT=0x02
IPROTO_REPLACE=0x03
IPROTO_UPDATE=0x04
IPROTO_DELETE=0x05
IPROTO_CALL_16=0x06
IPROTO_AUTH=0x07
IPROTO_EVAL=0x08
IPROTO_UPSERT=0x09
IPROTO_CALL=0x0a
IPROTO_EXECUTE=0x0b
IPROTO_NOP=0x0c
IPROTO_PREPARE=0x0d
IPROTO_BEGIN=0x0e
IPROTO_COMMIT=0x0f
IPROTO_ROLLBACK=0x10
IPROTO_RAFT_CONFIRM=0x28
IPROTO_RAFT_ROLLBACK=0x29
IPROTO_RAFT=0x1e
IPROTO_RAFT_PROMOTE=0x1f
IPROTO_RAFT_DEMOTE=0x20
IPROTO_PING=0x40
IPROTO_JOIN=0x41
IPROTO_SUBSCRIBE=0x42
IPROTO_VOTE_DEPRECATED=0x43
IPROTO_VOTE=0x44
IPROTO_FETCH_SNAPSHOT=0x45
IPROTO_REGISTER=0x46

В этом разделе мы опишем такие константы IPROTO, которые встречаются в запросах или ответах:

IPROTO_OK=0x00
IPROTO_REQUEST_TYPE=0x00
IPROTO_SYNC=0x01
IPROTO_REPLICA_ID=0x02
IPROTO_LSN=0x03
IPROTO_TIMESTAMP=0x04
IPROTO_SCHEMA_VERSION=0x05
IPROTO_FLAGS=0x09
IPROTO_STREAM_ID=0x0a
IPROTO_SPACE_ID=0x10
IPROTO_INDEX_ID=0x11
IPROTO_LIMIT=0x12
IPROTO_OFFSET=0x13
IPROTO_ITERATOR=0x14
IPROTO_INDEX_BASE=0x15
IPROTO_KEY=0x20
IPROTO_TUPLE=0x21
IPROTO_FUNCTION_NAME=0x22
IPROTO_USER_NAME=0x23
IPROTO_INSTANCE_UUID=0x24
IPROTO_CLUSTER_UUID=0x25
IPROTO_VCLOCK=0x26
IPROTO_EXPR=0x27
IPROTO_OPS=0x28
IPROTO_BALLOT=0x29
IPROTO_BALLOT_IS_RO_CFG=0x01
IPROTO_BALLOT_VCLOCK=0x02
IPROTO_BALLOT_GC_VCLOCK=0x03
IPROTO_BALLOT_IS_RO=0x04
IPROTO_BALLOT_IS_ANON=0x05
IPROTO_BALLOT_IS_BOOTED=0x06
IPROTO_BALLOT_CAN_LEAD=0x07
IPROTO_TUPLE_META=0x2a
IPROTO_OPTIONS=0x2b
IPROTO_DATA=0x30
IPROTO_ERROR_24=0x31
IPROTO_METADATA=0x32
IPROTO_BIND_METADATA=0x33
IPROTO_BIND_COUNT=0x34
IPROTO_SQL_TEXT=0x40
IPROTO_SQL_BIND=0x41
IPROTO_SQL_INFO=0x42
IPROTO_STMT_ID=0x43
IPROTO_ERROR=0x52
IPROTO_FIELD_NAME=0x00
IPROTO_FIELD_TYPE=0x01
IPROTO_FIELD_COLL=0x02
IPROTO_FIELD_IS_NULLABLE=0x03
IPROTO_FIELD_IS_AUTOINCREMENT=0x04
IPROTO_FIELD_SPAN=0x05
IPROTO_CHUNK=0x80
IPROTO_RAFT_TERM=0x00
IPROTO_RAFT_VOTE=0x01
IPROTO_RAFT_STATE=0x02
IPROTO_RAFT_VCLOCK=0x03

Для обозначения описаний сообщений мы будем вызывать msgpack(...), а внутри него будем использовать модифицированный YAML таким образом:

{...} в фигурные скобки заключают ассоциативный массив, в MsgPack это MP_MAP,
k: v — это пара «ключ-значение», также называемая элементом ассоциативного массива, в этом разделе k всегда является беззнаковым целым = одной из констант IPROTO,
italics используются для обозначения заменяемого текста в этом руководстве. Обычно это тип данных, но мы не показываем типы констант IPROTO, которые всегда будут беззнаковыми 8-битными целыми числами,
[...] — для неассоциативных массивов,
# начинает комментарий, особенно в начале раздела,
все остальное приводится «как есть».
Элементы ассоциативного массива могут появляться в любом порядке, но в примерах мы обычно используем тот порядок, который используется в net_box.c.

Кроме как во время соединения (что включает в себя приветствие сервера и необязательную аутентификацию, которую мы обсудим далее в этом разделе), протокол идет по схеме «запрос-ответ» (клиент запрашивает, сервер отвечает). Пакет может содержать более одного запроса.

Почти все запросы и ответы состоят из трех частей: размер, заголовок и тело. Размер — это unsigned integer (MP_UINT), обычно 32-битное беззнаковое целое число. Заголовок и тело — это ассоциативные массивы (MP_MAP).

# <size>
{MP_UINT unsigned integer}
# <header>
{MP_MAP with <header> map-items}
# <body>
{MP_MAP with <body> map-items}

<size> — это размер заголовка плюс размер тела сообщения. Есть смысл сравнивать его с количеством байтов, оставшихся в пакете.

Заголовок <header> может содержать такие данные в любом порядке:

msgpack({
    IPROTO_REQUEST_TYPE: {MP_UINT unsigned integer},
    IPROTO_SYNC: {MP_UINT unsigned integer},
    IPROTO_SCHEMA_VERSION: {MP_UINT unsigned integer}
    IPROTO_STREAM_ID: {MP_UINT unsigned integer}
})

IPROTO_REQUEST_TYPE или индикатор кода ответа (Response-Code-Indicator) = 0x00. Это беззнаковое число, которое указывает на то, что будет находиться в теле сообщения <body>. В запросах за IPROTO_REQUEST_TYPE будет следовать IPROTO_SELECT и т. д. В ответах за Response-Code-Indicator будет следовать IPROTO_OK и т. д.

IPROTO_SYNC = 0x01. Это беззнаковое целое число должно увеличиваться так, чтобы оно было уникальным в каждом запросе. Это целое число также возвращается в результате box.session.sync(). Значение IPROTO_SYNC в ответе должно быть таким же, как значение IPROTO_SYNC в запросе.

IPROTO_SCHEMA_VERSION = 0x05. Беззнаковое число, иногда называемое SCHEMA_ID, которое увеличивается при значительных изменениях. В заголовке запроса IPROTO_SCHEMA_VERSION указываеть необязательно, поэтому версия не будет проверяться, если она отсутствует. В заголовке ответа IPROTO_SCHEMA_VERSION присутствует всегда, и клиент должен сам проверить, не изменилась ли версия.

IPROTO_STREAM_ID = 0x0a. Беззнаковое число, которое должно быть уникально для каждого стрима. В запросах указывать значение IPROTO_STREAM_ID необязательно. Однако оно может понадобиться, если необходимо обрабатывать запросы внутри транзакций отдельными группами или выполнять запросы строго последовательно независимо от того, входят ли они в ту или иную транзакцию. В ответах IPROTO_STREAM_ID отсутствует. См. Бинарный протокол – стримы.

Чтобы понять, как Tarantool кодирует заголовок, обратите внимание на функцию xrow_header_encode в файле xrow.c. Чтобы увидеть, как Tarantool декодирует заголовок, посмотрите функцию netbox_decode_data в файле net_box.c. Например, при успешном ответе на box.space:select() значение индикатора кода ответа будет 0 = IPROTO_OK, а массив будет содержать все кортежи из результата.

Тело сообщения <body> содержит детали запроса или ответа. В запросе оно может отсутствовать или представлять собой пустой ассоциативный массив. И то, и другое будет интерпретироваться одинаково. Ответы будут содержать <body> в любом случае, даже после запроса IPROTO_PING.

Запрос содержит размер, заголовок, который содержит ключ IPROTO, и тело сообщения, как описано в этом разделе.

См. space_object:select(). Тело сообщения представляет собой ассоциативный массив из 6 элементов.

# <size>
msgpack({MP_UINT unsigned integer = size(<header>) + size(<body>)})
# <header>
msgpack({
    IPROTO_REQUEST_TYPE: IPROTO_SELECT,
    IPROTO_SYNC: {MP_UINT unsigned integer}
})
# <body>
msgpack({
    IPROTO_SPACE_ID: {MP_UINT unsigned integer},
    IPROTO_INDEX_ID: {MP_UINT unsigned integer},
    IPROTO_LIMIT: {MP_UINT unsigned integer},
    IPROTO_OFFSET: {MP_UINT unsigned integer},
    IPROTO_ITERATOR: {MP_UINT unsigned integer},
    IPROTO_KEY: {MP_ARRAY array of key values}
})

Пример: если ID спейса „tspace“ = 512 и это пятое сообщение,
conn.space.tspace:select({0},{iterator='GT',offset=1,limit=2}) вызовет:

<size>
msgpack(21)
# <header>
msgpack({
    IPROTO_SYNC: 5,
    IPROTO_REQUEST_TYPE: IPROTO_SELECT
})
# <body>
msgpack({
    IPROTO_SPACE_ID: 512,
    IPROTO_INDEX_ID: 0,
    IPROTO_ITERATOR: 6,
    IPROTO_OFFSET: 1,
    IPROTO_LIMIT: 2,
    IPROTO_KEY: [1]
})

Далее в разделе Примеры будут рассмотрены байт-коды сообщения IPROTO_SELECT.

См. space_object:insert(). Тело сообщения представляет собой ассоциативный массив из 2 элементов:

# <size>
msgpack({MP_UINT unsigned integer = size(<header>) + size(<body>)})
# <header>
msgpack({
    IPROTO_REQUEST_TYPE: IPROTO_INSERT,
    IPROTO_SYNC: {MP_UINT unsigned integer}
})
# <body>
msgpack({
    IPROTO_SPACE_ID: {MP_UINT unsigned integer},
    IPROTO_TUPLE: {MP_ARRAY array of field values}
})

Пример: если ID спейса „tspace“ = 512 и это пятое сообщение,
conn.space.tspace:insert{1, 'AAA'} вызовет:

# <size>
msgpack(17)
# <header>
msgpack({
    IPROTO_REQUEST_TYPE: IPROTO_INSERT,
    IPROTO_SYNC: 5
})
# <body>
msgpack({
    IPROTO_SPACE_ID: 512,
    IPROTO_TUPLE: [1, 'AAA']
})

См. space_object:replace(). Тело сообщения представляет собой ассоциативный массив из 2 элементов, как и в случае IPROTO_INSERT:

# <size>
msgpack({MP_UINT unsigned integer = size(<header>) + size(<body>)})
# <header>
msgpack({
    IPROTO_REQUEST_TYPE: IPROTO_REPLACE,
    IPROTO_SYNC: {MP_UINT unsigned integer}
})
# <body>
msgpack({
    IPROTO_SPACE_ID: {MP_UINT unsigned integer},
    IPROTO_TUPLE: {MP_ARRAY array of field values}
})

См. space_object:update().

Тело сообщения обычно представляет собой ассоциативный массив из 4 элементов:

# <size>
msgpack({MP_UINT unsigned integer = size(<header>) + size(<body>)})
# <header>
msgpack({
    IPROTO_REQUEST_TYPE: IPROTO_UPDATE,
    IPROTO_SYNC: {MP_UINT unsigned integer}
})
# <body>
msgpack({
    IPROTO_SPACE_ID: {MP_UINT unsigned integer},
    IPROTO_INDEX_ID: {MP_UINT unsigned integer},
    IPROTO_KEY: {MP_ARRAY array of index keys},
    IPROTO_TUPLE: {MP_ARRAY array of update operations}
})

Если в операции не указаны значения, то IPROTO_TUPLE — это массив из 2 элементов:
[MP_STR OPERATOR = '#', {MP_INT FIELD_NO = номер поля, начиная с 1]. Обычно номера полей начинаются с 1.

Если в операции задано одно значение, то IPROTO_TUPLE — это массив из трех элементов:
[MP_STR string OPERATOR = '+' or '-' or '^' or '^' or '|' or '!' or '=', MP_INT FIELD_NO, MP_OBJECT VALUE].

В остальных случаях IPROTO_TUPLE — это массив из 5 элементов:
[MP_STR string OPERATOR = ':', MP_INT integer FIELD_NO, MP_INT POSITION, MP_INT OFFSET, MP_STR VALUE].

Пример: если ID спейса „tspace“ = 512 и это пятое сообщение, br| conn.space.tspace:update(999, {{'=', 2, 'B'}}) вызовет:

# <size>
msgpack(17)
# <header>
msgpack({
    IPROTO_REQUEST_TYPE: IPROTO_UPDATE,
    IPROTO_SYNC: 5
})
# <body> ... the map-item IPROTO_INDEX_BASE is optional
msgpack({
    IPROTO_SPACE_ID: 512,
    IPROTO_INDEX_ID: 0,
    IPROTO_INDEX_BASE: 1,
    IPROTO_TUPLE: [['=',2,'B']],
    IPROTO_KEY: [999]
})

Далее в разделе Примеры будут рассмотрены байт-коды сообщения IPROTO_UPDATE.

См. space_object:delete(). Тело сообщения представляет собой ассоциативный массив из 3 элементов:

# <size>
msgpack({MP_UINT unsigned integer = size(<header>) + size(<body>)})
# <header>
msgpack({
    IPROTO_REQUEST_TYPE: IPROTO_DELETE,
    IPROTO_SYNC: {MP_UINT unsigned integer}
})
# <body>
msgpack({
    IPROTO_SPACE_ID: {MP_UINT unsigned integer},
    IPROTO_INDEX_ID: {MP_UINT unsigned integer},
    IPROTO_KEY: {MP_ARRAY array of key values}
})

См. conn:call(). _16 в конце подсказывает, что константа используется для call() до версии Tarantool 1.6. Эта константа объявлена устаревшей. Вместо нее используйте IPROTO_CALL. Тело сообщения представляет собой ассоциативный массив из 2 элементов:

# <size>
msgpack({MP_UINT unsigned integer = size(<header>) + size(<body>)})
# <header>
msgpack({
    IPROTO_REQUEST_TYPE: IPROTO_CALL_16,
    IPROTO_SYNC: {MP_UINT unsigned integer}
})
# <body>
msgpack({
    IPROTO_FUNCTION_NAME: {MP_STR string},
    IPROTO_TUPLE: {MP_ARRAY array of arguments}
})

Возвращается массив кортежей.

См. раздел Аутентификация в документации. См. раздел об аутентификации ниже.

См. conn:eval(). В качестве аргумента выступает выражение на Lua — так Tarantool обрабатывает небинарный код с помощью бинарного протокола. Любой запрос без собственного кода, например box.space.space-name:drop(), будет обработан с помощью либо IPROTO_CALL, либо IPROTO_EVAL. Административная утилита tarantoolctl активно использует eval. Тело сообщения представляет собой ассоциативный массив из 2 элементов:

# <size>
msgpack({MP_UINT unsigned integer = size(<header>) + size(<body>)})
# <header>
msgpack({
    IPROTO_REQUEST_TYPE: IPROTO_EVAL,
    IPROTO_SYNC: {MP_UINT unsigned integer}
})
# <body>
msgpack({
    IPROTO_EXPR: {MP_STR string},
    IPROTO_TUPLE: {MP_ARRAY array of arguments}
})

Пример. Если это пятое сообщение, conn:eval('return 5;') приведет к следующему:

# <size>
msgpack(19)
# <header>
msgpack({
    IPROTO_SYNC: 5
    IPROTO_REQUEST_TYPE: IPROTO_EVAL
})
# <body>
msgpack({
    IPROTO_EXPR: 'return 5;',
    IPROTO_TUPLE: []
})

См. space_object:upsert().

Тело сообщения обычно представляет собой ассоциативный массив из 4 элементов:

# <size>
msgpack({MP_UINT unsigned integer = size(<header>) + size(<body>)})
# <header>
msgpack({
    IPROTO_REQUEST_TYPE: IPROTO_UPSERT,
    IPROTO_SYNC: {MP_UINT unsigned integer}
})
# <body>
msgpack({
    IPROTO_SPACE_ID: {MP_UINT unsigned integer},
    IPROTO_INDEX_BASE: {MP_UINT unsigned integer},
    IPROTO_OPS: {MP_ARRAY array of update operations},
    IPROTO_TUPLE: {MP_ARRAY array of primary-key field values}
})

IPROTO_OPS — это то же самое, что и IPROTO_TUPLE в IPROTO_UPDATE.

См. conn:call(). Тело сообщения представляет собой ассоциативный массив из 2 элементов:

# <size>
msgpack({MP_UINT unsigned integer = size(<header>) + size(<body>)})
# <header>
msgpack({
    IPROTO_REQUEST_TYPE: IPROTO_CALL,
    IPROTO_SYNC: {MP_UINT unsigned integer}
})
# <body>
msgpack({
    IPROTO_FUNCTION_NAME: {MP_STR string},
    IPROTO_TUPLE: {MP_ARRAY array of arguments}
})

Вернется список значений, наподобие ответа IPROTO_EVAL.

См. box.execute(), используется только для SQL. Тело сообщения представляет собой ассоциативный массив из 3 элементов:

# <size>
msgpack(:samp:`{{MP_UINT unsigned integer = size(<header>) + size(<body>)}}`)
# <header>
msgpack({
    IPROTO_REQUEST_TYPE: IPROTO_EXECUTE,
    IPROTO_SYNC: :samp:`{{MP_UINT unsigned integer}}`
})
# <body>
msgpack({
    IPROTO_STMT_ID: :samp:`{{MP_INT integer}}` или IPROTO_SQL_TEXT: :samp:`{{MP_STR string}}`,
    IPROTO_SQL_BIND: :samp:`{{MP_INT integer}}`,
    IPROTO_OPTIONS: :samp:`{{MP_ARRAY array}}`
})

При выполнении подготовленного оператора используйте IPROTO_STMT_ID (0x43) и ID оператора (MP_INT), при работе со строками SQL — IPROTO_SQL_TEXT (0x40) и текст оператора (MP_STR), а затем IPROTO_SQL_BIND (0x41) и массив значений параметров для подстановочных знаков ? или :name, IPROTO_OPTIONS (0x2b) и массив параметров (обычно пустой).

Например, предположим, что мы создаем подготовленный оператор с двумя подстановочными знаками ? и выполняем его с двумя параметрами таким образом:
n = conn:prepare([[VALUES (?, ?);]])
conn:execute(n.stmt_id, {1,'a'})
Тогда тело сообщения будет выглядеть так:

# <body>
msgpack({
    IPROTO_STMT_ID: 0xd7aa741b,
    IPROTO_SQL_BIND: [1, 'a'],
    IPROTO_OPTIONS: []
})

Далее в разделе Примеры будут рассмотрены байт-коды сообщения IPROTO_EXECUTE.

Чтобы вызвать подготовленный оператор с именованными параметрами из коннектора, передайте параметры в массиве ассоциативных массивов. Клиент должен поместить каждый элемент в ассоциативный массив, где ключ содержит имя параметра (с двоеточием), а значение — фактическое значение. Так, чтобы связать foo и bar с 42 и 43, клиент должен отправить IPROTO_SQL_TEXT: <...>, IPROTO_SQL_BIND: [{"foo": 42}, {"bar": 43}].

Если оператор содержит как именованные, так и неименованные параметры, поместите в ассоциативный массив только именованные параметры. Остальные параметры являются позиционными и будут подставляться по порядку.

Нет такого запроса на Lua, который бы соответствовал IPROTO_NOP. IPROTO_NOP приводит к увеличению LSN. Иногда константу можно использовать для обновления значения, когда старое и новое значения одинаковы, но LSN нужно увеличить, поскольку нужно зарегистрировать изменение данных. Тело сообщения пустое.

См. box.prepare, используется только для SQL. Тело сообщения представляет собой ассоциативный массив из 1 элемента:

# <size>
msgpack(:samp:`{{MP_UINT unsigned integer = size(<header>) + size(<body>)}}`)
# <header>
msgpack({
    IPROTO_REQUEST_TYPE: IPROTO_PREPARE,
    IPROTO_SYNC: :samp:`{{MP_UINT unsigned integer}}`
})
# <body>
msgpack({
    IPROTO_STMT_ID: :samp:`{{MP_INT integer}}` или IPROTO_SQL_TEXT: :samp:`{{MP_STR string}}`
})

При выполнении подготовленного оператора используйте IPROTO_STMT_ID (0x43) и ID оператора (MP_INT), при работе со строками SQL — IPROTO_SQL_TEXT (0x40) и текст оператора (string). Таким образом, элемент ассоциативного массива IPROTO_PREPARE — это то же самое, что и первый элемент тела сообщения IPROTO_EXECUTE.

См. conn:ping(). В теле сообщения будет пустой ассоциативный массив, потому что IPROTO_PING в заголовке содержит всю информацию, необходимую экземпляру сервера.

# <size>
msgpack(5)
# <header>
msgpack({
    IPROTO_REQUEST_TYPE: IPROTO_PING,
    IPROTO_SYNC: {MP_UINT unsigned integer}
})
IPROTO_JOIN = 0x41 — для репликации
IPROTO_SUBSCRIBE = 0x42 — для репликации SUBSCRIBE
IPROTO_VOTE_DEPRECATED = 0x43 — для устаревшего типа голосования, взамен используется IPROTO_VOTE
IPROTO_VOTE = 0x44 — для выбора мастера
IPROTO_FETCH_SNAPSHOT = 0x45 — для начала анонимной репликации
IPROTO_REGISTER = 0x46 — для выхода из анонимной репликации

Константы Tarantool 0x41–0x46 (в десятичной системе 65-70) предназначены для репликации. Коннекторы и клиенты не должны отправлять репликационные пакеты. См. раздел о репликации.

Следующие два сообщения IPROTO используются в соединениях репликации между узлами Tarantool при синхронной репликации. Эти сообщения не должны использоваться клиентскими приложениями при обычном соединении.

Это сообщение подтверждает, что транзакции до LSN = IPROTO_LSN включительно из экземпляра с id = IPROTO_REPLICA_ID набрали кворум и могут пройти коммит. До версии Tarantool 2.10-beta1 запрос IPROTO_RAFT_CONFIRM назывался IPROTO_CONFIRM.

Тело сообщения представляет собой ассоциативный массив из 2 элементов:

# <size>
msgpack({MP_UINT unsigned integer = size(<header>) + size(<body>)})
# <header>
msgpack({
    IPROTO_REQUEST_TYPE: IPROTO_RAFT_CONFIRM,
    IPROTO_SYNC: {MP_UINT unsigned integer}
})
# <body>
msgpack({
    IPROTO_REPLICA_ID: {MP_INT integer},
    IPROTO_LSN: {MP_INT integer}
})

В этом сообщении говорится, что транзакции до LSN = IPROTO_LSN включительно из экземпляра с id = IPROTO_REPLICA_ID не смогли набрать кворум и будут отменены. До версии Tarantool 2.10 запрос IPROTO_RAFT_ROLLBACK назывался IPROTO_ROLLBACK.

Тело сообщения представляет собой ассоциативный массив из 2 элементов:

# <size>
msgpack({MP_UINT unsigned integer = size(<header>) + size(<body>)})
# <header>
msgpack({
    IPROTO_REQUEST_TYPE: IPROTO_RAFT_ROLLBACK,
    IPROTO_SYNC: {MP_UINT unsigned integer}
})
# <body>
msgpack({
    IPROTO_REPLICA_ID: {MP_INT integer},
    IPROTO_LSN: {MP_INT integer}
})

В ответе после заголовка идет тело сообщения. Если ошибки не было, оно будет содержать IPROTO_OK (0x00). Если была ошибка, то он будет содержать код ошибки, отличный от IPROTO_OK. Ответы на операторы SQL немного отличаются и будут описаны в последующем разделе Ответы на SQL-запросы.

Для IPROTO_OK индикатор кода ответа в заголовке будет 0, а тело сообщения будет представлять собой ассоциативный массив, состоящий из 1 элемента.

# <size>
msgpack({MP_UINT unsigned integer = size(<header>) + size(<body>)})
# <header>
msgpack({
    Response-Code-Indicator: IPROTO_OK,
    IPROTO_SYNC: {MP_UINT unsigned integer, may be 64-bit},
    IPROTO_SCHEMA_VERSION: {MP_UINT unsigned integer}
})
# <body>
msgpack({
    IPROTO_DATA: {any type}
})

В теле сообщения IPROTO_PING будет пустой ассоциативный массив. В теле большинства запросов доступа к данным (IPROTO_SELECT, IPROTO_INSERT, IPROTO_DELETE и т.д.) будет ассоциативный массив IPROTO_DATA с массивом кортежей, содержащих массив полей. Для IPROTO_EVAL и IPROTO_CALL телом обычно будет массив, но поскольку запросы на Lua могут возвращать самые разные структуры, сами тела могут содержать самые разные структуры.

Пример: если это пятое сообщение, запрос такой: box.space.space-name:insert{6}, и предыдущая версия схемы была 100, ответ после успешного выполнения будет выглядеть следующим образом:

# <size>
msgpack(32)
# <header>
msgpack({
    Response-Code-Indicator: IPROTO_OK,
    IPROTO_SYNC: 5,
    IPROTO_SCHEMA_VERSION: 100
})
# <body>
msgpack({
    IPROTO_DATA: [[6]]
})

Далее в разделе Примеры будут приведены байт-коды ответа на сообщение IPROTO_INSERT.

IPROTO_DATA возвращается в результате использования net_box и модуля buffer. То есть если бы мы использовали net_box, то могли бы интерпретировать результат с помощью msgpack.decode_unchecked() или преобразовать его в строку с помощью ffi.string(pointer,length). Также здесь можно использовать функцию pickle.unpack().

Если используется box.session.push(), значением заголовка Response-Code-Indicator для внеполосных сообщений будет IPROTO_CHUNK, а не IPROTO_OK.

Для ответа, отличного от IPROTO_OK, индикатор кода ответа в заголовке будет 0x8XXX, а тело будет представлять собой ассоциативный массив, состоящий из 1 элемента.

# <size>
msgpack(32)
# <header>
msgpack({
    Response-Code-Indicator: {0x8XXX},
    IPROTO_SYNC: {MP_UINT unsigned integer, may be 64-bit},
    IPROTO_SCHEMA_VERSION: {MP_UINT unsigned integer}
})
# <body>
msgpack({
    IPROTO_ERROR: {MP_STRING string}
})

где 0x8XXX — это индикатор ошибки, а XXX — это значение из файла src/box/errcode.h. В src/box/errcode.h также есть несколько удобных макросов, которые определяют шестнадцатеричные константы для кодов возврата.

Пример: в версии 2.4.0 и раньше, если это пятое сообщение и отправляется запрос на создание дубликата спейса с помощью conn:eval([[box.schema.space.create('_space');]]), то ответ на невыполненный запрос будет выглядеть следующим образом:

# <size>
msgpack(32)
# <header>
msgpack({
    Response-Code-Indicator: 0x800a,
    IPROTO_SYNC: 5,
    IPROTO_SCHEMA_VERSION: 0x78
})
# <body>
msgpack({
    IPROTO_ERROR:  "Space '_space' already exists"
})

Далее в разделе Примеры будут приведены байт-коды ответа на сообщение IPROTO_EVAL.

Заглянув в errcode.h, по коду ошибки 0x0a (10 в десятичной системе) мы обнаружим ER_SPACE_EXISTS, чему соответствует строка «Space „%s“ already exists» («Спейс „%s“ уже существует»).

Начиная с версии 2.4.1, ответы на ошибки содержат дополнительную информацию, как описано выше. Эта дополнительная информация передается с помощью расширения MP_ERROR. Подробную информацию см. в разделе расширения MessagePack.

В ответе на SQL-запрос после заголовка идет тело сообщения, которое немного отличается от тела сообщения, описанного в разделе Ответы на запросы без ошибок и без SQL.

Если SQL-запрос не содержит операторы SELECT, VALUES или PRAGMA, то тело ответа содержит только IPROTO_SQL_INFO (0x42). Обычно IPROTO_SQL_INFO представляет собой ассоциативный массив с одним элементом — SQL_INFO_ROW_COUNT (0x00) — количество измененных строк.

# <size>
msgpack({MP_UINT unsigned integer = size(<header>) + size(<body>)})
# <header>
msgpack({
    Response-Code-Indicator: IPROTO_OK,
    IPROTO_SYNC: {MP_UINT unsigned integer, may be 64-bit},
    IPROTO_SCHEMA_VERSION: {MP_UINT unsigned integer}
})
# <body>
msgpack({
    IPROTO_SQL_INFO: {
        SQL_INFO_ROW_COUNT: {MP_UINT}
    }
})

Например, для запроса INSERT INTO table-name VALUES (1), (2), (3) тело ответа содержит ассоциативный массив IPROTO_SQL_INFO с SQL_INFO_ROW_COUNT = 3. SQL_INFO_ROW_COUNT может быть 0, если оператор не изменяет строку, и может быть 1, если оператор создает новый объект.

Ассоциативный массив IPROTO_SQL_INFO может содержать второй элемент SQL_INFO_AUTO_INCREMENT_IDS (0x01), который представляет собой новое значение (или значения) первичного ключа в операции вставки INSERT в таблицу с автоматическим увеличением первичного ключа PRIMARY KEY AUTOINCREMENT. В этом случае MP_MAP будет содержать два ключа, и одним из двух ключей будет 0x01: SQL_INFO_AUTO_INCREMENT_IDS, который представляет собой массив целых беззнаковых чисел.

Если запрос содержит SQL-оператор SELECT, VALUES или PRAGMA, ответ будет:

# <size>
msgpack(32)
# <header>
msgpack({
    Response-Code-Indicator: IPROTO_OK,
    IPROTO_SYNC: {MP_UINT unsigned integer, may be 64-bit},
    IPROTO_SCHEMA_VERSION: {MP_UINT unsigned integer}
})
# <body>
msgpack({
    IPROTO_METADATA: {array of column maps},
    IPROTO_DATA: {array of tuples}
})
  • IPROTO_METADATA: array of column maps — массив ассоциативных массивов, причем каждый ассоциативный массив содержит по крайней мере IPROTO_FIELD_NAME (0x00) + MP_STR и IPROTO_FIELD_TYPE (0x01) + MP_STR. Кроме того, если в системном спейсе _session_settings задано значение TRUE для sql_full_metadata, то массив будет содержать такие дополнительные ассоциативные массивы, которые соответствуют компонентам, описанным в разделе box.execute():
IPROTO_FIELD_COLL (0x02) + MP_STR
IPROTO_FIELD_IS_NULLABLE (0x03) + MP_BOOL
IPROTO_FIELD_IS_AUTOINCREMENT (0x04) + MP_BOOL
IPROTO_FIELD_SPAN (0x05) + MP_STR или MP_NIL
  • IPROTO_DATA:array of tuples = строки результирующего набора.

Пример: Если мы запросим полные метаданные, вызвав
conn.space._session_settings:update('sql_full_metadata', {{'=', 'value', true}})
и выберем две строки из таблицы t1, в которой есть столбцы DD и Д, с помощью
conn:execute([[SELECT dd, дд AS д FROM t1;]])
мы можем получить такой ответ:

# <body>
msgpack({
    IPROTO_METADATA: [
        IPROTO_FIELD_NAME: 'DD',
        IPROTO_FIELD_TYPE: 'integer',
        IPROTO_FIELD_IS_NULLABLE: false,
        IPROTO_FIELD_IS_AUTOINCREMENT: true,
        IPROTO_FIELD_SPAN: nil,
        IPROTO_FIELD_NAME: 'Д',
        IPROTO_FIELD_TYPE: 'string',
        IPROTO_FIELD_COLL: 'unicode',
        IPROTO_FIELD_IS_NULLABLE: true,
        IPROTO_FIELD_SPAN: 'дд'
    ],
    IPROTO_DATA: [
        [1,'a'],
        [2,'b']'
    ]
})

Если бы вместо этого мы вызвали conn:prepare([[SELECT dd, дд AS д FROM t1;]]), мы получили бы почти такой же ответ, но без IPROTO_DATA и с двумя дополнительными элементами:
34 (IPROTO_BIND_COUNT) и 00 (MP_UINT = 0, нет параметров), 33 (IPROTO_BIND_METADATA) и 90 (MP_ARRAY размера 0, нет параметров).

# <body>
msgpack({
    IPROTO_STMT_ID: {MP_UINT unsigned integer},
    IPROTO_BIND_COUNT: {MP_INT integer},
    IPROTO_BIND_METADATA: {array of parameter descriptors},
        IPROTO_METADATA: [
            IPROTO_FIELD_NAME: 'DD',
            IPROTO_FIELD_TYPE: 'integer',
            IPROTO_FIELD_IS_NULLABLE: false
            IPROTO_FIELD_IS_AUTOINCREMENT: true
            IPROTO_FIELD_SPAN: nil,
            IPROTO_FIELD_NAME: 'Д',
            IPROTO_FIELD_TYPE: 'string',
            IPROTO_FIELD_COLL: 'unicode',
            IPROTO_FIELD_IS_NULLABLE: true,
            IPROTO_FIELD_SPAN: 'дд'
        ]
    })

Теперь обратитесь к файлу исходного кода net_box.c, где функция «decode_metadata_optional» показывает, как Tarantool сам интерпретирует дополнительные элементы.

Далее в разделе Примеры будут рассмотрены байт-коды ответов на вышеприведенные SQL сообщения.

Когда клиент подключается к экземпляру сервера, тот выдает в ответе 128-байтовое текстовое сообщение приветствия не в формате MsgPack:
64 байта — строка 1 текста приветствия
64 байта — строка 2 текста приветствия
44 байта —закодированное в формате base-64 значение соль
20 байтов — NULL

Приветствие содержит две 64-байтные строки текста в формате ASCII. Каждая строка заканчивается символом разрыва строки (\n). Первая строка описывает версию экземпляра и тип протокола. Вторая строка содержит случайную строку в кодировке base-64 размером до 44 байтов для использования в пакете аутентификации и заканчивается на пробелы (до 23).

Часть приветствия представляет собой закодированное в формате base-64 значение соль для сессии (случайная строка), которое можно использовать для аутентификации. Максимальная длина закодированного значения соль (44 байта) больше, чем размер создаваемого сообщения аутентификации. Остаток предназначается для будущих схем аутентификации.

Аутентификация необязательна: если аутентификация не проводится, то пользователем в сеансе будет 'guest' (пользователю 'guest' пароль не нужен).

Если аутентификация проводится, то в любое время может быть подготовлен пакет аутентификации с использованием приветствия, имени и пароля пользователя и функции sha-1, как показано ниже.

PREPARE SCRAMBLE:

    size_of_encoded_salt_in_greeting = 44;
    size_of_salt_after_base64_decode = 32;
     /* sha1() will only use the first 20 bytes */
    size_of_any_sha1_digest = 20;
    size_of_scramble = 20;

prepare 'chap-sha1' scramble:

    salt = base64_decode(encoded_salt);
    step_1 = sha1(password);
    step_2 = sha1(step_1);
    step_3 = sha1(first_20_bytes_of_salt, step_2);
    scramble = xor(step_1, step_3);
    return scramble;

Клиент отправляет пакет аутентификации в виде сообщения IPROTO_AUTH:

# <size>
msgpack({MP_UINT unsigned integer = size(<header>) + size(<body>)})
# <header>
msgpack({
    IPROTO_REQUEST_TYPE: IPROTO_AUTH,
    IPROTO_SYNC: {MP_UINT unsigned integer, usually = 1}
})
# <body>
msgpack({
    IPROTO_USER_NAME: {MP_STRING string <key>},
    IPROTO_TUPLE: ['chap-sha1', {MP_STRING 20-byte string}]
})

<key> содержит имя пользователя. <tuple> должен представлять собой массив из 2 полей: механизм аутентификации (на данный момент поддерживается только механизм «chap-sha1») и сообщение, зашифрованное в соответствии с указанным механизмом.

На пакет аутентификации экземпляр сервера отправляет стандартный ответ с 0 кортежей.

Чтобы понять, как Tarantool это обрабатывает, обратите внимание на функцию netbox_encode_auth` в файле net_box.c.

Стримы и интерактивные транзакции, добавленные в Tarantool v. 2.10.0-beta1, делают возможными две процедуры: последовательную обработку и чередование.

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

Чередование допускает, например, такую последовательность запросов: «begin для стрима 1», «begin для стрима 2», «insert для стрима 1», «insert для стрима 2», «delete для стрима 1», «commit для стрима 1», «rollback для стрима 2».

Эти процедуры возможны, если используется движок vinyl или memtx с mvcc. При этом клиент отвечает за наличие идентификатора стрима, беззнакового целого числа IPROTO_STREAM_ID, в заголовке запроса. Идентификатор IPROTO_STREAM_ID должен быть положительным 64-битовым числом, уникальным для соединения. Если IPROTO_STREAM_ID стрима равен нулю, экземпляр его проигнорирует.

Предположим, что клиент запустил стрим с помощью модуля net.box.

net_box = require('net.box')
conn = net_box.connect('localhost:3302')
stream = conn:new_stream()

В этот момент объект stream будет выглядеть так же, как объект conn, но включать один дополнительный элемент: stream_id. Пусть теперь клиент, используя stream вместо conn, отправит два запроса:

stream.space.T:insert{1}
stream.space.T:insert{2}

Заголовок и тело этих запросов будут такими же, как в обычных запросах IPROTO_INSERT, но заголовок будет содержать дополнительный элемент IPROTO_STREAM_ID=0x0a, где MP_UINT=0x01. В этом примере значение IPROTO_STREAM_ID равно 1, так как при вызове conn:new_stream() идентификатору каждого нового стрима присваивается уникальное значение, начиная с 1.

Клиент запускает транзакцию внутри стрима, отправляя запросы в следующем порядке: IPROTO_BEGIN, запросы на изменение и получение данных транзакции, затем IPROTO_COMMIT или IPROTO_ROLLBACK. Каждый запрос должен содержать переменную IPROTO_STREAM_ID с одним и тем же значением. Используя стримы, не нужно добавлять в заголовок последнего запроса транзакции IPROTO_FLAGS и IPROTO_FLAG_COMMIT. Если транзакция прервется до того, как можно будет выполнить коммит, она будет автоматически отменена.

Таким образом, у приложения есть несколько способов выполнять транзакции. Во-первых, можно использовать модуль net_box с методами stream:begin() и stream:commit()/stream:rollback(), которые отправляют запросы IPROTO_BEGIN и IPROTO_COMMIT/IPROTO_ROLLBACK с текущим значением stream.stream_id. Другой способ — применять методы box.begin() и box.commit()/box.rollback(). Наконец, можно пользоваться инструкциями SQL START TRANSACTION и COMMIT/ROLLBACK.

Сначала нужно отправить запрос IPROTO_JOIN.

# <size>
msgpack({MP_UINT unsigned integer = size(<header>) + size(<body>)})
# <header>
msgpack({
    IPROTO_REQUEST_TYPE: IPROTO_JOIN,
    IPROTO_SYNC: {MP_UINT unsigned integer}
})
# <body>
msgpack({
    IPROTO_INSTANCE_UUID: {uuid}
})

Затем экземпляр, к которому вы хотите подключиться, отправит свой последний SNAP-файл, просто создав несколько запросов типа INSERT (с дополнительными LSN и ServerID) (не отвечайте на них). Затем этот экземпляр отправит MP_MAP vclock и закроет сокет.

# <size>
msgpack({MP_UINT unsigned integer = size(<header>) + size(<body>)})
# <header>
msgpack({
    Response-Code-Indicator: 0,
    IPROTO_SYNC: {MP_UINT unsigned integer}
})
# <body>
msgpack({
    IPROTO_VCLOCK: {MP_INT SRV_ID, MP_INT SRV_LSN}
})

Теперь нужно отправить запрос IPROTO_SUBSCRIBE.

# <size>
msgpack({MP_UINT unsigned integer = size(<header>) + size(<body>)})
# <header>
msgpack({
    IPROTO_REQUEST_TYPE: IPROTO_SUBSCRIBE,
    IPROTO_SYNC: {MP_UINT unsigned integer},
    IPROTO_INSTANCE_UUID: {uuid},
    IPROTO_CLUSTER_UUID: {uuid},
})
# <body>
msgpack({
    IPROTO_VCLOCK: {MP_INT SRV_ID, MP_INT SRV_LSN}
})

Затем нужно обработать каждый запрос, который может прийти через других мастеров. В каждом запросе между мастерами будут дополнительные LSN и SERVER_ID.

Часто мастер отправляет реплике сообщение контрольного сигнала. Например, если есть реплика с ID = 2 и метка с моментом времени в 2020 году, мастер может послать такое сообщение:

# <header>
msgpack({
    IPROTO_REQUEST_TYPE: 0
    IPROTO_REPLICA_ID: 2
    IPROTO_TIMESTAMP: {Float 64 MP_DOUBLE 8-byte timestamp}
})

и реплика может отправить в ответ следующее:

# <header>
msgpack({
    Response-Code-Indicator: IPROTO_OK
    IPROTO_REPLICA_ID: 2
    IPROTO_VCLOCK: {1, 6}
})

Далее в разделе Примеры будут рассмотрены байт-коды вышеприведенных сообщений контрольного сигнала.

При подключении для репликации экземпляр отправляет запрос с заголовком IPROTO_VOTE (0x44). Обычно ответом будет ER_OK и IPROTO_BALLOT (0x29). Поля в IPROTO_BALLOT — это элементы ассоциативного массива:

IPROTO_BALLOT_IS_RO_CFG (0x01) + MP_BOOL
IPROTO_BALLOT_VCLOCK (0x02) + vclock
IPROTO_BALLOT_GC_VCLOCK (0x03) + vclock
IPROTO_BALLOT_IS_RO (0x04) + MP_BOOL
IPROTO_BALLOT_IS_ANON = 0x05 + MP_BOOL
IPROTO_BALLOT_IS_BOOTED = 0x06 + MP_BOOL
IPROTO_BALLOT_CAN_LEAD = 0x07 + MP_BOOL

IPROTO_BALLOT_IS_RO_CFG, IPRO_BALLOT_VCLOCK, IPROTO_BALLOT_GC_VCLOCK и IPROTO_BALLOT_IS_RO добавлены в версии 2.6.1. Константа IPROTO_BALLOT_IS_ANON добавлена в версии 2.7.1. Константа IPROTO_BALLOT_IS_BOOTED добавлена в версиях 2.7.3, 2.8.2 и 2.9.1. В версиях 2.7.3, 2.8.2, 2.9.1 и более поздних константа IPROTO_BALLOT_IS_RO переименована в IPROTO_BALLOT_IS_RO_CFG, а IPROTO_BALLOT_IS_LOADING — в IPROTO_BALLOT_IS_RO.

Значение IPROTO_BALLOT_IS_RO_CFG соответствует значению box.cfg.read_only.

IPROTO_BALLOT_GC_VCLOCK может принимать значение vclock самой старой записи журнала WAL на экземпляре. Это соответствует значению box.info.gc().vclock.

IPROTO_BALLOT_IS_RO принимает значение true, если экземпляр недоступен для записи. Причины у этого могут быть разные: например, экземпляр настроен как read_only, имеет статус orphan или является последователем (follower) при выполнении алгоритма Raft.

Значение IPROTO_BALLOT_IS_ANON соответствует значению box.cfg.replication_anon.

IPROTO_BALLOT_IS_BOOTED принимает значение true, если экземпляр завершил инициализацию или восстановление.

IPROTO_BALLOT_CAN_LEAD is true if the election_mode configuration setting is either „candidate“ or „manual“, so that during the leader election process this instance may be preferred over instances whose configuration setting is „voter“. IPROTO_BALLOT_CAN_LEAD support was added simultaneously in version 2.7.3 and version 2.8.2.

При репликации синхронных транзакций заголовок может содержать ключ = IPROTO_FLAGS и значение MP_UINT = один или несколько битов: IPROTO_FLAG_COMMIT, IPROTO_FLAG_WAIT_SYNC или IPROTO_FLAG_WAIT_ACK.

# <size>
msgpack(:samp:`{{MP_UINT unsigned integer = size(<header>) + size(<body>)}}`)
# <header>
msgpack({
    # ... другие элементы заголовка ...,
    IPROTO_FLAGS: :samp:`{{MP_UINT unsigned integer}}`
})
# <body>
msgpack({
    # ... message for a transaction ...
})

IPROTO_FLAG_COMMIT (0x01) указывает на последнее сообщение для транзакции. IPROTO_FLAG_WAIT_SYNC (0x02) указывает на последнее сообщение для транзакции, которую нельзя завершить немедленно. IPROTO_FLAG_WAIT_ACK (0x04) указывает на последнее сообщение для синхронной транзакции.

A node broadcasts the IPROTO_RAFT request to all the replicas connected to it when the RAFT state of the node changes. It can be any actions changing the state, like starting a new election, bumping the term, voting for another node, becoming the leader, and so on.

If there should be a response, for example, in case of a vote request to other nodes, the response will also be an IPROTO_RAFT message. In this case, the node should be connected as a replica to another node from which the response is expected because the response is sent via the replication channel. In other words, there should be a full-mesh connection between the nodes.

# <size>
msgpack({MP_UINT unsigned integer = size(<header>) + size(<body>)})
# <header>
msgpack({
    IPROTO_REQUEST_TYPE: IPROTO_RAFT,
    IPROTO_REPLICA_ID: {MP_INT integer},  # ID of the replica which the request came from

})
# <body>
msgpack({
    IPROTO_RAFT_TERM: {MP_UINT unsigned integer},     # RAFT term of the instance
    IPROTO_RAFT_VOTE: {MP_UINT unsigned integer},     # Instance vote in the current term (if any).
    IPROTO_RAFT_STATE: {MP_UINT unsigned integer},    # Instance state; one of the three numbers: 1---follower, 2---candidate, 3---leader.
    IPROTO_RAFT_VCLOCK: {MP_ARRAY {{MP_INT SRV_ID, MP_INT SRV_LSN, MP_INT SRV_ID, MP_INT SRV_LSN, ...}}}   # Current vclock of the instance. Presents only on the instances in the "candidate" state (IPROTO_RAFT_STATE == 2).
})

Чтобы выполнить примеры, приведенные в этом разделе, запустите на компьютере с Linux три командных оболочки (терминала).

– На терминале №1 запустите мониторинг порта 3302 с помощью tcpdump:

sudo tcpdump -i lo 'port 3302' -X

На терминале №2 запустите сервер так:

box.cfg{listen=3302}
box.schema.space.create('tspace')
box.space.tspace:create_index('I')
box.space.tspace:insert{280}
box.schema.user.grant('guest','read,write,execute,create,drop','universe')

На терминале №3 запустите ещё один сервер, который будет выступать в качестве клиента, так:

box.cfg{}
net_box = require('net.box')
conn = net_box.connect('localhost:3302')
conn.space.tspace:select(280)

Теперь посмотрите, что tcpdump покажет для запроса на подключение к порту 3302. После слов «length 32» идет пакет, который заканчивается этими 32 байтами (комментарии отделены отступами):

ce 00 00 00 1b   MP_UINT = 27, десятичное число = число байт после этого
82               MP_MAP, размер 2 (назовем это Main-Map)
01                 IPROTO_SYNC (1-й элемент Main-Map)
04                 MP_INT = 4 = число, которое увеличивается на 1 с каждым запросом
00                 IPROTO_REQUEST_TYPE (2-й элемент Main-Map)
01                 IPROTO_SELECT
86                 MP_MAP, размер 6 (назовем это Select-Map)
10                   IPROTO_SPACE_ID (1-й элемент Select-Map)
cd 02 00             MP_UINT = 512, десятичное число = id tspace (может быть больше)
11                   IPROTO_INDEX_ID (2-й элемент Select-Map)
00                   MP_INT = 0 = id индекса в tspace
14                   IPROTO_ITERATOR (3-й элемент Select-Map)
00                   MP_INT = 0 = константа Tarantool iterator_type.h ITER_EQ
13                   IPROTO_OFFSET (4-й элемент Select-Map)
00                   MP_INT = 0 = смещение
12                   IPROTO_LIMIT (5-й элемент Select-Map)
ce ff ff ff ff       MP_UINT = 4294967295 = наибольший возможный предел
20                   IPROTO_KEY (6-й элемент Select-Map)
91                   MP_ARRAY, размер 1 (назовем это Key-Array)
cd 01 18               MP_UINT = 280 (6-й элемент Select-Map, 1-й элемент Key-Array)
                       -- 280, ключевое значение, которое мы ищем

Теперь в файле исходного кода net_box.c перейдите к строке netbox_encode_select(lua_State *L). Из комментариев и из простых вызовов функций типа mpstream_encode_uint(&stream, IPROTO_SPACE_ID); можно понять, как net_box собирает воедино содержимое пакета, описанного выше с помощью tcpdump.

Существуют библиотеки для чтения и записи объектов в формате MessagePack. Программисты на языке C иногда включают msgpuck.h.

Теперь вы знаете, как сам Tarantool выполняет запросы по бинарному протоколу. Если какие-то детали остаются неясными, обратитесь к файлу net_box.c, где описаны процедуры для каждого запроса. Некоторые коннекторы написаны аналогично.

Рассмотрим пример IPROTO_UPDATE. Предположим, пользователь изменяет поле №2 кортежа №2 в спейсе №256 на 'BBBB'`. Тело будет выглядеть так (обратите внимание, что в этом случае дополнительный необязательный элемент ассоциативного массива IPROTO_INDEX_BASE подчеркивает, что номера полей начинаются с 1 — это можно опустить):

04               IPROTO_UPDATE
85               IPROTO_MAP, размер 5
10                 IPROTO_SPACE_ID, 1-й элемент ассоциативного массива
cd 02 00           MP_UINT 256
11                 IPROTO_INDEX_ID, 2-й элемент ассоциативного массива
00                 MP_INT 0 = номер индекса первичного ключа
15                 IPROTO_INDEX_BASE, 3-й элемент ассоциативного массива
01                 MP_INT = 1, т.е. нумерация полей начинается с 1
21                 IPROTO_TUPLE, 4-й элемент ассоциативного массива
91                 MP_ARRAY, размер 1, для массива операций
93                   MP_ARRAY, размер 3
a1 3d                   MP_STR = OPERATOR = '='
02                      MP_INT = FIELD_NO = 2
a5 42 42 42 42 42       MP_STR = VALUE = 'BBBB'
20                 IPROTO_KEY, 5--й элемент ассоциативного массива
91                 MP_ARRAY, размер 1, для массива ключей
02                   MP_UINT = значение первичного ключа = 2

Пример байт-кода IPROTO_EXECUTE:

0b               IPROTO_EXECUTE
83               MP_MAP, размер 3
43                 IPROTO_STMT_ID 1-й элемент ассоциативного массива
ce d7 aa 74 1b     MP_UINT значение n.stmt_id
41                 IPROTO_SQL_BIND 2-й элемент ассоциативного массива
92                 MP_ARRAY, размер 2
01                   MP_INT = 1 = значение первого параметра
a1 61                MP_STR = 'a' = значение второго параметра
2b                 IPROTO_OPTIONS 3-й элемент ассоциативного массива
90                 MP_ARRAY, размер 0 (никакие опции не выбраны)

Пример байт-кода ответа на запрос box.space.space-name:insert{6}:

ce 00 00 00 20                MP_UINT = размер заголовка и тела
83                            MP_MAP, размер 3
00                              индикатор кода ответа
ce 00 00 00 00                  MP_UINT = IPROTO_OK
01                              IPROTO_SYNC
cf 00 00 00 00 00 00 00 53      MP_UINT = значение синхронизации
05                              IPROTO_SCHEMA_VERSION
ce 00 00 00 68                  MP_UINT = версия схемы
81                            MP_MAP, размер 1
30                              IPROTO_DATA
dd 00 00 00 01                  MP_ARRAY, размер 1 (число строк)
91                              MP_ARRAY, размер 1 (число полей)
06                              MP_INT = 6 = добавленное значение

Пример байт-кода ответа на запрос conn:eval([[box.schema.space.create('_space');]]):

ce 00 00 00 3b                  MP_UINT = размер заголовка и тела
83                              MP_MAP, размер 3 (3 элемента в заголовке)
   00                              индикатор кода ответа
   ce 00 00 80 0a                  MP_UINT = шестнадцатеричное значение 800a
   01                              IPROTO_SYNC
   cf 00 00 00 00 00 00 00 26      MP_UINT = значение синхронизации
   05                              IPROTO_SCHEMA_VERSION
   ce 00 00 00 78                  MP_UINT = версия схемы
   81                              MP_MAP, размер 1
     31                              IPROTO_ERROR_24
     db 00 00 00 1d 53 70 61 63 ... MP_STR = "Space '_space' already exists"

Подключимся через то же соединение net.box, что приводилось в примерах, и выполним следующее:
conn:execute([[CREATE TABLE t1 (dd INT PRIMARY KEY AUTOINCREMENT, дд STRING COLLATE "unicode");]])
conn:execute([[INSERT INTO t1 VALUES (NULL, 'a'), (NULL, 'b');]])
Результат вывода tcpdump покажет два интересных момента: (1) инструкция CREATE привела к изменению схемы, так что в ответе указано новое значение IPROTO_SCHEMA_VERSION. При этом тело включает новое содержимое некоторых системных таблиц, полученное в результате невидимых для пользователя запросов от net.box; (2) последние байты ответа на INSERT будут следующими:

81   MP_MAP, размер 1
42     IPROTO_SQL_INFO
82     MP_MAP, размер 2
00       константа Tarantool (не из iproto_constants.h) = SQL_INFO_ROW_COUNT
02       1 = число строк
01       константа Tarantool (не из iproto_constants.h) = SQL_INFO_AUTOINCREMENT_ID
92       MP_ARRAY, размер 2
01         первое число с автоинкрементом
02         второе число с автоинкрементом

Пример байт-кода SQL SELECT. Запросим полные метаданные, вызвав conn.space._session_settings:update('sql_full_metadata', {{'=', 'value', true}}), и выберем две строки из только что созданной таблицы: conn:execute([[SELECT dd, дд AS д FROM t1;]]). tcpdump выдаст следующий ответ (после заголовка):

82                       MP_MAP, размер 2 (метаданные и строки)
32                         IPROTO_METADATA
92                         MP_ARRAY, размер 2 (2 столбца)
85                           MP_MAP, размер 5 (5 элементов для столбца 1)
00 a2 44 44                    IPROTO_FIELD_NAME и 'DD'
01 a7 69 6e 74 65 67 65 72     IPROTO_FIELD_TYPE и 'integer'
03 c2                          IPROTO_FIELD_IS_NULLABLE и false
04 c3                          IPROTO_FIELD_IS_AUTOINCREMENT и true
05 c0                          PROTO_FIELD_SPAN и nil
85                           MP_MAP, размер 5 (5 элементов для столбца 2)
00 a2 d0 94                    IPROTO_FIELD_NAME и 'Д' в верхнем регистре
01 a6 73 74 72 69 6e 67        IPROTO_FIELD_TYPE и 'string'
02 a7 75 6e 69 63 6f 64 65     IPROTO_FIELD_COLL и 'unicode'
03 c3                          IPROTO_FIELD_IS_NULLABLE и true
05 a4 d0 b4 d0 b4              IPROTO_FIELD_SPAN и 'дд' в нижнем регистре
30                         IPROTO_DATA
92                         MP_ARRAY, размер 2
92                           MP_ARRAY, размер 2
01                             MP_INT = 1: содержимое строки 1, столбца 1
a1 61                          MP_STR = 'a': содержимое строки 1, столбца 2
92                           MP_ARRAY, размер 2
02                             MP_INT = 2: содержимое строки 2, столбца 1
a1 62                          MP_STR = 'b': содержимое строки 2, столбца 2

Пример байт-кода SQL PREPARE. Если вызвать conn:prepare([[SELECT dd, дд AS д FROM t1;]]), вывод tcpdump будет почти таким же, но исчезнет IPROTO_DATA. Вместо этого появятся дополнительные байты:

34                       IPROTO_BIND_COUNT
00                       MP_UINT = 0

33                       IPROTO_BIND_METADATA
90                       MP_ARRAY, размер 0

MP_UINT = 0. Массив MP_ARRAY имеет размер 0, поскольку параметров нет. Вывод целиком:

84                       MP_MAP, размер 4
43                         IPROTO_STMT_ID
ce c2 3c 2c 1e             MP_UINT = ID инструкции
34                         IPROTO_BIND_COUNT
00                         MP_INT = 0 = число привязываемых параметров
33                         IPROTO_BIND_METADATA
90                         MP_ARRAY, размер 0 = нет привязываемых параметров
32                         IPROTO_METADATA
92                         MP_ARRAY, размер 2 (2 столбца)
85                           MP_MAP, размер 5 (5 элементов для столбца 1)
00 a2 44 44                    IPROTO_FIELD_NAME и 'DD'
01 a7 69 6e 74 65 67 65 72     IPROTO_FIELD_TYPE и 'integer'
03 c2                          IPROTO_FIELD_IS_NULLABLE и false
04 c3                          IPROTO_FIELD_IS_AUTOINCREMENT и true
05 c0                          PROTO_FIELD_SPAN и nil
85                           MP_MAP, размер 5 (5 элементов для столбца 2)
00 a2 d0 94                    IPROTO_FIELD_NAME + 'Д' в верхнем регистре
01 a6 73 74 72 69 6e 67        IPROTO_FIELD_TYPE и 'string'
02 a7 75 6e 69 63 6f 64 65     IPROTO_FIELD_COLL и 'unicode'
03 c3                          IPROTO_FIELD_IS_NULLABLE и true
05 a4 d0 b4 d0 b4              IPROTO_FIELD_SPAN и 'дд' в нижнем регистре

Пример байт-кода контрольного сигнала. Мастер может отправить следующее тело:

83                      MP_MAP, размер 3
00                        1-й элемент Main-Map IPROTO_REQUEST_TYPE
00                          MP_UINT = 0
02                        2-й элемент Main-Map IPROTO_REPLICA_ID
02                          MP_UINT = 2 = id
04                        3-й элемент Main-Map IPROTO_TIMESTAMP
cb                          MP_DOUBLE (MessagePack "Float 64")
41 d7 ba 06 7b 3a 03 21     8-байтовая временная отметка

Пример байт-кода ответа на контрольный сигнал. Реплика может вернуть следующее тело:

81                       MP_MAP, размер 1
00                         1-й элемент Main-Map: индикатор кода ответа
00                         MP_UINT = 0 = IPROTO_OK
81                         2-й элемент Main-Map: MP_MAP, размер 1 (Sub-Map)
26                           1-й элемент Sub-Map: IPROTO_VCLOCK
81                           2-й элемент Sub-Map: MP_MAP, размер 1
01                             MP_UINT = 1 = id (1-я часть vclock)
06                             MP_UINT = 6 = lsn (2-я часть vclock)

Файлы форматов .xlog и .snap выглядят практически одинаково. Заголовок выглядит так:

<type>\n                  SNAP\n или XLOG\n
<version>\n               в данный момент 0.13\n
Server: <server_uuid>\n   где UUID -- это 36-байтная строка
VClock: <vclock_map>\n    например, {1: 0}\n
\n

После файла заголовка идут кортежи с данными. Кортежи начинаются с маркера строки 0xd5ba0bab, а после последнего кортежа может стоять маркер конца файла 0xd510aded. Таким образом, между заголовком файла и маркером конца файла могут быть кортежи с данными в следующем виде:

0            3 4                                         17
+-------------+========+============+===========+=========+
|             |        |            |           |         |
| 0xd5ba0bab  | LENGTH | CRC32 PREV | CRC32 CUR | PADDING |
|             |        |            |           |         |
+-------------+========+============+===========+=========+
   MP_FIXEXT2    MP_INT     MP_INT       MP_INT      ---

+============+ +===================================+
|            | |                                   |
|   HEADER   | |                BODY               |
|            | |                                   |
+============+ +===================================+
     MP_MAP                     MP_MAP

См. пример в разделе Форматы файлов.