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
        
        
        
         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).