#include <sys/types.h> #include <sys/socket.h> int socket(int domain, int type, int protocol);
Jak widać funkcja oczekuje trzech parametrów:
PF_LOCAL (PF_UNIX) - wspomniana wcześniej komunikacja w obrębie jednej maszyny PF_INET - Internet, czyli używamy protokołów z rodziny TCP/IP PF_IPX - protokoły IPX/SPX (Novell) PF_PACKET - niskopoziomowy interfejs do odbierania pakietów w tzw. surowej (ang. raw) postaci
Wszystkie dopuszczalne wartości znajdują się w pliku bits/socket.h. Zaglądajac tam spostrzeżemy, że zamiennie używa się notacji PF_xxx (ang. PF - Protocol Family) oraz AF_xxx (ang. AF - Address Family). Nie zdziwmy się więc, kiedy w jednym kodzie zobaczymy PF_INET, a w innym AF_INET - to praktycznie jedno i to samo.
Najczęściej używanymi typami są trzy pierwsze. Wszystkie wartości są zdefiniowane w pliku bits/socket.h.
Skupmy się jeszcze przez chwilę na różnicy pomiędzy tzw. komunikacją połączeniową oraz bezpołączeniową. Kiedy korzystamy z tej pierwszej mamy duży stopień pewności, że wysyłane pakiety dotrą do celu ponieważ podczas inicjacji transmisji tworzony jest tzw. wirtualny obwód (ang. virtual circuit) i odbiór każdego pakietu przez zdalny proces jest za każdym razem potwierdzany. Do obsługi takiej komunikacji w domenie PF_INET używany jest protokoł TCP. Natomiast komunikacja bezpołączeniowa nie daje nam żadnej gwarancji, że pakiet danych (nazywany w tym przypadku datagramem) nie zaginie gdzieś po drodze do celu. Tak więc wysyłamy datagram "w sieć" na ślepo w nadziei, że przy sprzyjających warunkach dotrze on do adresata. W domenie PF_INET taki sposób komunikacji obsługiwany jest przez protokół UDP. Na pierwszy rzut oka mogłoby się zdawać, że SOCK_DGRAM jest zupełnie zbędny, jako "gorszy" (niepewny, zawodny) sposób. Jednak to, który jest lepszy zależy ściśle od tego, do czego wykorzystujemy dane gniazdo. Jeśli chcemy wysyłać pojedyńcze paczki danych i zależy nam na szybkości, a nie przywiązujemy większej wagi do niezawodności to uzasadnione będzie skorzystanie z SOCK_DGRAM. Natomiast, kiedy wysyłamy/odbieramy dużo danych w obydwu kierunkach lepszym rozwiązaniem jest SOCK_STREAM.
Wszystkie dopuszczalne wartości dla domeny PF_INET znajdziemy w linux/in.h.
To był ostatni z parametrów. Funkcja socket() zwraca wartość -1 w przypadku błędu albo deskryptor gniazda, jeśli udało się stworzyć dane gniazdo. Wspomniany deskryptor gniazda spełnia analogiczną rolę, jak deskryptor pliku. Dzięki temu do obsługi gniazd możemy wykorzystywać funkcje typowe dla obsługi plików (np. write(), read(), close()).
Przykłady użycia socket():
int sockfd; sockfd=socket(PF_INET,SOCK_STREAM,0); /* tworzy gniazdo korzystające z protokołu TCP */ sockfd=socket(PF_INET, SOCK_DGRAM,0); /* gniazdo korzystające z UDP */ sockfd=socket(PF_INET,SOCK_RAW,IPPROTO_ICMP); /* surowe pakiety IP przenoszące dane protokołu ICMP */
W tym momencie stworzyliśmy własne gniazdo i tutaj właśnie scieżki się rozwidlają zależnie od tego, czy będziemy odgrywać rolę klienta, czy też serwera. Na początek zajmiemy się tą pierwszą możliwością. Obsługą procesów pełniących fukcję serwera zajmiemy się później.
Gniazda używane przez procesy klienckie nazywane są gniazdami aktywnymi ponieważ to one inicjują połączenie. Takim właśnie gniazdem dysponujemy po wywołaniu funkcji socket(). Jak łatwo się domyślić serwery korzystają z gniazd pasywnych. Gniazda pasywne biernie oczekują na połączenie ze strony klienta, a do ich stworzenia potrzebna jest jeszcze jedna czynność ze strony programisty (szczegóły przy okazji omawiania procesu-serwera).
Od teraz nic nie stoi na przeszkodzie aby sprobować połączyć się ze zdalnym procesem-serwerem. W przypadku gniazd SOCK_STREAM odpowiedzialna za to jest funkcja connect() , trochę inaczej będzie z gniazdami bezpołączeniowymi ale narazie zajmiemy się tylko pierwszym przypadkiem. Oto prototyp + nagłówki (man 2 connect);
#include <sys/types.h> #include <sys/socket.h> int connect(int sockfd, struct sockaddr *serv_addr, int addrlen);
Ponownie trzy parametry do podania: sockfd - To deskryptor gniazda, które przed chwilą stworzyliśmy. serv_addr - Wskaźnik do gniazdowej struktury adresowej opisującej proces-serwera. Ogólna postać tej struktury zdefiniowana jest w linux/socket.h i wygląda tak:
struct sockaddr
{
sa_family_t sa_family; /* domena adresowa, AF_xxx */
char sa_data[14]; /* konkretny adres */
};
Jednak zamiast niej używa się zazwyczaj struktury odpowiedniej dla
domeny adresowej, z której korzystamy. W przypadku PF_INET mamy do
dyspozycji strukturę sockaddr_in (linux/in.h) zdefiniowaną tak:
struct sockaddr\_in
{
sa_family_t sin_family; /* domena adresowa */
unsigned short int sin_port; /* numer portu */
struct in_addr sin_addr; /* adres Internetowy */
/* dopelnienie do rozmiaru 'struct sockaddr' */
unsigned char __pad[__SOCK_SIZE__ - sizeof(short int) - sizeof(unsigned short int) - sizeof(struct in_addr)];
};
Element sin_family to dokładnie to samo, co podawaliśmy, jako
parametr domain dla wywołania funkcji socket(). Znaczenie
elementu sin_port jest chyba oczywiste, natomiast bliżej
przyjrzymy się elementowi sin_addr, a dokładniej jego strukturze
(linux/in.h):
/* adres Internetowy */
struct in_addr
{
__u32 s_addr;
};
Jakimi wartościami wypełnić poszczególne pola dowiemy się już
niedługo podczas tworzenia prymitywnego klienta POP3.
addrlen - wielkość (w bajtach) struktury zawierającej adres (*servaddr)
Funkcja connect() zwraca wartość -1, kiedy połączenie nie powiodło się oraz 0,
kiedy połączenie doszlo do skutku. Jeśli otrzymaliśmy wartość 0 to od tej pory
istnieje wspomniany wirtualny obwód pomiędzy naszym klientem oraz zdalnym
serwerem. Oznacza to, że od tego momentu możemy przystąpić do przesyłania
danych między procesami.
Do tego celu możemy wykorzystać szereg funkcji. Do dyspozycji mamy: read(), write(), recv(), send(), recvfrom(), sendto(), recvmsg(), sndmsg(), readv(), writev(). Co prawda od przybytku głowa nie boli ale spróbujemy trochę ograniczyć wybór. Funkcje recvfrom() i sendto() używane są głównie z gniazdkami bezpołączeniowymi. Natomiast recvmsg(), sndmsg(), readv() oraz writev() to różne specyficzne wariacje na temat podstawowych funkcji read(), write(), recv() oraz send() i nie będziemy się nimi zajmowali - zainteresowani mogą przejrzeć strony man'a dotyczące tych funkcji. W ten sposób zostały nam tylko cztery funkcje. read()/write() zostały stworzone z myślą o plikach i chociaż nic nie stoi na przeszkodzie aby ich używać w odniesieniu do gniazd to zaleca się korzystanie z pary recv()/send() gdyż funkcje te oferują możliwości specyficzne dla operacji na gniazdach. Na placu boju pozostały teraz tylko dwie funkcje, które zaraz omówimy natomiast w dalszej części zapoznamy się jeszcze z recvfrom()/sendto() (gniazda bezpołączeniowe).
Najpierw recv() (man 2 recv):
#include <sys/types.h> #include <sys/socket.h> int recv(int s, void *buf, int len, unsigned int flags);s - Deskryptor gniazda, z którego chcemy odbierać dane (ten sam, który zwróciło wywołanie socket() i który podawaliśmy jako parametr dla connect()). buf - Wskaźnik do bufora, w którym zostaną umieszczone odebrane dane. len - Wielkość wspomnianego bufora. flags - W tym właśnie parametrze uwidacznia się przewaga funkcji recv()/send() nad read()/write(). Niektóre z możliwych wartości:
MSG_OOB - Onacza chęć odebrania tzw. danych out-of-band. Trochę więcej na ten temat przy okazji opisu funkcji send(). MSG_PEEK - Powoduje odebranie danych z początku kolejki danych gotowych do odebrania ale bez usuwania odczytanej porcji danych z kolejki. To coś w stylu preview ;). Następne wywołanie recv() odczyta więc te same dane. MSG_WAITALL - Jeśli ta flaga nie jest włączona to recv() odczytuje tyle danych z kolejki, ile jest aktualnie dostępnych ale nie więcej niż każe parametr len. Zastanówmy się, co będzie jeśli parametr len ustawiliśmy na 100 bajtów, a w kolejce do odebrania znajduje się tylko 50 bajtów. W takim przypadku funkcja recv() wypełni podany bufor tylko 50 bajtami i jeśli chcemy mieć pewność, że otrzymay całe 100 bajtów to musimy wywoływać recv() kilkukrotnie aż do skutku. Właśnie aby ułatwić rozwiązanie takiego problemu możemy skorzystać z omawianej flagi. Spowoduje ona zablokowanie wywołania recv() do czasu aż cały zadeklarowany bufor zostanie wypełniony napływającymi danymi.
Flagi te można dodawać do siebie, np. MSG_PEEK | MSG_OOB. Funkcja recv() zwraca ilość bajtów odebranych albo -1 w przypadku błędu.
Pora na send() (man 2 send):
#include <sys/types.h> #include <sys/socket.h> int send(int s, const void *msg, int len, unsigned int flags);
Wiemy już, jak utworzyć gniazdo, jak przesyłać za jego pośrednictwem dane, pozostało już tylko dowiedzieć się, jak zakończyć zainicjowane połączenie. Do tego celu można użyć dwóch funkcji:
#include <unistd.h> int close(int fd);fd - Deskryptor gniazda skojarzonego z danym połączeniem.
albo,
#include <sys/socket.h> int shutdown(int s, int how);