Поток сообщений
В этом разделе описывается поток сообщений и семантика каждого типа сообщений. (Подробности о точном представлении каждого сообщения приведены в разделе «Форматы сообщений».) Существует несколько различных субпротоколов de-. в зависимости от состояния соединения: запуск, запрос, вызов функции, COPY и завершение. Существуют также специальные положения дл я асинхронных операций (включая ответы на уведомления и отмену команд), которые могут происходить в любое время после фазы запуска.
Начало работы
Чтобы начать сеанс, фронтенд открывает соединение с сервером и отправляет сообщение о запуске. Это сообщение включает в себя имена пользователя и базы данных, к которой он хочет подключиться; оно также определяет версию протокола, который будет использоваться. (По желанию, сообщение о запуске может включать дополнительные настройки параметров времени выполнения). Затем сервер использует эту информацию и содержимое своих конфигурационных файлов (например, pg_hba.conf), чтобы определить, является ли соединение условно приемлемым и какая дополнительная аутентификация требуется (если таковая имеется).
Затем сервер отправляет соответствующее сообщение запроса аутентификации, на которое фронтенд должен ответить соответствующим сообщением (например, паролем). Для всех методов аутентификации, кроме GSSAPI, SSPI и SASL, существует не более одного запроса и одного ответа. В некоторых методах от внешнего интерфейса вообще не требуется никакого ответа, поэтому запрос аутентификации не выполняется. Для GSSAPI, SSPI и 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() возвращает свой результат в виде шестнадцатеричной строки).
AuthenticationSCMCredential
Этот ответ возможен только для локальных соединений с Unix-доменом на платформах, поддерживающих учетные сообщения SCM. Фронтенд должен выдать сообщение SCM о полномочиях, а затем отправить один байт данных. (Содержимое байта данных не представляет интереса; оно используется только для того, чтобы сервер ждал достаточно долго, чтобы получить сообщение о полномочиях). Если учетные данные приемлемы, сервер отвечает AuthenticationOk, в противном случае он отвечает ErrorResponse. (Этот тип сообщения выдается только серверами версии до 9.1. Со временем он может быть исключен из спецификации протокола).
AuthenticationGSS
Теперь фронтенд должен инициировать GSSAPI-переговоры. В ответ на это фронтенд отправит сообщение GSSResponse с первой частью потока данных GSSAPI. Если потребуются дополнительные сообщения, сервер ответит AuthenticationGSSContinue.
AuthenticationSSPI
Теперь фронтенд должен инициировать переговоры по SSPI. В ответ на это внешний сервер отправит GSSResponse с первой частью потока данных SSPI. Если потребуются дополнительные сообщения, сервер ответит AuthenticationGSSContinue.
AuthenticationGSSContinue
Это сообщение содержит данные ответа от предыдущего шага переговоров GSSAPI или SSPI (AuthenticationGSS, AuthenticationSSPI или предыдущее AuthenticationGSSContinue). Если данные GSSAPI или SSPI в этом сообщении указывают на необходимость дополнительных данных для завершения аутентификации, фронтенд должен отправить эти данные в виде другого сообщения GSSResponse. Если GSSAPI или SSPI аутентификация завершена этим сообщением, сервер отправит 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 как начало цикла команд, или считать 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». Все остальные типы запросов обычно выдают только сообщение CommandComplete.
Поскольку строка запроса может содержать несколько запросов (разделенных точками с запятой), может быть несколько таких последовательностей ответов, прежде чем бэкенд закончит обработку строки запроса. ReadyForQuery выдается, когда вся строка обработана и бэкенд готов принять новую строку запроса.
Если получена абсолютно пустая (без содержимого, кроме пробелов) строка запроса, ответом будет EmptyQueryResponse, за которым последует ReadyForQuery.
В случае ошибки выдается ErrorResponse, за которым следует ReadyForQuery. Вся дальнейшая обработка строки запроса прерывается сообщением ErrorResponse (даже если в ней оставались еще запросы). Обратите внимание, что это может произойти на середине последовательности сообщений, генерируемых отдельным запросом.
В режиме простого запроса формат извлекаемых значений всегда текстовый, за исключением случаев, когда заданная команда является FETCH из курсора, объявленного с опцией BINARY. В этом случае получаемые значения имеют двоичный формат. Коды формата, указанные в сообщении RowDescription, показывают, какой формат используется.
Фронтенд должен быть готов принять сообщения ErrorResponse и NoticeResponse, если он ожидает сообщения любого другого типа. См. также раздел «Асинхронные операции» о сообщениях, которые бэкенд может генерировать в результате внешних событий.
Рекомендуется кодировать фронтенды в стиле машины состояний, которая будет принимать сообщения любого типа в любое время, когда это может иметь смысл, а не вводить предположения о точной последовательности сообщений.
55.2.3. Несколько утверждений в простом запросе
Когда сообщение простого запроса содержит более одного оператора 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, закрывающий существующий блок транзакций, то все последующие утверждения выполняются в неявном блоке транзакций. И наоборот, если BEGIN появляется в сообщении Query, состоящем из нескольких утверждений, то он начинает обычный блок транзакций, который будет завершен только явным COMMIT или ROLLBACK, независимо от того, появится ли он в этом сообщении Query или в более позднем. Если BEGIN следует за некоторыми утверждениями, которые были выполнены как неявный блок транзакций, эти утверждения не фиксируются немедленно; по сути, они задним числом включаются в новый регулярный блок транзакций.
COMMIT
или ROLLBACK
, появившиеся в неявном блоке транзакций, выполняются как обычно, закрывая неявный блок; однако будет выдано предупреждение, поскольку COMMIT
или ROLLBACK
без предшествующего BEGIN
может представлять собой ошибку. Если за этим последуют другие утверждения, для них будет запущен новый блок неявных транзакций.
Точки сохранения не допускаются в неявном блоке транзакции, поскольку они будут противоречить поведению автоматического закрытия блока при любой ошибке.
Помните, что, независимо от присутствующих команд управления транзакциями, выполнение сообщения Query останавливается при первой же ошибке. Таким образом, для примера дано
BEGIN;
SELECT 1/0;
ROLLBACK;
В одном сообщении Query, сессия окажется внутри неудачного блока обычной транзакции, поскольку ROLLBACK
не будет достигнут после ошибки деления на ноль. Чтобы вернуть сессию в пригодное для использования состояние, потребуется еще один ROLLBACK
.
Еще одно поведение, которое следует отметить, заключается в том, что первоначальный лексический и синтаксический анализ выполняется для всей строки запроса, прежде чем любой из них будет выполнен. Таким образом, простые ошибки (например, неправильно написанное ключевое слово) в последующих операторах могут предотвратить выполнение любого из них. Обычно это незаметно для пользователей, поскольку все операторы в любом случае откатываются назад, если выполняются как неявный блок транзакции. Однако это может быть заметно при попытке выполнить несколько транзакций в запросе с несколькими утвержд ениями. Например, если опечатка превратила наш предыдущий пример в
BEGIN;
INSERT INTO mytable VALUES(1);
COMMIT;
INSERT INTO mytable VALUES(2);
SELCT 1/0;
То ни один из операторов не будет запущен, что приведет к видимой разнице в том, что первый INSERT
не будет зафиксирован. Ошибки, обнаруженные при семантическом анализе или позже, такие как неправильное написание имени таблицы или столбца, не имеют такого эффекта.
Расширенный запрос
Расширенный протокол запроса разбивает описанный выше простой протокол запроса на несколько этапов. Результаты подготовительных этапов могут быть использованы многократно для повышения эффективности. Кроме того, доступны дополнительные возможности, такие как возможность предоставления значений данных в виде отдельных параметров вместо того, чтобы вставлять их непосредственно в строку запроса.
В расширенном протоколе фронтенд сначала отправляет сообщение Parse, которое содержит текстовую строку запроса, опционально некоторую информацию о типах данных держателей параметров и имя целевого объекта prepared-statement (пустая строка выбирает безымянный prepared statement). Ответом является либо ParseComplete, либо ErrorResponse. Типы данных параметров могут быть указаны через OID; если они не указаны, синтаксический анализатор пытается вывести типы данных таким же образом, как это делается для нетипизированных литеральных строковых констант.
Примечание:
Тип данных параметра можно не указывать, установив его в ноль или сделав массив OID-типов параметров короче, чем количество символов параметров (
$n
), используемых в строке запроса. Еще один особый случай - тип параметра может быть указан какvoid
(то есть OID псевдотипаvoid
). Это сделано для того, чтобы позволить использовать символы параметров для параметров функций, которые на самом деле являются параметрами OUT. Обычно не существует контекста, в котором можно было бы использовать параметрvoid
, но если такой символ параметра появляется в списке параметров функции, он фактически игнорируется. Например, вызов функцииfoo($1,$2,$3,$4)
может соответствовать функции с двумя IN и двумя OUT аргументами, если$3
и$4
указаны как имеющие типvoid
.
Примечание:
Строка запроса, содержащаяся в сообщении Parse, не может включать более одного оператора SQL; в противном случае сообщается о синтаксической ошибке. Этого ограничения нет в протоколе простых запросов, но оно есть в расширенном протоколе, потому что разрешение подготовленным операторам или порталам содержать несколько команд неоправданно усложнило бы протокол.
При успешном создании именованный объект prepared-statement действует до конца текущей сессии, если не будет явно уничтожен. Неименованный подготовленный оператор существует только до тех пор, пока не будет выпущен следующий оператор Parse, указывающий неименованный оператор в качестве назначения. (Обратите внимание, что простое сообщение Query также уничтожает неименованный оператор). Именованные подготовленные операторы должны быть явно закрыты, прежде чем они могут быть переопределены другим сообщением Parse, но для неименованного оператора это не требуется. Именованные подготовленные операторы можно также создавать и получать к ним доступ на уровне команд SQL, используя PREPARE и EXECUTE.
Если подготовленный оператор существует, его можно подготовить к выполнению с помощью сообщения Bind. В сообщении Bind указывается имя исходного подготовленного оператора (пустая строка обозначает безымянный подготовленный оператор), имя целевого портала (пустая строка обозначает безымянный портал) и значения, которые следует использовать для любых заполнителей параметров, присутствующих в подготовленном операторе. Предоставленный набор параметров должен соответствовать параметрам, необходимым подготовленному оператору. (Если в сообщении Parse объявили параметры void, передайте для них значения NULL в сообщении Bind). Bind также указывает формат, который будет использоваться для данных, возвращаемых запросом; формат может быть указан в целом или для каждого столбца. Ответом является либо BindComplete, либо ErrorResponse.
Примечание:
Выбор между текстовым и двоичным выводом определяется кодами формата, заданными в Bind, независимо от используемой команды SQL. Атрибут
BINARY
в объявлениях курсоров не имеет значения при использовании расширенного протокола запросов.
Планирование запросов обычно происходит во время обработки сообщения Bind. Если подготовленный запрос не имеет параметров или выполняется многократно, сервер может сохранить созданный план и повторно использовать его при последующих сообщениях Bind для того же подготовленного запроса. Однако он сделает это только в том случае, если обнаружит, что можно создать общий план, который будет не намного менее эффективен, чем план, зависящий от конкретных значений параметров. Это происходит прозрачно для протокола.
Если объект именованного портала успешно создан, он действует до конца текущей транзакции, если не уничтожен явно. Неименованный портал уничтожается в конце транзакции или как только будет выпущен следующий оператор Bind, указывающий неименованный портал в качестве места назначения. (Обратите внимание, что просто е сообщение Query также уничтожает неименованный портал). Именованные порталы должны быть явно закрыты, прежде чем их можно будет переопределить другим сообщением 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 (вариант statement) указывается имя существующего подготовленного оператора (или пустая строка для безымянного подготовленного оператора). В ответ выдается сообщение ParameterDescription, описывающее параметры, необходимые оператору, а затем сообщение RowDescription, описывающее строки, которые будут возвращены при выполнении оператора (или сообщение NoData, если оператор не будет возвращать строки). Если такого подготовленного оператора не существует, выдается сообщение ErrorResponse. Обратите внимание, что поскольку Bind еще не был выпущен, форматы, которые будут использоваться для возвращаемых столбцов, еще не известны бэкенду; поля кода формата в сообщении RowDescription в этом случае будут нулями.
Совет:
В большинстве сценариев фронтенд должен выдать один или другой вариант Describe перед выдачей Execute, чтобы убедиться, что он знает, как интерпретировать результаты, которые он получит в ответ.
Сообщение Close закрывает существующую подготовленную выписку или портал и освобождает ресурсы. Не является ошибкой выдача Close для несуществующего заявления или имени портала. Обычно ответом является CloseComplete, но может быть ErrorResponse, если при освобождении ресурсов возникли трудности. Обратите внимание, что закрытие подготовленного оператора неявно закрывает все открытые порталы, которые были созданы на основе этого оператора.
Сообщение Flush не вызывает генерации какого-либо конкретного вывода, но заставляет бэкенд доставить все данные, ожидающие в его выходных буферах. Сообщение Flush должно быть отправлено после любой команды расширенного запроса, кроме Sync, если фронтенд хочет просмотреть результаты этой команды перед выдачей других команд. Без Flush сообщения, возвращаемые бэкендом, будут объединены в минимально возможное количество пакетов, чтобы минимизировать сетевые накладные расходы.
Примечание:
Простое сообщение Query примерно эквивалентно последовательности Parse, Bind, portal Describe, Execute, Close, Sync, с использованием неименованного подготовленного оператора и объектов портала и без параметров. Одно отличие заключается в том, что оно принимает несколько SQL-операторов в строке запроса, автоматически выполняя последовательность связывания/описания/выполнения для каждого из них последовательно. Еще одно отличие заключается в том, что он не возвращает сообщения ParseComplete, BindComplete, CloseComplete или NoData.
Пайплайнинг (конвейерная обработка)
Использование протокола расширенных запросов позволяет выполнять конвейерную обработку, то есть отправлять серию запросов, не дожидаясь завершения предыдущих. Это сокращает количество сетевых обходов, необходимых для выполнения заданной серии операций. Однако пользователь должен тщательно продумать поведение в случае неудачи на одном из этапов, поскольку последующие запросы уже будут отправлены на сервер.
Один из способов справиться с этим - сделать всю серию запросов одной транзакцией, то есть обернуть ее в BEGIN ... COMMIT
. Однако это не поможет, если нужно, чтобы некоторые команды выполнялись независимо от других.
Расширенный протокол запросов предоставляет другой способ решения этой проблемы, который заключается в том, чтобы не посылать сообщения Sync между зависимыми шагами. Поскольку после ошибки бэкэнд будет пропускать командные сообщения, пока не найдет Sync, это позволяет автоматически пропускать последующие команды в конвейере при сбое предыдущей, без необходимости для клиента явно управлять этим с помощью BEGIN
и COMMIT
. Независимо коммитируемые сегменты конвейера могут быть разделены сообщениями Sync.
Если клиент не выдал явного BEGIN
, то каждая синхронизация обычно вызывает неявный COMMIT
, если предыдущий шаг (шаги) завершился успешно, или неявный ROLLBACK
, если они завершились неудачно. Однако есть несколько команд DDL (например, CREATE DATABASE
), которые не могут быть выполнены внутри блока транзакций. Если одна из них будет выполнена в конвейере, она завершится неудачей, если только она не является первой командой в конвейере. Более того, после успеха она заставит выполнить немедленную фиксацию, чтобы сохранить согласованность базы данных. Таким образом, Sync, следующий сразу за одной из этих команд, не имеет никакого эффекта, кроме ответа ReadyForQuery.
При использовании этого метода завершение конвейера должно определяться путем подсчета сообщений ReadyForQuery и ожидания, пока это количество не достигнет количества отправленных Syncs. Подсчет ответов на завершение команд ненадежен, поскольку некоторые команды могут быть пропущены и, таким образом, не выдадут сообщения о завершении.
Вызов функции
Подпротокол Function Call позволяет клиенту запросить прямой вызов любой функции, которая существует в системном каталоге pg_proc
базы данных. Клиент должен иметь разрешение на выполнение функции.
Примечание:
Подпротокол вызова функций - это унаследованная функция, которую, вероятно, лучше не использовать в новом коде. Аналогичных результатов можно добиться, создав подготовленный оператор, который выполняет
SELECT function($1, ...)
. Тогда цикл вызова функции можно заменить на Bind/Execute.
Цикл вызова функции начинается с того, что фронтенд отправляет бэкенду сообщение FunctionCall. Затем бэкенд отправляет одно или несколько ответных сообщений в зависимости от результатов вызова функции, и, наконец, ответное сообщение ReadyForQuery. ReadyForQuery информирует фронтенд о том, что он может безопасно отправить новый запрос или вызов функции.
Возможные ответные сообщения от бэкенда следующие:
ErrorResponse
Произошла ошибка.
FunctionCallResponse
Вызов функции был завершен и вернул результат, указанный в сообщении. (Обратите внимание, что протокол вызова функции может обрабатывать только один скалярный результат, а не тип строки или набор результатов).
ReadyForQuery
Обработка вызова функции завершена. ReadyForQuery будет отправлен всегда, независимо от того, завершится ли обработка успешно или с ошибкой.
NoticeResponse
В связи с вызовом функции было выдано предупреждение. Предупреждения являются дополнением к другим ответам, т. е. бэкэнд продолжит обработку команды.
Операции COPY
Команда COPY
позволяет осуществлять высокоскоростную массовую передачу данных на сервер или с сервера. Операции копирования внутрь и копирования наружу переключают соединение на отдельный субпротокол, который действует до завершения операции.
Режим Copy-in (передача данных на сервер) запускается, когда бэкэнд выполняет 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 как завершение режима копирования.
Сообщения NoticeResponse и ParameterStatus могут перемежаться между сообщениями CopyData; фронтэнды должны обрабатывать такие случаи, а также должны быть готовы к другим типам асинхронных сообщ ений см. [раздел «Асинхронные операции». В противном случае любой тип сообщения, кроме CopyData или CopyDone, может рассматриваться как завершающий режим копирования.
Существует еще один режим Copy, называемый copy-both, который позволяет осуществлять высокоскоростную массовую передачу данных на сервер и с сервера. Режим Copy-both запускается, когда бэкэнд в режиме walsender выполняет оператор 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: это server_version
, server_encoding
, client_encoding
, application_name
, default_transaction_read_only
, in_hot_standby
, is_superuser
, session_authorization
, DateStyle
, IntervalStyle
, TimeZone
, integer_datetimes
, и standard_conforming_strings
. (server_encoding
, TimeZone
и integer_datetimes
не сообщались в выпусках до 8.0; standard_conforming_strings
не сообщался в выпусках до 8.1; IntervalStyle
не сообщался в выпусках до 8.4; application_name
не сообщалось в выпусках до 9.0; default_transaction_read_only
и in_hot_standby
не сообщались в выпусках до 14). Обратите внимание, что server_version
, server_encoding
и integer_datetimes
- это псевдопараметры, которые не могут измениться после запуска. Этот набор может измениться в будущем или даже стать настраиваемым. Соответственно, фронтенд должен просто игнорировать ParameterStatus для параметров, которые ему непонятны или безразличны.
Если фронтенд подает команду LISTEN
, то бэкенд будет отправлять сообщение NotificationResponse (не путать с NoticeResponse!) каждый раз, когда для того же имени канала будет выполнена команда NOTIFY
.
Примечание:
В настоящее время NotificationResponse может быть отправлен только вне транзакции, и поэтому он не произойдет в середине серии команд-ответов, хотя может произойти непосредственно перед ReadyForQuery. Однако неразумно разрабатывать логику фронтенда, предполагающую это. Хорошей практикой является возможность принимать NotificationResponse в любой точке протокола.
Отмена выполняемых запросов
Во время обработки запроса фронтенд может запросить отмену запроса. Запрос на отмену не отправляется напрямую по открытому соединению с бэкендом по соображениям эффективности реализации: мы не хотим, чтобы бэкенд постоянно проверял наличие новых данных от фронтенда во время обработки запроса. Запросы на отмену должны быть относительно редкими, поэтому мы сделали их немного более тусклыми, чтобы избежать штрафа в обычном случае.
Чтобы отправить запрос на отмену, фронтенд открывает новое соединение с сервером и отправляет сообщение CancelRequest, а не StartupMessage, которое обычно отправляется через новое соединение. Сервер обработает этот запрос и закроет соединение. По соображениям безопасности прямой ответ на сообщение запроса отмены не дается.
Сообщение CancelRequest будет проигнорировано, если оно не с одержит тех же ключевых данных (PID и секретный ключ), которые были переданы фронтенду при запуске соединения. Если запрос совпадает с PID и секретным ключом выполняющегося в данный момент бэкенда, обработка текущего запроса прерывается. (В существующей реализации это делается путем отправки специального сигнала процессу бэкенда, который обрабатывает запрос).
Сигнал отмены может иметь или не иметь никакого эффекта - например, если он поступает после того, как бэкенд закончил обработку запроса, то он не будет иметь никакого эффекта. Если отмена эффективна, она приводит к досрочному завершению текущей команды с сообщением об ошибке.
В итоге, по соображениям безопасности и эффективности, у фронтенда нет прямого способа определить, был ли запрос на отмену успешным. Он должен продолжать ждать, пока бэкэнд ответит на запрос. Отправка отмены просто повышает вероятность того, что текущий запрос скоро завершится, и повышает вероятность того, что он не завершится успешно, а завершится с сообщением об ошибке.
Поскольку запрос на отмену отправляется через новое соединение с сервером, а не через обычный к анал связи между фронтендом и бэкендом, запрос на отмену может быть отправлен любым процессом, а не только тем фронтендом, запрос которого должен быть отменен. Это может обеспечить дополнительную гибкость при создании многопроцессных приложений. Это также создает риск безопасности, поскольку неавторизованные лица могут попытаться отменить запросы. Риск безопасности устраняется требованием предоставлять динамически генерируемый секретный ключ в запросах на отмену.
Завершение работы
Обычная процедура плавного завершения заключается в том, что фронтенд посылает сообщение Terminate и немедленно закрывает соединение. Получив это сообщение, бэкэнд закрывает соединение и завершает работу.
В редких случаях (например, при отключении базы данных по команде администратора) бэкенд может отключиться без запроса со стороны фронтенда. В таких случаях перед закрытием соединения бэкэнд попытается отправить сообщение об ошибке или уведомление с указанием причины разрыва соединения.
Другие сценарии завершения возникают из-за различных сбоев, таких как отказ ядра на одном или другом конце, потеря канала связи, потеря синхронизации границ сообщений и т. д. Если фронтенд или бэкенд видит неожиданное закрытие соединения, он должен очистить его и завершить. Фронтенд имеет возможность запустить новый бэкенд, повторно связавшись с сервером, если он не хочет завершать соединение самостоятельно. Закрытие соединения также целесообразно, если получено сообщение нераспознаваемого типа, поскольку это, вероятно, указывает на потерю синхронизации границ сообщений.
При нормальном или ненормальном завершении любая открытая транзакция откатывается, а не фиксируется. Однако следует отметить, что если фронтенд отключается от сети во время обработки запроса, не относящегося к категории SELECT
, бэкенд, скорее всего, завершит запрос до того, как заметит отключение. Если запрос находится за пределами какой-либо блок транзакции (последовательность BEGIN ... COMMIT
), то его результаты могут быть зафиксированы до того, как будет распознано раз ъединение.
Шифрование сеансов SSL
Если PostgreSQL был создан с поддержкой SSL, соединения между внешним и внутренним серверами могут быть зашифрованы с помощью SSL. Это обеспечивает безопасность связи в средах, где злоумышленники могут перехватить трафик сеанса.
Чтобы инициировать SSL-шифрованное соединение, фронтенд сначала отправляет сообщение SSLRequest, а не StartupMessage. Сервер отвечает одним байтом, содержащим S или N, что указывает на его желание или нежелание использовать SSL, соответственно. Фронтенд может закрыть соединение на этом этапе, если его не устраивает ответ. Чтобы продолжить после S, выполните с сервером начальное рукопожатие SSL (здесь не описывается, является частью спецификации SSL). Если оно прошло успешно, продолжите отправку обычного StartupMessage. В этом случае StartupMessage и все последующие данные будут зашифрованы по протоколу SSL. Чтобы продолжить работу после N, отправьте обычное сообщение StartupMessage и продолжите работу без шифрования. (В качестве альтернативы можно отправить сообщение GSSENCRequest после ответа N, чтобы попытаться использовать шифрование GSSAPI вместо SSL).
Фронтенд также должен быть готов к обработке ответа ErrorMessage на SSLRequest от сервера. Фронтенд не должен показывать это сообщение об ошибке пользователю/приложению, поскольку сервер не был аутентифицирован (CVE-2024-10977). В этом случае соединение должно быть закрыто, но фронтенд может выбрать открытие нового соединения и продолжить работу без запроса SSL.
Когда SSL-шифрование может быть выполнено, ожидается, что сервер отправит только один байт S
, а затем будет ждать, пока фронтенд инициирует SSL-рукопожатие. Если в этот момент доступны для чтения дополнительные байты, это, скорее всего, означает, что "человек посередине" пытается провести атаку с переполнением буфера (CVE-2021-23222). Перед передачей сокета библиотеке SSL фронтенды должны быть настроены либо на чтение ровно одного байта из сокета, либо на нарушение протокола, если они обнаружат, что прочитали дополнительные байты.
Начальный SSLRequest также может использоваться в открываемом соединении для отправки сообщения CancelRequest.
Хотя сам протокол не предоставляет серверу возможности принудительного шифрования SSL, администратор может настроить сервер на отклонение незашифрованных сеансов в качестве побочного продукта проверки подлинности.
Шифрование сеансов GSSAPI
Если PostgreSQL был создан с поддержкой GSSAPI, то соединения между фронтендом и бэкендом могут быть зашифрованы с помощью 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
ответ на попытку использовать SSL-шифрование вместо GSSAPI).
Фронтенд также должен быть готов к обработке ответа 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, администратор может настроить сервер на отклонение незашифрованных сессий в качестве побочного продукта проверки подлинности.