Поток сообщений

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



Запуск

Чтобы начать сеанс, клиент открывает подключение к серверу и посылает сообщение о запуске. В это сообщение включаются имена пользователя и базы данных, к которой этот пользователь хочет подключиться; также в нем определяется, какая именно версия протокола будет использоваться. (Кроме того, сообщение о запуске может включать дополнительные значения для параметров времени выполнения.) Затем сервер применяет эту информацию и содержимое своих файлов конфигурации (например, qhb_hba.conf), чтобы определить, можно ли предварительно разрешить это подключение и какая требуется дополнительная аутентификация (если требуется).

Потом сервер передает соответствующее сообщение с запросом аутентификации, на которое клиент должен ответить соответствующим сообщением с ответом аутентификации (например, с паролем). Для всех методов аутентификации, кроме GSSAPI и SASL, будет максимум один запрос и один ответ. Для некоторых методов ответ от клиента вообще не требуется, поэтому запрос аутентификации не осуществляется. Для GSSAPI и SASL завершение аутентификации может потребовать нескольких обменов пакетами.

Цикл аутентификации заканчивает сервер, либо отклоняя попытку подключения (ErrorResponse), либо отправляя AuthenticationOk.

В этой фазе возможны следующие ответные сообщения от сервера:

ErrorResponse
Попытка подключения была отклонена. Сразу после этого сервер закрывает соединение.

AuthenticationOk
Обмен сообщениями аутентификации завершен успешно.

AuthenticationKerberosV5
Теперь клиент должен принять участие в диалоге аутентификации по протоколу Kerberos V5 (здесь он не описывается, поскольку является частью спецификации Kerberos) с сервером. Если диалог завершается успешно, сервер отвечает AuthenticationOk, иначе — ErrorResponse. Этот вариант аутентификации больше не поддерживается.

AuthenticationCleartextPassword
Теперь клиент должен послать сообщение PasswordMessage, содержащее пароль в открытом виде. Если пароль правильный, сервер отвечает AuthenticationOk, иначе — ErrorResponse.

AuthenticationMD5Password
Теперь клиент должен послать сообщение PasswordMessage, содержащее пароль (с именем пользователя) зашифрованным посредством хеша MD5, с последующим повторным шифрованием с использованием 4-байтового случайного значения соли, указанного в сообщении AuthenticationMD5Password. Если пароль правильный, сервер отвечает AuthenticationOk, иначе — ErrorResponse. Фактическое сообщение PasswordMessage можно вычислить в SQL как concat('md5', md5(concat(md5(concat(password, username)), random-salt))). (Учтите, что функция md5() возвращает результат в виде шестнадцатеричной строки.)

AuthenticationGSS Теперь клиент должен инициировать согласование GSSAPI. В ответ на это сообщение клиент отправит сообщение GSSResponse с первой частью потока данных GSSAPI. Если потребуются дополнительные сообщения, сервер ответит AuthenticationGSSContinue.

AuthenticationGSSContinue
Это сообщение содержит данные ответа на предыдущий этап согласования GSSAPI (сообщение AuthenticationGSS или предыдущее AuthenticationGSSContinue). Если данные GSSAPI в этом сообщении указывают, что для завершения аутентификации требуются еще данные, клиент должен переслать эти данные в виде еще одного сообщения GSSResponse. Если аутентификация GSSAPI завершается этим сообщением, сервер затем отправит AuthenticationOk, извещая об успешной аутентификации, или ErrorResponse, указывая на ошибку.

AuthenticationSASL
Теперь клиент должен инициировать согласование SASL, воспользовавшись одним из механизмов SASL, перечисленных в этом сообщении. В ответ на это сообщение клиент отправит SASLInitialResponse с именем выбранного механизма и первую часть потока данных SASL. Если потребуются дополнительные сообщения, сервер ответит AuthenticationSASLContinue. Подробную информацию см. в разделе Аутентификация SASL.

AuthenticationSASLContinue
Это сообщение содержит данные вызова с предыдущего этапа согласования SASL (сообщение AuthenticationSASL или предыдущее AuthenticationSASLContinue). Клиент должен ответит сообщением SASLResponse.

AuthenticationSASLFinal
Аутентификация SASL завершилась с дополнительными специфичными для механизма данными для клиента. Затем сервер отправит AuthenticationOk извещая об успешной аутентификации, или ErrorResponse, указывая на ошибку. Это сообщение посылается, только если в механизме SASL указаны дополнительные данные, которые в завершение нужно передать от сервера клиенту.

NegotiateProtocolVersion
Сервер не поддерживает дополнительную версию протокола, запрошенную клиентом, но поддерживает более раннюю версию протокола; в этом сообщении указывается наибольшая поддерживаемая дополнительная версия. Кроме того, это сообщение будет отправлено, если клиент запросил в пакете запуска неподдерживаемые параметры протокола (т. е. начинающиеся с _pq_.). За этим сообщением должно последовать либо сообщение ErrorResponse, либо сообщение, извещающее об успехе или провале аутентификации.

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

