Салимóненко Дмитрий Александрович

Вычислительные Сети, Системы и Телекоммуникации

ЗАДАНИЕ 6: Вызов удаленных процедур RPC в LINUX




Введение

Технология вызова удаленных процедур (RPC) разработана компанией Sun Microsystems и является одной из стандартных технологий сетевого взаимодействия.

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

Технология RPC функционирует на основе модели "клиент-сервер", подразумевающей, как обычно, два основных этапа:

  • Запрос клиента –
  • Ответ сервера.

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

RPC предоставляет прикладным программистам сервис более высокого уровня, чем ранее рассмотренных два, т.к. обращение за услугой к удаленным процедурам выполняется в привычной для программиста манере вызова "локальной" функции языка программирования СИ. RPC реализовано, как правило, на базе socket-интерфейса и/или TLI. При пересылке данных между узлами сети в RPC для их внешнего представления используется стандарт XDR.

Средство RPC предоставляет программистам сервис трех уровней:

  • препроцессор rpcgen, преобразующий исходные тексты "монолитных" программ на языке программирования С в исходные тексты программы-клиента и программы-сервера по спецификации программиста (высокий уровень);
  • библиотека функций вызова удаленных процедур по их идентификаторам (средний уровень);
  • библиотека функций доступа к внутренним механизмам RPC и нижележащим протоколам транспортного уровня (низкий уровень).

В данном методическом указании рассматриваются только средства RPC среднего уровня.

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

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

Краткие теоретические сведения

Итак, в рамках технологии RPC узел (клиент или сервер) идентифицируется при помощи следующих четырех параметров:

  • имя узла сети (имя хоста);
  • номер программы на этом узле;
  • номер версии программы;
  • номер процедуры в программе.

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

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

Процедура-сервер, создаваемая службой RPC, должна иметь один единственный аргумент и единственный результат. Если есть необходимость возвращать, скажем, несколько параметров, то для этой цели возможно использовать, например, структуру, массив и т.д.

Минимальная реализация сетевых (распределенных) приложений на базе технологии RPC среднего уровня возможна при использовании  всего трех функций:

  • registerrpc, svc_run (на стороне сервера)
  • callrpc (на стороне клиента)

Т.е. средний уровень технологии RPC является достаточно ограниченным в своем функционале.

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

Описание основных функций интерфейса RPC

Регистрации процедуры-сервера на узле сети

Она осуществляется при помощи функции registerrpc():

   #include <sys/types.h>
   #include <rpc/rpc.h>
   int registerrpc (prognum, vernum, procnum, procname, inproc, outproc)

  • u_long prognum;
  • u_long vernum;
  • u_long procnum;
  • char *(*procname)();
  • xdrproc_t inproc;
  • xdrproc_t outproc;

Параметры prognum, vernum, procnum определяют номера программы, версии и процедуры соответственно. Номера версии и процедуры могут быть заданы произвольно (например, равны 1). Тогда как номер программы, находящейся в стадии разработки, должен назначаться из диапазона номеров 0x20000000...0x3fffffff.

procname определяет функцию С, регистрируемую в качестве сервера. Эта функция вызывается с указателем на ее аргумент и должна возвращать указатель на свой результат, располагаемый в статической или динамически выделенной (при помощи malloc или calloc) памяти. Для хранения результата нельзя использовать автоматически выделяемую память (напоминаем, что локальные переменные функций располагаются именно в такой памяти).

Аргументы inproc и outproc задают XDR-функции преобразования,  аргумента и результата функции registerrpc, соответственно.

При успешном выполнении функция registerrpc возвращает 0, в случае ошибки – возвращает "-1".

Прием запросов сервером от клиента

Функция svc_run

   #include <rpc/rpc.h>
   void svc_run();

Эту функция не имеет аргументов, вызывается после того, как осуществлена регистрации процедуры-сервера в службе RPC. Обратите внимание, что при успешном выполнении функция svc_run() никогда не возвращает управление в вызвавшую ее программу. Это избавляет от необходимости реализовывать бесконечный цикл, как это, как правило, делается при программировании серверов на сокетах. А вот если возникнет ошибка – тогда (и только тогда) сработает функция, идущая следующей в программного коде, после svc_run().

Запрос к серверу

Запрос выполняет функция callrpc():
   #include <sys/types.h>
   #include <rpc/rpc.h>
   int callrpc (host, prognum, vernum, procnum, inproc, in, outproc, out)

  • char *host (имя хоста);
  • u_long prognum (номер программы);
  • u_long vernum (номер версии);
  • u_long procnum (номер процедуры);
  • xdrproc_t inproc (входная программа);
  • char *in (входные параметры);
  • xdrproc_t outproc (выходная программа);
  • char *out (выходные параметры);

Аргументы:

  • host - имя узла, на котором функционирует сервер. Если программа сервер работает на локальном компьютере (т.е. на том же самом, что и клиент), то в качестве имени узла будет фигурировать, как правило, localhost (а можно ввести и его имя).
  • Узнать имя хоста можно при помощи консольной команды hostname -s
  • prognum, vernum и procnum – это номера программы, версии и процедуры-сервера. Как обычно, сервер должен быть запущен на соответствующем узле (имеющем имя host) до обращения клиента к нему (иначе будет ошибка вида «Connection refused» или «В соединении отказано»)
  • in должен указывать на данные, передаваемые серверу в качестве аргумента
  • out указывает на область памяти, предназначенную для размещения в ней результата работы сервера.
  • inproc и outproc представляют собой XDR-функции преобразования, аргумента процедуры-сервера и ее результата, соответственно.

Технология RPC функционирует по технологии «клиент-сервер». Соответственно, после того, как клиент послал запрос, он будет ожидать ответа от сервера.

При успешном выполнении вызова удаленной процедуры-сервера функция callrpc() вернет 0, в случае ошибки возвращается "-1".

XDR-функции

Эти функции предназначены для преобразования данных в/из XDR-формат. Среди них можно упомянуть такие функции, как:

  • xdr_int - для преобразования целых;
  • xdr_u_int - для преобразования беззнаковых целых;
  • xdr_short - для преобразования коротких целых;
  • xdr_u_short - для преобразования беззнаковых коротких целых;
  • xdr_long - для преобразования длинных целых;
  • xdr_u_long - для преобразования беззнаковых длинных целых;
  • xdr_char - для преобразования символов;
  • xdr_u_char - для преобразования беззнаковых символов;
  • xdr_wrapstring - для преобразования строк символов (заканчивающихся символом '\0').
Внимание! Вышеприведенные функции должны использоваться в качестве входной, выходной программ в функциях registerrpc() callrpc(), соответственно (см. примеры листингов сервера и клиента ниже).

Если аргумент в процедуру-сервер не передается (или когда она не возвращает результат), целесообразно использовать функцию-"заглушку" xdr_void().

XDR-функции являются универсальными. В зависимости от контекста их использования, они способны преобразовывать данные из внутреннего представления (характерного для компьютера конкретной архитектуры), во внешнее, согласно стандарту XDR и, наоборот; эти функции могут также динамически выделять или освобождать память для данных. Стандарт этих функций одинаков для всех (по крайней мере, наиболее известных) операционных систем. Т.е. вполне можно при помощи клиента из Linux направить сообщение на сервер, например, в Windows или даже в OpenVMS. При необходимости, можно также написать свои XDR-функций на основе имеющихся.

Итак, в рамках данного задания необходимо реализовать клиент и сервер, взаимодействующие друг с другом, с использованием технологии RPC.

Общее описание программ

Программы клиент и сервер, реализуемые в рамках данного задания, будут взаимодействовать по технологии «клиент-сервер», т.е. «запрос клиента – ответ сервера». Это означает, что:

  • Сервер должен быть запущен РАНЕЕ клиента, при этом, до поступления запроса от клиента, он находится в состоянии ожидания. При поступлении запроса сервер обрабатывает его и направляет ответ клиенту;
  • Клиент должен быть запущен ПОСЛЕ сервера, при этом он может направлять запросы к серверу и ожидает поступления ответа от него;
  • Перед запуском сервера обязательно должна быть запущена служба RPC (иначе система вернет сообщение о недоступности соединения).

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

Порт назначается, в качестве соответствия номерам RPC-программ. Это осуществляется регистратором - преобразователем портов (port mapper). По сути, последний представляет из себя (системный) сервер, который и осуществляет такое преобразование. Если этот сервер не запущен, исполнение (и, следовательно, ответ) RPC вызова будет невозможно.

Примечание. В технологиях RPC, основанных на транспортном интерфейсе TLI, вместо port mapper используется rpcbind.

Тогда как для технологии сокетов нет необходимости в реализации дополнительной службы. Зато ряд параметров (например, порт), которые используются в процессе клиент-серверного взаимодействия, необходимо устанавливать «вручную» (т.е. необходимо прописывать задание соответствующих параметров в программе). При этом (при использовании сырых сокетов - SOCK_RAW) возможно также «ручное» задание ряда других свойств сетевого соединения. Тогда как возможности технологии RPC в этом аспекте - ограничены. Для передачи сетевого адреса и номера порта используется служба (демон) portmap().

Вызов службы RPC возможен, по-видимому, из любого языка, который поддерживает реализацию сетевых соединений. Соответственно этой службе, есть и протокол RPC. Он основан на протоколах транспортного уровня, т.е. работает поверх них. При этом создание логического соединения, обеспечение надежности и т.п. реализуют транспортные протоколы (ТСР, UDP).

Протокол RPC имеет следующий формат заголовка запроса клиента:


IP-заголовок 20 Байт
TCP (UDP)-заголовок 8
Идентификатор транзакции (XID) 4
Вызов (0) 4
Версия RPC (2) 4
Номер программы 4
Номер версии 4
Номер процедуры 4
Полномочия до 400 Байт
Проверка до 400 Байт
Параметры процедуры (данные)
…………………………………


И формат заголовка ответа сервера:


IP-заголовок 20 Байт
TCP (UDP)-заголовок 8
Идентификатор транзакции (XID) 4
Отклик (1) 4
Статус (0 = принято) 4
Проверка до 400 Байт
Статус приема (0 = успешно) 4
Параметры процедуры (данные)
…………………………………

Т.е. данные протокола RPC отправляются клиентом и сервером в соответствующих форматах. Непосредственно данные (точнее, то, ради передачи чего и производился запрос клиента), которые были заданы в процедуре callrpc(), содержатся в параметрах процедуры.

Примечание. Протокол RPC является, по сути, надстройкой над протоколом транспортного уровня, в частности, протокола UDP.

Удобство службы RPC в том, что для прикладных приложений обеспечивается универсальная возможность обращения к удаленным процедурам – необходимо лишь (при работающей службе RPC и запущенном сервере, имеющем такие процедуры) вызвать требуемую процедуру, предварительно задав для нее параметры, в соответствии с форматом и смыслом ее аргументов. И… - дождаться ответа RPC-сервера. В этом плане вызов удаленной процедуры практически ничем не отличается от вызова локальной процедуры (коей может быть любая команда, например, fopen(), fprintf(), read() и т.п. или написанный программистом модуль, библиотека). Отличие, разве что, в том,  что при вызове локальной процедуры клиент (точнее, модуль, сделавший запрос), получает данные через стек или общие области памяти, тогда как при вызове удаленной процедуры данные приходят через сетевое соединение. Кроме того, возникает сложность при передаче значения через указатель: ведь удаленная процедура, в общем случае, выполняется на другом компьютере и, следовательно, в совсем другом адресном пространстве. Поэтому передача параметра возможна лишь по значению.

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

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

Т.е., еще раз: для этой же самой цели может быть использован и механизм сокетов (тем более, что сам по себе механизм RPC как раз и основан на сокетах, как на относительно низкоуровневом средстве). Точнее, или на сокетах, или на транспортном интерфейсе TLI. RPC всего лишь удобнее, быстрее для создания программного кода, способного осуществлять сетевое взаимодействие и, что тоже немаловажно, унифицированнее. Последнее, в силу применения стандарта XDR, дает возможность осуществлять обмен данными между компьютерами разных архитектур, например между узлами Unix, PC, Macintosh и даже VAX OpenVMS. Формат XDR определен в RFC 1014 [Sun Microsystems, 1987].

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

Кстати, технология RPC используется, к примеру, для файловой системы NFS (Linux) при доступе, в том числе, в рамках модели удаленного доступа к серверу, для чего узел (хост) клиента проводит монтирование (mounting) удаленного поддерева каталогов в свою собственную файловую систему, что осуществляется путем направления RPC-запроса к службе mount сервера.

Напомню, что существуют две основных модели доступа к серверам:

  • Модель загрузки-выгрузки
  • Модель удаленного доступа
Общая схема взаимодействия клиента и сервера в модели удаленного доступа

При этом взаимодействие будет иметь примерно такой вид:


Итак, необходимо создать программы клиент и сервер, которые:

  • Сервер, получив в качестве аргумента запроса (от клиента) целое число, равное 2, открывает файл с именем text.txt, читает его в строку и возвращает ее клиенту;
  • Клиент вначале открывает файл, имя которого задается вторым параметром в командной строке (при запуске клиента). Это - text.txt. Читает этот файл и выводит на экран его содержимое. Затем он делает запрос к серверу. Если второй (последний) параметр в командной строке равен text.txt, то клиент передает серверу целое число, равное 2, иначе должно передаваться число 0.

Разработка программных кодов клиента и сервера

Перед тем, как приводить программные коды, целесообразно обсудить оптимизацию программ. В частности, речь идет о вышеупомянутых параметрах, представляющих собой имя узла сети, номер программы, номер версии программы, номер процедуры в этой программе. Отметим сразу, что эти параметры должны быть указана в ОБОИХ программах, как в клиенте, так и в сервере. Это, своего рода, идентифицирующая клиента и сервера информация, подобно тому, как номер порта и IP-адрес узла идентифицируют программы сервер и клиент в технологии сокетов.

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

#define MY_PROG  (u_long) 0x20000001
#define MY_VER   (u_long) 1
#define MY_PROC1 (u_long) 1

Этот файл следует подключить как к клиенту, так и к серверу следующим образом:

#include "common.h"


Состав задания (файлы проекта)

Наш с Вами проект будет состоять из следующих файлов:

  • text.txt (текстовый файл, который будет считываться сервером и клиентом)
  • common.h (файл с константами – номер и версия программы)
  • server_RPC.c (исходный код сервера на языке С)
  • client_RPC.c  (исходный код клиента на языке С)
  • read_file.c (исходный код модуля, читающего файл и возвращающего его содержимое в виде строки)
  • client_RPC (исполняемый файл клиента)
  • server_RPC (исполняемый файл сервера)
  • Makefile (makefile для сборки клиента и сервера)

Файл text.txt

Содержимое файла text.txt

Этот файл имеет следующее содержимое:

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

Обратите внимание, что кодировка файла является UTF-8 без BOM (byte order marker). Мы потом поэкспериментируем с кодировками.

Подключаемый файл common.h

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

#define MY_PROG  (u_long) 0x20000001
#define MY_VER   (u_long) 1
#define MY_PROC1 (u_long) 1

Так как здесь заданы типы этих параметров (констант), то в кодах клиента и сервера нет необходимости их указывать.

Еще раз: они должны быть полностью идентичными как для клиента, так и для сервера. Это достигается подключением файла common.h к обоим этим программам.


Программный код сервера

Теперь рассмотрим программный код сервера:

  1. #include <rpc/rpc.h>
  2. #include <stdio.h>
  3. #include "common.h"
  4. // Объявляем функцию, т.к. вызов ее идет до ее описания
  5. char ** proc1 (char* indata_p);
  6. int main ()
  7. {
  8. printf ("Begin Server - Only ONE output.\n"); // А почему эта команда срабатывает только 1 раз при запуске сервера?
  9. int i;
  10. i = registerrpc (MY_PROG, MY_VER, MY_PROC1, (char*)proc1, (xdrproc_t)xdr_int, (xdrproc_t) xdr_wrapstring);
  11. svc_run();
  12. fprintf (stderr, "Error: svc_run returned \n");
  13. exit (1);
  14. }
  15. // Функция proc1()
  16. char ** proc1 (char *indata_p)
  17. {
  18. static char *res;
  19. // Выводим на экран аргумент функции proc1(),
  20. printf ("arg recieved is: %d\n", *indata_p);
  21. static char* file_name;
  22. file_name = "";
  23. if(*indata_p == 2) {
  24. file_name = "text.txt";
  25. }else{
  26. fprintf (stderr, "Error: Wrong arg\n");
  27. exit (2);
  28. }
  29. char *open_read_file(char *file_name);
  30. // Имя открываемого файла - text.txt
  31. printf ("file_name: %s\n",  file_name);
  32. static char* string;
  33. string = (char*) open_read_file( (char*) file_name);
  34. printf ("  From server_RPC:\n%s\n", string);
  35. printf ("Number of bytes: %lu\n\n", strlen(string));
  36. res = string;
  37. return &res;
  38. }
Описание программного кода сервера

Строки 1...2 описывают подключаемые файлы, содержащие определения для всех необходимых структур данных.

Строка 3 подключает файл common.h, в котором содержатся константы, задающие номер, версию программы (общие как для сервера, так и для клиента).

Строка 5 объявляет тип функции proc1. Необходимость объявления вызвана тем, что в программе эта функция используется (см. стр. 10) раньше, чем определяется (стр. 16).

В строке 10 эта функция регистрируется, при помощи registerrpc(), в качестве процедуры-сервера, которая имеет аргумент целого типа, а возвращаемый ею результат является строкой символов.

Кстати, применяя registerrpc() несколько раз, возможна регистрация требуемого количества процедур-серверов с соответствующими номерами программ и версий.

В строке 11 программа посредством обращения к функции svc_run() переводится в состояние ожидания запросов клиента. Как уже отмечалось, если эта функция выполнилась успешно, дальнейший программный код выполнен НЕ будет. Т.е. строки 12 и 13 выполнятся только в том случае, если возникнет ошибка.

Строки 12...28 содержат тело описания функции proc1(). Как видим, эта функция работает с указателями на ее аргумент и результат.

В строке 29 производится вызов функции open_read_file(), которая имеет аргументом имя файла, например, text.txt. Эту функция открывает файл, читает его выводит содержимое в консоль и, окончательно, возвращает его в виде строки.

Таким образом, при работе сервера (точнее, при каждом ответе сервера клиенту) должен наблюдаться вывод на экран (в консоль, в которой запущен сервер) примерно в следующем виде:

arg recieved is: 2
file_name: text.txt
 From open_read_file:
x����1234567ПЛ
Number of bytes: 17
 From server_RPC:
x����1234567ПЛ
Number of bytes: 17


Символы «1234567ПЛ» - это содержимое файла text.txt, который должен содержаться в том же каталоге, что и другие файлы, используемые в данном задании. Как видим, файл содержит 7 цифр и две русские буквы П и Л. Символы «x����» представляют собой символы, добавленные программой-сервером (об этом - ниже).

Т.е. содержимое текстового файла выводится дважды: из модуля open_read_file() (строки 31-32, см. ниже)  и из самой программы server_RPC (строки 33-35). Как видим, оба результата вывода - тождественны, но содержат в начале некие символы. Которые, кстати, меняются при запуске клиента.

Модуль open_read_file()

Как уже говорилось, это – модуль, читающий текстовый файл и возвращающий его содержимое в виде строки байтов (отображаемых на экране, как символы):

  1. // Функция, читающая файл (#5)
  2. #include <rpc/rpc.h>
  3. #include <stdio.h>
  4. char *open_read_file (char *file_name) {
  5. #define BS 2
  6. size_t bytes_read;
  7. char buf[BS+1], *str;
  8. ssize_t count;
  9. int i = 0;
  10. FILE *fd = fopen(file_name, "r");
  11. // Выделяем 1 байт памяти, для начала
  12. if((str = (char*)malloc(10)) == NULL){
  13. perror("Allocation error.");
  14. exit (0);
  15. }
  16. // Читаем файл до тех пор, пока не дойдем до его конца
  17. while(!feof(fd))  {
  18. i++;
  19. // Очищаем массив (подумайте, кстати, для чего?...)
  20. memset(buf, 0, sizeof(buf));
  21. //Считываем не более, чем BS байтов из файла за 1 итерацию цикла
  22. if(fgets(buf, BS, fd))
  23. // Добавляем еще BS байтов памяти перед тем, как записать считанные байты в массив str
  24. if((str = (char*)realloc(str, i*BS+1)) == NULL){
  25. perror("Allocation error.");
  26. exit (0);
  27. }
  28. strncat(str, buf, BS);
  29. }
  30. if (str == "")  {printf("Not to read from file");}
  31. printf ("  From open_read_file: \n%s\n",  str);
  32. printf ("Number of bytes: %lu\n\n",  strlen(str));
  33. fclose(fd);return str;
  34. }

Данная функция практически совпадает с этой функцией.

Обратите внимание: эта функция БЕЗ ИЗМЕНЕНИЙ будет подключаться как к серверу, так и к клиенту. Но, как увидим ниже, результаты ее работы будут разными… при открытии одного и того же файла. Так как функция open_read_file() описана в другом модуле, ее требуется подключить к файлу server_RPC.c и скомпилировать в единый исполняемый файл. Это будет реализовано средствами утилиты Makefile (см. ниже).

Запуск сервера

Прежде, чем запускать сервер, лучше бы убедиться, что на Вашем хосте (узле) запущена служба RPC. Это можно сделать при помощи команды rpcinfo

Если служба запущена, то она выведет примерно следующее:

Результаты работы функции rpcinfo

Как видим, слева направо идут: номер программы, версия, транспортный протокол, IP-адрес и сервис, работающий по этому адресу и имя владельца.

Введя команду

rpcinfo -p localhost,

получим более сокращенный перечень. Причем, при этом будут показаны порты, к которым подключились удаленные процедуры.

Можно, для проверки, проверить, подключена ли служба RPC на каком-нибудь хосте в интернете.

Например:
rpcinfo -p 4846d.ru

Вывод утилиты rpcinfo для сайта 4846d.ru

Получим примерно следующее:

Видим, что на сайте 4846d.ru запущен ряд служб. К ним можно обратиться при помощи RPC, используя номер программы и номер версии (третьим параметром, хостом при этом будет, естественно, доменное имя).

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

Вот теперь, убедившись, что служба RPC запущена, можно попробовать запустить сервер (см. предыдущий листинг). Он может не запуститься, может возникнуть ошибка такого рода:

Cannot register service: RPC: Unable to receive; errno = Success
couldn't register prog 536870913 vers 1

Это означает, что, несмотря на то, что служба RPC запущена, все-таки не запущена служба rpcbind. Запустить ее в Ubuntu можно командой:

/etc/init.d/rpcbind start

Примечание: Возможен запуск сервера в виде демона, при этом в консоли появится командная строка, но сервер будет находиться в ждущем режиме. Это делается, например, путем указания амперсанда после имени запускаемой программы (через пробел):

./server_RPC &


После чего сервер можно прервать (например, командой Ctrl + C). Это означает, что в «серверной» консоли можно будет пользоваться командной строкой. Однако, как только клиент сделает очередной запрос, сервер тут же запустится (точнее, его запустит операционная система) и, как ни в чем ни бывало, примет ответ от клиента, обработает его и вернет ответ. После чего, останется в ждущем режиме.


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

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


kill -9 51336


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

Итак, запустили сервер (на одной консоли). Далее, открываем еще одну консоль и в ней нужно будет запустить клиент.


Программный код клиента

  1. #include <rpc/rpc.h>
  2. #include <stdio.h>
  3. #include "common.h"
  4. int main (int argc, char** argv)
  5. {
  6. char *answer;
  7. int stat;
  8. if (argc < 3) exit (1);
  9. int arg;
  10. // Если нужно объявлять другие переменные, помимо int, например, массивы типа char (не знаю, как насчет других типов), следует объявлять их статическими. Иначе возникает ошибка сегментации (переполнение). Видимо, это - особенность использования службы RPC. Как это часто бывает, никакой информации об этом нет (ну, а зачем, мол). Ну, я, по крайней мере, не нашел.
  11. static char* file_name;
  12. file_name = argv[2];
  13. char *open_read_file(char *file_name);
  14. // Имя открываемого файла - второй параметр при запуске этой программы
  15. printf ("file_name: %s\n", file_name);
  16. static char* string;
  17. string = (char*) open_read_file( (char*) file_name);
  18. printf (" From client_RPC:\n%s\n", string);
  19. printf ("Number of bytes: %lu\n\n", strlen(string));
  20. arg = 0;
  21. if(strcmp (file_name, "text.txt") == 0) {arg = 2;}
  22. if (stat = callrpc (argv[1], MY_PROG, MY_VER, MY_PROC1, (xdrproc_t)xdr_int, (char*)&arg, (xdrproc_t) xdr_wrapstring, (char *)&answer) != 0) {
  23. clnt_perrno (stat);
  24. exit (2);
  25. };
  26. printf (" From server_RPC (answer):\n%s\n", answer);
  27. printf ("Number of bytes: %lu\n\n", strlen(answer));
  28. exit (0);
  29. }

Как видим, в  клиенте (равно как и в сервере – см. выше) используется функция open_read_file(). Он объявляется в строке 13, а вызов ее происходит в строке 17. Как и в сервере, подключаться она будет при помощи Makefile.

В строке 8 проверяется количество аргументов командной строки, с помощью которой программа-клиент вызывается на выполнение (аргументов не должно быть меньше двух). Здесь следует подумать – какие аргументы должны быть при вызове клиента. Дело в том, что если Вы просто запустите данную программу, не указав параметров, в виде

./client_RPC ,

то ничего не произойдет. Необходимо ввести верные аргументы. Какие именно, а также порядок их ввода следует выяснить, проанализировав приведенные выше программные коды.

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

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

Строки 26-27 выводят на экран (т.е. в стандартный поток вывода stdout) сообщение, содержащее ответ удаленной процедуры-сервера и его длину в байтах.

Вывод клиента в консоли (после очередного запуска) будет иметь примерно такой вид:

file_name: text.txt
 From open_read_file:
1234567ПЛ
Number of bytes: 11
 From client_RPC:
1234567ПЛ
Number of bytes: 11
 From server_RPC (answer):
(x����1234567ПЛ
Number of bytes: 17
Makefile

Содержимое файла Makefile

Выше уже говорилось, что как к клиенту, так и к серверу необходимо будет подключить модуль с функцией open_read_file(). Makefile для оптимизации сборки клиента и сервера может иметь следующий вид:

Примечания:
  1. Данный Makefile – неоптимизирован.
  2. Его устройство практически совпадает с Makefile, содержащимся в наших методических указаниях по операционным системам, посвященным разработке системной оболочки.
  3. Для компиляции и сборки сервера или клиента следует правильно запускать данный Makefile, указав через пробел соответствующую цель.
Анализ работы клиента и сервера

Итак, запустив сервер (на одной консоли), запускаем клиент (на другой консоли. После чего на экранах консолей возникнут результаты работы этих программ, соответственно.

Результаты работы клиента и сервера

Видим любопытную вещь. Клиент, вызывая функцию open_read_file(), читает файл text.txt как положено, выводы символы 1234567ПЛ на экран. Количество байтов «почему-то» равно 11, что превышает число символов, которое равно 7+2 = 9.

Дело в том, что в Linux используется кодировка UTF-8, а в ней кириллические (русские) буквы кодируются ДВУМЯ байтами каждая. Соответственно, буквы «ПЛ» кодируются четырьмя байтами. Тогда как для кодирования цифр используется по одному байту. Поэтому получается 7+2*2 = 11 байтов.

А, кстати, японские, арабские буквы могут кодироваться тремя и даже четырьмя байтами. Вставьте такие буквы в файл text.txt и убедитесь в этом. Учтите также, что если файл будет состоять из нескольких строк, символы их перевода также будут представлять собой байты.

Так вот, клиент, как и полагается, читает и выводит на экран ровно те самые символы (9 символов, закодированных 11 байтами в кодировке UTF-8), которые и содержатся в текстовом файле. А вот сервер, как видим, добавляет к ним в начале строки еще 6 байтов. Т.е., будучи подключенной и к клиенту, и к серверу, одна и та же функция open_read_file() ведет себя по-разному.

Причем, добавляемые дополнительно байты меняются после каждого запроса клиента. Чтобы в этом убедиться, можно в консоли вместо кодировки UTF-8 выбрать другую кодировку.

К сожалению, мне НЕ УДАЛОСЬ пока выяснить, что представляют собой эти байты. Информации об этом я не нашел. Возможно, они представляют собой некий хэш, на основе которого происходит распознавание клиентов. Правда, если это хэш – то весьма слабый – всего-то 6 байт.

Теперь попробуем изменить кодировку файла text.txt:

{UTF-8 без BOM} -> {UTF-8}
Для этого в Notepadqq делаем
Encoding -> Convert to UTF-8

Сохраняем файл. Запуская вновь клиент, видим, что количество байтов увеличилось на 3 – как в консоли клиента, так и в консоли сервера. Это означает, что добавился ВОМ (а он как раз и составляет 3 байта). Однако, как видно, в видимом содержимом файла – ни в текстовом редакторе, ни в выводе в консоль эти байты не показываются. Что вызвано, видимо, тем, что операционная система, при возврате содержимого файла, не выводит их, однако, учитывает в общем количестве байтов файла.

Заключение

В заключение следует сказать, что в данном задании приведены лишь основные механизмы RPC по взаимодействию клиента и сервера. На самом деле, такой механизм является опасным, так как обратиться к удаленному серверу может любая программа, получившая к нему доступ. В таком виде механизм реализации RPC-взаимодействия для проектов, требующих ограничения прав доступа, применяться не должен. Должны быть использованы, как минимум, аутенификация, авторизация клиентов и, возможно, шифрование передаваемых сообщений. До недавнего времени в операционной системе Windows неоднократно находили уязвимости, обусловленные этой службой. Отчасти они устранены, конечно. Одно время даже была рекомендация: отключить службу RPC на компьютере, если нет в ней особой необходимости. Так что ее использование, на мой взгляд, если и целесообразно, так лишь для неопасных операций. На сокетах - оно как-то надежнее будет. Хотя, и не очень кроссплатформенно.


Задание для самостоятельной разработки:
Схема взаимодействия сервера-RPC с несколькими (в данном случае - с двумя) клиентами
  1. Необходимо изучить коды клиента и сервера, запустить их и опробовать на практике;
  2. Напишите и зарегистрируйте на сервере (при помощи функции registerrpc()  ) еще одну функцию, например, proc2. Эта функция должна считать сумму чисел, полученных от клиента и возвращать эту сумму клиенту;
  3. Для вызова функции proc2 (например, для подсчета суммы чисел, введенных в качестве параметра, в виде одного многозначного числа, при запуске клиента) реализуйте вторую, аналогичную программу-клиент. Кроме того, следует внести соответствующие изменения в файл common.h, добавив в него еще три параметра для программы proc2. Конечно, эти три параметра нужны только для сервера (т.е. всего их для него будет шесть) и для второго клиента. Первому клиенту, если он не будет обращаться ко второй процедуре, необходимости в этих параметрах нет. Однако, чтобы обойтись единым файлом common.h, лучше указать в нем параметры для обоих процедур.

Иными словами, Ваш сервер должен быть рассчитан на обслуживание двух клиентов, в зависимости от запросов к нему.



С уважением, Салимоненко Д.А.