UNIX: взаимодействие процессов — страница 19 из 35

Пакет Sun RPC

16.1. Введение

Когда мы разрабатываем приложение, мы встаем перед выбором:

1. Написать одну большую монолитную программу, которая будет делать все.

2. Разделить приложение на несколько процессов, взаимодействующих друг с другом.

Если мы выбираем второй вариант, перед нами опять встает вопрос:

2.1. предполагать ли, что все процессы выполняются на одном узле, что допускает использование средств IPC для взаимодействия между ними, либо

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

Взгляните на рис. 15.1. Верхняя ситуация соответствует варианту 1, средняя — варианту 2.1, а нижняя — варианту 2.2. Большая часть этой книги посвящена обсуждению случая 2.1, то есть взаимодействию процессов в пределах одного узла с помощью таких средств IPC, как передача сообщений, разделяемая память и, возможно, некоторых форм синхронизации. Взаимодействие между потоками одного процесса или разных процессов является частным случаем этого сценария.

Когда мы выдвигаем требование поддержки возможности сетевого взаимодействия между составляющими приложения, для разработки чаще всего используется явное сетевое программирование. При этом в программе используются вызовы интерфейсов сокетов или XTI, о чем рассказывается в [24]. При использовании интерфейса сокетов клиенты вызывают функции socket, connect, read и write, а серверы вызывают socket, bind, listen, accept, read и write. Большая часть знакомых нам приложений (браузеры, серверы Web, клиенты и серверы Telnet) написаны именно так.

Альтернативным способом разработки распределенных приложений является неявное сетевое программирование. Оно осуществляется посредством удаленного вызова процедур (remote procedure call — RPC). Приложение кодируется с использованием тех же вызовов, что и обычное несетевое, но клиент и сервер могут находиться на разных узлах. Сервером называется процесс, предоставляющий другому возможность запускать одну из составляющих его процедур, а клиентом — процесс, вызывающий процедуру сервера. То, что клиент и сервер находятся на разных узлах и для связи между ними используется сетевое взаимодействие, большей частью остается скрыто от программиста. Одним из критериев оценки качества пакета RPC является именно скрытость лежащего в основе интерфейса сети.

Пример

В качестве примера использования RPC перепишем листинги 15.1 и 15.2 для использования Sun RPC вместо дверей. Клиент вызывает процедуру сервера с аргументом типа long, а возвращаемое значение представляет собой квадрат аргумента. В листинге 16.1[1] приведен текст первого файла, square.х.

Листинг 16.1. Файл спецификации RPC

//sunrpc/square1/square.x


1 struct square_in { /* входные данные (аргумент) */

2  long arg1;

3 };


4 struct square_out { /* возвращаемые данные (результат) */

5  long res1;

6 };


7 program SQUARE_PROG {

8  version SQUARE_VERS {

9   square_out SQUAREPROC(square_in) = 1; /* номер процедуры = 1 */

10  } = 1; /* номер версии */

11 } = 0x31230000; /* номер программы */

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

Определение аргумента и возвращаемого значения

1-6 Мы определяем две структуры — одну для аргументов (одно поле типа long) и одну для результатов (тоже одно поле типа long).

Определение программы, версии и процедуры

7-11 Мы определяем программу RPC с именем SQUARE_PROG, состоящую из одной версии (SQUARE_VERS), а эта версия представляет собой единственную процедуру сервера с именем SQUAREPROC. Аргументом этой процедуры является структура square_in, а возвращаемым значением — структура square_out. Мы также присваиваем этой процедуре номер 1, как и версии, а программе мы присваиваем 32-разрядный шестнадцатеричный номер. (О номерах программ более подробно говорится в табл. 16.1.)

Компилировать спецификацию нужно программой rpcgen, входящей в пакет Sun RPC.

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

Листинг 16.2. Функция main клиента, делающего удаленный вызов процедуры

//sunrpc/square1/client.c

1  #include "unpipc.h" /* наш заголовочный файл */

2  #include "square.h" /* создается rpcgen */

3  int

4  main(int argc, char **argv)

5  {

6   CLIENT *cl;

7   square_in in;

8   square_out *outp;

9   if (argc != 3)

10   err_quit("usage: client ");

11  cl = Clnt_create(argv[1], SQUARE_PROG, SQUARE_VERS, "tcp");

12  in.arg1 = atol(argv[2]);

13  if ((outp = squareproc_1(&in, cl)) == NULL)

14   err_quit("ls", clnt_sperror(cl, argv[1]));

15  printf("result: %ld\n", outp->res1);

16  exit(0);

17 }

Подключение заголовочного файла, создаваемого rpcgen

2 Мы подключаем заголовочный файл square.h, создаваемый функцией rpcgen.

Объявление дескриптора клиента

6 Мы объявляем дескриптор клиента (client handle) с именем cl. Дескрипторы клиентов выглядят как обычные указатели на тип FILE (поэтому слово CLIENT пишется заглавными буквами).

Получение дескриптора клиента

11 Мы вызываем функцию clnt_create, создающую клиент RPC:

#include 

CLIENT *clnt_create(const char *host, unsigned long prognum, unsigned long versnum, const char *protocol);

/* Возвращает ненулевой дескриптор клиента в случае успешного завершения. NULL – в случае ошибки */

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

Первым аргументом clnt_create должно быть имя или IP-адрес узла, на котором выполняется сервер. Вторым аргументом будет имя программы, третьим — номер версии. Оба эти значения берутся из спецификации (square.х, листинг 16.1). Последний аргумент позволяет указать протокол, обычно TCP или UDP.

Вызов удаленной процедуры и вывод результата

12-15 Мы вызываем процедуру, причем первый аргумент указывает на входную структуру (&in), а второй содержит дескриптор клиента. В большинстве стандартных функций ввода-вывода дескриптор файла является последним аргументом. Точно так же и в вызовах RPC дескриптор клиента передается последним. Возвращаемое значение представляет собой указатель на структуру, в которой хранится результат работы сервера. Место под входную структуру выделяет программист, а под возвращаемую — пакет RPC.

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

Со стороны сервера от нас требуется только написать процедуру. Функция main автоматически создается программой rpcgen. Текст процедуры приведен в листинге 16.3.

Листинг 16.3. Процедура сервера, вызываемая с помощью Sun RPC

//sunrpc/square1/server.c

1 #include "unpipc.h"

2 #include "square.h"


3 square_out *

4 squareproc_l_svc(square_in *inp, struct svc_req *rqstp)

5 {

6  static square_out out;

7  out.res1 = inp->arg1 * inp->arg1;

8  return(&out);

9 }

Аргументы процедуры

3-4 Прежде всего мы замечаем, что к имени процедуры добавился суффикс _svc. Это дает возможность использовать два прототипа функций ANSI С в файле square.x, один из которых определяет функцию, вызываемую клиентом в листинге 16.2 (она принимает дескриптор клиента), а второй — реальную функцию сервера (которая принимает другие аргументы).

При вызове процедуры сервера первый аргумент является указателем на входную структуру, а второй — на структуру, передаваемую библиотекой RPC времени выполнения, которая содержит информацию о данном вызове (в этом примере игнорируется для простоты).

Выполнение и возврат

6-8 Программа считывает входной аргумент и возводит его в квадрат. Результат сохраняется в структуре, адрес которой возвращается процедурой сервера. Поскольку мы возвращаем адрес переменной, эта переменная не может быть автоматической. Мы объявляем ее как статическую (static).

ПРИМЕЧАНИЕ

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

Откомпилируем клиент в системе Solaris, а сервер — в BSD/OS, запустим сервер, а затем клиент:

solaris % client bsdi 11

result: 121

solaris % client 209.75.135.35 22

result: 484

В первом случае мы указываем имя узла сервера, а во втором — его IP-адрес. Этим мы демонстрируем возможность использования как имен, так и IP-адресов для задания узла в функции clnt_create.

Теперь продемонстрируем некоторые ошибки, возникающие при работе clnt_create, если, например, не существует узел или на нем не запущена программа-сервер:

solaris % client nosuchhost 11

nosuchhost: RPC: Unknown host возвращается библиотекой RPC времени выполнения

clnt_create error             возвращается нашей функцией-оберткой

solaris % client localhost 11

localhost: rpc: program not registered

clnt_create error

Мы написали клиентскую и серверную части программы и продемонстрировали их использование вообще без явного сетевого программирования. Клиент просто вызывает две функции, а сервер вообще состоит из одной функции. Все тонкости использования XTI в Solaris, сокетов в BSD/OS и сетевого ввода-вывода обрабатываются библиотекой RPC времени выполнения. В этом и состоит предназначение RPC — предоставлять возможность создания распределенных приложений без знания сетевого программирования.

Другая немаловажная деталь данного примера заключается в том, что в системах Sparc под Solaris и Intel x86 под управлением BSD/OS используется разный порядок байтов. В Sparc используется порядок big endian («тупоконечный»[2]), а в Intel — little endian («остроконечный») (что мы показали в разделе 3.4 [24]). Отличия в порядке байтов также обрабатываются библиотекой RPC времени выполнения автоматически с использованием стандарта XDR (внешнее представление данных), который мы обсудим в разделе 16.8.

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

solaris % rpcgen-Сsquare.x

solaris % cc-сclient.с-оclient.о

solaris % cc-сsquare_clnt.c-оsquare_clnt.o

solaris % cc-сsquare_xdr.с-оsquare_xdr.o

solaris % cc-оclient client.оsquare_clnt.o square_xdr.o libunpipc.a –lnsl

Параметр –С говорит rpcgen о необходимости создания прототипов функций ANSI С в заголовочном файле square.h. Программа rpcgen также создает заглушку клиента (client stub) в файле с именем square_clnt.с и файл с именем square_xdr.с, который осуществляет преобразование данных в соответствии со стандартом XDR. Наша библиотека (содержащая функции, используемые в этой книге) называется libunpipc.a, а параметр –lnsl подключает системную библиотеку сетевых функций в Solaris (включая библиотеки RPC и XDR времени выполнения).

Аналогичные команды используются для создания сервера, хотя rpcgen уже не нужно запускать снова. Файл square_svc.c содержит функцию main сервера, и файл square_xdr.о, обсуждавшийся выше, также требуется для работы сервера:

solaris % cc –с server.с –о server.о

solaris % сc –с square_svc.c –о square_svc.o

solaris % cc –о server server.о square_svc.o libunpipc.a –lnsl

При этом создаются клиент и сервер, выполняемые в системе Solaris.

Если клиент и сервер должны быть построены для разных систем (как в предыдущем примере, где клиент выполнялся в Solaris, а сервер — в BSD/OS), могут потребоваться дополнительные действия. Например, некоторые файлы должны быть либо общими (через NFS), либо находиться в обеих системах, а файлы, используемые клиентом и сервером (например, square_xdr.o), должны компилироваться в каждой системе в отдельности. 

Рис. 16.1. Этапы создания приложения клиент-сервер с использованием RPC


На рис. 16.1 приведена схема создания приложения типа клиент-сервер. Три затемненных прямоугольника соответствуют файлам, которые мы должны написать. Штриховые линии показывают файлы, подключаемые через заголовочный файл square.h.

На рис. 16.2 изображена схема происходящего при удаленном вызове процедуры. Действия выполняются в следующем порядке:

1. Запускается сервер, который регистрируется в программе, управляющей портами на узле-сервере. Затем запускается клиент и вызывает clnt_create. Эта функция связывается с управляющей портами программой сервера и находит нужный порт. Функция clnt_create также устанавливает соединение с сервером по протоколу TCP (поскольку мы указали TCP в качестве используемого протокола в листинге 16.2). Мы не показываем эти шаги на рисунке и откладываем детальное обсуждение до раздела 16.3.

2. Клиент вызывает локальную процедуру, называемую заглушкой клиента. В листинге 16.2 эта процедура называлась squareproc_1, а файл, содержащий ее, создавался rpcgen автоматически и получал название square_clnt.c. С точки зрения клиента именно эта функция является сервером, к которому он обращается. Целью создания заглушки является упаковка аргументов для удаленного вызова процедуры, помещение их в стандартный формат и создание одного или нескольких сетевых сообщений. Упаковка аргументов клиента в сетевое сообщение называется сортировкой (marshaling). Клиент и заглушка обычно вызывают библиотеки функций RPC (clnt_create в нашем примере). При использовании редактора связей в Solaris эти функции загружаются из библиотеки –lnsl, тогда как в BSD/OS они входят в стандартную библиотеку языка С.

3. Сетевые сообщения отсылаются на удаленную систему заглушкой клиента. Обычно это требует локального системного вызова (например, write или sendto).

4. Сетевые сообщения передаются на удаленную систему. Для этого обычно используются сетевые протоколы TCP и UDP.

5. Заглушка сервера ожидает запросов от клиента на стороне сервера. Она рассортировывает аргументы из сетевых сообщений.

