Уильям Стивенс - UNIX: взаимодействие процессов
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 <hostname> <integer-value>");
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: %ldn", outp->res1);
16 exit(0);
17 }
Подключение заголовочного файла, создаваемого rpcgen2 Мы подключаем заголовочный файл square.h, создаваемый функцией rpcgen.
Объявление дескриптора клиента6 Мы объявляем дескриптор клиента (client handle) с именем cl. Дескрипторы клиентов выглядят как обычные указатели на тип FILE (поэтому слово CLIENT пишется заглавными буквами).
Получение дескриптора клиента11 Мы вызываем функцию clnt_create, создающую клиент RPC:
#include <rpc/rpc.h>
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), должны компилироваться в каждой системе в отдельности.