Sr-02-lab-1.0
External Data Representation
Wprowadzenie
XDR (ang. eXternal Data Representation) jest standardem reprezentacji danych niezależnym od architektury sprzętu i języków programowania. Jednocześnie XDR dostarcza narzędzi do konwersji danych z lokalnego formatu do formatu niezależnego. XDR został wprowadzony przez firmę Sun Microsystems równolegle z sieciowym systemem plików NFS. Szczegółowy opis standardu znajduje się w dokumencie RFC 1014 [RFC1014].
Podstawowe zasady kodowania XDR to:
- reprezentacja liczb całkowitych jako 32-bitowe ciągi typu big-endian:
+---------+---------+---------+---------+ | MSB | | | LSB | +---------+---------+---------+---------+ 0 1 2 3
- reprezentacja liczb rzeczywistych w formacie IEEE (również 4 bajty)
- reprezentacja wszystkich innych typów zajmuje zawsze wielokrotność 4 bajtów (niektóre bajty mogą zostać wypełnione zerami)
- interpretacja ciągów bajtów pozostawiona jest nadawcy i odbiorcy wiadomości — typy nie są kodowane
Pełna dokumentacja funkcji z biblioteki XDR znajduje się na stronie pomocy systemowej xdr(3)
.
Potoki XDR
Biblioteka XDR składa się ze zbioru funkcji w języku C służących do konwersji podstawowych typów danych do i z standardu XDR. W XDR wyróżnia się następujące dwa pojęcia:
- Potok XDR
- jest to strumień danych zakodowanych zgodnie ze standardem XDR. Istnieją trzy typy potoków XDR: potoki na standardowym wejściu/wyjściu, potoki w pamięci i potoki komunikatów.
- Filtr XDR
- jest to procedura, której zadaniem jest kodowanie/dekodowanie danych określonego typu. Filtry dla podstawowych typów danych są dostarczone z biblioteką XDR. Nowe filtry można konstruować w oparciu o filtry typów prostych.
Potok na standardowym wejściu/wyjściu
Potok taki umożliwia czytanie/pisanie bezpośrednio z pliku. Przykład pokazuje kod programu zapisującego do pliku liczbę całkowitą i znak:
1: #include <stdio.h> 2: #include <rpc/xdr.h> 3: 4: int main() 5: { 6: FILE* f; 7: XDR xdrh; 8: int x = 258; 9: char c = 'a'; 10: 11: f = fopen("/tmp/xdr.test", "w"); 12: xdrstdio_create(&xdrh, f, XDR_ENCODE); 13: xdr_int(&xdrh, &x); 14: xdr_char(&xdrh, &c); 15: fclose(f); 16: }
W linii 12 tworzony jest potok XDR funkcją xdrstdio_create()
, której argumentem jest predefiniowana stała XDR_ENCODE
wskazująca na potok do kodowania (zapisu). W przypadku odczytu należy użyć stałej XDR_DECODE
. Utworzenie potoku powoduje zainicjowanie struktury XDR
, która jest uchwytem reprezentującym potok. Funkcje xdr_*
służą do kodowania/dekodowania potoku w zależności od typu potoku. Po wykonaniu programu można przeanalizować jego zawartość w celu weryfikacji zasad kodowania XDR, np. wykonując poniższą komendę:
# hexdump -C /tmp/xdr.test 00000000 00 00 01 02 00 00 00 61 |.......a|
Plik ma więc 8 bajtów. Pierwsze 4 bajty reprezentują liczbę dziesiętną o wartości 258 (1*256+2). Zapis tej samej zmiennej za pomocą funkcji fwrite()
spowodowałby inne ułożenie bajtów w pliku. Pozostałe 4 bajty pliku reprezentują pojedynczy znak, co powoduje wyzerowanie nieużywanych bajtów.
Potok w pamięci
Tworzenie potoku XDR zapisującego dane do bufora w pamięci umożliwia funkcja:
void xdrmem_create(XDR* handle, char* addr, int size, xdr_op op);
Ten rodzaj potoku ma zastosowanie przy komunikacji procesów poprzez interfejs gniazdek BSD w protokole UDP. Wiadomość przed wysłaniem może być w całości przygotowana w pamięci.
Potok komunikatów
Potok ten umożliwia buforowanie danych przekazywanych między procesami. Potoki komunikatów mają zastosowanie przy komunikowaniu procesów poprzez gniazdka TCP. Do tworzenia potoku służy funkcja o poniższym nagłówku:
void xdrrec_create(XDR* handle, int sndSize, int rcvSize, char* io, readProc, writeProc);
Argument sndSize
określa rozmiar bufora nadawczego, a rcvSize
bufora odbiorczego. Parametr io
identyfikuje mechanizm komunikacyjny (struktura FILE
, gniazdko BSD). Parametr readProc
to nazwa procedury, która jest wywoływana gdy w buforze zabraknie danych do odczytu. Analogicznie writeProc
jest wywoływana gdy brakuje miejsca w buforze nadawczym. Nagłówek funkcji do obsługi bufora wygląda następująco:
int fun(char* io, char* buf, int n);
gdzie n
wskazuje ilość bajtów do przesłania. Funkcja powinna zwrócić ilość faktycznie przesłanych danych.
Korzystanie z potoków komunikatów ułatwiają następujące funkcje:
xdrrec_endofrecord(XDR* handle, bool_t sendNow)
- wymuszenie zakończenia komunikatu i jego wysłanie jeżeli parametr
sendNow
ma wartośćTRUE
. xdrrec_skiprecord(XDR* handle)
- przejście do czytania następnego komunikatu z pominięciem pozostałej części komunikatu bieżącego.
xdrrec_eof(XDR* handle)
- sprawdzenie dostępności danych do odczytu w buforze odbiorczym.
Inne przydatne funkcje
int
xdr_getpos(XDR* handle)
- zwraca bieżącą pozycję w potoku.
bool_t xdr_setpos(XDR* handle, int pos)
- ustawienie pozycji w potoku.
Filtry XDR
Filtr jest funkcją, której zadaniem jest kodowanie i dekodowanie określonych typów danych. Istnieją filtry proste, złożone i pochodne. Filtry proste umożliwiają kodowanie typów prostych języka C, np. char
, int
, np.:
bool_t xdr_int(XDR* handle, int* value);
Drugim argumentem filtru jest zawsze wskaźnik do danej określonego typu.
Filtry złożone
służą do konwersji danych złożonych z typów prostych, a więc np. łańcuchów znaków czy tablic. Obsługiwane są następujące typy danych:
string
- łańcuch znaków
opaque
- tablica bajtów o ustalonej wielkości
bytes
- tablica bajtów o zmiennej wielkości
vector
- tablica danych dowolnego typu o ustalonej wielkości
array
- tablica danych dowolnego typu o zmiennej wielkości
union
- unia
reference
- wskaźnik
pointer
- wskaźnik rozpoznający wskaźnik pusty NULL
Filtry pochodne
są tworzone w oparciu o inne istniejące filtry. Przykładem może być filtr do kodowania struktury danych.
Zarządzanie pamięcią
Odczytując złożoną strukturę danych odbiorca może nie wiedzieć ile ona będzie zajmować miejsca w pamięci. Alokację pamięci może jednak przeprowadzić automatycznie biblioteka XDR. Oto przykład dla łańcuchów tekstowych:
XDR xdr; char *buf; ... buf = NULL; xdrstdio_create(&xdr, f, XDR_DECODE); xdr_string(&xdr, &buf, 1024); /* odczyt napisu z potoku */ ... xdr_free(xdr_string, &buf);
Użyta funkcja xdr_free()
służy do zwalniania pamięci zaalokowanej przez procedury XDR. Parametr pierwszy tej procedury to wskaźnik na odpowiedni filtr XDR.
Filtry XDR tworzone przez rpcgen
Filtry pochodne XDR mogą być tworzone automatycznie przez generator kodu rpcgen
. Wymaga to przygotowania odpowiedniej specyfikacji zbliżonej notacją do składni języka C. W poniższym przykładzie zostanie utworzony filtr do kodowania struktury DANE
:
struct DANE { int x; char c; float f[10]; };
Struktura DANE
składa się z pola x
typu int
, pojedynczego znaku c
i 10-elementowej tablicy f
przechowującej liczby zmiennoprzecinkowe. Generowanie odpowiednich filtrów realizuje polecenie rpcgen
:
# rpcgen DANE.x
co powoduje powstanie plików DANE.h
i DANE_xdr.c
zawierających odpowiednio definicje typów na poziomie języka C i implementację filtru XDR. Pliki te zostały przedstawione w przykładach i:
1: #ifndef _DANE_H_RPCGEN 2: #define _DANE_H_RPCGEN 3: 4: #include <rpc/rpc.h> 5: 6: 7: #ifdef __cplusplus 8: extern "C" { 9: #endif 10: 11: 12: struct DANE { 13: int x; 14: char c; 15: float f[10]; 16: }; 17: typedef struct DANE DANE; 18: 19: /* the xdr functions */ 20: 21: #if defined(__STDC__) || defined(__cplusplus) 22: extern bool_t xdr_DANE (XDR *, DANE*); 23: 24: #else /* K&R C */ 25: extern bool_t xdr_DANE (); 26: 27: #endif /* K&R C */ 28: 29: #ifdef __cplusplus 30: } 31: #endif 32: 33: #endif /* !_DANE_H_RPCGEN */
Język programu rpcgen
pozwala na definiowanie tablic o zmiennym rozmiarze. Następujące zapisy mogą znaleźć się w specyfikacji struktury danych:
x[10]
- 10-elementowa tablica,
x<10>
- tablica o maksymalnym rozmiarze równym 10,
x<>
- tablica o nieokreślonym rozmiarze.
Wykorzystanie tablic o dynamicznym rozmiarze powoduje wygenerowanie struktur danych języka C dla każdej dynamicznej tablicy. W przypadku wspomnianej struktury DANE
jeżeli tablica f
miałaby określony tylko maksymalny rozmiar to definicja typu w języku C wyglądałaby następująco:
struct DANE { int x; char c; struct { u_int f_len; float *f_val; } f; };
W strumieniu XDR w takim przypadku przed wypisaniem zawartości tablicy zostanie najpierw umieszczony jej rozmiar.
Nowe typy danych oraz stałe definiowane są na poziomie specyfikacji dla rpcgen
podobnie jak w języku C:
const MAX = 100; typedef int Tablica<MAX>;
Wskaźniki
Definicja typów dla programu rpcgen
pozwala na definiowanie wskaźników, które będą następnie poprawnie odtwarzane przy odczycie. Wspomniana struktura DANE
mogłaby więc być uzupełniona o wskazanie na następny element:
struct DANE { int x; DANE* next; };
Do kodowania wskaźników wykorzystywany jest filtr xdr_pointer()
.
Zadania
- Napisz program dekodujący dane zapisane w pliku z przykładu.
- Sprawdź w jaki sposób odbywa się binarne kodowanie łańcuchów znaków.
- Napisz program zapisujący do pliku i odczytujący strukturę o następujących polach: liczba typu
int
, tablica znaków o długości 5, liczba zmiennoprzecinkowa. - Napisz program, który zapisze do pliku listę rekordów przechowujących liczby typu
int
. Zdefiniuj w tym celu strukturę zawierającą wskaźnik na następny rekord. Sprawdź zachowanie biblioteki XDR w przypadku zapisu listy cyklicznej. - Napisz program klienta i serwera aplikacji udostępniającej informacje o pliku: rozmiar pliku, identyfikator właściciela, daty modyfikacji i dostępu, itp.
- Zaproponuj implementację udoskonalonej funkcji
xdr_pointer()
obsługującej dowolnie złożone (a więc również i cykliczne) dynamiczne struktury danych.