При возвращении из локальной процедуры мы можем быть уверены, что она была выполнена ровно один раз. Однако если процесс завершает работу после вызова процедуры, мы не знаем, успела она выполниться или нет. Для удаленных вызовов процедур возможно несколько ситуаций.
■ Если используется протокол TCP и получен ответ, мы можем быть уверены, что удаленная процедура была вызвана ровно один раз. Однако если ответ не был получен (сервер вышел из строя), мы уже не можем сказать, была процедура выполнена или нет. Обеспечение семантики «ровно один раз» при учете возможности досрочного завершения работы сервера и неполадок в сети требует системы обработки транзакций, что лежит за границами возможностей RPC.
■ Если используется UDP без серверного кэша и был получен ответ, мы можем быть уверены, что процедура была вызвана по крайней мере один раз, но возможно, и несколько.
■ Если используется UDP с серверным кэшем и был получен ответ, мы можем быть уверены, что процедура была вызвана ровно один раз. Однако если ответ не был получен, мы оказывается в ситуации «не более одного раза» аналогично сценарию с TCP.
■ Если вы стоите перед выбором:
□ TCP,
□ UDP с кэшем повторных ответов,
□ UDP без кэша повторных ответов —
мы можем порекомендовать следующее:
□ всегда используйте TCP, если только для приложения не важны накладные расходы на обеспечение надежности;
□ используйте систему обработки транзакций для неидемпотентных процедур, корректное выполнение которых важно (работа с банковскими счетами, бронирование авиабилетов и т. п.);
□ для неидемпотентных процедур использование TCP предпочтительно по сравнению с UDP и кэшем, поскольку TCP был изначально ориентирован на надежность, а добавление кэша к приложению, использующему UDP, вряд ли даст то же самое, что и использование TCP (см., например, раздел 20.5 [24]);
□ для идемпотентных процедур можно использовать UDP без кэша;
□ для неидемпотентных процедур использование UDP без кэша опасно.
В следующем разделе будут рассмотрены дополнительные преимущества использования TCP.
16.7. Досрочное завершение сервера или клиента
Рассмотрим, что произойдет в случае досрочного завершения клиента или сервера при использовании транспортного протокола TCP. Поскольку протокол UDP не подразумевает установку соединения, при завершении процесса его собеседнику не отсылается никаких сообщений. При завершении работы одного из процессов второй дождется тайм-аута, после чего, возможно, повторно отошлет запрос и наконец прекратит попытки, выдав сообщение об ошибке, как показывалось в предыдущем разделе. При завершении работы процесса, установившего соединение по TCP, это соединение завершается отправкой пакета FIN [24, с. 36-37], и мы хотим узнать, что делает библиотека RPC при получении этого пакета.
Досрочное завершение сервера
Завершим работу сервера досрочно, в процессе обработки запроса клиента. Единственное изменение в программе-клиенте будет заключаться в удалении аргумента tcp из вызова clnt_call в листинге 16.2 и включении протокола в набор аргументов командной строки, как в листинге 16.9. В процедуру сервера мы добавим вызов abort. Это приведет к завершению работы процесса-сервера и отправке пакета FIN клиенту, что мы можем проверить с помощью tcpdump.
Запустим в системе Solaris клиент для сервера, работающего под BSD/OS:
solaris % client bsdi 22 tcp
bsdi: RPC: Unable to receive; An event requires attention
В момент получения клиентом пакета FIN библиотека RPC находилась в состоянии ожидания ответа сервера. Она получила неожиданный ответ и вернула ошибку в вызове squareproc_1. Ошибка (RPC_CANTRECV) сохраняется библиотекой в дескрипторе клиента, и вызов clnt_sperror (из функции-обертки Clnt_create) при этом печатает сообщение Unable to receive. Оставшаяся часть сообщения об ошибке (An event requires attention) соответствует ошибке XTI, сохраненной библиотекой, которая также выводится clnt_sperror. Вызов удаленной процедуры может вернуть одну из примерно 30 различных ошибок RPC_xxx. Все они перечислены в заголовочном файле rpc/clnt_stat.h.
Если мы поменяем клиент и сервер местами, мы увидим то же сообщение об ошибке, возвращаемое библиотекой RPC (RPC_CANTRECV), но при этом будет выведено дополнительное сообщение:
bsdi % client solaris 11 tcp
solaris: RPC: Unable to receive; errno = Connection reset by peer
Сервер в Solaris не был скомпилирован как многопоточный, и когда мы вызвали abort, была завершена работа всего процесса. Если мы запустим многопоточный сервер и завершим работу только одного потока — того, который обслуживает данный запрос клиента, — все изменится. Чтобы продемонстрировать это, заменим вызов abort на pthread_exit, как мы сделали в пpoгрaммe из листинга 15.20. Запустим клиент в BSD/OS, а многопоточный сервер — в Solaris: