portal Michała Hanćkowiaka
Begin main content
Sieci komputerowe (1 cz. 2)

Sieci komputerowe — ćwiczenia 1 (część 2)


Temat zajęć: Programowanie gniazd BSD.

Literatura:
  • R. Stevens, "Programowanie zastosowań sieciowych w systemie Unix"
  • A. Jones, J. Ohlund, "Programowanie sieciowe Microsoft Windows"
  • C. Petzold, "Programowanie Windows"
  • M. Gabassi, B. Dupouy, "Przetwarzanie rozproszone w systemie Unix"
  • E. Harold, "Java: programowanie sieciowe"
  • R. Stevens, "Biblia TCP/IP" (tom 1 - Protokoły i tom 2 - Implementacje)
  • C. Hunt, "TCP/IP - administracja sieci"
  • V. Toth, "Programowanie Windows 98/NT - księga eksperta"
  • Dokumenty RFC wersja on-line

Uwaga:
Opisy większości funkcji, z których będziemy korzystać na zajęciach znajdują się w man pages systemu. Zwykle obejmuje je rozdział 2 dokumentacji, zatem aby uzyskać np. opis funkcji socket, należy wpisać w terminalu
man 2 socket

Kompilacja kodu źródłowego z przykładów:
Zakładając, że kod źródłowy znajduje się w pliku program.c, a wynikowy kod ma znaleźć się w pliku program, należy użyć polecenia
gcc -o program program.c

Uruchomienie skompilowanego programu:
./program [parametry]

 Funkcje konwertujące liczby i adresy.


Funkcje konwertujące liczby całkowite z formatu lokalnego hosta na format sieciowy:
  • long htonl(long) (host-to-network-long)
  • short htons(short) (host-to-network-short)
Funkcje konwertujące liczby całkowite z formatu sieciowego na format lokalnego hosta:
  • long ntohl(long) (network-to-host-long)
  • short ntohs(short) (network-to-host-short)
Prototypy wyżej wymienionych funkcji znajdują się w pliku nagłówkowym netinet/in.h.

Funkcja konwertująca adres IP z postaci a.b.c.d na format sieciowy:
  • in_addr_t inet_addr(const char *abcd) (prototyp w arpa/inet.h)

Funkcja konwertująca adres IP z formatu sieciowego na postać a.b.c.d:
  • char* inet_ntoa(struct in_addr *in) (prototyp w arpa/inet.h)

Struktura in_addr zdefiniowana jest w netinet/in.h jako
struct in_addr {
unsigned long int s_addr;
};


Funkcje zamieniające nazwy domenowe na adresy IP i na odwrót (prototypy w netdb.h):
  • struct hostent* gethostbyname(const char *domainname)
  • struct hostent* gethostbyaddr(const char *adres, int dlugadr, int typ)
Struktura hostent zdefiniowana jest następująco:
struct hostent {
char* h_name;
char** h_aliases;
int h_addrtype;
int h_length;
char** h_addr_list;
};

Dodatkowo dla wygody w netdb.h zdefiniowane jest makro

#define h_addr h_addr_list[0]

zatem wyrażenie zmienna->h_addr jest tożsame z zmienna->h_addr_list[0].

 Przykład 1

Przykład obrazuje różnice w reprezentacji liczb całkowitych w lokalnym formacie hosta i w formacie sieciowym.

Plik c1p1.c pobierz
#include <stdio.h>
#include <netinet/in.h>

int main(void) {
in_addr_t h, n;
unsigned char *jako_bajty;
printf("Podaj liczbe calkowita: ");
scanf("%d",&h);
printf("Liczba w formacie lokalnym jako hex: %X\n",h);
jako_bajty = (unsigned char *) &h;
printf("Liczba w formacie lokalnym jako bajty hex: %X %X %X %X\n",
jako_bajty[0],
jako_bajty[1],
jako_bajty[2],
jako_bajty[3]);
n = htonl(h);
jako_bajty = (unsigned char *) &n;
printf("Liczba w formacie sieciowym jako bajty: %X %X %X %X\n",
jako_bajty[0],
jako_bajty[1],
jako_bajty[2],
jako_bajty[3]);
printf("Liczba w formacie sieciowym jako hex: %X\n", n);
printf("co daje dziesietnie %u\n", n);
return 0;
}

 Przykład 2

