Proste programowanie z użyciem biblioteki rpc
eax
Programowanie z użyciem biblioteki rpc
Biblioteka RPC ma za zadanie ułatwienie tworzenia systemów komunikacji klient serwer. Przy pomocy programu rpcgen możemy wygenerować prawie kompletny kod serwera (tzn. część odpowiedzialną za komunikację w sieci) oraz znaczną część kodu klienta (tu również tylko część sieciową).
Pisanie programów należy zacząć od utworzenia pliku .x, zawierającego definicje struktur używanych w naszych programach(tych używanych w serwerze i przesyłanych siecią), oraz deklaracje funkcji serwera.
Przykładowy plik może wyglądać tak:
[liczby.x]
struct liczba{
int a;
int b;
};
program liczby{
version ONE {
liczba licz(liczba) = 1;
int mnoz(liczba) = 2;
} = 1;
} = 0x30314323;
Warto zauważyć, że pojedyńczy serwer może zawierać kilka wersji funkcji, to która ma być użyta zależy od parametrów wywołania odpowiedniej funkcji w kliencie. Poszczególne wersje funkcji są oczywiście wewnątrz różnych bloków version. Oprócz tego każda z nich ma swój numer - funkcje serwera są wywoływane przy użyciu numerów a nie nazw. Również każdy program ma swój unikalny identyfikator (tutaj 0x30314323), jest to ważne ponieważ przy używaniu rpcgen'a nie podajemy numerów portów (biblioteka RPC sama odnajdzie odpowiedni port - pod warunkiem jednak, że na porcie 111 działa portmapper).
Oprócz wymienionych danych plik .x może zawierać także stałe, tablice, stringi, także wiersze które mają być przepisane bezpośrednio do pliku wynikowego etc. Należy pamiętać, że funkcje serwera mogą zawierać tylko jeden parametr - jeżeli chcemy użyć większej liczby parametrów należy zbudować z nich strukture, a jeżeli chcemy użyć np. tablic trzeba skorzystać z typedef'ow
Wydajemy polecenie:
$rpcgen liczby.x
Powinniśmy otrzymać pliki:
liczby_clnt.c - funkcje używane w kliencie
liczby_svc.c - kod serwera
liczby_xdr.c - funkcje konwertujące typy danych (jeżeli budowaliśmy własne typy, w przeciwnym wypadku ten plik nie zostanie utworzony)
liczby.h - definicje struktur, deklaracje funkcji
Trzeba pamiętać, że dla różnych systemów te zawartość tych plików może się różnić. Szczególnie trzeba pamiętać, że może to dotyczyć nazw funkcji serwera. Przed rozpoczęciem ich pisania musimy odnaleźć w liczby.h ich deklaracje. Np.
#if defined(STDC) || defined(__cplusplus)
#define licz 1
extern liczba * licz_1(liczba *, CLIENT *);
extern liczba * licz_1_svc(liczba *, struct svc_req *);
#define mnoz 2
extern int * mnoz_1(liczba *, CLIENT *);
extern int * mnoz_1_svc(liczba *, struct svc_req *);
extern int liczby_1_freeresult (SVCXPRT *, xdrproc_t, caddr_t);
W tym przypadku interesują nas funkcje z przyrostkiem _svc. Nazwy funkcji będą zależeć od języka w którym piszemy (w "starym" C będą to pojedyńcze funkcje ale bez podanej listy parametrów), oraz od systemu operacyjnego (a dokładniej od parametrów przekazanych do rpcgen'a - od tego zależy w jakim języku będzie wygenerowany kod).
Dopiszemy potrzebne funkcje do serwera:
[liczby_svc_proc.c]
#include "liczby.h"
//jako pierwsza skladowa struktury wynikowej wstawiamy sume, jak druga
//roznice
liczba* licz_1_svc(liczba* l, struct svc_req * tmp){
static liczba wynik;
wynik.a = l->a + l->b;
wynik.b = l->a - l->b;
return &wynik;
}
//zwracamy iloczyn skladowych struktury liczba
int* mnoz_1_svc(liczba* l, struct svc_req * temp){
static int wynik;
wynik = l->a * l->b;
return &wynik;
}
Parametry przekazywane do funkcji jak i wynik to wskaźniki.
Nieco większym problemem jest napisanie brakujących części kodu klienta. Będziemy do tego potrzebować kilku funkcji i struktur:
struct CLIENT
ta struktura to "uchwyt klienta" - używany do identyfikacji połączenia
CLIENT* clnt_create(char* host, u_long prog, u_long vers, char* proto);
ta funkcja tworzy połączenie
host - nazwa komputera
prog - nazwa programu
vers - wersja programu
proto - protokół ("tcp" albo "udp")
clnt_freeres(CLIENT* clnt, xdrproc_t outproc, char* out)
makro czyszczące zasoby zaalokowane przez program (a dokładniej dane zwrócone przez serwer)
clnt - identyfikator połączenia
out - adres danych do usunięcia
outproc - informuje o powodzeniu operacji
clnt_destroy(CLIENT* clnt)
makro zwalniające pamięć zajmowaną przez clnt
oprocz tego clnt_pcreateerror() wyswietla informację o błędzie w czasie nawiązywania połączenia a clnt_perror() o błędzie podczas wywoływania zdalnej funkcji.
A oto przykładowy kod klienta:
[liczby.c]
#include <rpc/rpc.h>
#include "liczby.h"
int main(int argc, int* argv[]){
//zadeklaruj uchwyt klienta
CLIENT* cl;
liczba dana;
liczba* wynik;
dana.a = 10;
dana.b = 5;
//stworz polaczenie
cl = clnt_create("localhost", liczby, ONE, "tcp");
if (!cl){
clnt_pcreateerror("Nie mozna sie polaczyc");
exit(1);
}
//wywolaj zdalna funkcje
wynik = licz_1(&dana, cl);
if (!wynik){
clnt_perror(cl, "Blad wykonywania funkcji");
exit(1);
}
printf("a+b=%d a-b=%d\n", wynik->a, wynik->b);
//zwolnij pamiec z wynikiem
clnt_freeres(cl, (xdrproc_t) &xdr_int, (char*) wynik);
//zwolnij uchwyt klienta
clnt_destroy(cl);
return 0;
}
teraz kompilujemy całość
$cc liczby_svc.c liczby_svc_proc.c liczby_xdr.c -o server
$cc liczby.c liczby_clnt.c liczby_xdr.c -o klient
Przy używaniu złożonych typów danych realizowanych na wskaźnikach, np. list wskaźnikowych jest przesyłana zawartość pamięci na którą wskaźnik wskazuje. Jeżeli jest tam inny wskaźnik to zawartość pamięci wskazywanej przez niego również jets przesyłana itd.
Oczywiscie to tylko część możliwości biblioteki RPC. Istnieje wiele innych funkcji których tutaj nie opisałem (odsyłam do man'a). Te informacje wystarczą jednak do napisania serwera i klienta niedużej usługi.
Eax