Personal tools

Sr-02-lab-1.0

From Studia Informatyczne

Spis treści

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

  1. Napisz program dekodujący dane zapisane w pliku z przykładu.
  2. Sprawdź w jaki sposób odbywa się binarne kodowanie łańcuchów znaków.
  3. 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.
  4. 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.
  5. Napisz program klienta i serwera aplikacji udostępniającej informacje o pliku: rozmiar pliku, identyfikator właściciela, daty modyfikacji i dostępu, itp.
  6. Zaproponuj implementację udoskonalonej funkcji xdr_pointer() obsługującej dowolnie złożone (a więc również i cykliczne) dynamiczne struktury danych.