Przykład pokazuje sposób wykonywania konwersji z adresu w formacie sieciowym na adres postaci a.b.c.d i na odwrót.

Plik c1p2a.c pobierz
#include <netinet/in.h>
#include <sys/socket.h>
#include <stdio.h>
#include <arpa/inet.h>

int main(void) {
char abcd[512];
in_addr_t adrsiec;
unsigned char *jako_bajty =
(unsigned char*) &adrsiec;

printf("Podaj adres IP w formacie a.b.c.d: ");
scanf("%s",abcd);
adrsiec = inet_addr(abcd);
if (adrsiec == 0xffffffff) {
printf("To nie jest prawidlowy adres IP!\n");
return 1;
}
printf("Adres w formacie sieci to %u, hex=%X\n",
adrsiec, adrsiec);
printf("Adres bajt po bajcie (hex): %X %X %X %X\n",
jako_bajty[0], jako_bajty[1],
jako_bajty[2], jako_bajty[3]);
printf("Adres bajt po bajcie (dec): %u %u %u %u\n",
jako_bajty[0], jako_bajty[1],
jako_bajty[2], jako_bajty[3]);
return 0;
}

Plik c1p2b.c pobierz
#include <stdio.h>
#include <netinet/in.h>
#include <sys/socket.h>
#include <arpa/inet.h>

int main(void) {
in_addr_t adr;
unsigned char *jako_bajty =
(unsigned char *) &adr;
char *abcd;
struct in_addr tmp;

printf("Podaj adres IP jako liczbe dziesietna w formacie sieci: ");
scanf("%u",&adr);
printf("Adres (hex): %X\n", adr);
printf("Adres jako bajty (hex): %X %X %X %X\n",
jako_bajty[0], jako_bajty[1],
jako_bajty[2], jako_bajty[3]);
printf("Adres jako bajty (dec): %u %u %u %u\n",
jako_bajty[0], jako_bajty[1],
jako_bajty[2], jako_bajty[3]);
tmp.s_addr = adr;
abcd = inet_ntoa(tmp);
printf("Adres w formacie a.b.c.d: %s\n", abcd);

return 0;
}

 Przykład 3

Przykład pokazuje sposób, w jaki można zamienić nazwę domenową na adres IP i odwrotnie. Faktyczny sposób zamiany zależy od konfiguracji systemu (zwykle najpierw sprawdzany jest plik /etc/hosts, a później odpytywany jest serwer DNS lub NIS, jednak można zmienić tą kolejność).

Plik c1p3a.c pobierz
#include <stdio.h>
#include <netinet/in.h>
#include <sys/socket.h>
#include <arpa/inet.h>
#include <netdb.h>

int main(void) {
char nazwa[512];
struct hostent *he;
struct in_addr adr;
int i;

printf("Podaj nazwe hosta: ");
scanf("%s", nazwa);
he = gethostbyname((const char*) nazwa);
if (he == NULL) {
printf("Nie ma hosta o takiej nazwie\n");
printf("lub blad serwera DNS.\n");
return 1;
}
i = 0;
while (he->h_addr_list[i]) {
adr.s_addr =
*((unsigned long*) he->h_addr_list[i]);
printf("Adres hosta: %s\n",
inet_ntoa(adr));
printf("W formacie sieci (hex): %X, (dec): %u\n",
adr.s_addr, adr.s_addr);
i++;
}
return 0;
}

Plik c1p3b.c pobierz
#include <stdio.h>
#include <string.h>
#include <netinet/in.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <arpa/inet.h>
#include <netdb.h>

int main(int argc, char **argv)
{
struct hostent *he;
struct in_addr addr;
char abcd[512];

printf("Podaj adres IP: ");
scanf("%s", abcd);
addr.s_addr = inet_addr(abcd);
he = gethostbyaddr((char*) &addr, sizeof(struct in_addr), AF_INET);
if (he == NULL) {
printf("Brak danych o %s\n", abcd);
return 1;
}
printf("Nazwa: %s\n",
he->h_name);

return 0;
}

 Przykład 4

