Написание драйверов 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
-
Клиент отправляет «магическое число» (
0x34c2bdc3) для версии протокола как 32-битное целое число в формате little-endian (4 байта).SEND c3 bd c2 34
-
В случае успеха, сервер отправляет ответ в формате 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?
-
Клиент отправляет версию протокола, метод аутентификации и аутентификационные данные в формате 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" } -
Сервер отправляет ответ в формате 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 включительно. -
Клиент отправляет «сообщение клиента — последнее сообщение» в формате JSON с нулевым завершением с тем же nonce и вычисленной ClientProof в соответствии с RFC.
{ "authentication": "c=biws,r=rOprNGfwEbeRWgbNEkqO%hvYDpWUa2RaTCAfuxFIlj)hNlF$k0, p=dHzbZapWIk4jUhN+Ute9ytag9zjfMHgsqmmiz7AndVQ=" } -
Сервер отправляет ответ в формате 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 или более поздней версии, ключ аутентификации будет сравниваться с паролем учетной записи администратора.
- Отправить версию протокола как 32-битное целое число в формате little-endian (4 байта). Примечание: все инструкции ниже предполагают протокол версии
V0_3или выше. Текущий протокол по состоянию на RethinkDB 2.0 равенV0_4. - Отправить длину ключа авторизации как 32-битное целое число в формате little-endian (4 байта). Отправить
0если ключа авторизации нет. - Отправить ключ авторизации как строку ASCII. Если ключа авторизации нет, пропустите этот шаг.
- Отправить тип протокола как 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"})
представлен следующим деревом:
Команды 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). Опции (иногда называемые «глобальными параметрами») — это опции, передаваемые самой команде run; см. документацию run для получения полного списка. (Команды, отправленные на сервер, используют нижний регистр, а не camelCase.)
Полный список значений QueryType следующий:
-
1START: Начать новый запрос. -
2CONTINUE: Продолжить запрос, который вернулSUCCESS_PARTIAL(см. Получение ответов). -
3STOP: Прервать запрос, который всё ещё выполняется. -
4NOREPLY_WAIT: Дождаться завершения операций noreply. Сервер вернёт ответWAIT_COMPLETE. -
5SERVER_INFO: Запросить информацию о сервере. Сервер вернёт ответSERVER_INFO.
CONTINUE и STOP должны быть отправлены по одному соединению с тем же токеном, сгенерированным для сообщения START этого запроса.
Отправка запросов
Итак, отправка запроса на сервер выполняется в следующей последовательности:
- Сериализовать запрос в виде JSON, закодированного в UTF8
- Отправить на сервер следующие данные:
- Уникальный 8-байтовый токен запроса
- Размер сериализованного в JSON, UTF8-закодированного запроса (в виде 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 запроса
Если опция 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.
-
1SUCCESS_ATOM: Весь запрос был возвращен, и результат находится в первом (и единственном) элементеr. -
2SUCCESS_SEQUENCE: Либо весь запрос был возвращён вr, либо последняя часть многокомпонентного запроса была возвращена. -
3SUCCESS_PARTIAL: Запрос вернул поток, который может быть или не быть полным. Чтобы получить больше результатов запроса, отправьте сообщениеCONTINUE(см. ниже). -
4WAIT_COMPLETE: ЭтотResponseTypeуказывает, что все запросы, запущенные в режимеnoreply, завершили выполнение.rбудет пустым. -
5SERVER_INFO: Ответ на запросSERVER_INFO. Данные будут в первом (и единственном) элементеr. -
16CLIENT_ERROR: Сервер не смог выполнить запрос из-за плохого запроса клиента. Сообщение об ошибке будет в первом элементеr. -
17COMPILE_ERROR: Сервер не смог выполнить запрос из-за ошибки компиляции ReQL. Сообщение об ошибке будет в первом элементеr. -
18RUNTIME_ERROR: Запрос был успешно скомпилирован, но завершился ошибкой во время выполнения. Сообщение об ошибке будет в первом элементеr.
Примечания к ответу
Поле n, если оно присутствует, будет массивом одного или нескольких значений ResponseNote, предоставляющих дополнительную информацию о типе возвращаемого потока. Это будут числовые значения, соответствующие примечаниям в ql2.proto.
Все примечания к ответу относятся к changefeed; для получения более подробной информации прочтите Changefeeds в RethinkDB.
-
1SEQUENCE_FEED: Поток — это changefeed. -
2ATOM_FEED: Поток — это точечный changefeed, т.е. он возвращает изменения из одного документа. -
3ORDER_BY_LIMIT_FEED: Поток — это changefeed, сгенерированный с помощью запросаorder_by().limit(). -
4UNIONED_FEED: Поток — это объединение нескольких типов changefeed, которые нельзя свести к одному типу, например,r.table('test').changes().union(r.table('test').get(0).changes()). -
5INCLUDES_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 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/