6. Заглушка сервера осуществляет локальный вызов процедуры для запуска настоящей функции сервера (процедуры squareproc_l_svc в листинге 16.3), передавая ей аргументы, полученные в сетевых сообщениях от клиента.

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

8. Заглушка сервера преобразовывает возвращаемые значения к нужному формату и рассортировывает их в сетевые сообщения для отправки обратно клиенту.

9. Сообщения передаются по сети обратно клиенту. 

10. Заглушка клиента считывает сообщения из локального ядра (вызовом read или recvfrom).

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

Рис. 16.2. Действия, происходящие при удаленном вызове процедуры

История

Наверное, одна из самых старых книг по RPC — это [26]. Как пишет [4], Уайт (White) затем перешел в Xerox и там создал несколько систем RPC. Одна из них была выпущена в качестве отдельного продукта в 1981 году под именем Courier. Классической книгой по RPC является [2]. В ней описаны средства RPC проекта Cedar, работавшего на однопользовательских рабочих станциях Dorado в фирме Xerox в начале 80-х. Xerox реализовал RPC на рабочих станциях еще до того, как большинство людей узнало о том, что рабочие станции существуют! Реализация Courier для Unix распространялась много лет с версиями BSD 4.x, но в настоящий момент эта система RPC представляет только исторический интерес.

Sun выпустила первую версию пакета RPC в 1985. Она была разработана Бобом Лайоном (Bob Lyon), ушедшим в Sun из фирмы Xerox в 1983. Официально она называлась ONC/RPC: Open Network Computing Remote Procedure Call (удаленный вызов процедур в открытых вычислительных сетях), но обычно ее называют просто Sun RPC. Технически она аналогична Courier. Первые версии Sun RPC были написаны с использованием интерфейса сокетов и работали с протоколами TCP и UDP. Общедоступный исходный код вышел под названием RPCSRC. В начале 90-х он был переписан под интерфейс транспортного уровня TLI (предшественник XTI), который описан в четвертой части [24]. Теперь этот код работает со всеми протоколами, поддерживаемыми ядром. Общедоступный исходный код обеих версий можно найти по адресу ftp://playground.sun.com/pub/rpc, причем версия, использующая сокеты, называется rpcsrc, а версия, использующая TLI, называется tirpcsrc (название TI-RCP образовано от Transport Independent — транспортно-независимый удаленный вызов процедур).

Стандарт RFC 1831 [18] содержит обзор средств Sun RPC и описывает формат сообщений RPC, передаваемых по сети. Стандарт RFC 1832 [19] содержит описание XDR — и поддерживаемых типов данных, и формата их передачи «по проводам». Стандарт RFC 1833 [20] описывает протоколы привязки: RPCBIND и его предшественника, программу отображения портов (port mapper).

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

В середине 80-х Apollo соперничала с Sun за рынок рабочих станций и создала свой собственный пакет RPC, призванный вытеснить Sun RPC. Этот новый пакет назывался NCA (Network Computing Architecture — архитектура сетевых вычислений). Протоколом RPC являлся протокол NCA/RPC, а аналогом XDR была схема NDR (Network Data Representation — сетевое представление данных). Интерфейсы между клиентами и серверами определялись с помощью языка NIDL (Network Interface Definition Language — язык определений сетевого интерфейса) аналогично нашему файлу .х из листинга 16.1. Библиотека времени выполнения называлась NCK (Network Computing Kernel).

Фирма Apollo была куплена Hewlett-Packard в 1989, и NCA переросла в OSF's DCE (Distributed Computing Environment — среда распределенных вычислений), основной частью которой является RPC. Более подробно о DCE можно узнать по адресу http://www.camp.opengroup.org/tech/dce. Реализация пакета DCE RPC свободно доступна на ftp://gatekeeper.dec.com/pub/DEC/DCE. Этот каталог также содержит описание внутреннего устройства пакета DCE RPC на 171 странице. Существует много версий DCE для разных платформ.

ПРИМЕЧАНИЕ

Sun RPC распространен шире, чем DCE RPC, возможно, благодаря свободно доступной реализации и включению в основную поставку большинства систем Unix. DCE RPC обычно поставляется в качестве дополнения (за дополнительную сумму). Переписывание свободно доступного кода под другие платформы пока еще не пошло полным ходом, хотя уже готовится версия для Linux. В этой главе мы опишем только средства Sun RPC. Все три пакета RPC — Courier, Sun и DCE — похожи друг на друга, поскольку основные концепции RPC остались неизменны.

Большинство поставщиков Unix предоставляют отдельную подробную документацию средств RPC. Например, документация Sun доступна по адресу http://docs.sun.com, и в первом томе Developer Collection можно найти 280-страничное руководство разработчика «ONC+ Developer's Guide». Документация Digital Unix по адресу http:// www.unix.digital.com/faqs/publications/pub_page/V40D_DOCS.HTM включает 116-страничный документ под заголовком «Programming with ONC RPC».

Сам по себе RPC вызывает противоречивые мнения. Восемь статей на эту тему можно найти по адресу http://www.kohala.com/~rstevens/papers.others/rpc.comments.txt.

 В этой главе мы предполагаем наличие TI-RPC (независимая от протокола версия RPC) для большинства примеров и говорим только о протоколах TCP и UDP, хотя TI-RPC поддерживает все протоколы, какие только могут быть на данном узле. 

16.2. Многопоточность

Вспомните листинг 15.6, где мы продемонстрировали автоматическое управление потоками, осуществляемое библиотекой дверей. При этом сервер по умолчанию являлся параллельным. Покажем теперь, что средства Sun RPC по умолчанию делают сервер последовательным. Начнем с примера из предыдущего раздела и изменим только процедуру сервера. В листинге 16.4 приведен текст новой функции, выводящей идентификатор потока, делающей 5-секундную паузу, выводящей идентификатор еще раз и завершающей работу.

Листинг 16.4. Процедура сервера с 5-секундной паузой

//sunrpc/square2/server.c

1  #include "unpipc.h"

2  #include "square.h"


3  square_out *

4  squareproc_1_svc(square_in *inp, struct svc_req *rqstp)

5  {

6   static square_out out;

7   printf("thread %ld started, arg = %ld\n",

8   pr_thread_id(NULL), inp->arg1);

9   sleep(5);

10  out.res1 = inp->arg1 * inp->arg1;

11  printf("thread %ld done\n", pr_thread_id(NULL));

12  return(&out);

13 }

Запустим сервер, а после этого запустим три экземпляра программы-клиента:

solaris % client localhost 22 & client localhost 33& client localhost 44 &

[3] 25179

[4] 25180

[5] 25181

solaris % result: 484 примерно через 5 секунд после появления подсказки

result: 1936          еще через 5 секунд

result: 1089          еще через 5 секунд

Хотя этого нельзя сказать по выводимому тексту, перед появлением очередного результата проходит примерно 5 секунд. Если мы посмотрим на текст, выводимый сервером, то увидим, что клиенты обрабатываются последовательно: сначала полностью обрабатывается запрос первого клиента, затем второго и третьего:

solaris % server

thread 1 started, arg = 22

thread 1 done

thread 1 started, arg = 44

thread 1 done

thread 1 started, arg = 33

thread 1 done

Один и тот же поток обслуживает все запросы клиентов. Сервер не является многопоточным по умолчанию.

ПРИМЕЧАНИЕ

Серверы дверей в главе 15 работали не в фоновом режиме, а запускались из интерпретатора. Это давало нам возможность добавлять отладочные вызовы printf в процедуры сервера. Однако серверы Sun RPC по умолчанию являются демонами и выполняют действия так, как это описано в разделе 12.4 [24]. Это требует вызова syslog из процедуры сервера для вывода диагностической информации. Однако мы указали флаг –DDEBUG при компиляции нашего сервера, что эквивалентно определению

#define DEBUG

в заглушке сервера (файле square_svc.c, создаваемом rpcgen). Это запрещает функции main становиться демоном и оставляет ее подключенной к терминалу, в котором она была запущена. Поэтому мы можем спокойно вызывать printf из процедуры сервера.

Возможность создания многопоточного сервера появилась в Solaris 2.4 и реализуется добавлением параметра –М в строку вызова rpcgen. Это делает код, создаваемый rpcgen, защищенным. Другой параметр, –А, позволяет автоматически создавать потоки по мере необходимости для обслуживания запросов клиентов. Мы включаем оба параметра при вызове rpcgen.

Однако для реализации многопоточности требуется внести изменения в текст клиента и сервера, чего мы могли ожидать, поскольку использовали тип static в листинге 16.3. Единственное изменение, которое нужно внести в файл square.х, — сменить номер версии с 1 на 2. В объявлениях аргументов процедуры и результатов ничего не изменится.

В листинге 16.5 приведен текст новой программы-клиента.

Листинг 16.5. Функция main клиента многопоточного сервера

//sunrpc/square3/client.c

1  #include "unpipc.h"

2  #include "square.h"


3  int

4  main(int argc, char **argv)

5  {

6   CLIENT *cl;

7   square_in in;

8   square_out out;

9   if (argc != 3)

10   err_quit("usage: client ");

11  cl = Clnt_create(argv[1], SQUARE_PROG, SQUARE_VERS, "tcp");

12  in.arg1 = atol(argv[2]);

13  if (squareproc_2(&in, &out, cl) != RPC_SUCCESS)

14   err_quit("%s", clnt_sperror(cl, argv[1]));

15  printf("result: %ld\n", out.res1);

16  exit(0);

17 }

Объявление переменной для помещения результата

8 Мы объявляем переменную типа square_out, а не указатель на нее. 

Новый аргумент в вызове процедуры

12-14 Вторым аргументом вызова squareproc_2 становится указатель на переменную out, а последним аргументом является дескриптор клиента. Вместо возвращения указателя на результат (как в листинге 16.2) эта функция будет возвращать либо RPC_SUCCESS, либо некоторое другое значение в случае возникновения ошибок. Перечисление enumclnt_stat в заголовочном файле содержит все возможные коды ошибок.

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

Листинг 16.6. Процедура многопоточного сервера

//sunrpc/square3/server.c

1  #include "unpipc.h"

2  #include "square.h"


3  bool_t

4  squareproc_2_svc(square_in *inp, square_out *outp, struct svc_req *rqstp)

5  {

6   printf("thread %Id started, arg = %ld\n",

7   pr_thread_id(NULL), inp->arg1);

8   sleep(5);

9   outp->res1 = inp->arg1 * inp->arg1;

10  printf("thread %ld done\n", pr_thread_id(NULL));

11  return(TRUE);

12 }


13 int

14 square_prog_2_freeresult(SVCXPRT *transp, xdrproc_t xdr_result,

15 caddr_t result)

16 {

17  xdr_free(xdr_result, result);

18  return(1);

19 }

Новые аргументы и возвращаемое значение

3-12 Требуемые для реализации многопоточности изменения включают изменение аргументов функций и возвращаемого значения. Вместо возвращения указателя на структуру результатов (как в листинге 16.3) указатель на эту структуру принимается в качестве второго аргумента функции. Указатель на структуру svc_req смещается на третью позицию. Теперь при успешном завершении функции возвращается значение TRUE, а при возникновении ошибок — FALSE.

Новая функция, освобождающая память XDR

13-19 Еще одно изменение заключается в добавлении функции, освобождающей все автоматически выделенные переменные. Эта функция вызывается из заглушки сервера после завершения работы процедуры сервера и отправки результата клиенту. В нашем примере просто делается вызов подпрограммы xdr_free (о ней будет говориться более подробно в связи с листингом 16.19 и упражнением 16.10). 

Если процедура сервера выделяла память под сохраняемый результат (например, в виде связного списка), этот вызов освободит занятую память.

Создадим программу-клиент и программу-сервер и запустим три экземпляра клиента одновременно:

solaris % client localhost 55 & client localhost 66 & client localhost 77 &

[3] 25427

[4] 25428

[5] 25429

solaris % result: 4356

result: 3025

result: 5929

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

solaris % server

thread 1 started, arg = 55

thread 4 started, arg = 77

thread 6 started, arg = 66

thread 6 done

thread 1 done

thread 4 done

ПРИМЕЧАНИЕ

Одним из печальных следствий изменений, требуемых для реализации многопоточности, является уменьшение количества систем, поддерживающих новый код. Например, в Digital Unix 4.0B и BSD/OS 3.1 используется старая система RPC, не поддерживающая многопоточность. Это означает, что если мы хотим компилировать и использовать нашу программу в системах обоих типов, нам нужно использовать условия #ifdef для обработки различий в вызовах клиента и сервера. Конечно, клиент в BSD/OS, не являющийся многопоточным, может вызвать процедуру многопоточного сервера в Solaris, но если мы хотим, чтобы клиент или сервер компилировался в обоих типах систем, исходный код нужно изменить, предусмотрев различия.

16.3. Привязка сервера

В описании листинга 16.5 мы достаточно бегло прошлись по действиям, выполняемым на нулевом этапе: регистрация сервера в локальной программе отображения портов и определение клиентом адреса порта не были разобраны детально. Отметим прежде всего, что на любом узле с сервером RPC должна выполняться программа port mapper (отображение портов). Этой программе присваивается адрес порта TCP 111 и UDP 111, и это единственные фиксированные значения портов Интернета для Sun RPC. Серверы RPC всегда связываются с временным портом, а затем регистрируют его в локальной службе отображения портов. После запуска клиент должен связаться с программой отображения портов, узнать номер временного порта сервера, а затем связаться с самим сервером через этот порт. Программа отображения портов предоставляет также службу имен, область действия которой ограничена системой.

ПРИМЕЧАНИЕ

Некоторые читатели могут возразить, что сетевая файловая система также имеет фиксированный номер порта 2049. Хотя во многих реализациях по умолчанию действительно используется именно этот порт, а в некоторых старых реализациях он вообще жестко «зашит» в клиентскую и серверную части NFS, большинство существующих реализаций позволяют использовать и другие порты. Большая часть клиентов NFS также связывается со службой отображения портов для получения номера порта.

В Solaris 2.x Sun переименовала службу отображения портов в RPCBIND. Причина этого изменения заключается в том, что термин «порт» подразумевает порт Интернета, тогда как пакет TI-RPC может работать с любым сетевым протоколом, а не только с TCP и UDP. Мы будем использовать традиционное название «программа отображения портов» (port mapper). Далее в этой главе мы будем подразумевать, что на данном узле поддерживаются только протоколы TCP и UDP.

Сервер и клиент работают следующим образом:

1. При переходе системы в многопользовательский режим запускается пpoгрaммa отображения портов. Исполняемый файл этого демона обычно называется portmap или rpcbind.

2. При запуске сервера его функция main, являющаяся частью заглушки сервера, создаваемой rpcgen, вызывает библиотечную функцию svc_create. Эта функция выясняет, какие сетевые протоколы поддерживаются узлом, и создает конечную точку (например, сокет) для каждого протокола, связывая временные порты с конечными точками протоколов TCP и UDP. Затем она связывается с локальной пpoгрaммoй отображения портов для регистрации временных номеров портов TCP и UDP вместе с номером программы и номером версии.

Сама программа отображения портов также представляет собой программу RPC, и сервер регистрируется с помощью вызовов RPC (обращенных к известному порту 111). Описание процедур, поддерживаемых пpoгрaммoй отображения портов, дается в стандарте RFC 1833 [20]. Существуют три версии этой программы RPC: вторая версия работает только с портами TCP и UDP, а версии 3и 4 представляют собой новые версии, работающие по протоколу RPCBIND.

Можно получить список всех пpoгрaмм RPC, зарегистрированных в пpoгрaм-ме отображения портов, запустив пpoгрaммy rpcinfo. Мы можем запустить эту программу, чтобы убедиться, что порт с номером 111 используется самой программой отображения портов:

solaris % rpcinfo –p

program vers proto port service

100000 4 tcp 111 rpcbind

100000 3 tcp 111 rpcbind

100000 2 tcp 111 rpcbind

100000 4 udp 111 rpcbind

100000 3 udp 111 rpcbind

100000 2 udp 111 rpcbind

(Мы исключили множество несущественных в данный момент строк вывода.) Мы видим, что Solaris 2.6 поддерживает все три версии протокола, все на порте 111, причем как TCP, так и UDP. Соответствие номеров пpoгрaмм RPC их именам обычно устанавливается в файле /etc/rpc. Запустив ту же программу в BSD/OS 3.1, увидим, что в этой системе поддерживается только вторая версия программы отображения портов:

bsdi % rpcinfo –p

program vers proto port

100000 2 tcp 111 portmapper

100000 2 udp 111 portmapper

В Digital Unix 4.0В также поддерживается только вторая версия:

alpha % rpcinfo –p

program vers proto port

100000 2 tcp 111 portmapper

100000 2 udp 111 portmapper

Затем процесс сервера приостанавливает работу, ожидая поступления запросов от клиентов. Это может быть новое соединение TCP или приход дей-тaгрaммы UDP в порт UDP. Если мы запустим rpcinfo после запуска сервера из листинга 16.3, мы увидим следующий результат:

solaris % rpcinfo –p

program vers proto port service

8243773444 1 udp

8243773444 1 tcp

где 824377344 соответствует 0x31230000 (номер пpoгрaммы, присвоенный ей в листинге 16.1). В том же листинге мы присвоили программе номер версии 1. Обратите внимание, что сервер готов принимать запросы от клиентов по протоколам TCP и UDP и клиент может выбирать, какой из этих протоколов он будет использовать при создании дескриптора клиента (последний аргумент clnt_create в листинге 16.2).

3. Клиент запускается и вызывает clnt_create. Аргументами (листинг 16.2) являются имя узла или IP-адрес сервера, номер пpoгрaммы, номер версии и строка, указывающая протокол связи. Запрос RPC направляется пpoгрaммe отображения портов узла сервера (для этого сообщения обычно используется протокол UDP), причем запрашивается информация об указанной версии указанной программы с указанным протоколом. В случае успеха номер порта сохраняется в дескрипторе клиента для обработки всех последующих вызовов RPC через этот дескриптор.

В листинге 16.1 мы присвоили нашей программе номер 0x31230000. 32-разрядные номера пpoгрaмм подразделяются на группы, приведенные в табл. 16.1.


Таблица 16.1. Диапазоны номеров программ для Sun RPC 

Номер программыОписание
0x00000000–0x1fffffffОпределена Sun
0x20000000–0x3fffffffОпределена пользователем
0x40000000–0x5fffffffВременная (для приложений, написанных заказчиком)
0x60000000–0xffffffffЗарезервированы

Пpoгрaммa rpcinfо выводит список программ, зарегистрированных в системе. Другим источником информации о пpoгрaммax RPC могут являться файлы с расширением .х в каталоге /usr/inciude/rpcsvc.

Inetd и серверы RPC

По умолчанию серверы, созданные с помощью rpcgen, могут вызываться сервером верхнего уровня inetd. Этот сервер описывается в разделе 12.5 [24]. Изучение содержимого заглушки сервера, создаваемой rpcgen, показывает, что при запуске функции main сервера она проверяет, является ли стандартный поток ввода конечной точкой XTI, и если так, то предполагается, что сервер был запущен демоном inetd.

После создания сервера RPC, который будет вызываться inetd, следует добавить информацию об этом сервере в файл /etc/inetd.conf. Туда помещаются следующие данные: имя программы RPC, поддерживаемые номера программ, протоколы и полное имя исполняемого файла сервера. В качестве примера мы приводим строку из конфигурационного файла Solaris:

rstatd/2-4 tli rpc/datagram_v wait root /usr/lib/netsvc/rstat/rpc.rstatd rpc.rstatd

Первое поле содержит имя пpoгрaммы (которому будет сопоставлен номер с помощью файла /etc/rpc); поддерживаются версии 2, 3 и 4. Следующее поле задает конечную точку XTI (или сокет), третье поле говорит о том, что поддерживаются все протоколы видимых дeйтaгрaмм. Если свериться с содержимым файла /etc/netconfig, мы узнаем, что таких протоколов два: UDP и /dev/clts. Глава 29 [24] описывает этот файл и адреса XTI. Четвертое поле (wait) указывает демону inetd на необходимость ожидания завершения этого сервера перед включением режима ожидания запроса клиента для конечной точки XTI. Все серверы RPC указывают атрибут wait в конфигурационном файле /etc/inetd.conf.

Следующее поле, root, указывает идентификатор пользователя, с которым будет выполняться пpoгрaммa. Последние два поля задают полное имя исполняемого файла пpoгрaммы и имя пpoгрaммы вместе с необходимыми аргументами командной строки (у данной пpoгрaммы они отсутствуют).

Демон inetd создаст конечные точки XTI и зарегистрирует их в программе отображения портов для соответствующих номеров пpoгрaмм и версий. Мы можем убедиться в этом с помощью rpcinfo:

solaris % rpcinfo | grep statd

100001 2 udp    0.0.0.0.128.11 rstatd superuser

100001 3 
udp    0.0.0.0.128.11 rstatd superuser

100001 4 
udp    0.0.0.0.128.11 rstatd superuser

100001 2 
ticlts \000\000\020.  rstatd superuser

100001 3 
ticlts \000\000\020.  rstatd superuser

100001 4 
ticlts \000\000\020.  rstatd superuser

Четвертое поле содержит адреса XTI, причем 128×256+11 = 32779, и данное значение является временным номером порта, присвоенным этой концевой точке UDP.

Когда дейтаграмма UDP поступает в порт 32779, демон inetd обнаруживает готовность этой дейтаграммы к обработке и вызывает fork, а затем exec для запуска программы /usr/lib/netsvc/rstat/rpc.rstatd. Перед вызовами fork и exec концевая точка XTI будет скопирована в дескрипторы 0, 1 и 2, а все прочие дескрипторы inetd будут закрыты (рис. 12.7 [24]). Демон inetd также прекратит слушать эту конечную точку XTI, не реагируя на запросы пользователей до тех пор, пока сервер (дочерний процесс по отношению к inetd) не завершит работу. Это поведение определяется атрибутом wait.

Предположим, что эта программа была создана с помощью rpcgen. Тогда она сможет распознать конечную точку XTI, подключенную к стандартному потоку ввода, и инициализировать ее как конечную точку сервера RPC. Это осуществляется вызовом функций RPC svc_tli_create и svc_reg, которые в данной книге не рассматриваются. Вторая функция (вопреки названию) не регистрирует сервер в программе отображения портов — это делается лишь однажды, при запуске сервера. Функция svc_run прочитает пришедшую дейтаграмму и вызовет соответствующую процедуру сервера для обработки запроса клиента.

В обычной ситуации серверы, запускаемые демоном inetd, обрабатывают один запрос клиента и завершают работу, после чего inetd переходит в режим ожидания следующего запроса. Для оптимизации работы системы серверы RPC, созданные rpcgen, ждут поступления нового запроса от клиента в течение некоторого времени (по умолчанию 2 минуты). В этом случае дейтаграмма обрабатывается уже запущенным сервером. Это исключает накладные расходы на вызов fork и exec при поступлении нескольких клиентских запросов подряд. По истечении периода ожидания сервер завершает работу, а демону inetd отсылается сигнал SIGCHLD, после чего он переходит в режим ожидания дейтаграмм по XTI.

16.4. Аутентификация

По умолчанию в запросе RPC не содержится информации о клиенте. Сервер отвечает на запрос клиента, не беспокоясь о том, что это за клиент. Это называется нулевой аутентификацией, или AUTH_NONE.

Следующий уровень проверки подлинности называется аутентификацией Unix, или AUTH_SYS. Клиент должен сообщить библиотеке RPC времени выполнения информацию о себе (имя узла, действующий идентификатор пользователя, действующий идентификатор группы, дополнительные идентификаторы группы) для включения в каждый запрос. Изменим программу из листинга 16.2 таким образом, чтобы она включала возможность осуществления аутентификации Unix. В листинге 16.7 приведен новый текст программы-клиента.

Листинг 16.7. Клиент, осуществляющий аутентификацию unix

//sunrpc/square4/client.с

1  #include "unpipc.h"

2  #include "square.h"


3  int

4  main(int argc, char **argv)

5  {

6   CLIENT *cl;

7   square_in in;

8   square_out out;

9   if (argc != 3)

10   err_quit("usage: client ");

11  cl = Clnt_create(argv[1], SQUARE_PROG, SQUARE_VERS, "tcp");

12  auth_destroy(cl->cl_auth);

13  cl->cl_auth = authsys_create_default();

14  in.arg1 = atol(argv[2]);

15  if (squareproc_2(&in, &out, cl) != RPC_SUCCESS)

16   err_quit("%s", clnt_sperror(cl, argv[1]));

17  printf("result: %ld\n", out.resl);

18  exit(0);

19 }

12-13 Эти строки были добавлены в данной версии программы. Сначала мы вызываем auth_destroy для удаления предыдущей аутентификационной информации, связанной с данным дескриптором клиента (то есть дескриптор нулевой аутентификации, создаваемый по умолчанию). Затем вызов authsys_create_default создает соответствующую аутентификационную структуру Unix и мы сохраняем ее в поле cl_auth структуры CLIENT. Оставшаяся часть клиента не претерпела изменений по сравнению с листингом 16.5.

В листинге 16.8 приведен текст процедуры сервера, измененный по сравнению с листингом 16.6. Мы не приводим текст процедуры square_prog_2_freeresult, которая не меняется.

Листинг 16.8. Процедура сервера, запрашивающая аутентификацию Unix

//sunrpc/square4/server.c