Получив сообщение AuthenticationOk, клиент должен ждать дальнейших сообщений от сервера. В этой фазе запускается внутренний процесс,а клиент выступает просто в качестве заинтересованного наблюдателя. Попытка запуска все еще может провалиться (ErrorResponse), или сервер может отказаться поддерживать запрошенную дополнительную версию протокола (NegotiateProtocolVersion), но в обычной ситуации сервер отправит несколько сообщений ParameterStatus, BackendKeyData и наконец ReadyForQuery.

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

В этой фазе возможны следующие ответные сообщения от сервера:

BackendKeyData
Это сообщение предоставляет данные секретного ключа, которые клиент должен сохранить, если хочет впоследствии иметь возможность выполнять отмену запросов. Клиент не должен отвечать на это сообщение, но должен продолжать ожидать сообщения ReadyForQuery.

ParameterStatus
Это сообщение информирует клиента о текущих (начальных) значениях параметров сервера, например, client_encoding или DateStyle. Клиент может игнорировать это сообщение или записать значения для дальнейшего использования; более подробную информацию см. в подразделе Асинхронные операции. Клиент не должен отвечать на это сообщение, но должен продолжать ожидать сообщения ReadyForQuery.

ReadyForQuery
Запуск завершен. Теперь клиент может выполнять команды.

ErrorResponse
Запуск не удался. После передачи этого сообщения соединение закрывается.

NoticeResponse
Было выдано предупреждающее сообщение. Клиент должен отобразить это сообщение, но продолжать ожидать сообщения ReadyForQuery или ErrorResponse.

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



Простой запрос

Цикл простого запроса запускается клиентом, посылающим серверу сообщение Query. Это сообщение содержит команду (или команды) SQL, выраженную в виде текстовой строки. Затем сервер отправляет одно или несколько ответных сообщений, в зависимости от содержимого командной строки запроса, и в конце ответное сообщение ReadyForQuery. ReadyForQuery информирует клиента о том, что он может безопасно передавать новую команду. (На самом деле клиенту не обязательно дожидаться ReadyForQuery, прежде чем послать следующую команду, но тогда он сам должен разбираться, что произошло, если предыдущая команда завершается ошибкой, а последующие выполняются успешно.)

В этой фазе возможны следующие ответные сообщения от сервера:

CommandComplete
Команда SQL завершилась нормально.

CopyInResponse
Сервер готов копировать данные, полученные от клиента, в таблицу; см. подраздел Операции COPY.

CopyOutResponse
Сервер готов копировать данные из таблицы клиенту; см. подраздел Операции COPY.

RowDescription
Показывает, что сейчас в ответ на запрос SELECT, FETCH и т. п. будут возвращены строки. В содержимом этого сообщения описывается структура столбцов этих строк. За ним для каждой строки, возвращаемой клиенту, последует сообщение DataRow.

DataRow
Одна строка из набора, возвращаемого запросом SELECT, FETCH и т. п.

EmptyQueryResponse
Была принята пустая строка запроса.

ErrorResponse
Произошла ошибка.

ReadyForQuery
Обработка строки запроса завершена. Известие об этом передается отдельным сообщением, поскольку строка запроса может содержать несколько SQL. (Сообщение CommandComplete отмечает конец обработки одной команды SQL, а не всей строки.) ReadyForQuery будет передаваться всегда, независимо от того, успешно ли завершилась обработка или с ошибкой.

NoticeResponse
Было выдано предупреждающее сообщение, касающееся запроса. Замечания дополняют другие ответы, т. е. сервер продолжит обрабатывать команду.

Ответ на запрос SELECT (или другие запросы, возвращающие наборы строк, например EXPLAIN или SHOW) обычно состоит из RowDescription, нуля и более сообщений DataRow и завершающего CommandComplete. Копирование (COPY) данных от клиента или клиенту активизирует специальный протокол, как описано в подразделе Операции COPY. Для остальных типов запросов обычно выдается только сообщение CommandComplete.

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

Если получена совершенно пустая (не содержащая ничего, кроме пробельных символов) строка, ответом будет EmptyQueryResponse, а затем ReadyForQuery.

В случае ошибки выдается ErrorResponse, а затем ReadyForQuery. Сообщением ErrorResponse прерывается вся дальнейшая обработка строки запроса (даже если в ней остались другие запросы). Обратите внимание, что это может произойти и в середине последовательности сообщений, сгенерированных в ответ на отдельный запрос.

В режиме простых запросов получаемые значения всегда имеют текстовый формат, за исключением результатов команды FETCH из курсора, объявленного с параметром BINARY. В этом случае получаемые значения имеют двоичный формат. На то, какой именно формат используется, указывают коды форматов, передаваемые в сообщении RowDescription.

Клиент должен быть готов принять сообщения ErrorResponse и NoticeResponse, ожидая любой другой тип сообщений. Информацию о сообщениях, которые сервер может сгенерировать в ответ на внешние события, см. в подразделе Асинхронные операции.

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


Несколько операторов в простом запросе

Когда простое сообщение Query содержит несколько операторов SQL (разделенных точкой с запятой), эти операторы выполняются в одной транзакции, если только среди них нет явных команд управления транзакциями, вызывающих другое поведение. Например, если сообщение содержит

INSERT INTO mytable VALUES(1);
SELECT 1/0;
INSERT INTO mytable VALUES(2);

то ошибка деления на ноль в SELECT приведет к откату первого INSERT. Более того, из-за прерывания выполнения этого сообщения на первой ошибке второй INSERT вообще не будет выполняться.

Если вместо этого сообщение содержит

BEGIN;
INSERT INTO mytable VALUES(1);
COMMIT;
INSERT INTO mytable VALUES(2);
SELECT 1/0;

то первый INSERT фиксируется явной командой COMMIT. Второй INSERT и SELECT по-прежнему воспринимаются как одна транзакция, поэтому ошибка деления на ноль приведет к откату второго INSERT, но не первого.

Это поведение реализуется путем выполнения операторов многооператорного сообщения Query в неявном блоке транзакции, если только в сообщении нет какого-нибудь явного блока транзакции, в котором они должны выполняться. Основное различие между неявным блоком транзакции и обычным заключается в том, что неявный блок автоматически закрывается в конце сообщения Query либо путем неявной фиксации, если нет ошибок, либо путем неявного отката, если ошибка есть. Это похоже на неявную фиксацию и откат, которые происходят с оператором, выполняемым отдельно (вне блока транзакции).

Если сеанс уже находится внутри блока транзакции в результате выполнения оператора BEGIN из некоторого предыдущего сообщения, то сообщение Query просто продолжает этот блок транзакции независимо от того, содержится ли в нем один оператор или несколько. Однако если сообщение Query содержит оператор COMMIT или ROLLBACK, закрывающий существующий блок транзакции, то любые последующие операторы выполняются в неявном блоке транзакции. И наоборот, если в многооператорном сообщении Query фигурирует оператор BEGIN, он начинает обычный блок транзакции, который будет закончен только явными COMMIT или ROLLBACK независимо от того, находятся они в этом сообщении Query или в последующем. Если BEGIN следует за операторами, которые выполнялись в неявном блоке транзакции, эти операторы не фиксируются немедленно; на практике они задним числом включаются в новый обычный блок транзакции.

Оператор COMMIT или ROLLBACK, находящийся в неявном блоке транзакции, выполняется как обычно, закрывая этот неявный блок; однако при этом будет выдано предупреждение, поскольку COMMIT или ROLLBACK без предшествующего BEGIN может оказаться ошибочным. Если за этими операторами следуют другие, для них будет начат новый неявный блок транзакции.

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

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

BEGIN;
SELECT 1/0;
ROLLBACK;

в одном сообщении Query, сеанс останется внутри не выполнившегося обычного блока транзакции, поскольку до ROLLBACK после ошибки деления на ноль выполнение не дошло. Чтобы восстановить сеанс до рабочего состояния потребуется выполнить еще один ROLLBACK.

Также нужно отметить, что первоначальный лексический и синтаксический анализ проводится для всей строки запроса, прежде чем выполняется какая-либо ее часть. Поэтому простые ошибки (например, опечатка в ключевом слове) в последующих операторах могут не дать выполниться предыдущим операторам. Обычно этого не видно пользователям, поскольку эти операторы все равно бы все откатились при выполнении в неявном блоке транзакции. Тем не менее это можно увидеть, если попытаться выполнить несколько транзакций в многооператорном Query. Например, если бы опечатка превратила наш предыдущий пример в

BEGIN;
INSERT INTO mytable VALUES(1);
COMMIT;
INSERT INTO mytable VALUES(2);
SELCT 1/0;

ни один из операторов не выполнился бы, приведя к видимому отличию, состоящему в том, что первый INSERT не зафиксируется. Ошибки, выявленные при семантическом анализе или позже, например, опечатки в имени таблицы или столбца, такого влияния не оказывают.

И наконец, обратите внимание, что все операторы в сообщении Query будут учитывать одно и то же значение функции statement_timestamp(), поскольку эта метка времени обновляется только при получении сообщения Query. Это приведет к тому, что все они будут также учитывать одно и то же значение функции transaction_timestamp(), за исключением случаев, когда строка запроса завершает ранее начатую транзакцию и начинает новую.



Расширенный запрос

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

В расширенном протоколе клиент сначала передает сообщение Parse, содержащее текстовую строку запроса и, возможно, некоторую информацию о типах данных шаблонов параметров и имя целевого объекта подготовленного оператора (если строка пустая, создается безымянный подготовленный оператор). Ответом на это будет ParseComplete или ErrorResponse. Типы данных параметров указываются по OID; если они не заданы, анализатор пытается определить типы данных так же, как он делал бы для нетипизированных строковых констант.

Примечание
Тип данных параметра можно оставить неопределенным, задав для него значение ноль или создав массив с OID типов параметров короче, чем количество символов параметров ($n), используемых в строке запроса. Другой особый случай — в качестве типа параметра можно указать void (то есть OID псевдотипа void). Это сделано для того, чтобы разрешить использование символов параметров для параметров функций, в действительности являющихся выходными параметрами (OUT). Обычно параметр void невозможно использовать ни в каком контексте, но если такой символ параметра присутствует в списке параметров функции, он по сути игнорируется. К примеру, вызову такой функции, как foo($1,$2,$3,$4), может соответствовать функция с двумя входными (IN) и двумя выходными (OUT) аргументами, если аргументы $3 и $4 заданы как имеющие тип void.