Przykład pokazuje komunikację między dwoma programami za pośrednictwem kolejki FIFO zrealizowanej w systemie plików. Program A przykładu odczytuje z kolejki liczbę 32-bitową, która jest do kolejki zapisywana przez program B. Dodatkowo wykonywana jest konwersja między formatem lokalnym i formatem sieciowym. Kolejka tworzona jest przez program A, który powinien być uruchamiany jako pierwszy. Przesyłaną liczbą jest 999999.
Wykorzystane funkcje:
  • int mkfifo(const char *filename, mode_t mode) (sys/stat.h)
    tworzy w systemie plików kolejkę FIFO, zwraca 0 w przypadku sukcesu, -1 w przypadku błędu
  • int open(const char *filename, int flags) (fcntl.h)
    otwiera zbiór o podanej nazwie, zwraca deskryptor utożsamiany z podanym plikiem lub -1 w przypadku błędu; parametr flags określa tryb dostępu i inne własności
  • size_t read(int filedes, void *buffer, size_t nbytes) (unistd.h)
    odczytuje co najwyżej nbytes bajtów danych z pliku reprezentowanego przez deskryptor filedes, umieszczając je w buforze buffer; zwraca liczbę przeczytanych bajtów lub -1 w przypadku błędu
  • size_t write(int filedes, void *buffer, size_t nbytes) (unistd.h)
    zapisuje nbytes bajtów danych do pliku reprezentowanego przez deskryptor filedes, pobierając je z bufora buffer; zwraca liczbę zapisanych bajtów (powinna być równa nbytes) lub -1 w przypadku błędu
  • int close(int filedes) (unistd.h)
    zamyka zbiór reprezentowany przez podany deskryptor (kończy pracę z danym zbiorem), zwraca 0 w przypadku sukcesu, -1 w przypadku błędu
UWAGA:
Programy z przykładu działają poprawnie tylko w systemach plików, które umożliwiają utworzenie pliku specjalnego typu kolejka FIFO (czyli nie będą działać np. dla systemu FAT).

Plik c1p4a.c pobierz
#include <stdio.h>
#include <sys/stat.h>
#include <sys/fcntl.h>
#include <unistd.h>
#include <netinet/in.h>

char *endpoint = "./p2.fifo";

int main(int argc, char **argv)
{
int fdFifo;
long lNetworkFormat, lHostFormat;

mkfifo(endpoint, 0666);
fdFifo = open(endpoint, O_RDONLY);
read(fdFifo, &lNetworkFormat, sizeof(long));
lHostFormat = ntohl(lNetworkFormat);
printf("odczytano: %ld\n", lHostFormat);
close(fdFifo);
return 0;
}


Plik c1p4b.c pobierz
#include <stdio.h>
#include <sys/stat.h>
#include <sys/fcntl.h>
#include <netinet/in.h>
#include <unistd.h>

char *endpoint = "./p2.fifo";

int main(int argc, char **argv)
{
int fdFifo;
long lNetworkFormat, lHostFormat;

lHostFormat = 999999;
lNetworkFormat = htonl(lHostFormat);
fdFifo = open(endpoint, O_WRONLY);
write(fdFifo, &lNetworkFormat, sizeof(long));
printf("zapisano: %ld\n", lHostFormat);
close(fdFifo);
return 0;
}


 Operacje na gniazdach

Struktury danych wykorzystywane w komunikacji sieciowej realizowanej na gniazdach BSD (definicje w netinet/in.h):
  • struct sockaddr {
    unsigned short sa_family; - rodzina adresów (AF_xxx), my używamy AF_INET
    char sa_data[14]; - adres (interpretacja zależy od rodziny)
    };
    Struktura ta reprezentuje adresy, które wykorzystywane są w operacjach na gniazdach. Nie muszą to być adresy IP, zależy to od stosowanej rodziny adresów (np. AF_UNIX - tzw. Unix sockets), my jednak skupimy się na adresach rodziny AF_INET, czyli adresach IP.

  • struct sockaddr_in {
    short sin_family; - rodzina adresów (AF_INET)
    unsigned short sin_port; - numer portu
    struct in_addr sin_addr; - adres
    unsigned char sin_zero[8]; - nieużywane (należy zerować!)
    };
    Wskaźniki tej struktury można przekazywać wszędzie tam, gdzie wymagane są wskaźniki na struct sockaddr (struktura ta ułatwia operowanie na zawartości pola sa_data struktury sockaddr).

  • struct in_addr {
    unsigned long s_addr; - adres IP w formacie sieci
    };