1  #include "unpipc.h"

2  #include "square.h"


3  bool_t

4  squareproc_2_svc(square_in *inp, square_out *outp, struct svc_req *rqstp)

5  {

6   printf("thread %Id started, arg = %ld, auth = %d\n",

7   pr_thread_id(NULL), inp->arg1, rqstp->rq_cred.oa_flavor);

8   if (rqstp->rq_cred.oa_flavor == AUTH_SYS) {

9    struct authsys_parms *au;

10   au = (struct authsys_parms *)rqstp->rq_clntcred;

11   printf("AUTH_SYS: host %s, uid %ld, gid %ld\n",

12    au->aup_machname, (long) au->aup_uid, (long) au->aup_gid);

13  }

14  sleep(5);

15  outp->res1 = inp->arg1 * inp->arg1;

16  printf("thread %ld done\n", pr_thread_id(NULL));

17  return(TRUE);

18 }

6-8 Теперь мы используем указатель на структуру svc_req, которая всегда передается в качестве одного из аргументов процедуры сервера:

struct svc_req {

 u_long rq_prog; /* номер программы */

 u_long rq_vers; /* номер версии */

 u_long rq_proc; /* номер процедуры */

 struct opaque_auth rq_cred:/* данные о клиенте */

 caddr_t rq_clntcred; /* готовые данные (только для чтения) */

 SVCXPRT *rq_xprt; /* транспортный дескриптор */

};


struct opaque_auth {

 enum_t oa_flavor; /* flavor: константа AUTH_xxx */

 caddr_t oa_base; /* адрес дополнительной аутентификационной информации */

 u_int oa_length; /* не должно превосходить MAX_AUTH_BYTES */

};

Поле rq_cred содержит неформатированную информацию о клиенте, а его поле oa_flavor содержит целое число, определяющее тип аутентификации. Термин «неформатированная» означает, что библиотека не обработала информацию, на которую указывает oa_base. Но если тип идентификации относится к одному из поддерживаемых библиотекой, то в готовой информации о клиенте, на которую указывает rq_clntcred, содержится некоторая структура, соответствующая данному типу аутентификации. Программа выводит тип аутентификации и прове-9_12 ряет, соответствует ли он AUTH_SYS.

Для аутентификации Unix указатель на готовую информацию (rq_clntcred) указывает на структуру authsys_parms, содержащую информацию о клиенте:

struct authsys_parms {

 u_long aup_time; /* время создания информации */

 char *aup_machname; /* имя узла клиента */

 uid_t aup_uid; /* действующий идентификатор пользователя */

 gid_t aup_gid; /* действующий идентификатор группы */

 u_int aup_len; /* количество элементов в aup_gids[] */

 gid_t *aup_gidsl; /* дополнительные идентификаторы группы */

};

Мы получаем указатель на эту структуру и выводим имя узла клиента, его EUID и EGID.

Запустив сервер и один экземпляр клиента, посмотрим на выводимый сервером текст:

solaris % server

thread 1 started, arg = 44, auth = 1

AUTH_SYS: host solaris.kohala.com, uid 765, gid 870

thread 1 done

Аутентификация Unix используется редко, поскольку ее легко обойти. Мы можем легко построить собственные пакеты RPC, содержащие аутентификационную информацию в формате Unix, присвоив идентификатору пользователя и группы произвольные значения, и отправить их на сервер. Сервер никак не может проверить, те ли мы, кем представляемся.

ПРИМЕЧАНИЕ

Вообще-то NFS по умолчанию использует именно аутентификацию Unix, но запросы обычно отсылаются ядром клиента NFS через зарезервированный порт (раздел 2.7 [24]). Некоторые серверы NFS настроены так, чтобы отвечать только на запросы, поступающие по зарезервированному порту. Если вы доверяете узлу клиента подключение к своим файловым системам, вы доверяете и его ядру в том, что оно правильно предоставляет информацию о своих пользователях. Если сервер не требует подключения по резервному порту, хакеры могут написать свою собственную программу, которая будет посылать запросы серверу NFS с произвольным идентификатором пользователя. Даже если сервер требует подключения по зарезервированному порту, а у вас есть своя система, в которой вы обладаете правами привилегированного пользователя, и вы можете подключиться к сети, вы сможете отправлять свои собственные запросы с произвольным содержимым на сервер NFS. 

Пакеты RPC — как запросы, так и ответы — содержат два поля, относящиеся к аутентификации: данные о пользователе и проверочную информацию (credentials, verifier). Примером такой структуры является документ с фотографией (паспорт, права и т. п.). Данные о пользователе соответствуют написанному в паспорте тексту (имя, адрес, дата рождения и т. п.), а проверочная информация — это фотография. Существуют разные формы проверочной информации: фотография в данном случае полезнее, чем, например, рост, вес и пол. Если документ не содержит проверочной информации (как, например, читательский билет в библиотеке), любой может воспользоваться им и сказать, что он его владелец.

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

■ AUTH_SHORT — альтернативная форма аутентификации Unix, отправляемая сервером в поле verifier в ответ на запрос клиента. Она содержит меньшее количество информации, чем в режиме аутентификации Unix, и клиент может отсылать ее серверу при последующих запросах. Используется для уменьшения количества передаваемой по сети информации.

■ AUTH_DES — аббревиатура DES означает Data Encryption Standard (стандарт шифрования данных). Эта форма аутентификации основана на использовании криптографии с секретным и открытым ключом. Эта схема также называется защищенным RPC (secure RPC), а если она используется в качестве основы для построения NFS, то такая NFS также называется защищенной.

■ AUTH_KERB — эта схема основана на стандарте Kerberos института MIT.

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

16.5. Тайм-аут и повторная передача

Рассмотрим стратегию обработки тайм-аутов и повторной передачи, используемую в средствах Sun RPC. Существуют два значения тайм-аутов:

1. Общий тайм-аут определяет время ожидания ответа сервера клиентом. Это значение используется протоколами TCP и UDP.

2. Тайм-аут повтора используется только UDP и определяет время ожидания между повторами запросов клиента, если ответ от сервера не приходит.

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

После создания дескриптора клиента можно использовать функцию clnt_control для получения информации и изменения свойств клиента. Эта функция работает аналогично fcntl для дескрипторов файлов или getsockopt и setsockopt для сокетов:

#include 

bool_t clnt_control(CLIENT *cl, unsigned int request, char *ptr);

/* Возвращает TRUE в случае успешного завершения, FALSE – в случае ошибки */

Здесь cl представляет собой дескриптор клиента, а на что указывает ptr — зависит от значения request.

Изменим программу-клиент из листинга 16.2, добавив в нее вызов данной функции, и выведем значения тайм-аутов. В листинге 16.9 приведен текст новой программы-клиента.

Листинг 16.9. Клиент, получающий и печатающий значения времени ожидания RPC

//sunrpc/square5/client.c

1  #include "unpipc.h"

2  #include "square.h"


3  int

4  main(int argc, char **argv)