Примечание
Строка запроса, содержащаяся в сообщении Parse, не может содержать больше одного оператора SQL, иначе выдается синтаксическая ошибка. Это ограничение отсутствует в протоколе простого запроса, но существует в расширенном протоколе, поскольку разрешение подготовленным операторам или порталам содержать несколько команд неоправданно усложнило бы протокол.

При успешном создании объект именованного подготовленного оператора продолжает существовать до конца текущего сеанса, если только он не будет уничтожен явно. Безымянный подготовленный оператор существует только до выполнения следующего оператора Parse, где этот безымянный оператор указан в качестве целевого. (Обратите внимание, что сообщение простого запроса тоже уничтожает безымянный оператор.) Именованные подготовленные операторы должны явно закрываться, прежде чем их сможет переопределить другое сообщение Parse, но для безымянного оператора этого не требуется. Кроме того, именованные подготовленные операторы можно создавать и вызывать на уровне команд SQL, используя PREPARE и EXECUTE.

Когда подготовленный оператор существует, его можно подготовить к выполнению с помощью сообщения Bind. В сообщении Bind задается имя исходного подготовленного оператора (пустая строка обозначает безымянный подготовленный оператор), имя целевого портала (пустая строка обозначает безымянный портал) и значения для любых шаблонов параметров, имеющихся в подготовленном операторе. Предоставляемый набор параметров должен соответствовать параметрам, необходимым подготовленному оператору. (Если в сообщении Parse вы объявили какие-либо параметры void, передайте для них значения NULL в сообщении Bind.) Bind также задает формат для данных, возвращаемых запросом; формат можно задать для всех данных или для на уровне столбцов. Ответом на это сообщение будет BindComplete или ErrorResponse.

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

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

При успешном создании объект именованного портала продолжает существовать до конца текущей транзакции, если только он не будет уничтожен явно. Безымянный портал уничтожается в конце транзакции или при выполнении следующего оператора Bind, где этот безымянный портал указан в качестве целевого. (Обратите внимание, что сообщение простого запроса тоже уничтожает безымянный портал.) Именованные порталы должны явно закрываться, прежде чем их сможет переопределить другое сообщение Bind, но для безымянного портала этого не требуется. Кроме того, именованные порталы можно создавать и вызывать на уровне команд SQL, используя DECLARE CURSOR и FETCH.

Когда портал существует, его можно выполнить с помощью сообщения Execute. В сообщении Execute задается имя портала (пустая строка обозначает безымянный портал) и максимальное число результирующих строк (ноль означает «выбрать все строки»). Число результирующих строк имеет значение только для порталов, содержащих команды, возвращающие наборы строк; в других случаях команда всегда выполняется до завершения и число строк игнорируется. Возможные ответы на Execute такие же, как описанные выше для запросов, выполняющихся по протоколу простых запросов, за исключением того, что в ответ на Execute не выдаются ReadyForQuery или RowDescription.

Если Execute заканчивается до завершения выполнения портала (вследствие достижения ненулевого ограничения на число результирующих строк), сервер передаст сообщение PortalSuspended; появление этого сообщения говорит клиенту, что для завершения операции следует выдать еще одно сообщение Execute для того же портала. Сообщение CommandComplete, указывающее на завершение исходной команды SQL, не передается, пока не завершится выполнение портала. Таким образом, фаза Execute всегда заканчивается появлением одного из этих сообщений: CommandComplete, EmptyQueryResponse (если портал был создан из пустой строки запроса), ErrorResponse или PortalSuspended.

При завершении каждой цепочки сообщений расширенных запросов клиент должен выдать сообщение Sync. Это сообщение без параметров заставляет сервер закрыть текущую транзакцию, если она находится не внутри блока транзакции BEGIN/COMMIT («закрытие» обозначает фиксацию при отсутствии ошибок или откат при ошибке). Затем выдается ответ ReadyForQuery. Цель сообщения Sync — предоставить точку повторной синхронизации для восстановления в случае ошибки. Если ошибка выявляется при обработке любого сообщения расширенного запроса, сервер выдает ErrorResponse, затем считывает и отбрасывает сообщения вплоть до Sync, затем выдает ReadyForQuery и возвращается к обычной обработке сообщений. (Но обратите внимание, что он не будет пропускать сообщения, если ошибка выявляется во время обработки Sync — это гарантирует, что для каждого сообщения Sync будет передаваться ровно одно сообщение ReadyForQuery.)

Примечание
Сообщение Sync не приводит к закрытию блока транзакции, открытого командой BEGIN. Однако эту ситуацию можно выявить, поскольку в сообщении ReadyForQuery содержится информация о состоянии транзакции.

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

В сообщении Describe (в варианте для портала) задается имя существующего портала (или пустая строка для безымянного портала). Ответом является сообщение RowDescription, описывающее строки, которые будут возвращены при выполнении портала, или сообщение NoData, если портал не содержит запроса, который возвратит строки, или ErrorResponse, если такого портала нет.

