Spec-Zone.ru › RethinkDB java

Написание драйверов RethinkDB

  • Начальные шаги
  • Открытие соединения
  • Выполнение рукопожатия
  • Сериализация запросов
  • Отправка сообщения
  • Получение ответов
  • Примечания по соединениям
  • Получение помощи

Клиентские драйверы RethinkDB отвечают за сериализацию запросов, отправку их на сервер с помощью протокола ReQL wire, а также получение ответов от сервера и их возврат вызывающему приложению. Этот процесс проходит следующие этапы:

  • Открытие соединения
  • Выполнение рукопожатия
  • Сериализация запроса
  • Отправка сообщения
  • Получение ответов

Для получения обновлений о изменениях протокола и поведения в новых версиях RethinkDB и общей помощи по написанию драйверов, присоединяйтесь к группе Google RethinkDB-Dev.

Начальные шаги

Типы и команды ReQL определены в файле ql2.proto.

Для получения JavaScript-версии файла, выполните make js-driver в репозитории rethinkdb, и получите JSON-версию файла в build/packages/js/proto-def.js. Также вы можете получить эквивалентный файл из rethinkdbdash.

Файл ql2.proto хорошо прокомментирован, показывая аргументы и вывод для каждой команды.

Открытие соединения

Открытие TCP-соединения с сервером на порту драйвера. По умолчанию порт равен 28015.

Выполнение рукопожатия

С версией V1_0 протокола, протокол рукопожатия изменился по сравнению с предыдущими версиями.

Версия V1_0

  1. Клиент отправляет «магическое число» (0x34c2bdc3) для версии протокола как 32-битное целое число в формате little-endian (4 байта).

     SEND c3 bd c2 34
    
  2. В случае успеха, сервер отправляет ответ в формате JSON с нулевым завершением, указывающий на успех, минимальную и максимальную версии протокола, а также версию сервера.

     {
         "success": true,
         "min_protocol_version": 0,
         "max_protocol_version": 0,
         "server_version": "2.3.0"
     }
    

    В случае неудачи, сервер отправляет строку ошибки с нулевым завершением (не JSON).

     ERROR: Received an unsupported protocol version. This port is for RethinkDB queries. Does your client driver version not match the server?
    
  3. Клиент отправляет версию протокола, метод аутентификации и аутентификационные данные в формате JSON с нулевым завершением. В настоящее время RethinkDB поддерживает только один метод аутентификации, SCRAM-SHA-256, как указано в IETF RFC 7677 и RFC 5802. RFC соблюдается за исключением обработки ошибок (RethinkDB использует собственную обработку ошибок более высокого уровня, а не поле e=). RethinkDB не поддерживает привязку каналов, и клиенты не должны запрашивать это. Значение "authentication" — «сообщение клиента — первое сообщение», указанное в RFC 5802 (флаг привязки канала, необязательная идентификация аутентификации SASL, имя пользователя (n=) и случайный nonce (r=)).

     {
         "protocol_version": 0,
         "authentication_method": "SCRAM-SHA-256",
         "authentication": "n,,n=user,r=rOprNGfwEbeRWgbNEkqO"
     }
    
  4. Сервер отправляет ответ в формате JSON с нулевым завершением, со значением "success" равным true или false. В случае true, тогда "authentication" будет содержать «сообщение сервера — первое сообщение», содержащее счетчик итераций (i=), соль (s=) и конкатенацию клиентского nonce с собственным nonce.

     {
         "success": true,
         "authentication": "r=rOprNGfwEbeRWgbNEkqO%hvYDpWUa2RaTCAfuxFIlj)hNlF$k0,
           s=W22ZaJ0SNY7soEsUEjb6gQ==,i=4096"
     }
    

    В случае false, сервер отправит ошибку и код ошибки.

     {
         "success": false,
         "error": "You mucked up.",
         "error_code": 12
     }
    

    Должна быть выброшена ошибка ReqlAuthError, если код ошибки находится в диапазоне от 10 до 20 включительно.

  5. Клиент отправляет «сообщение клиента — последнее сообщение» в формате JSON с нулевым завершением с тем же nonce и вычисленной ClientProof в соответствии с RFC.

     {
         "authentication": "c=biws,r=rOprNGfwEbeRWgbNEkqO%hvYDpWUa2RaTCAfuxFIlj)hNlF$k0,
           p=dHzbZapWIk4jUhN+Ute9ytag9zjfMHgsqmmiz7AndVQ="
     }
    
  6. Сервер отправляет ответ в формате JSON с нулевым завершением, со значением "success" равным true или false. В случае true, тогда "authentication" будет содержать «сообщение сервера — последнее сообщение» со значением ServerSignature. Клиент должен вычислить ServerSignature в соответствии с RFC и проверить, что значения идентичны.

     {
         "success": true,
         "authentication": "v=6rriTRBi23WpRR/wtup+mMhUZUn/dB5nLTJRsjl95G4="
     }
    

    В случае false, сервер отправит ошибку и код ошибки, как указано выше.

