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   *     Serwer - wersja lokalna       *
			  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   *     Klient - wersja lokalna       *
			 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   *       Serwer - wersja RPC         *
			 72   *************************************/
			 73
			 74  #include <unistd.h> // dla gethostname
			 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; //castujemy argument do poprawnego typu
			109    static int wynik = dodaj(*p); // wywolujemy wlasciwa funkcje
			110    return (char*)&wynik; // castujemy wynik do char*
			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: 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   *       Klient - wersja RPC         *
			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