В сообщении Describe (в варианте для оператора) задается имя существующего подготовленного оператора (или пустая строка для безымянного подготовленного оператора). Ответом является сообщение ParameterDescription, описывающее параметры, требующиеся для оператора, за которым следует сообщение RowDescription, описывающее строки, которые будут возвращены в результате выполнения оператора (или сообщение NoData, если оператор не возвратит строки). ErrorResponse выдается, если такого подготовленного оператора нет. Обратите внимание, что поскольку Bind еще не было выдано, сервер еще не знает, в каком формате будут возвращаться столбцы; в этом случае поля кодов форматов в сообщении RowDescription будут заполнены нулями.

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

Сообщение Close закрывает существующий подготовленный оператор или портал и освобождает ресурсы. Выполнение Close для имени несуществующего оператора или портала ошибкой не является. Ответом на это сообщение обычно является CloseComplete, но может быть и ErrorResponse, если при освобождении ресурсов возникло какое-то затруднение. Обратите внимание, что закрытие подготовленного оператора неявно закрывает все открытые порталы, собранные из этого оператора.

Сообщение Flush не приводит к генерированию каких-либо конкретных выходных данных, но указывает серверу передать все данных, находящиеся в очереди в его буферах вывода. Flush следует передавать после любой команды расширенного запроса, кроме Sync, если клиент желает проверить результаты этой команды, прежде чем выдавать следующие. Без Flush сообщения, возвращаемые сервером, будут объединяться в минимально возможное количество пакетов в целях максимального снижения сетевых издержек.

Примечание
Простое сообщение Query примерно равнозначно цепочке сообщений Parse, Bind, портального Describe, Execute, Close, Sync с использованием объектов подготовленного оператора и портала без имен и параметров. Одно различие заключается в том, что такое сообщение примет несколько операторов SQL в строке запроса, для каждого их них по очереди автоматически выполняя последовательность Bind/Describe/Execute. Другое различие заключается в том, что в ответ на него не передаются сообщения ParseComplete, BindComplete, CloseComplete или NoData.



Конвейерный режим

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

Один из способов этого добиться — объединить всю цепочку запросов в одну транзакцию, заключив ее в BEGIN ... COMMIT. Однако это не поможет, если нужно, чтобы какие-то команды фиксировались независимо от остальных.

Протокол расширенных запросов предоставляет и другой способ справиться с этой проблемой, исключив передачу сообщений Sync между зависимыми этапами. Так как после ошибки сервер будет пропускать командные сообщения, пока не найдет Sync, это позволяет в случае сбоя в предыдущей команде пропускать последующие команды в конвейере без необходимости клиенту явно управлять этим с помощью BEGIN и COMMIT. Независимо фиксируемые сегменты конвейера можно разделять сообщениями Sync.

Если клиент не выдает команду BEGIN явно, то каждое сообщение Sync обычно вызывает неявное выполнение COMMIT, если предыдущие этапы завершились успешно, или ROLLBACK в случае сбоя. Однако есть несколько команд DDL (например, CREATE DATABASE), которые нельзя выполнить в блоке транзакции. Если такая команда выполняется в конвейере, она выполнится успешно, только если будет в нем первой. Более того, в случае успеха она вызовет немедленную фиксацию для сохранения согласованности базы данных. Таким образом, сообщение Sync, отправленное сразу за такой командой, не вызовет ничего, кроме ответа ReadyForQuery.

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



Вызов функции

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

Примечание
Подчиненный протокол «Вызов функции» является устаревшей функциональностью, которую, вероятно, лучше не использовать в новом коде. Похожие результаты можно получить, установив подготовленный оператор с командой SELECT function($1, ...). Тогда цикл вызова функции можно заменить цепочкой Bind/Execute.

Цикл вызова функции запускается клиентом, передающим серверу сообщение FunctionCall. Затем сервер передает одно или несколько ответных сообщений, в зависимости от результатов вызова функции, и завершающее ответное сообщение ReadyForQuery. ReadyForQuery информирует клиента, что тот может безопасно передать новый запрос или вызов функции.

В этой фазе возможны следующие ответные сообщения от сервера:

ErrorResponse
Произошла ошибка.

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

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

NoticeResponse
Было выдано предупреждающее сообщение, касающееся вызова функции. Замечания дополняют другие ответы, т. е. сервер продолжит обрабатывать команду.



Операции COPY

Команда COPY обеспечивает скоростную передачу массива данных на сервер или с сервера. Операции входящего и исходящего копирования переключают соединение в отдельные подчиненные протоколы, которые продолжают существовать до завершения операции.

Режим входящего копирования (данные передаются на сервер) запускается, когда сервер выполняет оператор SQL COPY FROM STDIN. Сервер передает клиенту сообщение CopyInResponse. Затем клиент должен передать ноль или более сообщений CopyData, формируя поток входных данных. (Границам сообщений необязательно совпадать с границами строк, хотя зачастую это разумное решение.) Клиент может завершить режим входящего копирования, передав либо сообщение CopyDone (приводящее к успешному завершению), либо сообщение CopyFail (которое приведет к сбою и ошибке в операторе SQL COPY). Затем сервер возвращается в режим обработки команд, в котором он находился перед началом выполнения COPY, т. е. в протокол простых или расширенных запросов. Затем он передаст либо CommandComplete (в случае успеха), либо ErrorResponse (в противном случае).