Примечание: Можно оптимизировать рукопожатие, отправив сообщение #3 сразу после #1, не дожидаясь ответа сервера, и затем прочитав сообщения #2 и #4, обработав их соответствующим образом.

Версии V0_3 и V0_4

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

  1. Отправить версию протокола как 32-битное целое число в формате little-endian (4 байта). Примечание: все инструкции ниже предполагают протокол версии V0_3 или выше. Текущий протокол по состоянию на RethinkDB 2.0 равен V0_4.
  2. Отправить длину ключа авторизации как 32-битное целое число в формате little-endian (4 байта). Отправить 0 если ключа авторизации нет.
  3. Отправить ключ авторизации как строку ASCII. Если ключа авторизации нет, пропустите этот шаг.
  4. Отправить тип протокола как 32-битное целое число в формате little-endian (4 байта). Типы протоколов определены в перечислении Protocol в ql2.proto. Новые драйверы должны использовать JSON, 0x7e6970c7.

Сервер ответит строкой ASCII с нулевым завершением, описывающей результат рукопожатия. Если строка равна "SUCCESS", клиент может перейти к этапу 2 и начать отправку запросов. Любая другая строка указывает на ошибку. Сервер закроет соединение, и драйвер должен сообщить об этой ошибке пользователю.

Пример 1: Без ключа авторизации

Шаг Направление Элемент Байты
1 ОТПРАВИТЬ V0_4 20 2d 0c 40
2 ОТПРАВИТЬ размер ключа 00 00 00 00
3 ОТПРАВИТЬ ключ авторизации
4 ОТПРАВИТЬ JSON c7 70 69 7e
5 ПОЛУЧИТЬ успех 53 55 43 43 45 53 53

Пример 2: С ключом авторизации

Шаг Направление Элемент Байты
1 ОТПРАВИТЬ V0_4 20 2d 0c 40
2 ОТПРАВИТЬ размер ключа 07 00 00 00
3 ОТПРАВИТЬ ключ авторизации 68 75 6e 74 65 72 32
4 ОТПРАВИТЬ JSON c7 70 69 7e
5 ПОЛУЧИТЬ успех 53 55 43 43 45 53 53

Сериализация запросов

Ваш драйвер должен назначать каждому запросу уникальный 8-байтовый токен на каждое соединение. (Официальные драйверы RethinkDB реализуют это как безызбыточный 8-байтовый счётчик в формате little-endian на каждое соединение.) Сервер будет отправлять ответы на запросы, используя этот токен в качестве идентификатора, чтобы ответ можно было сопоставить с его запросом. Токен также может быть использован для запроса большего объёма данных для запроса, если все результаты не были возвращены в первом ответе.

Простой пример

Следующий раздел объяснит, как создавать сложные запросы. Сейчас мы просто отправим строку "foo" (r.expr("foo")) на сервер.

Отправка запроса на сервер проходит следующие этапы:

  • Сериализация запроса в UTF8-кодированный JSON
  • Отправка на сервер следующих данных:
    • Уникальный 8-байтовый токен запроса
    • Размер JSON-сериализованного, UTF8-кодированного запроса как 4-байтовое целое число в формате little-endian
    • Обёрнутое сообщение запроса (QueryType, сериализованный запрос и параметры)

Обёрнутое сообщение запроса, отправленное на сервер, представляет собой массив из трёх элементов:

[ QueryType, query, options ]

Следующий раздел рассмотрит подробности, но в нашем примере QueryType равно 1 (или START, как мы увидим позже), query — просто строка "foo", и нет параметров.

[ 1, "foo", {} ]

Таким образом, отправляемые на сервер данные имеют следующий вид:

Шаг Элемент Передаваемые байты
1 токен запроса 00 00 00 00 00 00 00 01
2 длина 0c 00 00 00
3 запрос [1,"foo",{}]

После отправки запроса вы можете получить объект ответа с сервера. Объект ответа имеет следующий вид:

  • Уникальный 8-байтовый токен запроса
  • Длина ответа как 4-байтовое целое число в формате little-endian
  • JSON-кодированный ответ
Шаг Элемент Байты в сети
1 токен запроса 00 00 00 00 00 00 00 01
2 длина 13 00 00 00
3 ответ {"t":1,"r":["foo"]}

При разборе строки ответа как JSON, вы получаете объект:

{
    t: 1,         // protodef.Response.ResponseType.SUCCESS_ATOM
    r: ["foo"]    // the response is the string 'foo"
}

Где t:1 означает, что ответ — значение, а r: ["foo"] — строка "foo".

Запросы подробно

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

Внутренне запросы представлены в виде деревьев. Запрос:

r.db("blog").table("users").filter({name: "Michel"})

представлен этим деревом:

Query tree illustration

Команды ReQL

Команды ReQL представлены как список из двух или трех элементов.

[<command>, [<arguments>], {<options>}]
  • <command> — целое число, представляющее команду, начиная с ql2.proto
  • <arguments> — список всех аргументов. Каждый аргумент сам по себе является запросом (списком команд или данными).
  • <options> — необязательные аргументы команды. Этот элемент можно опустить, если команда не имеет заданных необязательных аргументов.

Таким образом, наш предыдущий запрос представлен так:

r.db("blog").table("users").filter({name: "Michel"});

FILTER = 39     // from ql2.proto
TABLE = 15
DB = 14

r.db("blog") =>
    [14, ["blog"]]

r.db("blog").table("users") =>
    [15, [[14, ["blog"]], "users"]]

r.db("blog").table("users").filter({name: "Michel"}) =>
    [39, [[15, [[14, ["blog"]], "users"]], {"name": "Michel"}]]

Учитываемые моменты реализации

Если вы хотите использовать префиксную запись, вам нужно реализовать все команды в модуле. Если вы хотите использовать инфиксную запись, вам следует реализовать все функции в классе «term» и некоторые префиксные команды в модуле.

Вы можете проверять арность методов только до определенной степени. Если термин ARGS является одним из аргументов, только сервер может эффективно проверить, предоставлено достаточно аргументов (или не слишком много). Ошибки арности, сообщаемые сервером, предполагают префиксную запись. Вещи могут измениться, если решение в #2463 будет реализовано.

Данные ReQL

Данные (единственное число от «данных») — это любое значение, которое можно представить в формате JSON: булевы значения, числа, строки, объекты, массивы и null. Они отправляются на сервер в формате JSON.

Однако массивы — это особый случай: поскольку команды ReQL (как описано выше) отправляются как массивы, вы должны отправлять массивы данных как аргументы к команде MAKE_ARRAY. Таким образом, массив

[10, 20, 30]

будет отправлен на сервер как

// MAKE_ARRAY = 2 (from ql2.proto)

[2, [10, 20, 30]]

Псевдотипы ReQL

Некоторые встроенные типы данных ReQL не имеют непосредственного представления в JSON. Они реализованы как псевдотипы — JSON-объекты со специальным ключом $reql_type$. Три официальных драйвера ReQL преобразуют даты и бинарные типы в псевдотипы.

Псевдотип даты

{
    $reql_type: "TIME",
    epoch_time: <timestamp>,
    timezone: <string>
}

Поле epoch_time — это метка времени Unix, количество секунд с 1 января 1970 года с точностью до миллисекунды. Поле timezone — это строка в формате [+-]HH:MM, указывающая смещение от UTC. UTC — это +00:00; PST — -08:00; и так далее.

Псевдотип бинарного типа

{
    $reql_type$: "BINARY",
    data: <string>
}

Поле data — это строка Base64, закодированная из бинарного объекта.

Анонимные функции

Хорошая статья Билла Роуэна объясняет анонимные функции (или лямбда-функции) в драйверах. Статья охватывает, почему анонимные функции полезны и как они работают. Здесь мы сосредоточимся только на том, как сериализовать анонимные функции.

Когда драйвер находит анонимную функцию, он возвращает объект запроса, подобный этому:

// FUNC = 69, MAKE_ARRAY = 2 (from ql2.proto)

[69, [[2, [p1, p2, ...]], function body]]

Параметры представлены как значения <p1>, <p2>, и т.д.; значения произвольные, но должны быть уникальными для каждого запроса, чтобы избежать коллизий. Внутри тела функции значения ссылаются на термин запроса VAR, определенный как 10 в ql2.proto. Таким образом, значение параметра 1 извлекается с помощью [10, [1]].

Рассмотрим функцию:

function(x, y, z) {
    return r.add(x, y, z)
}

Функция будет сериализована как:

[FUNC, 
 [[MAKE_ARRAY, [1, 2, 3]],
  [ADD,
   [[VAR, [1]],
    [VAR, [2]],
    [VAR, [3]]]]]]

// FUNC = 69, MAKE_ARRAY = 2, ADD = 24, VAR = 10 (from ql2.proto)

[69, [[2, [1, 2, 3]], [24, [[10, [1]], [10, [2]], [10, [3]]]]]]

Детали реализации

Сериализация функций сильно зависит от языка вашего драйвера. Драйвер JavaScript делает это так:

  • Посмотрите, сколько аргументов принимает функция (num_args)
  • Создайте столько же терминов VAR
  • Вызовите функцию с этими терминами
  • Сериализуйте результат как тело функции

Если ваш драйвер использует инфиксную запись, вы должны убедиться, что термин VAR реализует все методы ReQL.

Сериализация IMPLICIT_VAR (r.row)

Термин IMPLICIT_VAR эквивалентен команде row в официальных драйверах JavaScript и Python. Это полезно для языков, где анонимные функции слишком громоздки.

Если вы поддерживаете IMPLICIT_VAR в своем драйвере, то каждый раз при разборе аргумента функции вы должны проверить, может ли метод принять функцию. Если может, вы должны искать термин IMPLICIT_VAR (т.е., row). Если найдете, оберните аргумент в функцию, принимающую один параметр:

[69, [[2, [1]], argument]]

Если не найдете, обработайте аргумент обычно.

В случае вложенных функций термин IMPLICIT_VAR неоднозначен и не должен использоваться. Ваш драйвер должен либо выдать ошибку, либо позволить серверу вернуть ошибку.

Сериализация BINARY

Бинарные объекты, созданные с помощью r.binary, могут быть сериализованы двумя разными способами.

Если аргумент является термином ReQL (не включая данные), сериализуйте его с использованием стандартного термина:

[BINARY, argument]

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

{
    $reql_type$: "BINARY",
    data: <base64 string>
}

Сериализация FUNCALL (r.do)

Команда r.do() сериализуется с помощью термина FUNCALL.

[FUNCALL, [function], arguments]

Рассмотрим команду do:

r.do(10, 20, function (x, y) {
  return r.add(x, y);
})

Это будет сериализовано как:

[FUNCALL,
  [FUNC,
    [[MAKE_ARRAY, [1, 2]],
      [ADD,
        [[VAR, [1]],
         [VAR, [2]]]]]],
  10,
  20]

// FUNCALL = 64, FUNC = 69, MAKE_ARRAY = 2, ADD = 24, VAR = 10

[64, [69, [[2, [1, 2]], [24, [[10, [1]], [10, [2]]]]]], 10, 20]

Обратите внимание, что хотя r.do() принимает функцию как последний аргумент, FUNCALL сериализует функцию как первый аргумент.

Отправка сообщения

Поскольку вы можете продолжать цепочку команд (или вызывать их в префиксной записи), вам нужна команда, которая сигнализирует о конце цепочки и отправке запроса на сервер. Эта команда — run в официальных драйверах.

