Pietrzak Roman
yosheeck (at) smiech (dot) net
Copyright 2002

3 Red Ants Studio
www.3redants.com

Gniazda (ang. SOCKETs)


Wstęp


Czym jest ten dokument?


Ten dokument jest próbą zebrania wiedzy ogólnej na temat socketów (gniazd) i wprowadzenia jej w przystępnym kształcie bez zagłębiania się w szczegóły. Wiedza w nim zawarta jest tylko wstępem do zagadnień socketów. Znajdziesz tu informacje związane z:
  • podstawami TCP/IP (pakiety IP, adresowanie)
  • podstawową wiedzą o socketach (rodzaje socketów, programowanie z wykorzystaniem socketów)

Nie znajdziesz tu żadnych dokładnych specyfikacji (choć postaram się dostarczyć linki to materiałów tego typu).

Socket jest pojęciem sięgającym poza TCP/IP. Jest używany jako element systemu pośredniczący w pracy w wielu innych protokołów (np. IPX). Jednak ten dokument koncentruje się tylko na pracy socketu jako elementu TCP/IP.

Wszelkie informacje na temat TCP/IP dotyczą wersji IPv4. Wersja IPv6 nie jest tu omawiana.

Ten dokument nie może być traktowany jako kompendium wiedzy, a jedynie jako wprowadzenie do tematu.

Podstawy TCP/IP


Warstwy


Budowa współczesnych sieci zdefiniowana jest przez International Organization for Standarization (ISO), i jej model OSI. OSI oparto o budowę warstwową. Budowa warstwowa umożliwia ustandaryzowanie i uniezależnienie od siebie różnych komponentów sieciowych.

Przykład budowy warstwowej (uproszczony model w celach pokazowych):
Dane jako plik znajdują się na komputerze X w Warszawie, komputer Y z Berlina żąda tych danych.
5 - X może udostępniać te dane jako cały plik lub jako np. żądanie SQL
4 - Plik lub żądanie SQL może zostać wysłane w postaci ciągu binarnego, lub jako dane textowe.
3 - Binaria lub text mogą być wysłane przez protokół TCP\IP albo NetBEUI albo IPX albo dowolny inny protokół, który umożliwia ich przesłanie.
2 - Dane w postaci na przykład pakietów IP, podróżują z Warszawy do Berlina przez sieci lokalne, modemy, routery. W międzyczasie wędrują przez urządzenia posługujące się różnymi protokołami połączeń sieciowych (Ethernet, PPP, Token Ring, ATM) itp.,
1 - Dane wysyłane są przez różne media fizyczne: kabel miedziany, światłowód, radio, podczerwień.

W powyższym przykładzie widać, że dowolna warstwa komunikuje się bezpośrednio tylko z warstwą leżącą pod nią. Warstwa najwyższa pakuje plik lub żądanie SQL w postać textową lub binarną, i NIE ISTOTNE dla niej jest czy dane te popłyną IP czy IPX, albo czy popłyną modemem czy kartą sieciową, albo kablem miedzianym czy światłowodem. To jest idea warstw.

W modelu OSI jest 7 warstw i są zdefiniowane następująco:
7 - Application
6 - Presentation
5 - Session
4 - Transport
3 - Network
2 - Data Link
1 - Physical

W zrozumieniu idei działania socketu potrzebujemy także innego modelu warstwowego. Jest nim stos TCP/IP. Jest on w pełni zgodny z modelem OSI, jednak pewne warstwy zostały w nim pominięte.
Model warstw TCP/IP:
4 - Application (zgodny z warstwami 7 i 6 modelu OSI)
3 - Transport (zgodny z warstwami 5 i 4 modelu OSI)
2 - Internet (zgodny z warstwą 3 modelu OSI)
1 - Network Access (zgodny z warstwami 2 i 1 modelu OSI)

IP - wstęp


IP jest protokołem międzysieciowym. Pozwala na stworzenie systemu komunikacji pomiędzy połączonymi sieciami. Elementy składowe takiej sieci nazywane są podsieciami. Podsieci łączone są za pośrednictwem routerów.
Podsieci mogą się charakteryzować różną topologią warstwy 2 (Ethernet, Token Ring itp.), różnymi formatami ramki, różnym medium (warstwa 1 - kabel miedziany, światłowód, radio). Dlatego każda podsieć może stosować własny schemat adresów MAC (Medium Access Control - adres warstwy 2 modelu OSI, najczęściej "ustawiony na sztywno" w każdym urządzeniu sieciowym przez jego producenta), służących do oznaczania węzłów w ramach tej samej podsieci. Ramki z jednej podsieci nie muszą być zgodne formatem z ramkami w innej. W tej sytuacji rolę przejmuje warstwa 3 - protokół IP - zapewniający zgodność na poziomie niezależnym od implementacji sprzętowej.

