Sr-01-lab-1.0: Różnice pomiędzy wersjami
Lab 01 - RPC |
(Brak różnic)
|
Aktualna wersja na dzień 19:46, 1 wrz 2006
Zdalne wywołanie procedur Sun RPC
Wprowadzenie
Mechanizm zdalnego wywołania procedur (ang. remote procedure call) umożliwia wywoływanie procedur udostępnianych przez zdalne serwery w sposób zapewniający dużą przezroczystość wywołań w stosunku do wywołań procedur lokalnych. Koncepcja wywołań zdalnych została zrealizowana w wielu różnych środowiskach programowych. W ramach ćwiczeń zapoznamy się z konkretną realizacją: Sun RPC. Sun RPC jest zestawem bibliotek programowych i narzędzi wspomagających umożliwiających tworzenie aplikacji rozproszonych w języku C.
Rysunek pokazuje warstwy oprogramowania zaangażowane w realizację zdalnego wywołania procedury. Transparentność (przezroczystość) wywołania zdalnego zapewniają pieńki (ang. stubs) po stronie klienta i serwera. Pieniek klienta dostarcza lokalnie interfejsu zdalnej procedury. Po wywołaniu procedury w aplikacji klienta następuje przygotowanie komunikatu zawierającego kod wywołania procedury z argumentami i wysłanie go do serwera. Pieniek serwera odbiera komunikat, odpakowuje argumenty i wywołuje lokalną procedurę zaimplementowaną w aplikacji serwera. Wynik wywołania procedury jest następnie pakowany do nowego komunikatu i przesyłany do klienta. Pieniek klienta odbiera komunikat i przekazuje wyniki do aplikacji. Kod pieńków jest generowany automatycznie, co umożliwia programiście skoncentrowanie się na właściwej funkcjonalności aplikacji.

Procedury Sun RPC identyfikowane są po numerach. W typowych usługach sieciowych (np. WWW czy FTP), identyfikacja usługi następuje poprzez wskazanie dwóch wartości: adresu IP serwera i numeru portu (TCP lub UDP). W przypadku usługi RPC zdalna procedura identyfikowana jest czwórką wartości: adres IP serwera, numer usługi RPC, numer wersji usługi oraz numer procedury w ramach usługi. Oczywiście w systemie rozproszonym komunikacja ostatecznie musi sprowadzić się do zastosowania protokołów warstwy transportowej, co oznacza, że aplikacja klienta musi mieć możliwość pozyskania w jakiś sposób numerów portów do komunikacji z serwerem. Funkcjonalność tą zapewnia dodatkowa usługa portmapper (implementowana jako program o nazwie portmap
), która pracuje na ogólnie znanym numerze portu (111). Zadaniem usługi jest odwzorowywanie numerów usług RPC na numery portów i nazwy protokołów transportowych poprzez które można się dostać do serwera zdalnej procedury. Wywołanie zdalnej procedury realizowane jest więc zgodnie ze schematem przedstawionym na rys.. Aplikacja klienta (a dokładnie pieniek) wysyła zapytanie do usługi portmapper na port 111 pytając o usługę RPC o konkretnym numerze i wersji (1). Uzyskuje informacje o protokołach transportowych i numerach portów, poprzez które odpowiedni serwer RPC jest osiągalny (2). Następny komunikat zawiera już właściwe zlecenie wywołania zdalnej procedury i jest kierowany do właściwej aplikacji-serwera (3). Serwer RPC po wykonaniu zdalnej procedury odsyła wyniki (4). W kolejnych dowołaniach procedur można pominąć etap odpytywania usługi portmapper, zakładając, że konfiguracja serwera RPC nie zmieniła się.

Szerszy opis RPC można znaleźć w [GD95]. Dokładna specyfikacja znajduje się w RFC 1050 [RFC1050].
Proste wywołanie zdalnej procedury
Wywołanie zdalnej procedury może być realizowane na różnych poziomach szczegółowości. Najwyższy poziom zapewnia maksimum przezroczystości i faktycznie nie różni się od wywołania lokalnej procedury. Zejście niżej ujawnia pewne szczegóły realizacji zdalnego wywołania, dając jednakże możliwość zoptymalizowania jego przebiegu. Najniższy poziom pozwala na uzyskanie niestandardowych schematów realizacji zdalnego wywołania. Kosztem jest tu jednakże konieczność zapoznania się ze szczegółami implementacyjnymi pieńków oraz ich ręczna modyfikacja.
Na najwyższym poziomie wywołanie zdalnej procedury RPC można zrealizować z wykorzystaniem funkcji:
callrpc(char* hostname, u_long prognum, u_long versnum, u_long procnum, xdrproc_t inproc, char* in, xdrproc_t outproc, char* out)
Przykładowy klient usługi rusers
został przedstawiony w przykładzie.
1: #include <stdio.h> 2: #include <rpc/rpc.h> 3: #include <rpcsvc/rusers.h> 4: 5: int main(int argc, char **argv) 6: { 7: long nusers; 8: enum clnt_stat stat; 9: 10: if (argc <= 1) 11: { 12: printf("Wywolanie: %s <serwer>\n", argv[0]); 13: exit(1); 14: } 15: 16: stat=callrpc(argv[1], RUSERSPROG, RUSERSVERS_3, RUSERSPROC_NUM, 17: xdr_void, NULL, xdr_u_long, &nusers); 18: if(stat != RPC_SUCCESS) 19: { 20: clnt_perrno(stat); 21: printf("\n"); 22: exit(1); 23: } 24: printf("W systemie %s pracuje %d użytkowników.\n", 25: argv[1], nusers); 26: }
Kompilację programu można przeprowadzić następującym wywołaniem:
# gcc -o rusers rusers.c rusers.c: In function `main': rusers.c:19: warning: passing arg 5 of `callrpc' from incompatible pointer type rusers.c:19: warning: passing arg 7 of `callrpc' from incompatible pointer type rusers.c:19: warning: passing arg 8 of `callrpc' from incompatible pointer type
Kompilacja kończy się ostrzeżeniami związanymi z niezgodnością typów danych niektórych argumentów. Dla uproszczenia zapisu przykładowego programu zrezygnowano bowiem z rzutowania argumentów do wymaganych przez funkcję callrpc()
argumentów. Powstały program rusers
można wykonać podając jako argument nazwę serwera:
# rusers localhost RPC: Program not registered
Zgłoszony błąd wynika z nieobecności serwera usługi rusers
. Każdy program RPC przed właściwym wywołaniem metody zdalnej musi bowiem dokonać odwzorowania numeru usługi RPC na numer portu w odpowiednim protokole transportowym. W powyższym przykładzie następuje odwołanie do usługi identyfikowanej jako RUSERSPROG
. W pliku /usr/include/rpcsvc/rusers.h
można znaleźć deklarację identyfikatora RUSERSPROG
wskazującą na wartość 100002. Aktualnie zarejestrowane usługi można wyświetlić komendą rpcinfo
:
# /usr/sbin/rpcinfo -p program vers proto port 100000 2 tcp 111 portmapper 100000 2 udp 111 portmapper 100007 2 udp 932 ypbind 100007 1 udp 932 ypbind 100007 2 tcp 935 ypbind 100007 1 tcp 935 ypbind
W kolumnie program
wyświetlany jest numer usługi RPC, kolumna vers
opisuje wersję danej usługi, kolumna proto
to nazwa protokołu transportowego, a kolumna port
zawiera numer portu, na którym nasłuchuje odpowiedni serwer. W przykładowym systemie dostępne są więc dwie usługi: portmapper
i ypbind
. Serwer usługi rusers
można uruchomić komendą:
# /usr/sbin/rpc.rusersd
której wykonanie powoduje uruchomienie w tle procesu o tej samej nazwie. Nowy serwer rejestruje swoją obecność w usłudze portmapper
:
# /usr/sbin/rpcinfo -p program vers proto port ... 100002 3 udp 32769 rusersd 100002 2 udp 32769 rusersd
Program rusers
wyświetla teraz poprawnie liczbę procesów:
# rusers localhost W systemie localhost pracuje 4 użytkowników.
Przykład aplikacji RPC
Poniższa kompletna aplikacja rkill
ma za zadanie umożliwić zatrzymywanie zdalnych procesów poprzez wysłanie do nich sygnału KILL.
Definicja usługi
Definicja jest zapisana w pliku rkill.x
:
program RKILL_PRG { version RKILL_VERSION_1 { int rkill(int pid) = 1; } = 1; } = 0x21000001;
W przykładzie zdefiniowano usługę RKILL_PRG
o numerze 0x21000001 (dziesiętnie: 553648129), która występuje w jednej wersji (identyfikowanej jako RKILL_VERSION_1
). Usługa definiuje jedną funkcję o następującym nagłówku:
int rkill(int pid);
Wymagany jest więc jeden argument będący identyfikatorem zatrzymywanego procesu. Funkcja zwraca status zakończenia operacji wysłania sygnału.
Generator kodu rpcgen
Na podstawie przygotowanego pliku definicyjnego można automatycznie wygenerować pliki źródłowe przykładowej aplikacji:
# rpcgen -a -N rkill.x # ls Makefile.rkill rkill.x rkill_clnt.c rkill_svc.c rkill.h rkill_client.c rkill_server.c
Powstałe pliki reprezentują odpowiednio:
rkill.h
- plik nagłówkowy z definicjami poszczególnych identyfikatorów,
rkill_client.c
- szkielet aplikacji klienckiej,
rkill_server.c
- szkielet serwera,
rkill_clnt.c
- pieniek klienta,
rkill_svc.c
- pieniek serwera,
Makefile.rkill
- plik
Makefile
zarządzający kompilacją projektu.
Kompilację najprościej jest przeprowadzić z wykorzystaniem programu make
(Zadaniem programu make
jest przeprowadzenie wszystkich niezbędnych kompilacji cząstkowych projektu w odpowiedniej kolejności z zachowaniem zależności pomiędzy plikami źródłowymi.):
# make -f Makefile.rkill cc -g -c -o rkill_clnt.o rkill_clnt.c cc -g -c -o rkill_client.o rkill_client.c cc -g -o rkill_client rkill_clnt.o rkill_client.o -lnsl cc -g -c -o rkill_svc.o rkill_svc.c cc -g -c -o rkill_server.o rkill_server.c cc -g -o rkill_server rkill_svc.o rkill_server.o -lnsl
W wyniku kompilacji powinny powstać m.in. dwa programy: rkill_client
i rkill_server
. Pozwala to na testowe uruchomienie serwera i klienta (np. w różnych oknach środowiska graficznego). Domyślna implementacja serwera nie powoduje jednak wykonania żadnego przetwarzania, stąd trudno zauważyć efekty pracy programu. Obserwację działania serwera umożliwi modyfikacja kodu serwera zawarta w pliku rkill_server.c
:
1: #include "rkill.h" 2: 3: int * 4: rkill_1_svc(int pid, struct svc_req *rqstp) 5: { 6: static int result; 7: 8: printf("Zdalna procedura\n"); 9: 10: return &result; 11: }
Dodanie linii 8 powoduje wyświetlanie komunikatu na ekranie przy każdorazowym wywołaniu zdalnej procedury. Dalsza modyfikacja kodu serwera umożliwia realizację powierzonego mu zadania:
result = kill(pid, 9);
Linia ta powinna zostać dopisana na pozycji 9. Argument pid
będzie dostarczony przez bibliotekę RPC, a wartość zwrotna funkcji kill()
powinna zostać zapisana do statycznej zmiennej result
, w celu późniejszego przesłania jej do klienta.
Implementacja kodu klienta wymaga wprowadzenia większej ilości zmian. Przykład przedstawia zmodyfikowaną wersję klienta:
1: #include "rkill.h" 2: 3: 4: int 5: rkill_prg_1(char *host, int pid) 6: { 7: CLIENT *clnt; 8: int *result_1; 9: int rkill_1_pid; 10: 11: #ifndef DEBUG 12: clnt = clnt_create (host, RKILL_PRG, RKILL_VERSION_1, "udp"); 13: if (clnt == NULL) { 14: clnt_pcreateerror (host); 15: exit (1); 16: } 17: #endif /* DEBUG */ 18: 19: result_1 = rkill_1(pid, clnt); 20: if (result_1 == (int *) NULL) { 21: clnt_perror (clnt, "call failed"); 22: } 23: #ifndef DEBUG 24: clnt_destroy (clnt); 25: #endif /* DEBUG */ 26: } 27: 28: 29: int 30: main (int argc, char *argv[]) 31: { 32: char *host; 33: 34: if (argc < 3) { 35: printf ("usage: %s server_host\n", argv[0]); 36: exit (1); 37: } 38: host = argv[1]; 39: rkill_prg_1 (host, atoi(argv[2])); 40: exit (0); 41: }
W linii 39 dodano odwołanie do drugiego argumentu wywołania programu wraz z konwersją do liczy dziesiętnej (funkcja atoi()
). Dodatkowy argument funkcji rkill_prg_1()
pojawił się również w nagłówku tej funkcji (linia 5). Identyfikator procesu przekazywany jest dalej do pieńka klienta w linii 19. Po każdorazowej zmianie kodu należy oczywiście wykonać ponownie komendę make
. W obecnej postaci możliwe jest więc zatrzymanie dowolnego procesu na komputerze z uruchomionym serwerem rkill
:
# rkill_client unixlab 1845
gdzie unixlab
jest przykładową nazwą docelowego serwera.
Dalsze modyfikacje kodu aplikacji klienta powinny umożliwić uzyskanie informacji o przebiegu wykonania wysłania sygnału. Należy w tym celu odwołać się do wartości zwracanej przez funkcję rkill_1
będącą implementacją pieńka klienta. Wartość ta jest wskaźnikiem na liczbę, która przechowuje zwróconą przez serwer liczbę.
Dodatkowy argument funkcji rkill()
Jeżeli w wyniku zmiany specyfikacji wymagań należy dodać nowy argument wywołania funkcji rkill()
, to możliwe są dwa rozwiązania: ręczne korygowanie odpowiednich pieńków i kodów klienta/serwera (niezalecane), lub ponowne wygenerowanie kodu aplikacji (zalecane). Zmodyfikowane wcześniej fragmenty kodu trzeba jednak w drugim przypadku ręcznie wprowadzić do nowej wersji aplikacji. Jako ćwiczenie należy więc zaimplementować usługę o następującym interfejsie:
program RKILL_PRG { version RKILL_VERSION_1 { int rkill(int pid, int sig) = 1; } = 1; } = 0x21000001;
Protokół transportowy
Kod programu klienta z przykładu zawiera w linii 18 wywołanie funkcji clnt_create()
tworzącej uchwyt komunikacyjny dla warstwy RPC. Parametrem tej funkcji jest nazwa protokołu transportowego. Zmiana domyślnego protokołu UDP na TCP nie spowoduje zmiany funkcjonalnej aplikacji, co pokazuje niezależność mechanizmu RPC od warstwy transportowej.
Plik Makefile
Nazwę standardowo generowanego przez rpcgen
pliku Makefile.rkill
warto zmienić na Makefile
aby nie używać przełącznika -f
komendy make
. Zawartość tego pliku również warto zmienić, ponieważ znajduje się tam wywołanie komendy rpcgen
powodujące ponowne tworzenie kodu źródłowego po stwierdzeniu modyfikacji specyfikacji w pliku rkill.x
. Należy w tym celu usunąć następujące 2 linie:
$(TARGETS) : $(SOURCES.x) rpcgen $(RPCGENFLAGS) $(SOURCES.x)
Drugą zalecaną modyfikacją pliku Makefile
jest modyfikacja kodu dla operacji clean
. Standardowo powoduje ona usunięcie kodu źródłowego w języku C! Należy usunąć odwołanie do zmiennej TARGETS
, uzyskując następujące linie:
clean: $(RM) core $(OBJECTS_CLNT) $(OBJECTS_SVC) $(CLIENT) $(SERVER)
Ostatnia zmiana ma na celu poprawę jakości tworzonego kodu. Zmienna CFLAGS
ustawiana w pliku Makefile
umożliwia przesłanie dodatkowych opcji dla kompilatora. Warto włączyć w tym miejscu wypisywanie wszystkich ostrzeżeń:
CFLAGS += -g -Wall
i doprowadzić kod aplikacji do stanu, w którym kompilacja będzie przebiegała bez żadnych ostrzeżeń.
Zadania
- Przetestuj działanie klienta usługi
rusers
z przykładu. - Stwórz aplikację
rkill
korzystając z generatora kodurpcgen
. Aplikacja klienta powinna być wywoływana z 2 argumentami: nazwą komputera docelowego i identyfikatorem procesu. Serwer powinien wysyłać sygnał SIGINT do wskazanego procesu i zwracać wynik z funkcjikill()
. Wynik wykonania zdalnej procedury powinien zostać przedstawiony po stronie klienta. - Stwórz rozszerzoną aplikację
rkill
udostępniającą dwuargumentową funkcjęrkill()
umożliwiającą wysłanie dowolnego sygnału do dowolnego procesu. - Przetestuj działanie aplikacji dla protokołów transportowych UDP i TCP.