Оборачивание запросов

После обработки команды run, сериализованный запрос должен быть обернут в сообщение, отправленное на сервер. Полное сообщение имеет вид:

[ QueryType, query, options ]

Типы запросов определены в ql2.proto. Когда запрос впервые отправляется на сервер, он будет отправлен с QueryType значения START (1). Опции (иногда называемые «глобальными optargs») — это опции, передаваемые самой команде run; см. документацию run для получения полного списка. (Команды, отправляемые на сервер, используют snake_case, а не camelCase.)

Полный список значений QueryType следующий:

  • 1 START: Начать новый запрос.
  • 2 CONTINUE: Продолжить запрос, который вернул SUCCESS_PARTIAL (см. Получение ответов).
  • 3 STOP: Остановить запрос, который все еще выполняется.
  • 4 NOREPLY_WAIT: Подождать завершения операций noreply. Сервер вернет ответ WAIT_COMPLETE.
  • 5 SERVER_INFO: Запросить информацию о сервере. Сервер вернет ответ SERVER_INFO.

CONTINUE и STOP должны быть отправлены по одному соединению с тем же токеном, сгенерированным для сообщения START запроса.

Отправка запросов

Итак, отправка запроса на сервер включает следующие шаги:

  • Сериализуйте запрос в виде JSON, закодированного в UTF8
  • Отправьте следующие данные на сервер:
    • Уникальный 8-байтовый токен запроса
    • Размер закодированного в UTF8 JSON-сериализованного обернутого запроса, как 4-байтовое целое число в формате little-endian
    • Обернутое сообщение запроса (QueryType, сериализованный запрос и опции)

Токен — это уникальное целое число для каждого подключения. Ведение счетчика для каждого подключения — простой способ его реализации.

Таким образом, наш исходный пример запроса:

r.db("blog").table("users").filter({name: "Michel"})

отправляется по сети следующим образом:

Шаг Семантическая команда Переданное
1 токен запроса 00 00 00 00 00 00 00 01
2 длина 3C 00 00 00
3 запрос [1,[39,[[15,[[14,["blog"]],"users"]],{"name":"Michel"}]],{}]

Оборачивание опции запроса к базе данных

Если опция db передается команде run, ее значение должно быть термином DB. Запрос:

r.table("users").run({db: "blog"});

должен быть отправлен так, как будто аргумент к db был r.db("blog"):

[1,[15,["users"]],{"db":[14,["blog"]]}]

Получение ответов

Ответы от сервера имеют следующий вид:

  • 8-байтовый уникальный токен запроса, которому соответствует ответ
  • Размер закодированного в JSON ответа, как 4-байтовое целое число в формате little-endian
  • Закодированный в JSON объект Response

Объект Response будет иметь следующие поля:

  • t: ResponseType, как определено в ql2.proto
  • r: данные из результата, как массив JSON
  • b: трассировка стека, если t является типом ошибки; это поле не будет присутствовать в противном случае
  • p: профиль, если был указан глобальный параметр profile: true; это поле не будет присутствовать в противном случае
  • n: необязательный массив значений ResponseNote, как определено в ql2.proto

Типы ответов

Эти значения будут числовыми, соответствующими типам в ql2.proto.

  • 1 SUCCESS_ATOM: Весь запрос возвращён, и результат находится в первом (и единственном) элементе r.
  • 2 SUCCESS_SEQUENCE: Либо весь запрос возвращён в r, либо возвращена последняя часть многочастного запроса.
  • 3 SUCCESS_PARTIAL: Запрос вернул поток, который может быть или не быть полным. Чтобы получить больше результатов для запроса, отправьте сообщение CONTINUE (см. ниже).
  • 4 WAIT_COMPLETE: Этот ResponseType указывает, что все запросы, выполненные в режиме noreply, завершили выполнение. r будет пустым.
  • 5 SERVER_INFO: Ответ на запрос SERVER_INFO. Данные будут в первом (и единственном) элементе r.
  • 16 CLIENT_ERROR: Сервер не смог выполнить запрос из-за плохого запроса клиента. Сообщение об ошибке будет в первом элементе r.
  • 17 COMPILE_ERROR: Сервер не смог выполнить запрос из-за ошибки компиляции ReQL. Сообщение об ошибке будет в первом элементе r.
  • 18 RUNTIME_ERROR: Запрос был успешно скомпилирован, но завершился ошибкой во время выполнения. Сообщение об ошибке будет в первом элементе r.