При возникновении ошибки (выявленной сервером) в режиме входящего копирования (включая получение сообщения CopyFail), сервер выдаст сообщение ErrorResponse. Если команда COPY была передана в сообщении расширенного запроса, сервер будет пропускать сообщения клиента, пока не получит сообщение Sync, после чего он выдаст ReadyForQuery и вернется в режим обычной обработки. Если команда COPY была передана в простом сообщении Query, остальная часть этого сообщения не учитывается и сразу выдается ReadyForQuery. В любом случае все последующие сообщения CopyData, CopyDone или CopyFail, выданные клиентом, будут просто удаляться.

В режиме входящего копирования сервер будет игнорировать полученные сообщения Flush и Sync. Получение сообщений любого другого типа, не относящегося к копированию, вызывает ошибку, которая прервет состояние входящего копирования, как описано выше. (Исключение для Flush и Sync введено для удобства клиентских библиотек, которые всегда посылают Flush или Sync после сообщения Execute, не проверяя, является ли выполняемая команда командой COPY FROM STDIN.)

Режим исходящего копирования (данные передаются с сервера) запускается, когда сервер выполняет оператор SQL COPY TO STDOUT. Сервер передает клиенту сообщение CopyOutResponse, за ним ноль или более сообщений CopyData (всегда по одному на строку), за ними CopyDone. Затем сервер возвращается в режим обработки команд, в котором он находился перед началом выполнения COPY, и передает CommandComplete. Клиент не может прервать передачу (кроме как закрыв соединение или выдав запрос Cancel), но он может не учитывать нежелательные сообщения CopyData и CopyDone.

При возникновении ошибки (выявленной сервером) в режиме исходящего копирования, сервер выдаст сообщение ErrorResponse и вернется в режим обычной обработки. Клиент должен воспринимать получение ErrorResponse как завершение режима исходящего копирования.

Сообщения CopyData могут перемежаться сообщениями NoticeResponse и ParameterStatus; клиенты должны их обрабатывать и быть готовыми получить и другие типы асинхронных сообщений (см. подраздел Асинхронные операции). В остальном, сообщения любых типов, кроме CopyData или CopyDone, могут восприниматься как завершающие режим исходящего копирования.

Имеется еще один режим, связанный с копированием, называемый двусторонним копированием, который обеспечивает скоростную передачу массива данных на сервер и с сервера. Режим двустороннего копирования запускается, когда сервер в режиме передачи WAL выполняет оператор START_REPLICATION. Сервер передает клиенту сообщение CopyBothResponse. Затем клиент и сервер могут обмениваться сообщениями CopyData, пока кто-то из них не передаст сообщение CopyDone. Когда сообщение CopyDone передает клиент, соединение переключается с режима двустороннего в режим исходящего копирования, и клиент больше не может передавать сообщения CopyData. Аналогично когда сообщение CopyDone передает сервер, соединение переключается с режима двустороннего в режим входящего копирования, и сервер больше не может передавать сообщения CopyData. Когда сообщение CopyDone передали обе стороны, режим копирования завершается, и сервер возвращается в режим обработки команд. При возникновении ошибки (выявленной сервером) в режиме двустороннего копирования, сервер выдаст сообщение ErrorResponse, будет пропускать сообщения клиента, пока не получит сообщение Sync, а затем выдаст ReadyForQuery и вернется в режим обычной обработки. Клиент должен воспринимать получение ErrorResponse как завершение двустороннего копирования; в этом случае CopyDone передаваться не должно. Более подробную информацию о подчиненном протоколе, распространяющемся на режим двустороннего копирования, см. в разделе Протокол потоковой репликации.

Сообщения CopyInResponse, CopyOutResponse и CopyBothResponse содержат поля, информирующие клиента о количестве столбцов в строке и коде формата для каждого столбца. (В текущей реализации для всех столбцов в заданной операции COPY используется один формат, но в конструкции сообщения это не предусмотрено.)



Асинхронные операции

Существует несколько сценариев, в которых сервер будет передавать сообщения, которые не были вызваны непосредственно потоком команд клиента. Клиенты должны быть готовы принять эти сообщения в любое время, даже когда они не занимаются запросом. Как минимум следует проверять такие сообщения, прежде чем начинать читать ответ на запрос.

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

Сообщения ParameterStatus будут генерироваться всякий раз, когда меняется активное значение любого параметра, о котором, по мнению сервера, должен знать клиент. Чаще всего это происходит в ответ на команду SQL SET, выполняемую клиентом, и в этом случае сообщение по сути синхронно — но статус параметра также может измениться, потому что администратор меняет файл конфигурации, а затем передает серверу сигнал SIGHUP. Кроме того, если команда SET откатывается, будет сгенерировано соответствующее сообщение ParameterStatus, содержащее текущее действительное значение.

В настоящее время есть жестко закодированный набор параметров, для которых будет генерироваться ParameterStatus:

application_name
client_encoding
DateStyle
default_transaction_read_only
in_hot_standby
integer_datetimes
IntervalStyle
is_superuser
scram_iterations
server_encoding
server_version
session_authorization
standard_conforming_strings
TimeZone

(default_transaction_read_only и in_hot_standby не отслеживались до версии 1.5.0; scram_iterations не отслеживался до версии 1.5.3.) Обратите внимание, что server_version, server_encoding и integer_datetimes являются псевдопараметрами, которые нельзя изменить после запуска сервера. В будущем этот набор может измениться или даже стать настраиваемым. Соответственно, клиент должен просто игнорировать ParameterStatus для параметров, которые ему неизвестны или не представляют интереса.

Если клиент выдает команду LISTEN, сервер будет передавать сообщение NotificationResponse (не путать с NoticeResponse!) всякий раз, когда для канала с тем же именем будет выполняться команда NOTIFY.

Примечание
В настоящее время сообщение NotificationResponse может быть передано только вне транзакции и поэтому не может оказаться в середине цепочек ответов на команду, хотя может прийти сразу перед ReadyForQuery. Однако конструировать логику клиента, полагаясь на это, неразумно. Лучше, чтобы клиент был способен принимать NotificationResponse в любой момент протокола.



Отмена обрабатывающихся запросов

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

Чтобы выдать запрос на отмену, клиент открывает новое соединение с сервером и передает сообщение CancelRequest вместо сообщения StartupMessage, обычно передаваемого по новому соединению. Сервер обработает этот запрос, а затем закроет соединение. Из соображений безопасности на сообщение с запросом на отмену прямой ответ сервер не передает.

Сообщение CancelRequest будет игнорироваться, если только оно не содержит те же ключевые данные (PID и секретный ключ), что были переданы клиенту при запуске подключения. Если запрос соответствует PID и секретному ключу выполняющегося в данный момент внутреннего процесса, обработка текущего запроса прерывается. (В существующей реализации это производится путем передачи специального сигнала внутреннему процессу, обрабатывающему запрос.)

Сигнал отмены может подействовать, а может и не подействовать — к примеру, если он поступает после того, как сервер закончил обработку запроса, он не подействует. Если отмена срабатывает, она приводит к досрочному завершению текущей команды с сообщением об ошибке.

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

Поскольку запрос на отмену передается через новое соединение с сервером, а не через обычный канал связи клиент-сервер, такие запросы может выдавать любой процесс, а не только клиент, запрос которого нужно отменить. Это может обеспечить дополнительную гибкость при построении многопроцессных приложений. Также это приводит к угрозе безопасности, поскольку попытаться отменить запросы могут и неавторизованные пользователи. Для устранения этой угрозы безопасности требуется передавать в запросах на отмену динамически генерируемый секретный ключ.



Завершение

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

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

Другие сценарии завершения обусловлены различными случаями сбоя, например, при отказе дампа памяти на одной или другое стороне, при потере канала связи, при потере синхронизации по границам сообщений и т. п. Если клиент или сервер видит, что соединение неожиданно закрылось, он должен очистить ресурсы и завершиться. У клиента при этом есть вариант запустить новый серверный процесс, переподключившись к серверу, если он сам не хочет завершиться. Закрытие соединения также рекомендуется при получении сообщения нераспознаваемого типа, поскольку это может указывать на потерю синхронизации по границам сообщений.

При обычном или аварийном завершении сеанса все открытые транзакции откатываются, а не фиксируются. Однако следует обратить внимание, что если клиент отключается в процессе обработки запроса, отличного от SELECT, сервер, скорее всего, завершит запрос, прежде чем заметит отключение. Если запрос выполняется вне блока транзакции (последовательности BEGIN ... COMMIT), то его результаты могут быть зафиксированы до обнаружения отключения.



Шифрование сеанса с SSL

Если QHB была собрана с поддержкой SSL, взаимодействия клиента и сервера могут быть зашифрованы с использованием SSL. Это обеспечивает защиту связи в средах, где взломщики могут перехватить трафик сеанса. Более подробную информацию о шифровании сеансов QHB с помощью SSL см. в разделе Защита TCP/IP-соединений посредством SSL.

Чтобы запустить соединение с SSL-шифрованием, клиент сначала передает сообщение SSLRequest вместо StartupMessage. Затем сервер отвечает одним байтом, содержащим S или N, показывая, что он желает или не желает выполнять SSL-шифрование соответственно. На этом этапе клиент может закрыть соединение, если ответ его не удовлетворяет. Чтобы продолжить после получения S, он выполняет начальное согласование SSL с сервером (не описывается здесь, т. к. является частью спецификации SSL). Если это согласование проходит успешно, клиент продолжает соединение, передавая обычное сообщение StartupMessage. В этом случае StartupMessage и все последующие данные будут зашифрованы SSL. Чтобы продолжить после получения N, клиент передает обычное сообщение StartupMessage и продолжает взаимодействие с сервером без шифрования. (Как вариант, клиенту разрешается выдать сообщение GSSENCRequest после получения в ответ N, чтобы попытаться использовать вместо SSL шифрование GSSAPI.)

