53.2. Поток сообщений#
53.2. Поток сообщений #
Этот раздел описывает поток сообщений и семантику каждого типа сообщения. (Подробности точного представления каждого сообщения приведены в Раздел 53.7). Существует несколько различных подпротоколов в зависимости от состояния соединения: запуск, запрос, вызов функции, COPY
и завершение. Также есть специальные положения для асинхронных операций (включая ответы на уведомления и отмену команд), которые могут происходить в любое время после фазы запуска.
53.2.1. Запуск #
Для начала сессии клиентское приложение открывает соединение с сервером и отправляет
сообщение о запуске. Это сообщение включает имена пользователя и базы данных, к которой пользователь хочет подключиться; также оно определяет конкретную
версию протокола, которая будет использоваться. (По желанию, сообщение о запуске может содержать
дополнительные настройки для параметров времени выполнения).
Затем сервер использует эту информацию и
содержимое своих файлов конфигурации (таких как
pg_hba.conf
) для определения
того, является ли соединение предварительно приемлемым, и какая дополнительная
аутентификация требуется (если требуется).
Сервер затем отправляет соответствующее сообщение с запросом аутентификации, на которое клиент должен ответить соответствующим сообщением с ответом на аутентификацию (например, паролем). Для всех методов аутентификации, кроме GSSAPI, SSPI и SASL, существует не более одного запроса и одного ответа. В некоторых методах от клиента не требуется никакого ответа, и поэтому запрос аутентификации не происходит. Для GSSAPI, SSPI и SASL может потребоваться несколько обменов пакетами для завершения аутентификации.
Цикл аутентификации завершается либо отклонением сервером попытки подключения (ErrorResponse), либо отправкой AuthenticationOk.
Возможные сообщения от сервера на этой фазе:
- ErrorResponse
Попытка подключения была отклонена. Затем сервер немедленно закрывает соединение.
- CompressionAck
Сервер принимает запрос клиента на сжатие. Сжатие запрашивается, когда подключение клиента включает опцию "compression", которая содержит список запрошенных алгоритмов сжатия. Сервер пересекает запрошенные алгоритмы сжатия с разрешенными (управляемыми с помощью настройки сервера
libpq_compression
). Если пересечение не пусто, сервер отвечает с сообщением CompressionAck, содержащим окончательный список алгоритмов сжатия, которые могут быть использованы для сжатия сообщений libpq между клиентом и сервером. Если пересечение пусто (сервер не принимает ни один из запрошенных алгоритмов), то он отвечает с сообщением CompressionAck, содержащим пустой список, и клиенту остается решить, продолжать ли без сжатия или сообщить об ошибке. После отправки сообщения CompressionAck сервер может отправить сообщение SetCompressionMethod для установки текущего алгоритма сжатия для сжатия трафика от сервера к клиенту. После получения сообщения CompressionAck клиент может отправить сообщение SetCompressionMethod для установки текущего алгоритма сжатия для сжатия трафика от клиента к серверу.- 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.
- AuthenticationSSPI
Фронтенд теперь должен инициировать процесс SSPI. Фронтенд отправит GSSResponse с первой частью потоковых данных SSPI в ответ на это. Если требуются дополнительные сообщения, сервер ответит с AuthenticationGSSContinue.
- AuthenticationGSSContinue
Это сообщение содержит данные ответа от предыдущего шага GSSAPI или SSPI-переговоров (AuthenticationGSS, AuthenticationSSPI или предыдущего AuthenticationGSSContinue). Если данные GSSAPI или SSPI в этом сообщении указывают на необходимость дополнительных данных для завершения аутентификации, клиент должен отправить эти данные в виде другого сообщения GSSResponse. Если аутентификация GSSAPI или SSPI завершается этим сообщением, сервер затем отправит AuthenticationOk для указания успешной аутентификации или ErrorResponse для указания неудачи.
- AuthenticationSASL
Фронтенд теперь должен инициировать процесс SASL-аутентификации, используя один из механизмов SASL, перечисленных в сообщении. Фронтенд отправит SASLInitialResponse с именем выбранного механизма и первой частью потоковых данных SASL в ответ на это. Если требуются дополнительные сообщения, сервер ответит с AuthenticationSASLContinue. Подробности смотрите в разделе Раздел 53.3.
- 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. Фронтенд может игнорировать это сообщение или записывать настройки для будущего использования; см. Раздел 53.2.7 для получения более подробной информации. Фронтенд не должен отвечать на это сообщение, но должен продолжать прослушивать сообщение ReadyForQuery.
- ReadyForQuery
Запуск завершен. Теперь фронтенд может выполнять команды.
- ErrorResponse
Не удалось запуститься. Соединение закрыто после отправки этого сообщения.
- NoticeResponse
Выдано предупреждение. Фронтенд должен отобразить сообщение, но продолжать прослушивать ReadyForQuery или ErrorResponse.
Сообщение ReadyForQuery - это то же самое сообщение, которое бэкенд будет выдавать после каждого цикла команд. В зависимости от потребностей кодирования фронтенда, разумно рассматривать ReadyForQuery как начало цикла команд или как окончание фазы запуска и каждого последующего цикла команд.
53.2.2. Простой запрос #
Весь цикл запроса инициируется фронтендом, отправляющим сообщение Query на бэкенд. Сообщение включает команду SQL (или команды), выраженные в виде текстовой строки. Затем бэкенд отправляет одно или несколько ответных сообщений в зависимости от содержимого строки команды запроса, а затем сообщение ReadyForQuery. ReadyForQuery информирует фронтенд о том, что он может безопасно отправить новую команду. (Фактически фронтенду не обязательно ждать ReadyForQuery перед отправкой следующей команды, но в этом случае фронтенд должен самостоятельно разобраться, что произойдет, если предыдущая команда не выполнится, а последующие команды будут успешными).
Возможные сообщения ответа от сервера:
- CommandComplete
SQL-команда успешно выполнена.
- CopyInResponse
Бэкенд готов копировать данные с фронтенда в таблицу; см. Раздел 53.2.6.
- CopyOutResponse
Бэкенд готов копировать данные из таблицы во фронтенд; см. Раздел 53.2.6.
- RowDescription
Указывает, что строки собираются быть возвращены в ответ на запрос
SELECT
,FETCH
и т. д. Содержимое этого сообщения описывает структуру столбцов строк. За этим будет следовать сообщение DataRow для каждой возвращаемой строки к клиентской части.- DataRow
Один из набора строк, возвращаемых запросом
SELECT
,FETCH
и т. д.- EmptyQueryResponse
Обнаружена пустая строка запроса.
- ErrorResponse
Произошла ошибка.
- ReadyForQuery
Обработка строки запроса завершена. Отдельное сообщение отправляется для указания этого, поскольку строка запроса может содержать несколько SQL-команд. (CommandComplete обозначает окончание обработки одной SQL-команды, а не всей строки). ReadyForQuery всегда будет отправлено, независимо от того, завершается ли обработка успешно или с ошибкой.
- NoticeResponse
Выдано предупреждающее сообщение относительно запроса. Уведомления добавляются к другим ответам, то есть сервер продолжит обработку команды.
Ответ на запрос SELECT
(или другие запросы, которые возвращают наборы строк, такие как EXPLAIN
или SHOW
) обычно состоит из сообщений RowDescription, ноль или более сообщений DataRow, а затем CommandComplete.
Команда COPY
в или из клиента вызывает специальный протокол, описанный в Раздел 53.2.6.
Все остальные типы запросов обычно порождают только сообщение CommandComplete.
Поскольку строка запроса может содержать несколько запросов (разделенных точкой с запятой), может быть несколько таких последовательностей ответов, пока бэкенд не закончит обработку строки запроса. ReadyForQuery выполняется, когда вся строка была обработана, и бэкенд готов принять новую строку запроса.
Если получен полностью пустой (без содержимого, кроме пробелов) запрос, ответом будет EmptyQueryResponse, за которым следует ReadyForQuery.
В случае ошибки, будет выдано сообщение ErrorResponse, за которым следует ReadyForQuery. Все последующие обработки строки запроса прерываются сообщением ErrorResponse (даже если в ней остались еще запросы). Обратите внимание, что это может произойти в середине последовательности сообщений, сгенерированных отдельным запросом.
В режиме простого запроса формат получаемых значений всегда является текстовым, за исключением случая, когда указанная команда является FETCH
из курсора, объявленного с опцией BINARY
. В этом случае полученные значения имеют двоичный формат. Коды формата, указанные в сообщении RowDescription, указывают, какой формат используется.
Фронтенд должен быть готов принимать сообщения ErrorResponse и NoticeResponse, когда ожидается любой другой тип сообщения. См. также Раздел 53.2.7 относительно сообщений, которые могут быть сгенерированы бэкендом из-за внешних событий.
Рекомендуется разрабатывать фронтенды в стиле конечного автомата, который будет принимать любой тип сообщения в любое время, когда это имеет смысл, а не встраивать предположения о точной последовательности сообщений.
53.2.2.1. Несколько операторов в простом запросе #
Когда простое сообщение 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
, но не первую.
Это поведение реализуется путем выполнения операторов в сообщении многокомандного запроса в неявном блоке транзакции, если для них нет явного блока транзакции, в котором они могут выполняться. Основное отличие неявного блока транзакции от обычного заключается в том, что неявный блок автоматически закрывается в конце сообщения запроса, либо неявным коммитом, если не было ошибки, либо неявным откатом, если произошла ошибка. Это аналогично неявному коммиту или откату, который происходит для оператора, выполняемого самостоятельно (когда он не находится в блоке транзакции).
Если сессия уже находится в блоке транзакции в результате команды BEGIN
в предыдущем сообщении, то сообщение Query просто продолжает этот блок транзакции, независимо от того, содержит ли сообщение один оператор или несколько. Однако, если сообщение Query содержит команду COMMIT
или ROLLBACK
, закрывающую существующий блок транзакции, то любые следующие операторы выполняются в неявном блоке транзакции.
Наоборот, если в многооператорном сообщении Query присутствует команда BEGIN
, то она запускает обычный блок транзакции, который будет завершен только явной командой COMMIT
или ROLLBACK
, независимо от того, появится ли она в этом сообщении Query или в последующем.
Если команда BEGIN
следует за некоторыми операторами, которые были выполнены в неявном блоке транзакции, эти операторы не коммитятся немедленно; по сути, они ретроспективно включаются в новый обычный блок транзакции.
Встречающаяся в неявном блоке транзакции команда COMMIT
или ROLLBACK
выполняется как обычно, закрывая неявный блок. Однако будет выдано предупреждение, так как COMMIT
или ROLLBACK
без предшествующей команды BEGIN
может быть ошибкой. Если следуют еще команды, для них будет создан новый неявный блок транзакции.
Сохранение точек сохранения не разрешено в неявном блоке транзакции, так как они будут конфликтовать с поведением автоматического закрытия блока при возникновении ошибки.
Помните, что независимо от наличия команд управления транзакциями, выполнение сообщения Query останавливается при первой ошибке. Таким образом, например, при наличии
BEGIN; SELECT 1/0; ROLLBACK;
В одном сообщении запроса сессия останется внутри неудавшегося обычного блока транзакции, так как ROLLBACK
не будет достигнут после ошибки деления на ноль. Для восстановления сессии в рабочем состоянии потребуется еще одна команда ROLLBACK
.
Другое важное поведение заключается в том, что начальный лексический и синтаксический анализ выполняется на всей строке запроса перед выполнением любой части запроса. Таким образом, простые ошибки (например, неправильно написанное ключевое слово) в последующих операторах могут предотвратить выполнение любого из операторов. Обычно это невидимо для пользователей, так как операторы все равно откатываются при выполнении неявного блока транзакции. Однако это может быть видимо при попытке выполнить несколько транзакций в рамках многократного запроса. Например, если опечатка превратила наш предыдущий пример в
BEGIN; INSERT INTO mytable VALUES(1); COMMIT; INSERT INTO mytable VALUES(2); SELCT 1/0;
Если бы ни один из операторов не выполнился, то это привело бы к видимому отличию, заключающемуся в том, что первая команда INSERT
не была бы подтверждена. Ошибки, обнаруженные на этапе семантического анализа или позже, такие как неправильно написанное имя таблицы или столбца, не имеют такого эффекта.
53.2.3. Расширенный запрос #
Расширенный протокол запросов разбивает описанный выше простой протокол запросов на несколько шагов. Результаты подготовительных шагов могут быть использованы несколько раз для повышения эффективности. Кроме того, доступны дополнительные функции, такие как возможность предоставления значений данных в виде отдельных параметров, а не вставлять их непосредственно в строку запроса.
В расширенном протоколе клиентская часть сначала отправляет сообщение Parse, которое содержит текстовую строку запроса, опционально некоторую информацию о типах данных параметров-заполнителей и имя объекта подготовленного оператора (пустая строка выбирает безымянный подготовленный оператор). Ответом является либо ParseComplete, либо ErrorResponse. Типы данных параметров можно указать с помощью OID; если они не указаны, парсер пытается вывести типы данных таким же образом, как он делает это для неопределенных литеральных строковых констант.
Примечание
Тип данных параметра может быть не указан, установив его значение равным нулю или сделав массив идентификаторов типов параметров короче, чем количество символов параметров ($
n
) в строке запроса. Еще один особый случай - тип параметра может быть указан как void
(то есть OID псевдотипа void
). Это позволяет использовать символы параметров для параметров функции, которые фактически являются OUT-параметрами. Обычно нет контекста, в котором можно использовать параметр типа void
, но если такой символ параметра появляется в списке параметров функции, он фактически игнорируется. Например, вызов функции, такой как foo($1,$2,$3,$4)
, может соответствовать функции с двумя входными и двумя выходными аргументами, если $3
и $4
указаны как имеющие тип void
.
Примечание
Строка запроса, содержащаяся в сообщении Parse, не может включать более одного SQL-запроса; в противном случае будет сообщена ошибка синтаксиса. Это ограничение не существует в простом протоколе запросов, но оно существует в расширенном протоколе, потому что разрешение подготовленных операторов или порталов содержать несколько команд усложнило бы протокол.
Если успешно создан, объект с именованным подготовленным оператором длится до конца текущей сессии, если явно не уничтожен. Безымянный подготовленный оператор длится только до следующего оператора Parse, указывающего безымянный оператор в качестве назначения. (Обратите внимание, что простое сообщение Query также уничтожает безымянный оператор). Именованные подготовленные операторы должны быть явно закрыты, прежде чем они могут быть переопределены другим сообщением Parse, но это не требуется для безымянного оператора. Именованные подготовленные операторы также могут быть созданы и доступны на уровне SQL-команд с использованием PREPARE
и EXECUTE
.
После создания подготовленного оператора его можно подготовить к выполнению с помощью сообщения Bind. Сообщение Bind содержит имя исходного подготовленного оператора (пустая строка обозначает безымянный подготовленный оператор), имя целевого портала (пустая строка обозначает безымянный портал) и значения, которые следует использовать для любых параметров-заполнителей, присутствующих в подготовленном операторе. Предоставленный набор параметров должен соответствовать требуемым подготовленным оператором. (Если вы объявили параметры void
в сообщении Parse, передайте для них значения 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).
Примечание
Синхронизация не приводит к закрытию блока транзакции, открытого с помощью BEGIN
. Возможно обнаружить эту ситуацию, так как сообщение ReadyForQuery содержит информацию о статусе транзакции.
В дополнение к этим основным, обязательным операциям, существует несколько дополнительных операций, которые могут использоваться с расширенным протоколом запросов.
Сообщение Describe (вариант портала) указывает имя существующего портала (или пустую строку для безымянного портала). Ответом является сообщение RowDescription, описывающее строки, которые будут возвращены при выполнении портала; или сообщение NoData, если портал не содержит запроса, который вернет строки; или сообщение ErrorResponse, если такого портала не существует.
Сообщение Describe (вариант оператора) указывает имя существующего подготовленного оператора (или пустую строку для безымянного подготовленного оператора). Ответом является сообщение ParameterDescription, описывающее параметры, необходимые для выполнения оператора, за которым следует сообщение RowDescription, описывающее строки, которые будут возвращены при выполнении оператора (или сообщение NoData, если оператор не будет возвращать строки). Если такого подготовленного оператора не существует, будет выдано сообщение ErrorResponse. Обратите внимание, что поскольку операция Bind еще не была выполнена, форматы, которые будут использоваться для возвращаемых столбцов, пока неизвестны серверу; в этом случае поля кода формата в сообщении RowDescription будут содержать нули.
Подсказка
В большинстве сценариев фронтенд должен выполнять одну из двух вариантов Describe перед выполнением Execute, чтобы убедиться, что он знает, как интерпретировать полученные результаты.
Сообщение Close закрывает существующий подготовленный оператор или портал и освобождает ресурсы. Не является ошибкой выполнять Close для несуществующего имени оператора или портала. Ответом обычно является CloseComplete, но может быть ErrorResponse, если возникли проблемы при освобождении ресурсов. Обратите внимание, что закрытие подготовленного оператора неявно закрывает все открытые порталы, созданные из этого оператора.
Сообщение Flush не вызывает генерацию какого-либо конкретного вывода, но заставляет бэкенд доставить любые данные, ожидающие отправки из его буферов вывода. Flush должен быть отправлен после любой команды расширенного запроса, кроме Sync, если клиентское приложение хочет изучить результаты этой команды перед отправкой дополнительных команд. Без Flush сообщения, возвращаемые бэкендом, будут объединены в минимально возможное количество пакетов для минимизации издержек на сеть.
Примечание
Простое сообщение запроса примерно эквивалентно серии Parse, Bind, portal Describe, Execute, Close, Sync, используя неименованные подготовленные операторы и порталы без параметров. Одно отличие заключается в том, что оно принимает несколько SQL-запросов в строке запроса и автоматически выполняет последовательность bind/describe/execute для каждого из них по очереди. Еще одно отличие заключается в том, что оно не возвращает сообщения ParseComplete, BindComplete, CloseComplete или NoData.
53.2.4. Пайплайнинг #
Использование расширенного протокола запросов позволяет пайплайнинг, что означает отправку серии запросов без ожидания завершения предыдущих. Это снижает количество сетевых обращений, необходимых для выполнения заданной серии операций. Однако пользователь должен детально продумать необходимое поведение, если один из шагов завершится неудачей, поскольку последующие запросы уже будут отправлены на сервер.
Один из способов справиться с этим - сделать всю серию запросов единой транзакцией, то есть обернуть ее в BEGIN
... COMMIT
. Однако это не помогает, если требуется, чтобы некоторые команды коммитились независимо от других.
Расширенный протокол запросов предоставляет еще один способ управления этим вопросом, который заключается в том, чтобы опустить отправку сообщений Sync между зависимыми шагами. Поскольку после ошибки бэкенд пропускает командные сообщения до тех пор, пока не найдет Sync, это позволяет автоматически пропускать более поздние команды в конвейере, когда ранее выполненная команда завершается с ошибкой, без необходимости явного управления этим клиентом с помощью BEGIN
и COMMIT
. Отдельные сегменты конвейера, которые можно фиксировать независимо, могут быть разделены сообщениями Sync.
Если клиент не выполнил явную команду BEGIN
,
то каждая команда Sync обычно вызывает неявную команду COMMIT
,
если предыдущий шаг(и) успешно выполнен, или неявную команду ROLLBACK
,
если они завершились неудачей. Однако есть несколько команд DDL (например, CREATE DATABASE
),
которые не могут быть выполнены внутри блока транзакции. Если одна из них
выполняется в конвейере, она завершится неудачей, если не является первой
командой в конвейере. Кроме того, при успешном выполнении она принудительно
выполняет коммит для сохранения согласованности базы данных. Таким образом, команда Sync,
непосредственно следующая за одной из этих команд, не имеет эффекта, кроме как
ответить ReadyForQuery.
При использовании этого метода завершение конвейера должно быть определено путем подсчета сообщений ReadyForQuery и ожидания достижения числа отправленных Syncs. Подсчет ответов о завершении команды ненадежен, поскольку некоторые команды могут быть прне указаны и, следовательно, не производить сообщение о завершении.
53.2.5. Вызов функции #
Протокол вызова функций позволяет клиенту запросить прямой вызов любой функции, существующей в системном каталоге pg_proc
базы данных. Клиент должен иметь права на выполнение функции.
Примечание
Функциональный подпротокол вызова является устаревшей функцией, которую, вероятно, лучше избегать в новом коде. Подобные результаты можно достичь, настроив подготовленный оператор, которое выполняет SELECT function($1, ...)
. Цикл вызова функции можно заменить на привязку/выполнение.
Цикл вызова функции инициируется фронтендом, отправляющим сообщение FunctionCall на бэкенд. Затем бэкенд отправляет одно или несколько сообщений ответа в зависимости от результатов вызова функции, а затем сообщение ответа ReadyForQuery. ReadyForQuery информирует фронтенд о том, что он может безопасно отправить новый запрос или вызов функции.
Возможные сообщения ответа от сервера:
- ErrorResponse
Произошла ошибка.
- FunctionCallResponse
Функция была вызвана и вернула результат, указанный в сообщении. (Обратите внимание, что протокол вызова функции может обрабатывать только один скалярный результат, а не тип строки или набор результатов).
- ReadyForQuery
Обработка вызова функции завершена. ReadyForQuery всегда будет отправлен, независимо от того, завершается ли обработка успешно или с ошибкой.
- NoticeResponse
Выпущено предупреждающее сообщение относительно вызова функции. Уведомления добавляются к другим ответам, то есть сервер будет продолжать обработку команды.
53.2.6. Операции COPY #
Команда COPY
позволяет осуществлять высокоскоростной массовый
перенос данных на сервер или с сервера. Операции копирования внутрь и копирования наружу
переключают соединение на отдельный подпротокол, который действует до
завершения операции.
Режим копирования (передача данных на сервер) инициируется, когда бэкенд выполняет SQL-запрос COPY FROM STDIN
. Бэкенд отправляет сообщение CopyInResponse фронтенду. Затем фронтенд должен отправить ноль или более сообщений CopyData, формируя поток входных данных. (Границы сообщений не обязательно должны совпадать с границами строк, хотя это часто является разумным выбором). Фронтенд может завершить режим копирования, отправив сообщение CopyDone (позволяющее успешное завершение) или сообщение CopyFail (что приведет к ошибке выполнения SQL-запроса COPY
). Затем бэкенд возвращается в режим обработки команды, в котором он находился до начала выполнения SQL-запроса COPY
, который может быть либо простым, либо расширенным протоколом запросов. Затем он отправит либо сообщение CommandComplete (в случае успешного выполнения), либо сообщение ErrorResponse (в случае неудачи).
В случае обнаружения ошибки на стороне сервера во время режима копирования (включая получение сообщения CopyFail), сервер выдаст сообщение ErrorResponse. Если команда COPY
была выполнена через расширенное сообщение запроса, сервер будет отбрасывать сообщения клиента до получения сообщения Sync, затем он выдаст сообщение ReadyForQuery и вернется к нормальной обработке. Если команда COPY
была выполнена в простом сообщении запроса, остаток этого сообщения будет отброшен, и будет выдано сообщение ReadyForQuery. В любом случае, любые последующие сообщения CopyData, CopyDone или CopyFail, выданные клиентом, будут просто отброшены.
Бэкенд игнорирует сообщения Flush и Sync, полученные во время режима копирования. Получение любого другого типа сообщения, не относящегося к копированию, является ошибкой, которая прерывает состояние копирования, как описано выше. (Исключение для Flush и Sync сделано для удобства клиентских библиотек, которые всегда отправляют Flush или Sync после сообщения Execute, не проверяя, является ли команда, которую нужно выполнить, командой COPY FROM STDIN
).
Режим копирования (передача данных с сервера) инициируется, когда бэкенд выполняет SQL-запрос COPY TO STDOUT
. Бэкенд отправляет сообщение CopyOutResponse на фронтенд, за которым следуют ноль или более сообщений CopyData (всегда одно сообщение на каждую строку), а затем CopyDone. Затем бэкенд возвращается в режим обработки команды, который был до начала COPY
, и отправляет сообщение CommandComplete. Фронтенд не может прервать передачу (кроме как закрыть соединение или отправить запрос на отмену), но он может отбросить нежелательные сообщения CopyData и CopyDone.
В случае обнаружения ошибки на стороне сервера во время режима копирования данных, сервер отправит сообщение ErrorResponse и вернется к нормальной обработке. Клиентская часть должна рассматривать получение сообщения ErrorResponse как завершение режима копирования данных.
Возможно, между сообщениями NoticeResponse и ParameterStatus могут встречаться сообщения CopyData; клиентские приложения должны обрабатывать такие случаи и быть готовыми к другим асинхронным типам сообщений (см. Раздел 53.2.7). В противном случае, любой тип сообщения, отличный от CopyData или CopyDone, может быть рассмотрен как завершение режима копирования.
Существует еще один режим, связанный с копированием, называемый copy-both, который позволяет осуществлять передачу данных массового объема с высокой скоростью как на сервер, так и с сервера. Режим copy-both инициируется, когда бэкенд в режиме walsender выполняет оператор START_REPLICATION
. Бэкенд отправляет сообщение CopyBothResponse фронтенду. И бэкенд, и фронтенд могут затем отправлять сообщения CopyData до тех пор, пока один из концов не отправит сообщение CopyDone. После того, как клиент отправляет сообщение CopyDone, соединение переходит из режима copy-both в режим copy-out, и клиент больше не может отправлять сообщения CopyData. Аналогично, когда сервер отправляет сообщение CopyDone, соединение переходит в режим copy-in, и сервер больше не может отправлять сообщения CopyData. После того, как обе стороны отправили сообщение CopyDone, режим копирования завершается, и бэкенд возвращается в режим обработки команд. В случае обнаружения ошибки бэкендом в режиме copy-both, бэкенд отправит сообщение ErrorResponse, отбросит сообщения фронтенда до получения сообщения Sync, а затем отправит сообщение ReadyForQuery и вернется к нормальной обработке. Фронтенд должен рассматривать получение сообщения ErrorResponse как завершение копирования в обоих направлениях; в этом случае сообщение CopyDone не должно быть отправлено. Дополнительную информацию о подпротоколе, передаваемом в режиме copy-both, см. в разделе Раздел 53.4.
Сообщения CopyInResponse, CopyOutResponse и CopyBothResponse содержат поля, которые информируют клиентскую часть о количестве столбцов в каждой строке и используемых форматных кодах для каждого столбца. (На данный момент все столбцы в данной операции COPY
используют один и тот же формат, но дизайн сообщения не предполагает это).
53.2.7. Асинхронные операции #
Существует несколько случаев, когда серверная часть отправляет сообщения, которые не вызываются командами клиента. Клиентская часть должна быть готова обрабатывать эти сообщения в любое время, даже когда не выполняется запрос. Как минимум, перед началом чтения ответа на запрос следует проверить наличие этих случаев.
Возможно генерирование сообщений NoticeResponse в результате внешней активности; например, если администратор базы данных выполняет команду “быстрого” завершения работы базы данных, бэкенд отправит сообщение NoticeResponse, указывающее на этот факт перед закрытием соединения. Следовательно, клиентские приложения всегда должны быть готовы принимать и отображать сообщения NoticeResponse, даже когда соединение формально находится в режиме ожидания.
Сообщения ParameterStatus будут генерироваться каждый раз, когда активное значение изменяется для любого из параметров, о которых бэкенд считает, что фронтенд должен знать. В большинстве случаев это происходит в ответ на выполнение SQL-команды SET
фронтендом, и в этом случае это эффективно синхронно - но также возможно изменение статуса параметра, потому что администратор изменил файл конфигурации и затем отправил сигнал SIGHUP серверу. Кроме того, если команда SET
откатывается, будет сгенерировано соответствующее сообщение ParameterStatus для сообщения текущего эффективного значения.
В настоящее время существует жестко заданный набор параметров, для которых будет сгенерирован ParameterStatus. Они:
application_name | is_superuser |
client_encoding | scram_iterations |
DateStyle | server_encoding |
default_transaction_read_only | server_version |
in_hot_standby | session_authorization |
integer_datetimes | standard_conforming_strings |
IntervalStyle | TimeZone |
(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; scram_iterations
не сообщались в выпусках
до 16.)
Обратите внимание, что
server_version
,
server_encoding
и
integer_datetimes
являются псевдопараметрами, которые не могут изменяться после запуска.
Этот набор может измениться в будущем или даже стать настраиваемым.
Соответственно, интерфейс должен просто игнорировать ParameterStatus для
параметров, которые он не понимает или о которых не заботится.
Если фронтенд отправляет команду LISTEN
, то бэкенд будет отправлять сообщение NotificationResponse (не путать с NoticeResponse!), когда выполняется команда NOTIFY
для того же имени канала.
Примечание
В настоящее время NotificationResponse может быть отправлен только вне транзакции, и поэтому он не будет возникать посередине серии команд-ответов, хотя он может возникнуть прямо перед ReadyForQuery. Однако неразумно разрабатывать логику клиента, которая предполагает это. Хорошей практикой является возможность принимать NotificationResponse в любой точке протокола.
53.2.8. Отмена выполняющихся запросов #
Во время обработки запроса фронтенд может запросить отмену запроса. Запрос на отмену не отправляется напрямую на открытое соединение с бэкендом по причинам эффективности реализации: мы не хотим, чтобы бэкенд постоянно проверял наличие нового ввода от фронтенда во время обработки запроса. Запросы на отмену должны быть относительно редкими, поэтому мы делаем их немного неудобными, чтобы избежать штрафа в обычном случае.
Для отправки запроса на отмену, клиентское приложение открывает новое соединение с сервером и отправляет сообщение CancelRequest, вместо сообщения StartupMessage, которое обычно отправляется по новому соединению. Сервер обработает этот запрос, а затем закроет соединение. По соображениям безопасности, на сообщение запроса на отмену не делается прямого ответа.
Запрос CancelRequest будет проигнорирован, если он не содержит тех же ключевых данных (PID и секретный ключ), которые были переданы фронтенду при установке соединения. Если запрос соответствует PID и секретному ключу текущего выполняющегося бэкенда, обработка текущего запроса будет прервана. (В существующей реализации это делается путем отправки специального сигнала бэкенд-процессу, который обрабатывает запрос).
Сигнал отмены может или не может иметь какой-либо эффект - например, если он поступает после того, как бэкенд закончил обработку запроса, то он не будет иметь никакого эффекта. Если отмена действительна, это приводит к преждевременному завершению текущей команды с сообщением об ошибке.
В результате всего этого следует, что по причинам безопасности и эффективности фронтенд не имеет прямого способа узнать, удалось ли успешно выполнить запрос на отмену. Он должен продолжать ожидать ответа от бэкенда на запрос. Отправка запроса на отмену просто увеличивает вероятность того, что текущий запрос завершится скоро, и увеличивает вероятность того, что он завершится с сообщением об ошибке вместо успешного выполнения.
Так как запрос на отмену отправляется через новое соединение к серверу, а не через обычную связь между клиентом и сервером, возможно выполнять запрос на отмену любым процессом, а не только клиентом, чей запрос должен быть отменен. Это может предоставить дополнительную гибкость при создании многопроцессных приложений. Однако это также вводит риск безопасности, поскольку неавторизованные лица могут попытаться отменить запросы. Риск безопасности решается путем требования предоставления динамически генерируемого секретного ключа в запросах на отмену.
53.2.9. Завершение #
Обычная, гармоничная процедура завершения состоит в том, что фронтенд отправляет сообщение Terminate и немедленно закрывает соединение. По получении этого сообщения бэкенд закрывает соединение и завершается.
В редких случаях (например, при команде администратора остановить базу данных) бэкенд может отключиться без запроса от фронтенда. В таких случаях бэкенд попытается отправить сообщение об ошибке или уведомление, объясняющее причину отключения, перед закрытием соединения.
Другие сценарии завершения возникают из различных случаев сбоев, таких как ядро сбросить на одном из концов или другом, потеря связи, потеря синхронизации границ сообщений и т. д. Если какой-либо из фронтендов или бэкендов видит неожиданное закрытие соединения, он должен очистить и завершить. Фронтенд может запустить новый бэкенд путем повторного контакта с сервером, если он не хочет завершиться самостоятельно. Закрытие соединения также рекомендуется, если получен неузнаваемый тип сообщения, так как это, вероятно, указывает на потерю синхронизации границ сообщений.
Для нормального или аварийного завершения любая открытая транзакция откатывается, а не коммитится. Однако следует отметить, что если клиент отключается во время обработки запроса, отличного от SELECT
, то серверная часть, скорее всего, завершит выполнение запроса, прежде чем заметит отключение. Если запрос находится вне блока транзакции (BEGIN
... COMMIT
), то его результаты могут быть подтверждены до того, как будет распознано отключение.
53.2.10. SSL Шифрование сессии #
Если Tantor SE был собран с поддержкой SSL, то связь между клиентом и сервером может быть зашифрована с использованием SSL. Это обеспечивает безопасность связи в средах, где злоумышленники могут перехватывать трафик сессии. Дополнительную информацию о шифровании сессий Tantor SE с использованием SSL см. в разделе Раздел 18.9.
Для инициализации зашифрованного соединения SSL, клиентская часть сначала отправляет сообщение SSLRequest, а не StartupMessage. Затем сервер отвечает одним байтом, содержащим S
или N
, указывающим, готов ли он выполнять SSL или нет, соответственно. Если клиент не удовлетворен ответом, он может закрыть соединение на этом этапе. Чтобы продолжить после S
, необходимо выполнить начальное рукопожатие SSL (не описано здесь, часть спецификации SSL) с сервером. Если это успешно, можно продолжить отправку обычного StartupMessage. В этом случае StartupMessage и все последующие данные будут зашифрованы SSL. Чтобы продолжить после N
, отправьте обычный StartupMessage и продолжайте без шифрования.
(В качестве альтернативы, после ответа N
можно отправить сообщение GSSENCRequest, чтобы попытаться использовать шифрование GSSAPI вместо SSL).
Фронтенд также должен быть готов обработать ответ ErrorMessage на SSLRequest от сервера. Фронтенд не должен отображать это сообщение об ошибке пользователю/приложению, так как сервер не был аутентифицирован (CVE-2024-10977). В этом случае соединение должно быть закрыто, но фронтенд может выбрать открытие нового соединения и продолжить без запроса SSL.
Когда может выполняться шифрование SSL, сервер ожидает отправки только одного байта S
и затем ожидает, чтобы клиент инициировал рукопожатие SSL. Если в этот момент доступны дополнительные байты для чтения, это, скорее всего, означает, что злоумышленник пытается выполнить атаку с переполнением буфера (buffer-stuffing attack) (CVE-2021-23222). Клиентские приложения должны быть написаны таким образом, чтобы сначала прочитать ровно один байт из сокета, а затем передать сокет своей библиотеке SSL, или считать это нарушением протокола, если они обнаружат, что они прочитали дополнительные байты.
Исходный SSLRequest также может быть использован в открываемом соединении для отправки сообщения CancelRequest.
В то время как сам протокол не предоставляет серверу возможности принудительного использования шифрования SSL, администратор может настроить сервер таким образом, чтобы отклонять нешифрованные сессии в результате проверки аутентификации.
53.2.11. GSSAPI Шифрование сессии #
Если Tantor SE был собран с поддержкой GSSAPI, то связь между клиентом и сервером может быть зашифрована с использованием GSSAPI. Это обеспечивает безопасность связи в средах, где злоумышленники могут перехватывать трафик сессии. Дополнительную информацию о шифровании сессий Tantor SE с использованием GSSAPI см. в разделе Раздел 18.10.
Чтобы инициировать зашифрованное соединение с использованием GSSAPI,
клиент сначала отправляет сообщение GSSENCRequest вместо
StartupMessage. Сервер затем отвечает одним байтом,
содержащим G
или N
, указывая,
готов ли он выполнять шифрование с использованием GSSAPI
или нет, соответственно. Клиент может закрыть соединение на этом этапе,
если его не устраивает ответ. Чтобы продолжить после
G
, используя привязки C для GSSAPI, как обсуждается в
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 со стороны клиента. Если в этот момент доступны дополнительные байты для чтения, это, скорее всего, означает, что злоумышленник пытается выполнить атаку с переполнением буфера (buffer-stuffing attack) (CVE-2021-23222). Клиентские приложения должны быть написаны таким образом, чтобы сначала прочитать ровно один байт из сокета, а затем передать сокет своей библиотеке GSSAPI, или считать это нарушением протокола, если они обнаружат, что прочитали дополнительные байты.
Исходный запрос GSSENCRequest также может быть использован в открытом соединении для отправки сообщения CancelRequest.
После успешного установления шифрования GSSAPI используйте функцию gss_wrap()
для шифрования обычного сообщения StartupMessage и всех последующих данных, предварительно добавив длину результата функции gss_wrap()
в виде четырехбайтового целого числа в сетевом порядке байтов к фактической зашифрованной нагрузке. Обратите внимание, что сервер принимает только зашифрованные пакеты от клиента, размер которых не превышает 16 КБ; клиент должен использовать функцию gss_wrap_size_limit()
, чтобы определить размер незашифрованного сообщения, которое поместится в этот предел, и более крупные сообщения должны быть разбиты на несколько вызовов функции gss_wrap()
. Типичные сегменты состоят из 8 КБ незашифрованных данных, что приводит к зашифрованным пакетам немного больше 8 КБ, но в пределах максимального значения 16 КБ. От сервера можно ожидать, что он не будет отправлять зашифрованные пакеты размером более 16 КБ клиенту.
В то время как сам протокол не предоставляет серверу возможности принудительного использования шифрования GSSAPI, администратор может настроить сервер таким образом, чтобы отклонять нешифрованные сессии в результате проверки аутентификации.