Примечания к ответам

Поле n, если оно присутствует, будет массивом одного или нескольких значений ResponseNote, предоставляющих дополнительную информацию о типе возвращаемого потока. Эти значения будут числовыми, соответствующими заметкам в ql2.proto.

Все заметки к ответам относятся к changefeeds; прочитайте Changefeeds в RethinkDB для более подробной информации.

  • 1 SEQUENCE_FEED: Поток является changefeed.
  • 2 ATOM_FEED: Поток является точечным changefeed, т.е. возвращает изменения из одного документа.
  • 3 ORDER_BY_LIMIT_FEED: Поток является changefeed, сгенерированным с запросом order_by().limit().
  • 4 UNIONED_FEED: Поток является объединением нескольких типов changefeed, которые нельзя свести к одному типу, например, r.table('test').changes().union(r.table('test').get(0).changes()).
  • 5 INCLUDES_STATES: Поток является changefeed, который включает заметки о состояниях, например, `{state: ‘initializing’}.

Многочастные ответы

Потоки и каналы — это лениво вычисляемые последовательности, и они возвращают ResponseType SUCCESS_PARTIAL (3), с текущими доступными данными в массиве r. Когда драйвер получает канал или поток, он должен вернуть курсор (или объект с интерфейсом, похожим на курсор). *Примечание*: ответы SUCCESS_SEQUENCE и SUCCESS_PARTIAL оба должны быть представлены как курсоры. В зависимости от размера результатов запроса и времени, необходимого для их возврата, вы можете получить один результат SUCCESS_SEQUENCE, или один или несколько результатов SUCCESS_PARTIAL, за которым следует окончательный результат SUCCESS_SEQUENCE.

Чтобы получить больше данных для курсора, драйвер должен отправить запрос с QueryType CONTINUE на том же соединении с тем же токеном. Как и другие запросы, это должно быть отправлено с токеном запроса, размером запроса и самим запросом, просто [2].

Шаг Элемент Переданные байты
1 токен 00 00 00 00 00 00 00 01
2 длина 03 00 00 00
3 запрос [2]

Вы получите другой ответ, либо типа SUCCESS_PARTIAL, указывая, что больше данных доступно, либо SUCCESS_SEQUENCE , если вы достигли конца потока. (Это никогда не будет возвращено для канала.) Обратите внимание, что эти ResponseType могут быть возвращены без данных (пустой массив в качестве значения r). Драйвер может отправить CONTINUE для получения следующей порции последовательности, как только ответ будет получен.

Чтобы закрыть курсор и прекратить получение данных из потока или канала, отправьте запрос с QueryType STOP на том же соединении с тем же токеном.

Примечания по подключениям

Начиная с RethinkDB 2.0 (V0_4), сервер будет обрабатывать несколько запросов параллельно, а не последовательно, и нет гарантии, что чтение после записи на том же соединении «увидит» результаты записи, пока она успешна. (Предыдущие версии сервера обрабатывали несколько запросов на одном соединении последовательно.)

Вы не должны освобождать соединение в пуле сразу после получения ответа. Освобождайте соединение только при получении ответа другого типа, чем SUCCESS_PARTIAL.

Получение помощи

Вы можете задавать вопросы и получать информацию о внесённых изменениях в новых версиях RethinkDB на форуме Google Group RethinkDB-Dev. Вы также можете посетить IRC-канал RethinkDB, где часто бывают разработчики ядра и другие разработчики драйверов. Кроме того, вы можете задавать вопросы на Stack Overflow с тегом «rethinkdb».

© RethinkDB contributors
Licensed under the Creative Commons Attribution-ShareAlike 3.0 Unported License.
https://rethinkdb.com/docs/writing-drivers/

Spec-Zone.ru

Настройки Оффлайн Что нового Помощь О нас
Spec-Zone .ru
спецификации, руководства, описания, API