IP jest protokołem warstwy 3 modelu OSI. Jest on podstawowym protokołem dla routingu pakietów w Internecie i innych sieciach TCP/IP. Zapewnia uniwersalny sposób "pakowania" informacji, umożliwiający jej wysłanie poza granice danej sieci.

Do transmisji IP używa się pakietu IP.

Pakiet IP


Pakiet jest pełną pojedynczą informacją IP. Pakiet składa się z nagłówka i danych. W nagłówku znajdują się wszelkie parametry potrzebne do ustalenia funkcji danego pakietu, między innymi:

  • Version - numer wersji protokołu,
  • Source Address - adres źródła,
  • Destination Address - adres celu,
  • TTL (Time To Live) - licznik, który zmniejsza się po każdym przejściu pakietu przez router. Gdy osiągnie zero, to pakiet ulega zniszczeniu (np. "zabłądził"),
  • Total Length - długość całkowita pakietu,
  • Fragment offset, Flags i Identification - pola związane z dzieleniem dużych partii danych oraz dużych pakietów na mniejsze

Pozostałą część pakietu zajmuje Data Field - pole przenoszące dane.
Maxymalna długość pakietu (razem z nagłówkiem) to 65535 bajtów (bo pole Total Length jest 16-bitowe).
Szczegółowy opis budowy pakietu można znaleźć w RFC 791.

Adresy IP


Komputer, router i każdy inny obiekt sieci nazywamy hostem. Każdy host ma swój numer nazywany adresem IP. Adres jest wartością 32-bitową, jednoznacznie identyfikującą komputer w danej sieci.

Dla ułatwienia adresy IP zapisujemy najczęściej jako 4 liczby oddzielone kropkami. Każda z nich reprezentuje kolejne 8-bitów adresu, a więc każda z nich ma wartość od 0 do 255. Przykłady adresów IP:
  • 194.204.159.1
  • 1.1.1.1
  • 100.100.100.100
  • 127.0.0.1

SOCKET


Historia


Gniazda powstały jako koncepcja systemowej obsługi protokołu TCP/IP. Początki gniazd i powstania ich pierwszych specyfikacji sięgają Uniwersytetu Berkeley i ich dystrybucji UNIX'a Berkeley Software Distribution (BSD). Pełen mechanizm socketów zaimplementowano w 4.2 BSD Unix w roku 1983. Później z powodzeniem zastosowano standard SOCKET w innych systemach operacyjnych (m. in. WINDOWS, LINUX).

Definicja i opis ogólny


Gniazdem (socket) nazywamy mechanizm komunikacyjny stosowany między procesami działającymi w środowiskach UNIX oraz TCP/IP. Gniazda definiują metody wymiany informacji oraz interfejs programowania pozwalający budować aplikacje klient/serwer.

Socket jest zdefiniowany jako końcówka (endpoint) dla komunikacji głównie przez protokoły TCP/IP. Jest także zgodny z innymi protokołami ( m. in. IPX/SPX) oraz komunikacją międzyprocesową, ale zaznaczono, że w niniejszej pracy się nimi nie zajmujemy. Socket z punktu widzenia protokołu IP nie wnosi nic nowego, gdyż jest on "ponad nim" (leży w wyższych warstwach).

Socket jest obiektem utrzymywanym przez system operacyjny i rezerwowanym przez program użytkowy do jednoznacznej identyfikacji połączenia TCP lub strumienia UDP, a więc definiującym dostęp aplikacji do warstwy transportowej i sesji ( Transport & Session Layer ).

Z punktu widzenia aplikacji użytkowej socket jest containerem, uchwytem. Jest elementem na którym wykonuje się wszelkie działania dotyczące transmisji danych.

Socket zdefiniowany jest przez 4 liczby:
- adres zdalny (adres IP komputera po "przeciwnej stronie"),
- port zdalny (numer portu do którego się odnosimy po "przeciwnej stronie"),
- adres lokalny,
- port lokalny,

Socket działa w strukturze klient/serwer.
Jeden z socketów jest serwerem. Nasłuchuje na określonym porcie oczekując danych.
Drugi socket to klient. Kiedy tego potrzebuje, wysyła dane (np. żądanie połączenia).

Socket jest obiektem systemowym. Program użytkowy aby użyć komunikacji sieciowej musi:
  • stworzyć socket (zarezerwować obszar pamięci pod obiekt),
  • opisać socket (opisać właściwości - czy jest to UDP albo TCP albo ICMP. Jaki jest cel i źródło danych, itp.)
  • w przypadku socketów pracujących na połączeniach (TCP) - nawiązać połączenie,

Od tej chwili program używa socketu jako obiektu strumienia do i z którego wprowadza/wyprowadza dane.