Funkcje wykorzystywane w operacjach na gniazdach:
  • int socket(int namespace, int style, int protocol (sys/socket.h)
    Tworzy gniazdo i zwraca jego deskryptor lub -1 w przypadku błędu. Parametr namespace określa rodzinę adresów (w naszym przypadku AF_INET lub PF_INET - stałe te mają tę samą wartość). Parametr style określa rodzaj gniazda. My będziemy używać gniazd rodzaju SOCK_STREAM (protokół TCP) i SOCK_DGRAM (protokół UDP). Parametr protocol zawsze będzie miał wartość 0 - jest to domyślny protokół dla danego rodzaju gniazda.

  • int bind(int socket, struct sockaddr *addr, socklen_t length) (sys/socket.h)
    Przypisuje gniazdu socket adres addr o długości (rozmiarze struktury) length. Zwraca 0 jeśli operacja powiodła się lub -1 w przypadku błędu (np. zażądano przypisania portu, który jest już wykorzystywany przez inny proces). Najczęśćiej wykorzystywana do łączenia gniazda nasłuchującego z konkretnym numerem portu i interfejsem sieciowym hosta.

  • int listen(int socket, unsigned int backlog) (sys/socket.h)
    Umożliwia wykorzystanie podanego gniazda do nasłuchiwania w oczekiwaniu na nadchodzące połączenia (musi mu być wcześniej przypisany adres za pomocą bind). Parametr backlog określa ile maksymalnie połączeń może być skolejkowanych (w przypadku, gdy połączenie w danym momencie nawiązuje więcej niż jeden klient). Funkcja zwraca 0 (sukces) lub -1 (błąd). Funkcja wykorzystywana tylko w przypadku protokołu TCP.

  • int accept(int socket, struct sockaddr *inaddr, socklen_t *len_ptr) (sys/socket.h)
    Funkcja rozpoczyna oczekiwanie na połączenie. Jest blokująca, tzn. sterowanie opuszcza program do momentu nawiązania połączenia przez jakiś proces. W momencie nawiązania połączenia następuje powrót i funkcja zwraca nowy deskryptor gniazda, który następnie używany jest do wymiany danych z konkretnym klientem. Gniazdo takie należy później normalnie zamknąć przy użyciu close. Dodatkowo adres "rozmówcy" umieszczany jest w strukturze wskazywanej przez inaddr, a jego długość w zmiennej wskazywanej przez len_ptr. Funkcja używana tylko w przypadku protokołu TCP.

  • int recv(int socket, void *buffer, size_t nbytes, int flags) (sys/socket.h)
    Odpowiednik funkcji read dla gniazd. Powoduje odczytanie co najwyżej nbytes bajtów z połączenia reprezentowanego przez socket i umieszczenie ich w obszarze pamięci wskazywanym przez buffer. Zwraca liczbę faktycznie odczytanych bajtów lub -1 w przypadku błędu. Parametr flags ma zwykle wartość 0, a umożliwia określenie pewnych szczególnych zachowań funkcji recv. Dla przykładu, podanie MSG_PEEK jako flags powoduje sprawdzenie, czy są jakieś dane do odczytania jednak nie zostaną one faktycznie odczytane.

  • int connect(int socket, struct sockaddr *adr, socklen_t addrlen) (sys/socket.h)
    Nawiązuje połączenie przy użyciu podanego gniazda z gniazdem nasłuchującym, którego adres (host, protokół i port) określa adr. W parametrze addrlen podajemy wielkość struktury wkazywanej przez addr. Funkcja zwraca 0 jeśli udało się nawiązać połączenie lub -1 w przypadku błędu.

  • int send(int socket, void *buffer, size_t nbytes, int flags) (sys/socket.h)
    Odpowiednik funkcji write. Powoduje wysłanie nbytes bajtów z obszaru wskazywanego przez buffer poprzez gniazdo socket. Parametr flags ma zwykle wartość 0 i przydaje się jedynie w bardzo szczególnych sytuacjach (podobnie jak w przypadku recv). Funkcja zwraca liczbę faktycznie wysłanych bajtów.

Schemat transmisji

gniazdabsd_tcp.png

 Przykład 5

Bardzo prosta aplikacja klient-serwer oparta na protokole TCP. Program A nasłuchuje na konkretnym porcie (jego numer podaje użytkownik), a program B łączy się na podany adres IP i port, po czym przesyła wiadomość tekstową, która jest wyświetlana na terminalu, na którym działa program A.

Plik c1p5a.c pobierz
#include <sys/types.h>
#include <sys/socket.h>
#include <stdio.h>
#include <netinet/in.h>
#include <string.h>
#include <arpa/inet.h>
#include <unistd.h>

int main(void) {
unsigned int port;
char bufor[1024];
int gniazdo, gniazdo2;
struct sockaddr_in adr, nadawca;
socklen_t dl = sizeof(struct sockaddr_in);

printf("Na ktorym porcie mam sluchac? : ");
scanf("%u", &port);
gniazdo = socket(PF_INET, SOCK_STREAM, 0);
adr.sin_family = AF_INET;
adr.sin_port = htons(port);
adr.sin_addr.s_addr = INADDR_ANY;
if (bind(gniazdo, (struct sockaddr*) &adr,
sizeof(adr)) < 0) {
printf("Bind nie powiodl sie.\n");
return 1;
}
if (listen(gniazdo, 10) < 0) {
printf("Listen nie powiodl sie.\n");
return 1;
}
printf("Czekam na polaczenie ...\n");
while ((gniazdo2 = accept(gniazdo,
(struct sockaddr*) &nadawca,
&dl)) > 0
)
{
memset(bufor, 0, 1024);
recv(gniazdo2, bufor, 1024, 0);
printf("Wiadomosc od %s: %s\n",
inet_ntoa(nadawca.sin_addr),
bufor);
close(gniazdo2);
}
close(gniazdo);
return 0;
}

Plik c1p5b.c pobierz
#include <sys/socket.h>
#include <stdio.h>
#include <netinet/in.h>
#include <string.h> #include <arpa/inet.h> #include <unistd.h>
int main(void) {
char abcd[512];
char wiadomosc[1024];
struct sockaddr_in adr;
unsigned int port;
int gniazdo;

printf("Podaj adres IP odbiorcy: ");
scanf("%s", abcd);
printf("Podaj numer portu odbiorcy: ");
scanf("%u", &port);
gniazdo = socket(PF_INET, SOCK_STREAM, 0);
adr.sin_family = AF_INET;
adr.sin_port = htons(port);
adr.sin_addr.s_addr = inet_addr(abcd);
if (connect(gniazdo, (struct sockaddr*) &adr,
sizeof(adr)) < 0)
{
printf("Nawiazanie polaczenia nie powiodlo sie.\n");
return 1;
}
printf("Polaczenie nawiazane.\nPodaj wiadomosc: ");
fflush(stdout); fgetc(stdin);
fgets(wiadomosc, 1024, stdin);
send(gniazdo, wiadomosc, strlen(wiadomosc), 0);
printf("Wiadomosc wyslana.\n");
close(gniazdo);
return 0;
}

Zadanie 1

Zmodyfikować kod programu B z przykładu 5 tak, aby użytkownik mógł podać nazwę domenową komputera odbiorcy zamiast jego adresu IP (oczywiście nadal musi podawać numer portu).
(modify example „Przykład 5” so that user can provide a domain name instead of an IP address; of course they still have to provide a port number)

Zadanie 2

Zmodyfikować kod programów A i B z przykładu 5 tak, aby możliwe było wysłanie więcej niż jednej wiadomości podczas jednego połączenia. Dokładniej, program A powinien czekać na nawiązanie połączenia, po czym bez końca (w nieskończonej pętli) odbierać i wyświetlać wiadomości. Program B powinien nawiązać połączenie, po czym w nieskończoność prosić użytkownika o wprowadzenie wiadomości i wysyłać ją do programu A (Modify „Przykład 5” so that it can send more than one message during a single connection; program A should wait for a connection, then in infinite loop, receive and display incoming messages; program B shoud connect to program A, and, in infinite loop, prompt a user for messages to send).


uwaga: portal używa ciasteczek tylko do obsługi tzw. sesji...