next up previous contents
Next: TCP: SOCK_STREAM Up: Gniazda sieciowe - podstawy Previous: Gniazda sieciowe - podstawy   Contents

Podstawowe funkcje

Czas przejść do rzeczy. Niezależnie od tego, do jakich specyficznych zastosowań zamierzamy wykorzystać gniazda, za każdym razem będziemy zaczynali od tego samego - utworzenia naszej (klienta lub serwera) końcówki polączenia (czyli właśnie jednego z pary gniazd). Gniazdowe API (ang. Application Programmer's Interface - interfejs programisty) systemu BSD desygnuje do tego celu funkcję socket(). Spójrzmy na jej prototyp i potrzebne pliki nagłówkowe (man 2 socket):

  #include <sys/types.h>
  #include <sys/socket.h>
  
  int socket(int domain, int type, int protocol);
  

Jak widać funkcja oczekuje trzech parametrów:

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);
  
send() zwraca ilość bajtów przekazanych do warstwy transportowej albo -1 (błąd).

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);
  
Warto w tym miejscu podkreślić różnicę między wywołaniami close(s) oraz shutdown(s,2). To pierwsze spowoduje rzeczywiste zakończenie połączenia (czyli czterosegmentową sekwencję FIN/ACK) tylko jeśli licznik odniesień deskryptora gniazda jest równy 1. Licznik ten jednak może mieć większą wartość jeśli z jednego gniazda korzysta w tym samym czasie kilka procesów (np. rodzic i potomek). Z kolei wywołanie shutdown() natychmiast zamyka połączenie niezależnie od licznika odniesień.


next up previous contents
Next: TCP: SOCK_STREAM Up: Gniazda sieciowe - podstawy Previous: Gniazda sieciowe - podstawy   Contents
Paweł Niewiadomski
2000-10-17