Socket, po zakończonej pracy powinien zostać skasowany. Przede wszystkim zwalnia to zasoby przydzielone mu w systemie, ale także w przypadku socketów TCP, zamyka połączenie.

Dla TCP programy po dwóch stronach połączenia mogą wysyłać i odbierać dane przez socket, a protokół TCP gwarantuje, że to co zostanie wysłane z punktu A trafi do punktu B w niezmienionej postaci (niezmieniona kolejność bloków i pełna informacja bez fragmentacji niezależnie od tego w jaki sposób dane podróżowały pomiędzy end-punktami).

Dla UDP socket rozgłaszający tylko wysyła dane pod wskazany adres (ewentualnie listę adresów - multipoint datagrams), a socket nasłuchujący (listening socket) je odbiera, jeśli do niego dotrą. Nie ma tu żadnego mechanizmu sprawdzania czy dane dotarły. Nie mniej, dane jeśli dotrą, to zostaną przez socket "poskładane" w spójną całość, niezależnie od fragmentacji podczas transportu.

Rodzaje socketów


Podział ogólny socketów przedstawia rysunek:


Podział ze względu na dostęp aplikacji do pakietów:
  • Standard socket obsługuje wszystkie typowe mechanizmy połączeń TCP i UDP i daje aplikacji gotowy engine połączeń sieciowych. Typowa aplikacja korzysta ze Standard Socket.
  • Jeśli jednak aplikacja potrzebuje dostęp bezpośredni do przesyłanych pakietów, ma możliwość operowania na Raw Socket, co daje dostęp do każdego bitu przesyłanych pakietów IP. Ze względu na zagrożenia spowodowane bezpośrednim dostępem do nagłówków IP (IP Spoofing, SYNC flood) w środowisku LINUX raw sockety są dostępne tylko dla root'a.

Podział ze względu na rodzaj transmisji:
  • TCP - (SOCK_STREAM) - zapewnia pewne, dwukierunkowe, sekwencyjne połączenie TCP/IP
  • UDP - (SOCK_DGRAM) - przesyła datagramy bez połączeń i mechanizmów kontroli
  • RAW - (SOCK_RAW) - raw socket (polskie "surowy socket") - tylko dla super-user'

API SOCKETÓW


Tworzenie socketu


Przed użyciem dowolnego socketu, należy go stworzyć, czyli zażądać od systemu by zarezerwował zasoby pod socket i przydzielił mu jednoznaczny identyfikator (handler - podobnie jak przy plikach).
Do tego celu służy funkcja socket(). Prototyp funkcji:

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


domain - domena protokołu. Dla TCP/IP należy wstawić wartość AF_INET. W pliku socket.h zdefiniowana jest pełna gama dostępnych protokołów.
type - rodzaj transmisji, wg tabeli
SOCK_STREAM Połączenie TCP
SOCK_DGRAM Rozgłaszanie UDP
SOCK_RAW Raw socket
SOCK_SEQPACKET Nie zaimplementowane dla TCP/IP
SOCK_RDM Nie zaimplementowane dla TCP/IP
protocol - określa szczegółowy protokół w rodzinie protokołów. Dla TCP/IP wstawia się tu 0

socket() zwraca identyfikator (handler) socketu, potrzebny przy późniejszym użyciu. Jeśli nastąpi błąd zwracane jest -1

Opcje socketu


Zachowanie socketu można ustalać zmieniając towarzyszące mu opcje. Służy do tego:

#include <sys/socket.h>
int setsockopt(int s, int level, int optname, char *optval, int *optlen)


Za jego pomocą ustawia się szereg opcji socketu związanych z utrzymywaniem połączeń, wielkościami buforów wejścia i wyjścia, debugowaniem itp. Szczegóły TUTAJ lub w dokumentacji.

Adresowanie


Przy tworzeniu połączeń, musimy określić adres i port hosta. W tym celu używa się struktury:
struct  sockaddr_in
{
      short    sin_family;
      u_short  sin_port;
      struct  in_addr sin_addr;
      char    sin_zero;
}


sin_family - rodzina protokołów. Dla TCP/IP podajemy AF_INET
sin_port - numer portu TCP/UDP
sin_addr - 4 bajtowy adres IP

Pole sin_addr jest typu in_add, którego definicja to:
struct in_addr
{
        union
        {
              struct { u_char s_b1, s_b2, s_b3, s_b4; };
              struct { u_short s_w1, s_w2; };
              u_long S_addr;
      } S_un;
}


Przy adresowaniu może być też przydatna funkcja

#include <sys/socket.h>
#include <netdb.h>
struct hostent *gethostbyname(char *)


która rozwiązuje nazwy DNS na adresy IP

Nawiązanie połączenia od strony klienta


