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