5  {

6   CLIENT *cl;

7   square_in in;

8   square_out *outp;

9   struct timeval tv;

10  if (argc != 4)

11   err_quit("usage: client ");

12  cl = Clnt_create(argv[1], SQUARE_PROG, SQUARE_VERS, argv[3]);

13  Clnt_control(cl, CLGET_TIMEOUT, (char*)&tv);

14  printf("timeout = %ld sec, %ld usec\n", tv.tv_sec, tv.tv_usec);

15  if (clnt_control(cl, CLGET_RETRY_TIMEOUT, (char *) &tv) == TRUE)

16   printf("retry timeout = %ld sec, %ld usec\n", tv.tv_sec, tv.tv_usec);

17  in.arg1 = atol(argv[2]);

18  if ((outp = squareproc_1(&in, cl)) == NULL)

19   err_quit("%s", clnt_sperror(cl, argv[1]));

20  printf(result: %ld\n", outp->res1);

21  exit(0);

22 }

Используемый протокол является аргументом командной строки

10-12 Теперь протокол, являющийся последним аргументом clnt_create, указывается в качестве нового параметра командной строки.

Получение значения общего тайм-аута

13-14 Первым аргументом clnt_control является дескриптор клиента, вторым — тип запроса, а третьим — указатель на буфер. Наш первый запрос имеет значение CLGET_TIMEOUT; при этом возвращается значение общего тайм-аута в структуре timeval, адрес которой передается третьим аргументом. Этот запрос корректен для всех протоколов.

Попытка получения тайм-аута повтора

15-16 Следующий запрос имеет значение CLGET_RETRY_TIMEOUT. При этом должно возвращаться значение тайм-аута повтора, но этот запрос корректен только для протокола UDP. Следовательно, если функция возвращает значение FALSE, мы ничего не печатаем.

Изменим также и программу-сервер, добавив в нее ожидание продолжительностью 1000 секунд вместо 5, чтобы гарантировать получение тайм-аута по запросу клиента. Запустим сервер на узле bsdi, а клиент запустим дважды, один раз указав в качестве протокола TCP, а другой — UDP. Результат будет не таким, как мы ожидали:

solaris % date ; client bsdi 44 tcp ; date

Wed Apr 22 14:46:57 MST 1998

timeout = 30 sec, 0 usec       тайм-аут 30 секунд

bsdi: RPC: Timed out

Wed Apr 22 14:47:22 MST 1998   но прошло только 25 секунд

solaris % date ; client bsdi 55 udp ; date

Wed Apr 22 14:48:05 MST 1998

timeout = –1 sec, –1 usec      ерунда какая-то

retry timeout = 15 sec, 0 usec это значение кажется правильным

bsdi: RPC: Timed out

Wed Apr 22 14:48:31 MST 1998   около 25 секунд спустя

В случае с протоколом TCP значение тайм-аута, возвращенное clnt_control, было 30 секунд, но библиотека возвратила ошибку через 25 секунд. Для протокола UDP было получено значение общего тайм-аута –1.

Чтобы понять, что тут происходит, изучим текст заглушки клиента — функции squareproc_1 в файле square_clnt.c, созданном rpcgen. Эта функция вызывает библиотечную функцию с именем clnt_call, причем последним аргументом является структура типа timeval с именем TIMEOUT, объявляемая в этом файле, и инициализируется она значением 25 секунд. Этот аргумент clnt_call отменяет значение общего тайм-аута в 30 секунд для TCP и –1 для UDP. Он используется всегда, если клиент не устанавливает общий тайм-аут явно вызовом clnt_control с запросом CLSET_TIMEOUT. Если мы хотим изменить значение общего тайм-аута, следует вызывать clnt_control, а не изменять содержимое заглушки клиента. 

ПРИМЕЧАНИЕ

Единственный способ проверить значение тайм-аута повтора для протокола UDP заключается в просмотре пакетов с помощью tcpdump. При этом можно увидеть, что первая дейтаграмма отправляется сразу после запуска клиента, а следующая — примерно 15 секунд спустя.

Управление соединением по TCP

Если мы будем наблюдать с помощью tcpdump за работой клиента и сервера из предыдущего примера, связывающихся по протоколу TCP, мы увидим, что сначала происходит установка соединения (трехэтапное рукопожатие TCP), затем отправляется запрос клиента и сервер отсылает уведомление о приеме этого запроса. Через 25 секунд после этого клиент отсылает серверу FIN, что вызвано завершением работы клиента, после чего следуют оставшиеся три этапа завершения соединения по TCP. В разделе 2.5 [24] эти этапы описаны подробно.

Мы хотим показать, что Sun RPC использует соединение по TCP следующим образом: новое соединение по TCP устанавливается при вызове clnt_create и оно используется для всех вызовов процедур, связанных с указанной программой и версией. Соединение по TCP завершается явно вызовом clnt_destroy или неявно по завершении процесса клиента:

#include 

void clnt_destroy(CLIENT *cl);

Начнем с клиента из листинга 16.2 и изменим его, добавив второй вызов процедуры сервера, вызовы clnt_destroy и pause. В листинге 16.10 приведен текст новой программы-клиента.

Листинг 16.10. Клиент для изучения свойств соединения по TCP

//sunrpc/square9/client.c

1  #include "unpipc.h" /* наш заголовочный файл*/

2  #include "square.h" /* создается rpcgen */


3  int

4  main(int argc, char **argv)

5  {

6   CLIENT, *cl;

7   square_in in;

8   square_out *outp;

9   if (argc != 3)

10   err_quit("usage: client ");

11  cl = Clnt_create(argv[1], SQUARE_PROG, SQUARE_VERS, "tcp");

12  in.arg1 = atol(argv[2]);

13  if ((outp = squareproc_1(&in, cl)) == NULL)

14   err_quit("%s", clnt_sperror(c1, argv[1]));

15  printf("result: %ld\n", outp->res1);

16  in.arg1 *= 2;

17  if ((outp = squareproc_1(&in, cl)) == NULL)

18   err_quit("%s", clnt_sperror(cl, argv[1]));

19  printf("result: %ld\n", outp->res1);

20  clnt_destroy(cl);

21  pause();

22  exit(0);

23 }

После запуска получим ожидаемый результат:

solaris % client kalae 5

result: 25

result: 100

программа в состоянии ожидания, пока мы не завершим ее вручную

Однако проверить наши предыдущие утверждения можно лишь с помощью результатов работы программы tcpdump. Она показывает, что создается одно соединение по TCP (вызовом clnt_create) и оно используется для обоих запросов клиента. Соединение завершается вызовом clnt_destroy, хотя клиент при этом и не завершает свою работу.

Идентификатор транзакций

Другая часть стратегии тайм-аутов и повторных передач заключается в использовании идентификаторов транзакций (transaction ID или XID) для распознавания запросов клиента и ответов сервера. Когда клиент вызывает функцию RPC, библиотека присваивает этому вызову 32-разрядный целочисленный номер и это значение отсылается в запросе RPC. Сервер должен добавить к своему ответу этот номер. При повторной отсылке запроса идентификатор не меняется. Служит он двум целям:

1. Клиент проверяет, что XID ответа совпадает с XID запроса. Если совпадения нет, ответ игнорируется. Если используется протокол TCP, у клиента практически нет шансов получить ответ с неправильным идентификатором, но при использовании протокола UDP поверх плохой сети вероятность получения неправильного XID достаточно высока.

2. Серверу разрешается помещать отсылаемые ответы в кэш, и для проверки идентичности ответов используется, в частности, именно XID. Об этом мы вскоре расскажем.

Пакет TI-RPC использует определенный алгоритм вычисления XID для нового запроса. Алгоритм этот описан ниже. Значок ^ означает побитовую операцию XOR (исключающее ИЛИ):

struct timeval now;

gettimeofday(&now, NULL);

xid = getpid() ^ now.tv_sec ^ now.tv_usec;

Кэш повторных ответов

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

#include 

int svc_dg_enablecache(SVCXPRT *xprt, unsigned long size);

/* Возвращает 1 в случае успешного завершения. 0 – в случае ошибки */

Здесь xprt представляет собой транспортный дескриптор, являющийся полем структуры svc_req (раздел 16.4). Адрес этой структуры является аргументом процедуры сервера. Размер определяет количество записей в выделяемом кэше.

Итак, эта функция включает поддержку кэширования всех отсылаемых ответов в очереди размером size записей. Каждый ответ однозначно определяется следующими параметрами:

■ номером программы;

■ номером версии;

■ номером процедуры;

■ XID;

■ адресом клиента (IP-адрес + порт UDP).

При получении запроса клиента библиотека RPC ищет в кэше ответ на такой запрос. В случае его наличия ответ отсылается клиенту без повторного вызова процедуры сервера.

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

16.6. Семантика вызовов

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

Вызовы процедур могут быть разбиты на три группы:

1. «Ровно один раз» означает, что процедура была выполнена только один раз. Такого трудно достичь ввиду ненулевой вероятности сбоев в работе сервера.

2. «Не более одного раза» означает, что процедура вовсе не была выполнена или что она была выполнена один раз. Если вызвавшему процессу возвращается результат, мы знаем, что процедура была выполнена. Если процессу возвращается сообщение об ошибке, мы не знаем, была ли процедура выполнена хотя бы один раз или не была выполнена вовсе. 

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

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

■ Если используется протокол 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 (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:

bsdi % client solaris 33 tcp

solaris: RPC: Timed out

После завершения работы потока сервера соединение с клиентом по TCP не разрывается. Оно остается открытым, поэтому клиенту не отсылается пакет FIN. Клиент выходит по тайм-ауту. Мы увидели бы то же сообщение об ошибке, если бы узел, на котором находится сервер, прекратил работу после получения запроса от клиента и отправки уведомления.

Досрочное завершение клиента

Если клиент, использующий TCP, завершает работу в процессе выполнения процедуры RPC, серверу отправляется пакет FIN. Мы хотим узнать, как библиотека сервера реагирует на этот пакет и уведомляет об этом процедуру сервера. (В разделе 15.11 мы говорили, что поток сервера дверей отменяется при досрочном завершении клиента.)

Чтобы сымитировать такую ситуацию, клиент вызывает alarm(3) непосредственно перед вызовом процедуры сервера, а процедура сервера вызывает slеер (6). Так же мы поступили и в нашем примере с дверьми в листингах 15.25 и 15.26. Поскольку клиент не перехватывает сигнал SIGALRM, процесс завершается ядром примерно за 3 секунды до отправки ответа серверу. Запустим клиент в BSD/OS, а сервер в Solaris:

bsdi % client solaris 44 tcp

Alarm call

Случилось то, что мы и ожидали. А вот на сервере не происходит ничего необычного. Процедура сервера благополучно заканчивает 6-секундную паузу и возвращается. Если мы взглянем на передаваемую по сети информацию с помощью tcpdump, мы увидим следующее:

■ при завершении работы клиента (через 3 секунды после запуска) серверу отправляется пакет FIN, и сервер высылает уведомление о его приеме. В TCP для этого используется термин half-close (наполовину закрытое соединение, раздел 18.5 [22]);

■ через 6 секунд после запуска клиента сервер отсылает ответ, который переправляется клиенту протоколом TCP. По соединению TCP можно отправлять данные после получения FIN, поскольку соединения TCP являются двусторонними, о чем говорится в книге [24, с. 130-132]. Клиент отвечает пакетом RST, поскольку он уже завершил работу. Он будет получен сервером при следующем открытии этого соединения, но это ни к чему не приведет.

Подведем итоги.

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

■ Клиент RPC, использующий TCP, может узнать о возникших на сервере проблемах, поскольку при досрочном завершении сервера его конец соединения автоматически закрывается. Это, однако, не помогает, если сервер является многопоточным, поскольку такой сервер не закрывает соединение в случае отмены потока с процедурой сервера. Мы также не получаем информации в случае сбоя узла, поскольку при этом соединение TCP не закрывается. Во всех этих случаях следует использовать выход по тайм-ауту.

16.8. XDR: представление внешних данных

В предыдущей главе мы использовали двери для вызова процедуры одного процесса из другого процесса. При этом оба процесса выполнялись на одном узле, поэтому необходимости в преобразовании данных не возникало. Однако RPC используется для вызова процедур на разных узлах, которые могут иметь различный формат хранения данных. Прежде всего могут отличаться размеры фундаментальных типов (в некоторых системах long имеет длину 32 бита, а в других — 64). Кроме того, может отличаться порядок битов (big-endian и little-endian, о чем говорится в книге [24, с. 66-69 и 137-140]. Мы уже столкнулись с этой проблемой, когда обсуждали листинг 16.3. Сервер у нас работал на компьютере с little-endian х86, а клиент — на big-endian Sparc, но мы могли без проблем обмениваться данными (в нашем примере — одно длинное целое).

Sun RPC использует стандарт XDR (External Data Representation — представление внешних данных) для описания и кодирования данных (RFC 1832 [19]). XDR является одновременно языком описания данных и набором правил для их кодирования. В XDR используется скрытая типизация (implicit typing), то есть отправитель и получатель должны заранее знать тип и порядок данных. Например, два 32-разрядных целых, одно число с плавающей точкой и одинарной точностью и строка символов.

ПРИМЕЧАНИЕ

Приведем сравнение из мира OSI. Для описания данных обычно используется нотация ASN.1 (Abstract Syntax Notation one), а для кодирования — BER (Basic Encoding Rules). Эта схема также использует явную типизацию, то есть перед каждым значением указывается его тип. В нашем примере поток байтов содержал бы: спецификатор типа целого, целое, спецификатор типа целого, целое, спецификатор типа single, число с плавающей точкой и одинарной точностью, спецификатор типа строки символов, строку символов.

Представление всех типов согласно XDR требует количества байтов, кратного четырем. Эти байты всегда передаются в порядке big-endian. Целые числа со знаком передаются в дополнительном коде, а числа с плавающей точкой передаются в формате IEEE. Поля переменной длины могут содержать до 3 байтов дополнения в конце, так чтобы подогнать начало следующего элемента до адреса, кратного четырем. Например, 5-символьная строка АSСII будет передана как 12 байтов:

■ 4-байтовое целое, содержащее значение 5;

■ 5-байтовая строка;

■ 3 байта со значением 0 (дополнение).

При описании XDR и поддерживаемых типов данных следует уточнить три момента.

1. Как объявляются переменные различных типов в файле спецификации RPC (файл с расширением .х)? В наших примерах пока что использовалось только длинное целое.

2. В какой тип языка С преобразуется данный тип программой rpcgen при составлении заголовочного файла?

3. Каков реальный формат передаваемых данных?

Таблица 16.2 содержит ответы на первых два вопроса. Для составления этой таблицы мы создали файл спецификации RPC со всеми поддерживаемыми стандартом XDR типами. Этот файл был обработан rpcgen, после чего мы изучили получившийся заголовочный файл.


Таблица 16.2. Типы данных, поддерживаемые xdr и rpcgen 

Файл спецификации RPC (.x)Заголовочный файл языка С (.h)
1const name = value#define name value
2typedef declaration;typedef declaration;
3char var; short var; int var; long var; hyper var;char var; short var; int var; long var; longlong_t var;
4unsigned char var; unsigned short var; unsigned int var; unsigned long var; unsigned hyper var;u_char var; u_short var; u_int var; u_long var; u_longlong_t var;
5float var; double var; quadruple var;float var; double var; quadruple var;
6bool var;bool_t var;
7enum var {name = const, …}enum var {name = const, …}; typedef enum var var;
8opaque var[n];char var[n];
9opaque var;struct { u_int var_len; char *var_val; } val;
10string varchar *var;
11datatype var[n];datatype var[n];
12datatype varstruct { uint var_len; datatype *var_val; } var;
13struct var {members … };struct var {members … }; typedef struct var var;
14union var switch (int disc) { case discvalueA: armdeclA; case discvalueB: amrdeclB; … default: defaultdecl; };struct var { int disc; union { armdeclA; armdeclB; …  defaultdecl; } var_u; }; typedef struct var var;
15datatype *name;datatype *name;

Опишем содержимое таблицы более подробно.

1. Декларация const преобразуется в #define.

2.  Декларация typedef преобразуется в typedef.

3. Пять целых типов со знаком. Передаются XDR как 32-разрядные значения (первые четыре типа), а последний — как 64-разрядное.

ПРИМЕЧАНИЕ

64-разрядное целое поддерживается многими компиляторами С как формат long long int или просто long long. Многие, но не все компиляторы и операционные системы поддерживают такой формат. Поскольку в созданном заголовочном файле объявляются переменные типа longlong_t, в другом заголовочном файле должно содержаться следующее определение:

typedef long long longlong_t;

Длинное целое в XDR занимает 32 бита, но длинное целое языка С в 64-разрядных системах Unix может занимать и 64 бита (например, в модели LP64, описанной в [24, с. 27]). Имена формата XDR устарели лет на десять и не слишком соответствуют современным стандартам. Лучше, если бы они назывались как-нибудь вроде int8_t, int16_t и т. д.

4. Пять целых типов без знака. Первые 4 передаются как 32-разрядные значения, а последнее — как 64-разрядное.

5. Три типа данных с плавающей точкой. Первый передается как 32-разрядное значение, второй — как 64-разрядное, а третий — как 128-разрядное.

ПРИМЕЧАНИЕ

Четверная точность для чисел с плавающей точкой (quadruple precision) поддерживается в С для типов long double. He все компиляторы и операционные системы его воспринимают. Ваш компилятор может пропустить long double, но работать с этой переменной как с double. Поскольку в созданном заголовочном файле объявляются переменные типа quadruple, нужно создать другой заголовочный файл с объявлением

typedef long double quadruple;

В Solaris 2.6, например, нам пришлось бы включить строку

%#include 

в начало файла спецификации RPC, потому что этот заголовочный файл содержит требуемое определение. Знак процента перед #include говорит программе rpcgen о необходимости поместить остаток строки непосредственно в создаваемый заголовочный файл.

6. Тип boolean эквивалентен целому со знаком. Заголовки RPC также определяют константу TRUE равной 1, a FALSE равной 0.

7. Перечисление (enumeration) эквивалентно целому со знаком и совпадает с типом данных enum в С. rpcgen также создает определение типа для данной переменной.

8. Скрытые данные фиксированной длины передаются библиотекой как 8-разрядные значения без интерпретации.

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

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

11. Массив фиксированной длины любого типа передается как последовательность n элементов данного типа.

12. Массив переменной длины любого типа передается как целое без знака, указывающее количество элементов, и последовательность элементов данного типа. Максимальное количество элементов в объявлении может быть опущено. Но если это количество указать при компиляции программы, библиотека будет проверять, не превосходит ли реальная длина указанного значения m.

13. Структура передается как последовательность полей. rpcgen также создает определение типа для данного имени переменной (typedef).

14. Размеченное объединение состоит из целочисленного дискриминанта и набора типов данных (ветвей), зависящих от значения дискриминанта. В табл. 16.2 мы показываем, что дискриминант должен быть типа int, но он может быть и unsigned int, и enum, и bool (все эти типы передаются как 32-разрядные целые). При передаче размеченного объединения передается 32-разрядное значение дискриминанта, за которым следует значение той ветви, которая ему соответствует. В ветви default часто объявляется тип void, что означает отсутствие передаваемой вслед за дискриминантом информации. Ниже мы продемонстрируем это на примере.

15. Дополнительные данные представляют собой специальный тип объединения, описанный в примере из листинга 16.24. Объявление XDR выглядит как объявление указателя в языке С, и именно указатель объявляется в созданном заголовочном файле.

На рис. 16.3 сведена информация о кодировании различных типов данных в XDR.

Рис. 16.3. Кодирование типов данных в XDR

Пример: использование XDR без RPC

Приведем пример использования XDR без RPC. Мы воспользуемся стандартом XDR для кодирования структуры данных в машинно-независимое представление, в котором они могут быть обработаны другими системами. Этот метод может использоваться для написания файлов или для отправки данных по сети в машинно-независимом формате. В листинге 16.11 приведен текст файла спецификации data .х, который на самом деле является файлом спецификации XDR, поскольку мы не объявляем никаких процедур RPC.

ПРИМЕЧАНИЕ

Суффикс имени файла (.х) происходит от термина «файл спецификации XDR». Спецификация RPC утверждает, что язык RPC (RPCL) идентичен XDR в части, относящейся к описанию данных. В RPCL была добавлена только возможность описания процедур.

Листинг 16.11. Файл спецификации XDR

//sunrpc/xdr1/data.x

1  enum result_t {

2   RESULT_INT = 1, RESULT_DOUBLE = 2

3  };


4  union union_arg switch (result_t result) {

5  case RESULT_INT:

6   int intval;

7  case RESULT_DOUBLE:

8   double doubleval;

9  default:

10  void;

11 };


12 struct data {

13  short short_arg;

14  long long_arg;

15  string vstring_arg<128>; /* строка переменной длины */

16  opaque fopaque_arg[3]; /* скрытые данные фиксированной длины */

17  opaque vopaque_arg<>; /* скрытые данные переменной длины */

18  short fshort_arg[4]; /* массив фиксированной длины */

19  long vlong_arg<>; /* массив переменной длины */

20  union_arg uarg;

21 };

Объявление перечисления и размеченного объединения

1-11 Мы объявляем перечислимый тип с двумя значениями и размеченное объединение, использующее это перечисление в качестве дискриминанта. Если дискриминант имеет значение RESULT_INT, после значения дискриминанта передается целое число. Если дискриминант имеет значение RESULT_DOUBLE, за ним передается число с плавающей точкой двойной точности. В противном случае после дискриминанта не передается ничего. 

Объявление структуры

12-21 Мы объявляем структуру, состоящую из различных типов, поддерживаемых XDR.

Поскольку мы не объявляем процедур RPC, программа rpcgen не создаст заглушку клиента и заглушку сервера. Однако она создаст заголовочный файл data.h и файл data_xdr.с, содержащий функции XDR, обеспечивающие кодирование и декодирование данных, объявленных в файле data.х.

В листинге 16.12 приведен получающийся в результате работы rpcgen заголовочный файл data.h. Содержимое этого файла выглядит так, как мы и предполагали (табл. 16.2).

Листинг 16.12. Заголовочный файл data.h, созданный rpcgen из файла data.x

//sunrpc/xdr1/data.h

1  /*

2   * Please do not edit this file. It was generated using rpcgen.

3   */

4  #ifndef _DATA_H_RPCGEN

5  #define _DATA_H_RPCGEN


6  #include 


7  enum result_t {

8   RESULT_INT = 1,

9   RESULT_DOUBLE = 2

10 };

11 typedef enum result_t result_t;


12 struct union_arg {

13  result_t result;

14  union {

15   int intVal;

16   double doubleval;

17  } union_arg_u;

18 };

19 typedef struct union_arg union_arg;


20 struct data {

21  short short_arg;

22  long long_arg;

23  char *vstring_arg;

24  char fopaque_arg[3];

25  struct {

26   u_int vopaque_arg_len;

27   char *vopaque_arg_val;

28  } vopaque_arg;

29  short fshort_arg[4];

30  struct {

31   u_int vlong_arg_len;

32   long *vlong_arg_val;

33  } vlong_arg;

34  union_arg uarg;

35 };

36 typedef struct data data:


37 /* 4the xdr functions */

38 extern bool_t xdr_result_t(XDR *, result_t*);

39 extern bool_t xdr_union_arg(XDR *, union_arg*);

40 extern bool_t xdr_data(XDR *, data*);


41 #endif /* !_DATA_H_RPCGEN */

В файле data_xdr.с объявляется функция xdr_data, вызываемая для кодирования и декодирования структуры data, которую мы определили. Суффикс имени функции _data соответствует имени нашей структуры из листинга 16.11. Первая программа, которую мы напишем, будет называться write.с. Она будет присваивать значения полям структуры data, вызывать xdr_data для кодирования всех полей в формат XDR и записывать результат в стандартный поток вывода.

Эта программа приведена в листинге 16.13.

Листинг 16.13. Инициализация структуры и кодирование ее в XDR

//sunrpc/xdr1/write.c

1  #include "unpipc.h"

2  #include "data.h"


3  int

4  main(int argc, char **argv)

5  {

6   XDR xhandle;

7   data out; /* структура, с которой мы работаем */

8   char *buff; /* результат кодирования в XOR */

9   char vop[2];

10  long vlong[3];

11  u_int size;

12  out.short_arg = 1;

13  out.long_arg = 2;

14  out.vstring_arg = "hello, world"; /* присваиваем значение указателю */

15  out.fopaque_arg[0] = 99; /* скрытые данные фиксированной длины */

16  out.fopaque_arg[1] = 88;

17  out.fopaque_arg[2] = 77;

18  vop[0] = 33; /* скрытые данные переменной длины */

19  vop[1] = 44;

20  out.vopaque_arg.vopaque_arg_len = 2;

21  out.vopaque_arg.vopaque_arg_val = vop;

22  out.fshort_arg[0] = 9999; /* массив фиксированной длины */

23  out.fshort_arg[1] = 8888;

24  out.fshort_arg[2] = 7777;

25  out.fshort_arg[3] = 6666;

26  vlong[0] = 123456; /* массив переменной длины */

27  vlong[l] = 234567;

28  vlong[2] = 345678;

29  out.vlong_arg.vlong_arg_len = 3;

30  out.vlong_arg.vlong_arg_val = vlong;

31  out.uarg.result = RESULT_INT; /* размеченное объединение */

32  out.uarg.union_arg_u.intval = 123;

33  buff = Malloc(BUFFSIZE); /* кратен 4-м байтам */

34  xdrmem_create(&xhandle, buff, BUFFSIZE, XDR_ENCODE);

35  if (xdr_data(&xhandle, &out) != TRUE)

36   err_quit("xdr_data error");

37  size = xdr_getpos(&xhandle);

38  Write(STDOUT_FILENO, buff, size);

39  exit(0);

40 }

Инициализация элементов структуры ненулевыми значениями

12-32 Сначала мы присваиваем полям структуры ненулевые значения. В случае полей переменной длины мы должны установить длину этих полей. Мы присваиваем дискриминанту размеченного объединения значение RESULT_INT и помещаем в его соответствующее поле значение 123.

Выделение буфера

33 Мы вызываем malloc для выделения буфера, в который подпрограммы XDR будут помещать результаты своей работы. Адрес и размер буфера должны быть кратны четырем. Выделение массива char не гарантирует этого.

Создание потока XDR в памяти

34 Функция библиотеки времени выполнения xdrmem_create инициализирует буфер, на который указывает buff, предназначенный для использования функциями XDR как поток в памяти. Мы выделяем переменную типа XDR с именем xhandle и передаем адрес этой переменной в качестве первого аргумента. Библиотека XDR времени выполнения хранит в этой переменной всю необходимую информацию (указатель на буфер, текущее положение в буфере и т. п.). Последний аргумент имеет значение XDR_ENCODE, что указывает XDR на необходимость преобразования данных из формата узла в формат XDR.

Кодирование структуры

35-36 Мы вызываем функцию xdr_data, созданную rpcgen в файле data_xdr.c, и она кодирует структуру out в формат XDR. Возвращаемое значение TRUE говорит об успешном завершении работы функции.

Получение размера кодированных данных и запись их в поток вывода

37-38 Функция xdr_getpos возвращает текущее положение библиотеки XDR в выходном буфере (то есть сдвиг байта, в который будут помещены очередные данные). Его мы трактуем как размер готовых к записи данных. 

В листинге 16.14 приведен текст программы read, которая считывает данные из файла, записанного предыдущей программой, и выводит значения всех полей структуры data.

Листинг 16.14. Считывание структуры data из формата XDR

//sunrpc/xdr1/read.c

1  #include "unpipc.h"

2  #include "data.h"


3  int

4  main(int argc, char **argv)

5  {

6   XDR xhandle;

7   int i;

8   char *buff;

9   data in;

10  ssize_t n;

11  buff = Malloc(BUFFSIZE); /* адрес должен быть кратен 4-м байтам */

12  n = Read(STDIN_FILENO, buff, BUFFSIZE);

13  printf("read %ld bytes\n", (long) n);

14  xdrmem_create(&xhandle, buff, n, XDR_DECODE);

15  memset(&in, 0, sizeof(in));

16  if (xdr_data(&xhandle, &in) != TRUE)

17   err_quit("xdr_data error");

18  printf("short_arg = %d, long_arg = %ld, vstring_arg = '%s'\n",

19   in.short_arg, in.long_arg, in.vstring_arg);

20  printf("fopaque[] = %d, %d, %d\n",

21   in.fopaque_arg[0], in.fopaque_arg[1], in.fopaque_arg[2]);

22  printf("vopaque<> =");

23  for (i = 0; i < in.vopaque_arg.vopaque_arg_len; i++)

24   printf(" %d", in.vopaque_arg.vopaque_arg_val[i]);

25  printf("\n");

26  printf("fshort_arg[] = %d, %d, %d, %d\n", in.fshort_arg[0],

27   in.fshort_arg[1], in.fshort_arg[2], in.fshort_arg[3]);

28  printf("vlong<> =");

29  for (i = 0; i < in.vlong_arg.vlong_arg_len; i++)

30   printf(" %ld", in.vlong_arg.vlong_arg_val[i]);

31  printf("\n");

32  switch (in.uarg.result) {

33  case RESULT_INT:

34   printf("uarg (int) = %d\n", in.uarg.union_arg_u.intval);

35   break;

36  case RESULT_DOUBLE:

37   printf("uarg (double) = %g\n", in.uarg.union_arg_u.doubleval);

38   break;

39  default:

40   printf("uarg (void)\n");

41   break;

42  }

43  xdr_free(xdr_data, (char*)&in);

44  exit(0);

45 }

Выделение правильно расположенного буфера

11-13 Вызывается функция malloc для выделения буфера. В этот буфер считывается файл, созданный предыдущей программой.

Создание потока XDR, инициализация буфера, декодирование

14-17 Инициализируем поток XDR, указав флаг XDR_DECODE, означающий, что преобразование производится из формата XDR в формат узла. Мы инициализируем структуру i n нулями и вызываем xdr_data для декодирования содержимого буфера buff в эту структуру. Мы обязаны инициализировать принимающую структуру нулями, поскольку некоторые из подпрограмм XDR (например, xdr_string) требуют выполнения этого условия. xdr_data — это та же функция, которую мы вызывали в листинге 16.13. Изменился только последний аргумент xdrmem_create: в предыдущей программе мы указывали XDR_ENCODE, а в этой — XDR_DECODE. Это значение сохраняется в дескрипторе XDR (xhandle) функцией xdrmem_create и затем используется библиотекой XDR для выбора между кодированием и декодированием данных.

Вывод значений полей структуры

18-42 Мы выводим значения всех полей структуры data.

Освобождение выделенной под XDR памяти

43 Для освобождения памяти мы вызываем функцию xdr_free (см. упражнение 16.10).

Запустим программу write на компьютере Sparc, перенаправив стандартный вывод в файл с именем data:

solaris % write > data

solaris % ls -l data

-rw-rw-r-- 1 rstevens other1 76 Apr 23 12:32 data

Мы видим, что размер файла равен 72 байтам что соответствует рис. 16.4, на котором изображена схема хранения данных.

Прочитав этот файл в BSD/OS или Digital Unix, мы получим те результаты, на которые и рассчитывали:

bsdi % read < data

read 76 bytes

short_arg = 1, long_arg = 2, vstring_arg = 'hello, world'

fopaque[] =99, 88, 77

vopaque<> = 33 44

fshort_arg[] = 9999, 8888, 7777, 6666

vlong<> = 123456 234567 345678

uarg (int) = 123

alpha % read < data

read 76 bytes

short_arg = 1, long_arg = 2, vstring_arg = 'hello, world'

fopaque[] = 99, 88, 77

vopaque<> = 33 44

fshort_arg[] = 9999, 8888, 7777, 6666

vlong<> = 123456 234567 345678

uarg (int) = 123

Рис. 16.4. Формат потока XDR, записанный в листинге 16.13

Пример: вычисление размера буфера

В предыдущем примере мы выделяли буфер размера BUFFSIZE (определенного в файле unpiрс.h в листинге В.1), и этого было достаточно. К сожалению, не существует простого способа вычислить объем памяти, нужный XDR для кодирования конкретных данных. Вычислить размер структуры вызовом sizeof недостаточно, потому что каждое поле кодируется XDR по отдельности. Нам придется перебирать элементы структуры, прибавляя к конечному результату объем памяти, нужный XDR для кодирования очередного элемента. В листинге 16.15 приведен пример простой структуры с тремя полями.

Листинг 16.15. Спецификация XDR для простой структуры

//sunrpc/xdrl/examplе.х

1 const MAXC = 4;

2 struct example {

3  short a;

4  double b;

5  short c[MAXC];

6 };

Программа, текст которой приведен в листинге 16.16, вычисляет размер буфера, требуемого XDR для кодирования этой структуры. Он получается равным 28 байт.

Листинг 16.16. Программа, вычисляющая размер буфера XDR

//sunrpc/xdr1/example.c

1  #include "unpipc.h"

2  #include "example.h"


3  int

4  main(int argc, char **argv)

5  {

6   int size;

7   example foo;

8   size = RNDUP(sizeof(foo.a)) + RNDUP(sizeof(foo.b)) +

9    RNDUP(sizeof(foo.c[0])) * MAXC;

10  printf("size = %d\n", size);

11  exit(0);

12 }

8-9 Макрос RNDUP определен в файле . Он округляет аргумент к ближайшему кратному BYTES_PER_XDR_UNIT (4). Для массива фиксированного размера вычисляется размер каждого элемента, который затем умножается на количество элементов.

Проблема возникает в случае использования типов данных переменной длины. Если мы объявим stringd<10>, максимальный размер будет RNDUP(sizeof( int)) (для длины) плюс RNDUP(sizeof(char)*10) (для символов строки). Но мы не можем вычислить размер буфера, если максимальный размер не указан в объявлении переменной (например, float e<>). Лучше всего в этом случае выделять буфер с запасом, а потом проверять, не возвращают ли подпрограммы XDR ошибку (упражнение 16.5).

Пример: необязательные данные

Существуют три способа задания необязательных данных в файле XDR, примеры для всех приведены в листинге 16.17.

Листинг 16.17. Файл спецификации XDR, иллюстрирующий способы задания необязательных данных

//sunrpc/xdr1/opt1.x

1  union optlong switch (bool flag) {

2  case TRUE:

3   long val;

4  case FALSE:

5   void;

6  };


7  struct args {

8   optlong arg1; /* объединение с булевским дискриминантом */

9   long arg2<1>; /* массив переменной длины с одним элементом */

10  long *arg3; /* указатель */

11 };

Объявление объединения с булевским дискриминантом

1-8 Мы определяем объединение с ветвями FALSE и TRUE и структуру этого типа. Если флаг дискриминанта TRUE, за ним следует значение типа long; в противном случае за ним ничего не следует. После кодирования библиотекой XDR это объединение будет закодировано как:

■ 4 байта флага со значением 1 (TRUE) и 4 байта целочисленного значения либо

■ 4 байта флага со значением 0 (FALSE).

Объявление массива переменной длины

9 Если мы указываем массив переменной длины с одним возможным элементом, он будет передан как:

■ 4 байта со значением 1 и 4 байта значения либо

■ 4 байта со значением 0.

Объявление указателя XDR

10 Новый способ определения необязательных данных заключается в объявлении указателя. Он будет закодирован как:

■ 4 байта со значением 1 и 4 байта значения либо

■ 4 байта со значением 0

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

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

В листинге 16.18 приведен текст заголовочного файла, созданного программой rpcgen для данного файла спецификации.

Листинг 16.18. Заголовочный файл, получившийся в результате обработки листинга 16.17

//sunrpc/xdr1/opt1.h

7  struct optlong {

8   bool_t flag;

9   union {

10  long val;

11  } optlong_u;

12 };

13 typedef struct optlong optlong;


14 struct args {

15  optlong arg1;

16  struct {

17   u_int arg2_len;

18   long *arg2_val;

19  } arg2;

20  long *arg3;

21 };

22 typedef struct args args;

14-21 Хотя все три аргумента кодируются одинаково, способы присваивания и получения их значений в языке С различны.

В листинге 16.19 приведен текст простой пpoгрaммы, устанавливающей значения всех трех аргументов так, что ни одно из полей long не кодируется.

Листинг 16.19. Ни одно из значений не будет закодировано

//sunrpc/xdr1/opt1z.с

1  #include "unpipc.h"

2  #include "opt1.h"


3  int

4  main(int argc, char **argv)

5  {

6   int i;

7   XDR xhandle;

8   char *buff;

9   long *lptr;

10  args out;

11  size_t size;

12  out.arg1.flag = FALSE;

13  out.arg2.arg2_len = 0;

14  out.arg3 = NULL;

15  buff = Malloc(BUFFSIZE); /* Адрес должен быть кратен четырем */

16  xdrmem_create(&xhandle, buff, BUFFSIZE, XOR_ENCODE);

17  if (xdr_args(&xhandle, &out) != TRUE)

18   err_quit("xdr_args error");

19  size = xdr_getpos(&xhandle);

20  lptr = (long*)buff;

21  for (i = 0; i < size; i += 4)

22   printf("%ld\n", (long) ntohl(*lptr++));

23  exit(0);

24 }

Присваивание значений

12-14 Дискриминанту объединения присваивается значение FALSE, длина массива переменной длины устанавливается в 0, а указатель делается нулевым (NULL).

Выделение буфера и кодирование

15-19 Мы выделяем буфер и кодируем структуру out в поток XDR. 

Вывод содержимого буфера XDR

20-22 Мы выводим содержимое буфера XDR по 4 байта, используя функцию ntohl (host-to-network long integer) для преобразования из порядка XDR big-endian в байтовый порядок узла. В результате получается именно то, что должно было быть помещено в буфер библиотекой XDR времени выполнения:

solaris % opt1z

0

0

0

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

В листинге 16.20 приведена измененная версия программы, которая присваивает значения всем трем аргументам, кодирует их в поток XDR и выводит его содержимое.

Листинг 16.20. Присваивание значений аргументам из листинга 16.17

//sunrpc/xdr1/opt1.c

1  #include "unpipc.h"

2  #include "opt1.h"


3  int

4  main(int argc, char **argv)

5  {

6   int i;

7   XOR xhandle;

8   char *buff;

9   long lval2, lval3, *lptr;

10  args out;

11  size_t size;

12  out.arg1.flag = TRUE;

13  out.arg1.optlong_u.val = 5;

14  lval2 = 9876;

15  out.arg2.arg2_len = 1;

16  out.arg2.arg2_val = &lval2;

17  lval3 = 123;

18  out.arg3 = &lval3;

19  buff = Malloc(BUFFSIZE); /* адрес должен быть кратен 4 */

20  xdrmem_create(&xhandle, buff, BUFFSIZE, XDR_ENCODE);

21  if (xdr_args(&xhandle, &out) != TRUE)

22   err_quit("xdr_args error");

23  size = xdr_getpos(&xhandle);

24  lptr = (long *) buff;

25  for (i = 0; i < size; i += 4)

26   printf("%ld\n", (long) ntohl(*lptr++));

27  exit(0);

28 }

Присваивание значений

12-18 Для присваивания значения объединению мы устанавливаем дискриминант в TRUE, а затем присваиваем значение полю long. Длину массива мы также сначала устанавливаем в 1. Указатель мы устанавливаем на соответствующее значение в памяти.

При запуске этой программы мы получим ожидаемые шесть 4-байтовых значений:

solaris % opt1

1 значение дискриминанта TRUE

5

1 длина массива переменной длины

9876

1 флаг для ненулевого указателя

123

Пример: обработка связного списка

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

Листинг 16.21. Спецификация XDR для связного списка пар имя-значение

//sunrpc/xdr1/opt2.x

1 struct mylist {

2  string name<>;

3  long value;

4  mylist *next;

5 };


6 struct args {

7  mylist *list;

8 };

1-5 Структура mylist содержит одну пару имя-значение и указатель на следующую структуру такого типа. Указатель в последней структуре списка будет нулевым.

В листинге 16.22 приведен текст заголовочного файла, созданного программой rpcgen из файла opt2.х.

Листинг 16.22. Заголовочный файл, созданный программой rpcgen

//sunrpc/xdr1/opt2.h

7  struct mylist {

8   char *name;

9   long value;

10  struct mylist *next;

11 };

12 typedef struct mylist mylist;


13 struct args {

14  mylist *list;

15 };

16 typedef struct args args;

В листинге 16.23 приведен текст программы, инициализирующей связный список с тремя парами имя-значение и кодирующей его с помощью библиотеки XDR.

Листинг 16.23. Инициализация, кодирование связного списка и вывод результата

1  //sunrpc/xdr1/opt2.c

2  #include "unpipc.h"

3  #include "opt2.h"


4  int

5  main(int argc, char **argv)

6  {

7   int i;

8   XDR xhandle;

9   long *lptr;

10  args out; /* структура, которую мы заполняем */

11  char *buff; /* результат кодирования */

12  mylist nameval[4]; /* до четырех элементов в списке */

13  size_t size;

14  out.list = &nameval[2]; /* [2] –> [1] –> [0] */

15  nameval[2].name = "name1";

16  nameval[2].value = 0x1111;

17  nameval[2].next = &nameval[1];

18  nameval[1].name = "namee2";

19  nameval[1].value = 0x2222;

20  nameval[1].next = &nameval[0];

21  nameval[0].name = "nameee3";

22  nameval[0].value = 0x3333;

23  nameval[0].next = NULL;

24  buff = Malloc(BUFFSIZE); /* адрес должен быть кратен 4 */

25  xdrmem_create(&xhandle, buff, BUFFSIZE, XDR_ENCODE);

26  if (xdr_args(&xhandle, tout) != TRUE)

27   err_quit("xdr_args error");

28  size = xdr_getpos(&xhandle);

29  lptr = (long*)buff;

30  for (i = 0; i < size; i += 4)

31   printf("%8lx\n", (long)ntohl(*lptr++));

32  exit(0);

33 }

Инициализация связного списка

11-22 Мы выделяем память под четыре элемента, но инициализируем только три из них. Первая запись nameval[2], потом nameval[1] и nameval[0]. Указатель на начало списка (out.list) устанавливается на &nameval[2]. Мы инициализируем список в таком порядке, чтобы показать, что библиотека XDR обрабатывает указатели и порядок в списке оказывается именно таким, каким он был в нашей программе, и не зависит от того, какие массивы для этого используются. Мы также инициализируем значения элементов списка шестнадцатеричными величинами, поскольку будем выводить их в этом формате.

Вывод программы показывает, что перед каждым элементом списка идет значение 1 в 4 байтах (что мы можем считать длиной массива переменной длины с одним элементом или булевским значением TRUE). Четвертая запись состоит из 4 байт, в которых записан 0. Она обозначает конец списка:

solaris % opt2

1        дальше идет один элемент

5        длина строки

6e616d65 имя(name)

31000000 1 и три байта дополнения

1111     значение

1        один элемент

6        длина строки

6e616d65 имя

65320000 е 2 и 2 байта дополнения

2222     значение

1        один элемент

7        длина строки

6e616d65 имя

65653300 е е 3 и 1 байт дополнения

3333     значение

0        конец списка

При декодировании списка библиотека XDR будет динамически выделять память под его элементы и указатели и связывать все это вместе, что позволит легко переходить от одного элемента списка к другому в программе на С.

16.9. Форматы пакетов RPC

На рис. 16.5 приведен формат запроса RPC в пакете TCP.

Поскольку TCP передает поток байтов и не предусматривает границ сообщений, приложение должно предусматривать способ разграничения сообщений. Sun RPC определяет запись как запрос или ответ, и каждая запись состоит из одного или более фрагментов. Каждый фрагмент начинается с 4-байтового значения: старший бит является флагом последнего фрагмента, а следующие 31 бит представляют собой счетчик (длина фрагмента). Если бит последнего фрагмента имеет значение 0, данный фрагмент не является последним в записи.

ПРИМЕЧАНИЕ

Это 4-байтовое значение передается в порядке big-endian, так же как и 4-байтовые целые в XDR, но оно не относится к стандарту XDR, поскольку он не предусматривает передачи битовых полей.

Если вместо TCP используется UDP, первое поле в заголовке UDP будет идентификатором транзакции (XID), как показано на рис. 16.7.

ПРИМЕЧАНИЕ

При использовании TCP на размер запроса и ответа RPC ограничения не накладываются, поскольку может использоваться любое количество фрагментов, а длина каждого из них задается 31-разрядным целым. Но при использовании протокола UDP запрос (и ответ) должен помещаться в дейтаграмму целиком, а максимальное количество данных в ней — 65507 байт (для IPv4). Во многих реализациях, предшествовавших TI-RPC, размер ограничивался значением около 8192 байт, поэтому если требуется передавать более 8000 байт, следует пользоваться протоколом TCP.

Рис. 16.5. Запрос RPC в пакете TCP


Приведем спецификацию XDR для запроса RPC, взятую из RFC 1831. Имена на рис. 16.5 взяты из этой спецификации:

enum autn_flavor {

 AUTH_NONE = 0,

 AUTH_SYS = 1,

 AUTH_SHORT = 2

 /* and more to be defined */

};

struct opaque_auth {

 auth_flavor flavor;

 opaque body<400>;

};

enum msg_type {

 CALL = 0,

 REPLY = 1

};

struct call_body {

 unsigned int rpcvers; /* версия RPC: должна быть 2 */

 unsigned int prog; /* номер программы */

 unsigned int vers; /* номер версии */

 unsigned int proc; /* номер процедуры */

 opaque_auth cred; /* данные вызывающего */

 opaque_auth verf; /* проверочная информация вызывающего */

/* параметры, относящиеся к процедуре */

};

struct rpc_msg {

 unsigned int xid;

 union switch (msg_type mtype) {

 case CALL:

  call_body cbody;

 case REPLY:

  reply_body rbody;

 } body;

};

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

struct authsys_parms {

 unsigned int stamp;

 string machinename<255>;

 unsigned int uid;

 unsigned int gid;

 unsigned int gids<16>;

};

Если тип аутентификации AUTH_SYS, тип проверки должен быть AUTH_NONE. Формат ответа RPC сложнее, чем формат запроса, поскольку в нем могут передаваться сообщения об ошибках. На рис. 16.6 показаны возможные варианты. На рис. 16.7 показан формат ответа RPC в случае успешного выполнения процедуры. Ответ передается по протоколу UDP. 

Ниже приводится текст спецификации XDR ответа RPC, взятый из RFC 1831.

enum reply_stat {

 MSG_ACCEPTED = 0,

 MSG_DENIED = 1

};

enum accept_stat {

 SUCCESS = 0, /* успешное завершение вызова RPC */

 PROG_UNAVAIL = 1, /* требуемый номер программы недоступен */

 PROG_MISMATCH = 2, /* требуемый номер версии недоступен */

 PROC_UNAVAIL = 3, /* номер процедуры недоступен */

 GARBAGE_ARGS = 4, /* не могу декодировать аргументы */

 SYSTEM_ERR = 5 /* ошибка выделения памяти и т. п. */

};

struct accepted_reply {

 opaque_auth verf;

 union switch (accept_stat stat) {

 case SUCCESS:

  opaque results[0]; /* результаты, возвращаемые процедурой */

 case PROG_MISMATCH:

  struct {

   unsigned int low; /* наименьший поддерживаемый номер программы */

   unsigned int high; /* наибольший поддерживаемый номер программы */

  } mismatch_info;

 default: /* PROG_UNAVAIL, PROC_UNAVAIL, GARBAGE_ARGS, SYSTEM_ERR */

  void;

 } reply_data;

};

union reply_body switch (reply_stat stat) {

case MSG_ACCEPTED:

 accepted_reply areply;

case MSG_DENIED:

 rejected_reply rreply;

} reply;

Рис. 16.6. Возможные варианты ответов RPC


Вызов может быть отклонен сервером, если номер версии RPC не тот или возникает ошибка аутентификации:

enum reject_stat {

 RPC_MISMATCH = 0, /* номер версии RPC отличен от 2 */

 AUTH_ERROR =1 /* ошибка аутентификации */

};

enum auth_stat {

 AUTH_OK = 0, /* успешное завершение */

 /* ошибки на сервере */

 AUTH_BADCRED = 1, /* ошибка в личных данных пользователя (нарушена контрольная сумма) */

 AUTH_REJECTEDCRED = 2, /* клиент должен начать сеанс заново */

 AUTH_BADVERF = 3, /* ошибка в проверочных данных (нарушена контрольная сумма) */

 AUTH_REJECTEDVERF = 4, /* проверочные данные устарели или были повторы */

 AUTH_TOOWEAK = 5, /* запрос отклонен системой безопасности */

 /* ошибки клиента */

 AUTH_INVALIDRESP = 6, /* фальшивые проверочные данные в ответе */

 AUTH_FAILED = 7 /* причина неизвестна */

};

union rejected_reply switch (reject_stat stat) {

case RPC_MISMATCH:

 struct {

  unsigned int low; /* наименьший номер версии RPC */

  unsigned int high; /* наибольший номер версии RPC */

 } mismatch_info;

case AUTH_ERROR:

 auth_stat stat;

};

Рис. 16.7. Ответ на успешно обработанный вызов в дейтаграмме UDP

16.10. Резюме

Средства Sun RPC дают возможность создавать распределенные приложения, в которых клиентская часть может выполняться на одном узле, а серверная — на другом. Сначала следует определить процедуры сервера, которые могут быть вызваны клиентом, и написать файл спецификации RPC, описывающий аргументы и возвращаемые значения этих процедур. Затем пишется функция main клиента, вызывающая процедуры сервера, а потом сами эти процедуры. Программа клиента выглядит так, как будто она просто вызывает процедуры сервера, но на самом деле их скрытое взаимодействие по сети обеспечивается библиотекой RPC.

Программа rpcgen является краеугольным камнем приложения, использующего RPC. Она считывает файл спецификации и создает заглушку клиента и заглушку сервера, а также функции, вызывающие требуемые подпрограммы XDR, которые осуществляют все необходимые преобразования данных. Библиотека XDR также является важной частью процесса. XDR определяет стандарт обмена данными различного формата между разными системами, у которых может быть по-разному определен, например, размер целого, порядок байтов и т. п. Как мы показали, XDR можно использовать и отдельно от RPC для обмена данными в машинно-независимом стандартном формате. Для передачи данных можно использовать любой механизм (сокеты, XTI, дискеты, компакт-диски или что угодно).

В Sun RPC используется свой стандарт именования программ. Каждой программе присваивается 32-разрядный номер программы, 32-разрядный номер версии и 32-разрядный номер процедуры. Каждый узел с сервером RPC должен выполнять программу отображения портов в фоновом режиме (RPCBIND). Серверы RPC привязывают временные порты TCP и UDP к своим процедурам, а затем регистрируют эти порты в программе отображения портов, указывая номера программ и версий. При запуске клиент RPC связывается с программой отображения портов узла, где запущен сервер RPC, и выясняет номер нужного ему порта, а затем связывается с самим сервером по протоколам TCP или UDP.

По умолчанию клиенты RPC не предоставляют аутентификационной информации и серверы RPC обрабатывают все приходящие запросы. Это аналогично написанию собственного приложения клиент-сервер с использованием сокетов или XTI. В Sun RPC предоставляются три дополнительные формы аутентификации: аутентификация Unix (предоставляется имя узла клиента, идентификатор пользователя и группы), аутентификация DES (основанная на криптографии с секретным и открытым ключом) и аутентификация Kerberos.

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

Любой вызов процедуры может быть отнесен к группе «ровно один», «не более чем один» или «не менее чем один». Для локальных вызовов этот вопрос можно не принимать во внимание, но при использовании RPC эту разницу следует понимать. Следует также понимать различия между идемпотентными и неидемпотентными процедурами.

Sun RPC — это большой пакет, и мы лишь вкратце обсудили особенности его использования. Тем не менее сведений, приведенных в этой главе, должно быть достаточно для написания приложений целиком. Использование rpcgen скрывает многие детали и упрощает кодирование. Документация Sun описывает различные уровни кодирования RPC — упрощенный интерфейс, верхний уровень, средний уровень, уровень экспертов и низкий уровень, но эти категории достаточно бессмысленны. Количество функций в библиотеке RPC — 164, они могут быть разделены следующим образом:

■ 11 auth_ (аутентификация);

■ 26 clnt_ (клиентские);

■ 5 pmap_ (доступ к программе отображения портов);

■ 24 rpc_ (общего назначения);

■ 44 svc_ (серверные);

■ 54 xdr_ (преобразования XDR).

Для сравнения отметим, что интерфейсы сокетов и XTI содержат по 25 функций, а интерфейсы дверей, очередей сообщений Posix и System V, семафоров и разделяемой памяти содержат по 10 функций. 15 функций используются для работы с потоками в стандарте Posix, 10 — для работы с условными переменными. 11 функций используются для работы с блокировками чтения-записи Posix и одна при работе с блокировкой записей fcntl.

Упражнения

1. При запуске сервер регистрируется в программе отображения портов. Что происходит при завершении сервера, например, клавишей завершения программы с терминала? Что произойдет, если на этот сервер впоследствии придет запрос от клиента?

2. Клиент взаимодействует с сервером RPC по протоколу UDP, и кэш не включен. Клиент посылает запрос на сервер, но серверу требуется 20 секунд до отправки ответа. Клиент посылает запрос повторно через 15 секунд, что приводит к повторному запуску процедуры сервера. Что произойдет со вторым ответом сервера?

3. Тип XDR string всегда кодируется в значение длины и последовательность символов. Что произойдет, если мы напишем char с[10] вместо string s<10>?

4. Измените максимальный размер string в листинге 16.11 со 128 на 10 и запустите программу write. Что произойдет? Уберите ограничение длины и сравните файл data_xdr.с с тем, который был создан, когда длина была ограничена. Что изменилось?

5. Измените третий аргумент в вызове xdrmem_create (размер буфера) в листинге 16.13 на 50 и посмотрите, что произойдет.

6. В разделе 16.5 мы описали включение кэша повторных ответов при использовании протокола UDP. Мы можем сказать, что протокол TCP имеет свой собственный кэш такого рода. О чем мы говорим и как велик этот кэш у протокола TCP? (Подсказка: как протокол TCP определяет, что принята копия полученных ранее данных?)

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

8. При просмотре передаваемых пакетов с помощью tcpdump в примере из раздела 16.5, где использовался TCP, мы узнаем, что размер запроса 48 байт, а размер ответа 32 байт (без заголовков TCP и IPv4). Получите этот размер из рисунка 16.3. Каков был бы размер при использовании UDP вместо TCP?

9. Может ли клиент RPC в системе, не поддерживающей потоки, вызвать процедуру сервера, скомпилированную с поддержкой потоков? Что можно сказать о различии в передаваемых аргументах, о котором говорилось в разделе 16.2? 

10. В программе read из листинга 16.19 мы выделяли место под буфер, в который считывался файл, и этот буфер содержал указатель vstring_arg. Но где хранится строка, на которую указывает vstring_arg? Измените программу так, чтобы проверить ваше предположение.

11. Sun RPC определяет нулевую процедуру как процедуру с номером 0 (по этой причине мы всегда начинали нумерацию процедур с 1, как в листинге 16.1). Более того, любая заглушка сервера, созданная rpcgen, автоматически определяет эту процедуру (в чем вы можете легко убедиться, посмотрев текст любой заглушки, созданной для одного из наших примеров). Нулевая процедура не принимает никаких аргументов и ничего не возвращает. Часто она используется для проверки работы сервера и измерения скорости передачи пакетов на сервер и обратно. Но если мы посмотрим на заглушку клиента, мы увидим, что в ней не содержится заглушки для этой процедуры. Посмотрите в документации описание функции clnt_call и используйте ее для вызова нулевой процедуры для любого сервера этой главы.

12. Почему в табл А.1 нет записи для сообщения размером 65536 для Sun RPC поверх UDP? Почему нет записей для сообщений длиной 16384 и 32768 в табл. А.2 для Sun RPC поверх UDP?

13. Проверьте, что удаление вызова xdr_free из листинга 16.19 приведет к утечке памяти. Добавьте оператор

for(;;) {

непосредственно перед вызовом xdrmem_create и завершающую скобку непосредственно перед вызовом xdr_free. Запустите программу и следите за ее размером в памяти с помощью ps. Удалите закрывающую скобку и поставьте ее после вызова xdr_free. Запустите программу снова и последите за ее размером еще раз. 

Эпилог