Referat z Równoległych Algorytmów Sztucznej Inteligencji
Autor: Marek Langiewicz
Wstęp
Przedstawimy tutaj mechanizm zdalnego wywoływania procedur (RPC - Remote Procedure Call) w wercji Sun RPC
(na podstawie książki "Programowanie współbieżne i rozproszone" - Zbigniew Weiss, Tadeusz Gruźlewski).
Całość przedstawimy na przykładzie prostego programu. Program składa się z klienta i serwera.
Serwer udostępnia dwie procedury zdalne a klient je wywołuje. Pierwsza procedura
cześć
pobiera jako argument napis (np. "cześć jestem jakimś klientem"
) i zwraca też napis
(odpowiedź serwera). Druga procedura dodaj
wykonuje dodawanie dwóch liczb całkowitych i zwraca wynik.
Argumenty tej procedury są przesyłane w postaci strukturki struct para {int x,y; };
ponieważ procedury
zdalne mogą mieć conajwyżej jeden argument. Żeby dokładniej zobaczyć jakie są różnice między zdalnym wywołaniem
procedur za zwykłym, najpierw przedstawimy program w wersji lokalnej. Poprostu będą dwa moduły: serwer - moduł z
implementacją procedur czesc
i dodaj
; klient - program wywołujący procedury zawarte w module
serwer.
Serwer - wersja lokalna
1
2
3
4
5
6 #include <iostream>
7 using namespace std;
8
9 struct para { int x,y; };
10
11 char *czesc(char *text) {
12 char *hostname = "lokalny_serwerek";
13 cout << "<< " << text << endl;
14 static const int o_len = 100;
15 static char odpowiedz[o_len];
16 strcpy(odpowiedz,"No witam, nazywam sie ");
17 strcpy(odpowiedz+strlen(odpowiedz),hostname);
18 cout << ">> " << odpowiedz << endl;
19 return odpowiedz;
20 }
21
22 int dodaj(para p) {
23 return p.x + p.y;
24 }
25
26
27
Widzimy tutaj że dodaj poprostu dodaje dwie liczby x i y, a czesc oprócz zwrócenia wyniku (przywitania) wypisuje na
konsoli to co dostał i to co zwraca. W linijce 12 wizimy że serwer na stałe przyjmuje nazwę "lokalny_serwerek".
W wersji RPC tu będzie prawdziwa nazwa komputera na którym został uruchomiony serwer.
Klient - wersja lokalna
28
29
30
31
32
33 #include <iostream>
34 using namespace std;
35
36 struct para { int x,y; };
37
38 char *czesc(char *text)
39 int dodaj(para p);
40
41 int main(int argc,char **argv) {
42 if(argc != 4) {
43 cerr << "Podaj nazwe komputera z ktorym mam pogadac" << endl
44 << "i dwie liczby ktore mam mu kazac dodac." << endl
45 << "(to jest wersja lokalna wiec nazwa komputera bedzie zignorowana)" << endl;
46 exit(-1);
47 }
48
49 char *hostname = "lokalny_klient";
50
51 static const int arg_len = 100;
52 char arg[arg_len];
53 strcpy(arg,"Czesc! Jestem ");
54 strcpy(arg+strlen(arg),hostname);
55
56 cout << ">> " << arg << endl;
57 char *odpowiedz = czesc(arg);
58 cout << "<< " << odpowiedz << endl;
59
60 para p;
61 p.x = atoi(argv[2]);
62 p.y = atoi(argv[3]);
63 int wynik = dodaj(p);
64 cout << "Wynik dodawania: " << wynik << endl;
65 }
66
67
68
A to jest lkient. Program pobiera jako parametry nazwę komputera (tutaj ignorowana) i dwie liczby do dodania;
potem wywołuje procedurki zdefiniowane w module serwer i wyświetla wyniki. Klient przed wywołaniem procedury
czesc
wyswietla argument ktory jej przekaze a po wywolaniu wyswietla otrzymany wynik. Na koncu
wywoluje dodaj
dla podanych liczb i wyswietla otrzymany wynik. Podobnie jak w przypadku serwera
nazwa komputera jest wpisana na stałe (linia 49). W wersji RPC to będzie prawdziwa nazwa komputera na którym
został uruchomiony klient.
Serwer - wersja RPC
69
70
71
72
73
74 #include <unistd.h>
75 #include <rpc/rpc.h>
76 #include <iostream>
77 using namespace std;
78
79 const unsigned long PROG = 0x20000000;
80 const unsigned long VERS = 1;
81 const unsigned long PROC_CZESC = 1;
82 const unsigned long PROC_DODAJ = 2;
83
84 struct para { int x,y; };
85
86 int xdr_para(XDR *xdrsp,para *p) {
87 return xdr_int(xdrsp,&p->x) && xdr_int(xdrsp,&p->y);
88 }
89
90 char *czesc(char *text) {
91 static const int hn_len = 100;
92 static char hostname[hn_len];
93 gethostname(hostname,hn_len);
94 cout << "<< " << text << endl;
95 static const int o_len = 100;
96 static char odpowiedz[o_len];
97 strcpy(odpowiedz,"No witam, nazywam sie ");
98 strcpy(odpowiedz+strlen(odpowiedz),hostname);
99 cout << ">> " << odpowiedz << endl;
100 return odpowiedz;
101 }
102
103 int dodaj(para p) {
104 return p.x + p.y;
105 }
106
107 char *dodaj_rpc(char *arg) {
108 para *p = (para*)arg;
109 static int wynik = dodaj(*p);
110 return (char*)&wynik;
111 }
112
113 int main() {
114 registerrpc(PROG,VERS,PROC_CZESC,czesc,xdr_wrapstring,xdr_wrapstring);
115 registerrpc(PROG,VERS,PROC_DODAJ,dodaj_rpc,xdr_para,xdr_int);
116 svc_run();
117 }
118
119
Najpierw popatrzmy na procedury czesc
i dodaj
. Okazuje się że niczym nie różnią się
od wersji lokalnej (oprócz tego że w 93 linii pobieramy prawdziwą nazwę komputera zamiast wymyślonej). Ponieważ
dodaj
pobiera jako argument przez nas zdefiniowaną strukturkę, musimy stworzyć pewne opakowanie
dodaj_rpc
które poprostu rzutuje typ argumentu i wyniku do char*
. Ponieważ
dodaj_rpc
zwraca wskaźnik, to napis wskazywany przez ten wskaźnik musi istnieć po zakończeniu procedury
i z tego powodu mamy static
w linii 109. Popatrzmy teraz na main. Zawiera ona dwa wywołania
'rejestrujące' nasze procedury czesc
i dodaj
oraz wywołanie svc_run()
które nigdy się nie kończy i powoduje że serwer przechodzi w stan nasłuchiwania i jest gotowy do wykonywania
zarejestrowanych procedur. Serwer musi mieć identyfikator programu i nr wersji i dodatkowo każda rejestrowana
procedura też musi mieć swój identyfiaktor. Odpowiednie stałe są zdefiniowane na początku (linie 79-82) i są
używane przy rejestrowaniu procedur (linie 114,115). Identyfikator programu musi być z zakresu
0x20000000 - 0x3fffffff i oczywiście musi być unikalny dla każdego programu - serwera uruchomionego na
danym komputerze. Jest jeszcze jedna różnica między zdalnym a lokalnym wywoływaniem procedur: filtry.
Ponieważ na serwerze dane mogłyby mieć inną reprezentację niż na kliencie, więc trzeba przeprowadzać odpowiednie
konwersje. Do tego służą filtry które trzeba ręcznie definiować dla każdego nowego typu danych. Nawet jeśli
reprezentacja dandych jst akurat taka sama to i tak musimy definiować filtry
(tak ustalono żeby uzyskać przenośny, spójny mechanizm). Każdy filtr zwraca 0 w przypadku błędu lub
liczbę różną od zera w przypadku powodzenia. Dla podstawowych typów danych mamy zdefiniowane filtry:
-
xdr_int
(dla typu int
)
-
xdr_float
(dla typu float
)
-
xdr_char
(dla typu char
)
-
xdr_wrapstring
(dla typu char *
)
W naszym programie przekazujemy zdalnie jeden nowy typ danych para
.
Odpowiedni filtr jest zdefiniowany w liniach 86-88.
Nie wchodząc za głęboko w szczegóły można powiedzieć że filtry zawsze tworzymy w sposób analogiczny jak w tym
programie. Filtr ma pobierać jeden parametr typu XDR *
i poprostu przekazywać go wywoływanym podfiltrom,
i parametr 'wskaźnik do struktury'. Filtr powinien poprostu filtry dla wszystkich pól struktury i zwrócić 0 jeśli
któryś z filtrów zwróci 0. Nie musimy się przejmować takimi rzeczami jak kierunek konwersji danych, bo to jest
zrobione automatycznie. Wystarczy że rejestrując procedurę zdalną podamy do registerrpc
odpowiedni filtr dla argumentu i wartości rejestrowanej procedury (linie 114, 115).
Klient - wersja RPC
120
121
122
123
124
125 #include <rpc/rpc.h>
126 #include <iostream>
127 using namespace std;
128
129 const unsigned long PROG = 0x20000000;
130 const unsigned long VERS = 1;
131 const unsigned long PROC_CZESC = 1;
132 const unsigned long PROC_DODAJ = 2;
133
134 struct para { int x,y; };
135
136 int xdr_para(XDR *xdrsp,para *p) {
137 return xdr_int(xdrsp,&p->x) && xdr_int(xdrsp,&p->y);
138 }
139
140 char *czesc(char *komp,char *text) {
141 static const int wynik_len = 500;
142 static char wynik[wynik_len];
143 callrpc(komp,PROG,VERS,PROC_CZESC,xdr_wrapstring,text,xdr_wrapstring,wynik);
144 return wynik;
145 }
146
147 int dodaj(char *komp,para p) {
148 int wynik = 0;
149 callrpc(komp,PROG,VERS,PROC_DODAJ,xdr_para,(char*)&p,xdr_int,(char*)&wynik);
150 return wynik;
151 }
152
153 int main(int argc,char **argv) {
154 if(argc != 4) {
155 cerr << "Podaj nazwe komputera z ktorym mam pogadac" << endl
156 << "i dwie liczby ktore mam mu kazac dodac." << endl;
157 exit(-1);
158 }
159
160 static const int hn_len = 100;
161 static char hostname[hn_len];
162 gethostname(hostname,hn_len);
163
164 static const int arg_len = 100;
165 char arg[arg_len];
166 strcpy(arg,"Czesc! Jestem ");
167 strcpy(arg+strlen(arg),hostname);
168
169 cout << ">> " << arg << endl;
170 char *odpowiedz = czesc(argv[1],arg);
171 cout << "<< " << odpowiedz << endl;
172
173 para p;
174 p.x = atoi(argv[2]);
175 p.y = atoi(argv[3]);
176 int wynik = dodaj(argv[1],p);
177 cout << "Wynik dodawania: " << wynik << endl;
178 }
179
180
Najpierw popatrzmy na main
. Od wersji lokalnej różni się to tylko tym, że pobieramy prawdziwą nazwę komputera
(linia 162) zamiast wymyślonej i procedurom czesc
i dodaj
podajemy dodatkowo nazwę komputera
na którym jest uruchomiony serwer. Wywoływane tutaj procedury czesc
i dodaj
są opakowaniami
które dopiero wywołyją zdalne wersje procedur. Popatrzmy teraz na te opakowania. czesc
zwraca wynik typu
char *
więc musimy mieć statyczny bufor na wynik żeby nie zniknął po zakończeniu procedury (linie 141,142).
Wywołanie procedury zdalnej odbywa się wywołując callrpc
i podając nazwę komputera który udostępnia żądaną
procedurę zdalną, identyfikator programu, wersję, identyfikator procedury zdalnej, filtr dla argumentu procedury,
argument, filtr dla zwracanej wartości procedury i wartość (a dokładniej wskaźnik do bufora w którym ma być umieszczona
zwrócona wartość). Druga procedurka opakowująca wywołanie zdalneg procedury dodaj
jest podobna.
Jedyna różnica jest taka że wskażniki do argumentu i zwracanej wartości muszą być zrzutowane do char *
.
Wywołanie callrpc
blokuje proces klienta do czasu aż serwer wykona żądaną procedurę i zwróci wynik.
Podsumowanie
Przykład pokazuje że przekształcenie programu tak, żeby niektóre procedury wykonywał zdalnie nie jest trudne. Oczywiście
w Sun RPC jest dużo więcej dostępnych funkcji ale reszta jest bardziej pomocnicza lub optymalizacyjna. Te pokazane
tutaj wystarczają do pisania programów RPC. W kodzie programów zostały pominięte sprawdzania błędów dla zwiększenia
przejrzystości.
Wykorzystane funkcje RPC
-
int registerrpc(unsigned long PROG, unsigned long VERS, unsigned long PROC,
char *(* proc)(), int (* xdr_arg)(), int (* xdr_res)());
Ta funkcja rejestruje nową zdalną procedurę.
-
PROG, VERS, PROC
oznaczają odpowiednio numer programu, wersji i rejestrowanej procedury
-
proc
jest wskaźnikiem do funkcji zdefiniowanej w serwerze odpowiadającej rejestrowanej
procedurze.
-
xdr_arg
i xdr_res
są wskaźnikami do filtrów odpowiednio dla parametrów wejściowych
i wynikowych rejestrowanej procedury.
- Zwracana wartość: 0 - gdy procedura została zarejestrowana; różne od 0 - w przypadku błędu.
-
void svc_run()
wywołanie tej procedury nigdy się nie kończy i powoduje że serwer przechodzi w stan
oczekiwania na żądania wywołań procedur RPC.
-
int callrpc(char *server, unsigned long PROG, unsigned long VERS, unsigned long PROC,
int (* xdr_arg)(), char *arg, int (* xdr_res)(), char *res);
Ta funkcja wywołuje żądaną procedurę zdalną na podanym komputerze. Funkcja jest blokująca, czyli
kończy się dopiero jak serwer zwróci odpowiedź (albo jak wystąpi jakiś błąd)
-
server
nazwa komputera na którym działa serwer udostępniający żądaną procedurę zdalną
-
PROG, VERS i PROC
Numer programu, wersji i wywoływanej procedury.
-
xdr_arg
i xdr_res
są wskaźnikami do filtrów odpowiednio dla parametrów wejściowych
i wynikowych wywoływanej procedury.
-
arg i res
są adresami parametru wejściowego i wynikowego
- Zwracana wartość: 0 - udane wywołanie procedury zdalnej; różne od 0 - błąd