Do nawiązania połączenia służy funkcja

#include <sys/socket.h>
int connect(int s, struct sockaddr *name, int namelen)


s - handler socketu
name - to pole gdzie podajemy adres (np. wygenerowany powyższą strukturą). Ze względu na różną długość adresu w różnych protokołach stosuje się tu strukturę:
struct sockaddr
{
      u_short  sa_family;        /* address family */
      char      sa_data[14];    /* actual address */
}


Wystarczy jednak wskazać strukturę sockaddr_in, która jest z sockaddr "kompatybilna"

Namelen - to długość struktury name

W przypadku powodzenia, funkcja zwraca 0
W innym przypadku zwraca -1

Połączenia przychodzące - wiązanie


Serwer nasłuchuje na ściśle określonym porcie. Przed rozpoczęciem nasłuchiwania należy powiązać (ang. bind) socket z portem na którym będzie oczekiwał na połączenie (nasłuchiwał):

#include <sys/types.h>
#include <sys/socket.h>
int bind(int s, struct sockaddr *name, int namelen)


argumenty są identyczne jak dla connect(), z tymże w sockaddr_in w polu należy podać adres z jakiego akceptowane będą zapytania. Typowo podaje się:
sin_addr.s_addr = INADDR_ANY;

Połączenia przychodzące - nasłuchiwanie


Po powiązaniu socketu z portem, wprowadza się go w stan nasłuchiwania:

int listen(int s, int backlog)

backlog - określa maxymalną liczbę oczekujących na przyjęcie połączeń w kolejce (buforze)

Połączenia przychodzące - akceptowanie połączenia


Gdy socket już nasłuchuje, wprowadzamy program w oczekiwanie na połączenie funkcją:

#include <sys/socket.h>
int accept(int s, struct sockaddr *addr, int *addrlen)


s - handler socketu
addr - ta struktura, po przyjęciu połączenia zawiera adres na hosta z którym nawiązano połączenie
addrlen - długość struktury addr

Funkcja jest funkcją blokującą, czyli po jej wywołaniu, program czeka na połączenie.

Funkcja zwraca handler do nowego socketu, który będzie teraz używany przy transmisji w zaakceptowanym połączeniu. Dzięki temu, program dalej może nasłuchiwać na poprzednim sockecie (w celu przyjęcia kolejnych połączeń).
Nie ma możliwości odrzucenia połączenia. Można jedynie zamknąć je close().

Przyjmowanie (odczyt) danych


Przy odczycie danych z socketu, można używać typowych funkcji do odczytu z plików albo funkcji dedykowanych dla socketów:
read() - identycznie jak w przypadku plików, może być używana tylko dla SOCK_STREAM
recv() - nadaje się tylko dla połączonych socketów (SOCK_STREAM) wg prototypu:

int recv(int s, char *buff, int len, int flags)

buf - bufor do którego trafią odczytane dane
len - długość bufora
flags - znaczniki specjalne (MSG_OOB lub MSG_PEEK - szczegóły w manualach)

recvfrom() - podobna do recv(), określa skąd odczytywać, dzięki czemu może być używana we wszystkich rodzajach socketów (recv() nie może odczytywać z SOCK_DGRAM) wg prototypu:
int recvfrom(int s, void *buf, size_t len, int flags, struct sockaddr *from, int *fromlen);

recvmsg() - używa struktury msghdr w celu minimalizacji argumentów podawanych wprost w wywołaniu.

Wysyłanie danych


Podobnie jak przy odczycie, także tu jest możliwość korzystania z funkcji plikowych, a także z funkcji przeznaczonych tylko dla socketów:
write() - identycznie jak w plikach, może być używana tylko dla SOCK_STREAM
send() - nadaje się tylko dla połączonych socketów (SOCK_STREAM) wg prototypu:
#include <sys/socket.h>
int send(int s, char *msg, int len, int flags)


sendto() i sendmsg() - podobnie jak w przypadku odczytu, funkcje mogące pracować na wszystkich rodzajach socketu.

Zamykanie socketu


Gdy socket ma zostać zamknięty wywołujemy funkcję

int close(int s);

Zwalnia ona zasoby przydzielone przez socket(), a także zamyka połączenia dla socketów SOCK_STREAM.

Zakończenie


Ta praca jest tylko ogólnym przedstawieniem teorii socketów. Szczegółów należy szukać w dokumentacji do socketów, funkcji UNIX, protokołów TCP/IP. Cennym źródłem wiedzy są także kody OpenSource gotowych programów.
Źródła:
RFC147 - The Definition of a Socket
RFC791 - INTERNET PROTOCOL - PROTOCOL SPECIFICATION
RFC793 - TCP
RFC768 - UDP
RFC792 - ICMP
Sockets programming
MSDN - Microsoft Development Network