Кроме того, клиент должен быть готов обработать сообщение ErrorMessage, полученное от сервера в ответ на SSLRequest. Клиент не должен отображать это сообщение об ошибке пользователю/приложению, поскольку сервер не был аутентифицирован (CVE-2024-10977). В этом случае соединение должно быть закрыто, но клиент может предпочесть открыть новое соединение и продолжить взаимодействие с сервером, не запрашивая SSL.

Когда SSL-шифрование может быть выполнено, ожидается, что сервер передаст только один байт с S, а затем будет ожидать от клиента начала согласования SSL. Если на этом этапе доступны для чтения дополнительные байты, это, скорее всего, означает, что «незаконный посредник» пытается выполнить атаку с переполнением буфера (CVE-2021-23222). Клиенты должны кодироваться так, чтобы либо прочитать из сокета ровно один байт, прежде чем передать этот сокет своей библиотеке SSL, либо, если ими были прочитаны дополнительные байты, считать это нарушением протокола.

Начальное сообщение SSLRequest также может передаваться в соединении, которое открывается для передачи сообщения CancelRequest.

Хотя сам протокол не предоставляет способа принудительно включить шифрование SSL сервером, администратор может сконфигурировать сервер так, чтобы в качестве дополнения при проверке подлинности клиента тот не принимал незашифрованные сеансы.



Шифрование сеанса с GSSAPI

Если QHB была собрана с поддержкой GSSAPI, взаимодействия клиента и сервера могут быть зашифрованы с использованием GSSAPI. Это обеспечивает защиту связи в средах, где взломщики могут перехватить трафик сеанса. Более подробную информацию о шифровании сеансов QHB с помощью GSSAPI см. в разделе Защита TCP/IP-соединений посредством шифрования GSSAPI.

Чтобы запустить соединение с GSSAPI-шифрованием, клиент сначала передает сообщение GSSENCRequest вместо StartupMessage. Затем сервер отвечает одним байтом, содержащим G или N, показывая, что он желает или не желает выполнять GSSAPI-шифрование соответственно. На этом этапе клиент может закрыть соединение, если ответ его не удовлетворяет. Чтобы продолжить после получения G, используя привязки GSSAPI на языке C в соответствии со стандартом RFC 2744 или равнозначным, выполните запуск GSSAPI, вызывая функцию gss_init_sec_context() в цикле и передавая результат серверу, сначала без входных данных, а затем с каждым результатом от сервера, пока функция не перестанет выдавать выходные данные. При передаче результатов gss_init_sec_context() серверу перед сообщением добавьте его длину в виде четырехбайтового целого в сетевом порядке байтов. Чтобы продолжить после получения N, клиент передает обычное сообщение StartupMessage и продолжает взаимодействие с сервером без шифрования. (Как вариант, клиенту разрешается выдать сообщение SSLRequest после получения в ответ N, чтобы попытаться использовать вместо GSSAPI шифрование SSL.)

Кроме того, клиент должен быть готов обработать сообщение ErrorMessage, полученное от сервера в ответ на GSSENCRequest. Клиент не должен отображать это сообщение об ошибке пользователю/приложению, поскольку сервер не был аутентифицирован (CVE-2024-10977). В этом случае соединение должно быть закрыто, но клиент может предпочесть открыть новое соединение и продолжить взаимодействие с сервером, не запрашивая GSSAPI-шифрование.

Когда шифрование GSSAPI может быть выполнено, ожидается, что сервер передаст только один байт с G, а затем будет ожидать от клиента начала согласования GSSAPI. Если на этом этапе доступны для чтения дополнительные байты, это, скорее всего, означает, что «незаконный посредник» пытается выполнить атаку с переполнением буфера (CVE-2021-23222). Клиенты должны кодироваться так, чтобы либо прочитать из сокета ровно один байт, прежде чем передать этот сокет своей библиотеке GSSAPI, либо, если ими были прочитаны дополнительные байты, считать это нарушением протокола.

Начальное сообщение GSSENCRequest также может передаваться в соединении, которое открывается для передачи сообщения CancelRequest.

Если GSSAPI-шифрование было успешно установлено, используйте функцию gss_wrap() для шифрования обычного сообщения StartupMessage и всех последующих данных, добавляя длину результата gss_wrap() в виде четырехбайтового целого в сетевом порядке байтов перед собственно зашифрованным информационным наполнением. Обратите внимание, что сервер примет от клиента зашифрованные пакеты, только если они меньше 16 КБ; чтобы определить размер незашифрованного сообщения, которое уложится в это ограничение, клиент должен использовать функцию gss_wrap_size_limit(), а более крупные сообщения следует разбивать на части с помощью нескольких вызовов функции gss_wrap(). Обычно сегменты незашифрованных данных имеют размер 8 КБ, поэтому зашифрованные пакеты получаются размером чуть больше 8 КБ, но вполне умещаются в лимит 16 КБ. Можно ожидать, что сервер тоже не будет передавать клиенту зашифрованные пакеты размером больше 16 КБ.

Хотя сам протокол не предоставляет способа принудительно включить шифрование GSSAPI сервером, администратор может сконфигурировать сервер так, чтобы в качестве дополнения при проверке подлинности клиента тот не принимал незашифрованные сеансы.