Socket je obecný mechanizmus pro síťovou komunikaci, původně pomocí rodiny protokolů rodiny TCP/IP, nezávisle na implementaci protokolů, poprvé použitý v systémech BSD UNIX. Socket (z angličtiny zásuvka, objímka, hrdlo trubky) je programové komunikační rozhraní uzlu pro příjem a odesílání dat po síti (datagramů, paketů i rámců). Socket obsahuje informace potřebné pro přenos dat a datové spojení (protokol, adresy atd.). Programové rozhraní Socket API z BSD UNIX je stejné pro všechny unixové operační systémy (a tedy i GNU/Linux) a velice podobné na systémech MS Windows, které jej převzaly.
V unixových systémech je Socket API přímo součástí C knihovny. Pro
použití funkcí API je nutné vkládat různé hlavičkové soubory. U každé
funkce jsou potřebné hlavičkové soubory uvedeny. Číselné chybové kódy
funkcí se ukládají do globální proměnné errno
nebo h_errno
. Pro
podrobnosti k funkcím viz manuálové stránky, které jsou uvedeny u
každé funkce.
V MS Windows jsou funkce pro práci se sockety implementovány v
knihovně Windows Socket API (Winsock API). Winsock API je velice
podobné původnímu Socket API z BSD UNIX, ale některé funkce a
struktury se mírně odlišují a API bylo rozšířeno o vlastnosti
událostmi řízeného prostředí. Podstatné rozdíly budou zmíněny,
drobnější (např. typy parametrů a položek struktur) viz MSDN. V
novějších verzích 2.* jsou zavedeny také nové funkce WSA*, které se
dají použít místo původních (které jsou označeny jako "staré"), ale z
důvodu minimální odlišnosti (téměř shodnosti) se Socket API budeme
používat starší verzi Winsock API 1.1. Verze 2.* jsou s verzí 1.1
zpětně kompatibilní. Knihovnu Winsock je před používáním socketů nutné
nejdříve inicializovat pomocí funkce WSAStartup. Bez inicializace knihovny nebudou funkce pro
sockety fungovat. Všechny funkce jsou deklarovány v hlavičkovém
souboru winsock.h
(popř. winsock2.h
), který je vkládán z hlavičkového
souboru windows.h
Win32 API (winsock2.h
ovšem ne), a implementovány v
knihovním souboru wsock32.dll
(popř. ws2_32.dll
), který je nutné
předat linkeru (skrze wsock32.lib
, popř. ws2_32.lib
). Pro podrobnosti
k funkcím viz MSDN.
int WSAStartup(WORD wVersionRequested, LPWSADATA lpWSAData);
|
|
Po ukončení práce se sockety v MS Windows by se měla zavolat funkce WASCleanup. Kód chyby při selhání nějaké funkce lze získat funkcí WSAGetLastError, pro návratový kód -1 funkcí API existují makra SOCKET_ERROR a INVALID_SOCKET.
Před příjmem nebo odesíláním dat po síti je potřeba vytvořit/otevřít rozhraní - socket. Otevření socketu je analogické otevření souboru, socket se chová jako otevřený soubor (ve Winsock 2), je tedy identifikovaný souborovým deskriptorem nebo handlem (file descriptor, socket/file handle).
socket(2) |
#include <sys/types.h> #include <sys/socket.h>
|
int socket(int domain, int type, int protocol);
SOCKET socket(int af, int type, int protocol);
|
|
Pro zrušení socketu lze na unixových systémech použít funkci close pro uzavření souboru, na MS Windows je nutné použít funkci closesocket (close socket nezruší!).
Různá čísla (protokolů, adres atd.) přenášená sítí jsou funkcím v Socket API předávána a funkcemi vracena v tzv. síťovém tvaru (network byte order), tvaru pro přenos sítí. Toto je binární tvar s pořadím bytů big endian, tj. v pořadí od nevýznamějších bytů k méně významným. Lokální uložení čísel např. na architektuře i386 je ale opačné, little endian. Pro převod čísel mezi lokálním tvarem a síťovým tvarem slouží funkce htonl, htons, ntohl a ntohs.
byteorder(3) |
#include <arpa/inet.h> |
uint32_t htonl(uint32_t hostlong);
uint16_t htons(uint16_t hostshort);
uint32_t ntohl(uint32_t netlong);
uint16_t ntohs(uint16_t netshort);
u_long htonl(u_long hostlong);
u_short htons(u_short hostshort);
u_long ntohl(u_long netlong);
u_short ntohs(u_short netshort);
|
|
Nyní můžeme skrze vytvořený socket vysílat nebo přijímat data do/ze sítě. Pro sockety typu SOCK_RAW a SOCK_DGRAM existují k tomuto účelu speciální funkce identifikující příjemce/odesílatele.
send(2) |
#include <sys/types.h> #include <sys/socket.h>
|
ssize_t sendto(int s, const void *buf, size_t len, int flags, const struct sockaddr *to, socklen_t tolen);
int sendto(SOCKET s, const char *buf, int len, int flags, const struct
sockaddr *to, int tolen);
|
|
recv(2) |
#include <sys/types.h> #include <sys/socket.h>
|
ssize_t recvfrom(int s, void *buf, size_t len, int flags, struct sockaddr *from, socklen_t *fromlen);
int recvfrom(SOCKET s, char *buf, int len, int flags, struct sockaddr *from, int *fromlen);
|
|
Pro na hardwaru nezávislé uložení adresy se používá struktura sockaddr. V každé doméně (rodině protokolů, resp. adres) se ale používá jiný typ adres a pro uložení každého typu adresy se používá jiná struktura. Pro uložení adresy v doméně PF_INET je to struktura sockaddr_in, v doméně PF_PACKET struktura sockaddr_ll a v doméně PF_UNIX struktura sockaddr_un. Datový typ sockaddr vlastně není struktura, ale union, který může nést libovolnou z výše uvedených struktur, proto se ve funkcích používajících typ ukazatele na sockaddr používá typ ukazatele na některou tuto strukturu přetypovaný na ukazatel na sockaddr.
Pro odesílání a příjem linkových rámců se používají sockety z domény PF_PACKET, tzv. paketové (packet) sockety. Zde můžeme vytvořit jen dva typy socketů, SOCK_DGRAM nebo SOCK_RAW. Při použití socketu typu SOCK_RAW odesíláme nebo příjímáme celé linkové rámce včetně záhlaví a zápatí, naopak při použití socketu typu SOCK_DGRAM pracujeme pouze s daty linkového rámce (části paketů síťové vrtsvy).
Linkových protokolů, které můžeme použít je spousta, ale omezíme se Ethernet, tj. makro ETH_P_ALL. Dále se můžeme omezovat na ethernetové rámce nesoucí určitý síťový paket, např. ETH_P_IP, ETH_P_ARP, a jiné (makra pro čísla dle normy IEEE 802.3 jsou definována v linux/if_ether.h). Čísla protokolů jsou dvoubajtová, proto je nutné použít funkci htons pro převod z lokálního tvaru do síťového. U paketových socketů typu SOCK_DGRAM není při příjmu odstraněna a při odesílání doplňována IEEE 802.2 LLC hlavička (např. pro IEEE 802.3 ethernetové rámce, protokol ETH_P_802_3).
Při příjmu rámců jsou všechny rámce zvoleného protokolu vloženy do socketu a pak teprve dále zpracovány systémem. Paketové sockety může otevřít pouze proces superuživatele (root) nebo proces se speciálními právy (capability CAP_NET_RAW).
Pro doménu PF_PACKET se používá pro linkovou adresu struktura sockaddr_ll. Vybrané položky:
packet(7) |
#include <sys/socket.h> #include <netpacket/packet.h> #include <net/ethernet.h>
|
struct sockaddr_ll;
|
|
Pro odeslání rámce je potřeba vyplnit pole sll_family, sll_addr, sll_halen a sll_ifindex, ostatní by měla být nulová. U přijatých rámců je struktura sockaddr_ll vyplněna přijímací funkcí.
Linková adresa je ve struktuře sockaddr_ll (a v datech rámce) uložena v (binárním) síťovém tvaru. Pro převod na řetězec pro MAC šesti šestnáctkových čísel z rozsahu 0 až FF oddělených dvojtečkami (dvojtečkový tvar) a obráceně slouží funkce ether_ntoa a ether_aton.
ether_aton(3) |
#include <netinet/ether.h> |
char * ether_ntoa(const struct ether_addr *addr);
struct ether_addr *ether_aton(const char *asc);
|
|
S daty z hlavičky ethernetového rámce můžeme pracovat pomocí struktury ether_header:
#include <net/ethernet.h> |
struct ether_header;
|
|
Př. Napište program pro příjem všech ethernetových rámců vypisující délku rámce, linkový protokol (druh ethernetu) a všechny informace ze záhlaví rámce. Adresy odesílatele a příjemce vypisujte v dvojtečkovém tvaru.
Síťové pakety lze odesílat a přijímat pomocí paketových socketů (z domény PF_PACKET) typu SOCK_DGRAM, kdy pracujeme s celými pakety včetně hlavičky. Paketové sockety jsou ale dostupné jen na unixových systémech. Proto si ukážeme práci se síťovými pakety pomocí (obyčejných) socketů z rodiny PF_INET, které jsou dostupné i ve Winsock API. Rodina PF_INET pracuje se síťovými pakety protokolu IP verze 4.
Podobně jako linkové rámce včetně záhlaví můžeme odesílat a přijímat pomocí socketu typu SOCK_RAW z rodiny PF_PACKET, pro odesílání a příjem IP paketů včetně záhlaví se používají také sockety typu SOCK_RAW, tentokrát z rodiny PF_INET. Tyto tzv. syrové (raw) sockety zpřístupňují hlavičky IP paketu i transportního segmentu, popř. linkového "podprotokolu", např. ARP, nebo IP "podprotokolu", např. služebního ICMP. Níže (dle vrstvového modelu k linkovým rámcům) se se sockety z rodiny PF_INET dostat nelze. Stejně jako u paketových socketů při příjmu paketů jsou všechny pakety zvoleného protokolu vloženy do raw socketu a pak teprve dále zpracovány systémem a raw sockety může otevřít pouze proces superuživatele (root nebo Administrátor) nebo proces se speciálními právy (capability CAP_NET_RAW).
Pro odeslání paketu skrze raw socket s vyplněním hlavičky paketu lze jako protokol (ve funkci socket) použít makro IPPROTO_RAW. Hodnoty (pseudo)protokolů rodiny PF_INET jsou jednobytové, není tedy nutné používat konverzní funkce hton* do síťového tvaru. U Winsock API je jestě nutné zapnout volbu socketu IP_HDRINCL (viz dále, na unixových systémech je volba zapnuta automaticky), a navíc lze makro IPPROTO_RAW použít až s Winsock API verzí 2.*.
Pro doménu PF_INET se používá pro IP adresu (spolu s portem transportního protokolu) struktura sockaddr_in. Položky:
ip(7) |
#include <sys/socket.h> #include <netinet/ip.h>
|
struct sockaddr_in;
|
|
Pro odeslání paketu je potřeba vyplnit všechny, u přijatých paketů je struktura sockaddr_in vyplněna přijímací funkcí.
IP adresa je ve struktuře sockaddr_in (a v datech paketu) uložena v (binárním) síťovém tvaru. Pro převod na řetězec čtyř desítkových čísel z rozsahu 0 až 255 oddělených tečkami (tečkový tvar) a obráceně slouží funkce inet_ntoa a inet_aton.
inet(3) |
#include <sys/socket.h> #include <netinet/in.h> #include <arpa/inet.h>
|
char *inet_ntoa(struct in_addr in);
int inet_aton(const char *cp, struct in_addr *inp);
|
|
Pro zpřístupnění povinných položek hlavičky IP paketu lze použít strukturu iphdr:
#include <netinet/ip.h> |
struct iphdr;
|
|
Při odesílání paketu skrze raw socket s pseudoprotokolem IPPROTO_RAW musí být součástí odesílaných dat IP hlavička s vyplněnými (téměř) všemi položkami. Pro výpočet kontrolního součtu vyplněného záhlaví můžeme použít funkci v RFC 1071, pole pro kontrolní součet v záhlaví, ze kterého se součet teprve počítá, je rovno 0. Skutečně odeslaná hlavička nemusí být stejná jako vyplněná, na unixových systémech jsou automaticky vyplňovány kontrolní součet a celková délka, a při nulových hodnotách také identifikátor (při nenulovém hrozí kolize s jiným paketem!) a IP adresa odesílatele. Za IP adresu příjemce se bere ta ze struktury sockaddr_in předaná funkci sendto. Pakety větší než MTU linky nejsou systémem automaticky fragmentovány a jejich velikost je tak omezena na MTU linky.
Pseudoprotokol IPPROTO_RAW ovšem nelze použít pro příjem paketů včetně hlavičky. K tomuto účelu lze použít protokol IPPROTO_IP a volbu socketu IP_HDRINCL (viz dále). Pak je součástí přijímaných dat i IP hlavička. Přijímané IP pakety jsou automaticky systémem defragmentovány, pro příjem jednotlivých fragmentů je nutné použít paketový socket (např. s protokolem ETH_P_IP). Protokol IPPROTO_IP s volbou IP_HDRINCL lze použít i pro odesílání paketů (součástí dat musí být vyplněná IP hlavička).
Volby socketu
Parametry socketu a přenášených protokolů (zejména položky hlaviček) lze upravovat pomocí voleb socketu (socket options). K získání a nastavování hodnot voleb slouží dvojice funkcí getsockopt a setsockopt.
getsockopt(2) |
#include <sys/types.h> #include <sys/socket.h>
|
int getsockopt(int s, int level, int optname, void *optval, socklen_t *optlen);
int getsockopt(SOCKET s, int level, int optname, char* optval, int* optlen);
int setsockopt(int s, int level, int optname, const void *optval, socklen_t optlen);
int setsockopt(SOCKET s, int level, int optname, const char* optval, int optlen);
|
|
Voleb socketu existuje mnoho. Na úrovni protokolů volby ovlivňují každý odesílaný nebo přijímaný paket (IP) nebo segment/datagram (TCP/UDP) a kromě voleb pro parametry protokolu existují i volby odpovídající některým položkám záhlaví protokolu. Např. pro protokol IP, tj. úroveň IPPROTO_IP, jsou volby pro přítomnost IP záhlaví před daty (IP_HDRINCL, pouze u raw socketů, viz výše), u odchozích paketů pro typ služby (IP_TOS), dobu živostnosti paketu (IP_TTL), volitelné položky IP záhlaví (IP_OPTIONS), dále zjištění a nastavení MTU linky (IP_MTU_DISCOVER), aktuální MTU linky (IP_MTU, u socketů spojení), pro skupinové adresování (multicast, IP_ADD_MEMBERSHIP, IP_DROP_MEMBERSHIP, IP_MULTICAST_TTL, IP_MULTICAST_LOOP) a další.
Na úrovni socketu jsou dostupné volby pro obecné parametry socketu pro odesílání, příjem či spojení, např. max. velikosti vstupního (SO_RCVBUF) a výstupního (SO_SNDBUF) bufferu, možnost odesílat/přijímat na/z všesměrové adresy (SO_BROADCAST), priorita odesílaných dat (SO_PRIORITY), časový interval pro odeslání (SO_SNDTIMEO) a příjem (SO_RCVTIMEO) dat před ukončením blokování odesílací nebo přijímací funkce s chybou a další.
Př. Modifikujte (nebo rozšiřte) předchozí program tak, aby přijímal všechny IP pakety a vypisoval všechny informace ze záhlaví paketu. Fragmentační údaje vypisujte pohromadě a IP adresy odesílatele a příjemce v tečkovém tvaru.
Protokol ICMP
Pro odesílání a příjem ICMP paketů lze použít makro IPPROTO_ICMP jako protokol funkce socket při vytváření raw socketu. Při odesílání pak pracujeme pouze s ICMP hlavičkou a daty, IP hlavičku vyplňuje OS. Některé položky IP hlavičky nicméně můžeme měnit pomocí voleb socketu. Přijímaná data ovšem budou obsahovat jak ICMP, tak IP hlavičku! Pro práci s ICMP hlavičkou lze využít strukturu icmphdr:
#include <netinet/ip_icmp.h> |
struct icmphdr;
|
|
V hlavičkovém souboru netinet/ip_icmp.h jsou také definovány konstanty pro různé typy ICMP paketů, např. ICMP_ECHO, ICMP_ECHOREPLY, ICMP_DEST_UNREACH, ICMP_TIME_EXCEEDED, a kódy typů, např. pro typ ICMP_TIME_EXCEEDED jsou ICMP_EXC_TTL a ICMP_EXC_FRAGTIME.
Nepsaným pravidlem pro jednoznačný identifikátor v ICMP echo žádosti je identifikátor procesu, který žádost vysílá. Ten lze na unixových systémech získat funkcí getpid(2), na MS Windows funkcí GetCurrentProcessId.
Př. Modifikujte (nebo rozšiřte) předchozí program tak, aby přijímal všechny ICMP pakety a vypisoval všechny informace ze záhlaví paketu. U ICMP echo paketů vypisujte identifikátor a pořadové číslo žádosti/odpovědi.
Př. Implementujte zjednodušenou verzi programu ping. Program odešle několik ICMP echo žádostí na IP adresu zadanou jako parametr programu a po odeslání každé přijme odpovídající echo odpověď. Nastavte TTL odchozích IP paketů na 255 a časový interval pro příjem odpovídající odpovědi na 5 sekund (pomocí volby socketu SO_RCVTIMEO). U každé dvojice žádost-odpověď vypište čas mezi nimi (round trip time, RTT) pomocí funkce gettimeofday(2). Pro výpočet kontrolního součtu ICMP záhlaví použijte funkci z RFC 1071.
Protokol TCP poskytuje spojovanou, "spolehlivou" službu typu klient/server. Klient navazuje spojení se serverem čekajícím na spojení na určeném portu a přijetí dat (TCP segmentů) ve správném pořadí je potvrzováno, případně jsou zaslána znovu. Při přenosu dat je použito řízení toku dat a další techniky. Poté je spojení ukončeno. Naproti tomu protokol UDP poskytuje "pouze" nespojovanou "nespolehlivou" službu, taktéž charakteru klient/server. Spojení mezi uzly se nenavazuje, klient data (UDP datagram) odešle na port serveru, server přijme, nic se nepotvrzuje, neexistuje žádný tok dat, uzly si vyměňují kousky dat (datagramy), které mohou dorazit v jiném pořadí a duplikovaně. UDP narozdíl od TCP téměř nijak nezatěžuje síť.
Celé transportní TCP segmenty nebo UDP datagramy včetně hlavičky můžeme odesílat a přijímat pomocí raw socketů (typu SOCK_RAW z rodiny PF_INET). V tomto případě při odesílání použijeme ve funkci socket pseudoprotokol IPPROTO_RAW a vyplníme (vedle záhlaví IP paketu) záhlaví TCP segmentu nebo UDP datagramu a při příjmu protokol IPPROTO_IP s volbou socketu IP_HDRINCL a záhlaví obdržíme jakou součást dat. Jako protokol vyšší vrstvy v položce protocol IP záhlaví použijeme makra IPPROTO_TCP nebo IPPROTO_UDP. Pokud nechceme pracovat s IP záhlavím, ale jen s TCP nebo UDP záhlavím, můžeme pro odesílání i příjem použít ve funkci socket také makra IPPROTO_TCP a IPROTO_UDP. Cílový port protější strany se uvádí v položce sin_port struktury sockaddr_in (v síťovém tvaru).
S hlavičkou TCP segmentu nebo UDP datagramu můžeme pracovat pomocí struktury tcphdr nebo udphdr:
#include <netinet/tcp.h> |
struct tcphdr;
|
|
#include <netinet/udp.h> |
struct udphdr;
|
|
Při odesílání TCP segmentů nebo UDP datagramů tímto způsobem NENÍ na unixových systémech rozhodující číslo portu v položce sin_port ve struktuře sockaddr_in adresy (ignorováno, mělo by být 0), ale číslo cílového portu v odesílaném záhlaví. Na MS Windows u Winsock si naopak MUSÍ být tato čísla rovna. Pro výpočet kontrolního součtu TCP i UDP záhlaví můžeme opět použít funkci v RFC 1071.
Př. Modifikujte (nebo rozšiřte) předchozí program tak, aby přijímal všechny TCP segmenty a UDP datagramy a vypisoval všechny informace z jejich záhlaví. U TCP segmentu vypište přehledně všechny nastavené příznaky.
Navázání a ukončení spojení – streamovaný a datagramový socket
Odesílání a přijímání transportních paketů pomocí raw socketu není zrovna nejpříjemnější, zvláště pro TCP segmenty, jejichž záhlaví je poměrně složité a navíc je potřeba ("ručně") navazovat, udržovat a ukončovat spojení a řídit tok dat, včetně potvrzování přijetí ve správném pořadí a případného opakování přenosu dat. Tyto poměrně náročné a složité akce za nás může zařídit knihovna implementující socket API.
Operaci navázání TCP spojení na straně klienta si můžeme výrazně zjednodušit využitím funkce connect, pokud použijeme socket typu SOCK_STREAM, tzv. streamovaný (spojovaný) socket. U protokolu UDP pro socket typu SOCK_DGRAM (z rodiny PF_INET), tzv. datagramový (nespojovaný) socket, nám funkce connect nastaví výchozí adresu serveru, kterou pak již nemusíme zadávat při odesílání a přijímání dat. Parametr protocol funkce socket může obsahovat hodnotu 0, tj. použije se výchozí protokol pro rodinu PF_INET a typ socketu, tzn. makro IPPROTO_TCP nebo IPPROTO_UDP. U těchto "vysokoúrovňových" socketů se už nestaráme o záhlaví IP paketů a TCP segmentů nebo UDP datagramů, tj. nevyplňujeme při odesílání ani neobdržímě při přijímání dat. Odesíláme a přijímáme pouze aplikační data.
U socketu typu SOCK_STREAM a SOCK_DGRAM (z rodiny PF_INET) jsou přijatá data vložena do socketu procesu a již nejsou dále zpracovávána systémem, jsou určena a předávána pouze procesu. Tyto sockety již samozřejmě může otevřít proces libovolného uživatele, bez potřeby speciálních práv.
connect(2) |
#include <sys/types.h> #include <sys/socket.h>
|
int connect(int sockfd, const struct sockaddr *serv_addr, socklen_t addrlen);
int connect(SOCKET s, const struct sockaddr *name, int namelen);
|
|
Server narozdíl od klienta TCP spojení očekává, na určeném portu a lokální IP adrese (síťového rozhraní). Opět musíme socket na tuto adresu s portem připojit a to pomocí funkce bind. Na straně serveru se tomuto přiřazení adresy socketu nazývá pojmenování socketu; ostatní sockety jsou potom nepojmenované, anonymní. Pojmenovat socket, tj. přiřadit mu adresu pro příchozí data, lze i kterýkoliv z ostatních, paketový, raw i datagramový (pro UDP). Pak socket přijímá data (rámce, pakety apod.) pouze z této adresy (a portu). U TCP spojení je před vlastní komunikací s klientem (přijímání a odesílání dat) navíc ještě potřeba zahájit na serveru naslouchání příchozích spojení od klientů (což zařídí OS) a vytvořit frontu pro příchozí požadavky na spojení, funkce listen.
Na privilegovaný port (< 1024) může připojit socket pouze proces superuživatele (root nebo Administrátor) nebo proces se speciálními právy (capability CAP_NET_BIND_SERVICE).
bind(2) |
#include <sys/types.h> #include <sys/socket.h>
|
int bind(int sockfd, const struct sockaddr *my_addr, socklen_t addrlen);
int bind(SOCKET s, const struct sockaddr* name, int namelen);
|
|
listen(2) |
#include <sys/socket.h> |
int listen(int sockfd, int backlog);
int listen(SOCKET s, int backlog);
|
|
Jednotlivé požadavky na TCP spojení poté z fronty vybíráme pomocí funkce accept, čímž vlastně spojení přijmeme. Funkce vrátí nový socket pro toto spojení, pomocí kterého pak můžeme komunikovat s klientem, tj. přijímat od něj a odesílat mu data. Starý socket slouží pouze k přijímání spojení a pro každé spojení od klientů je vytvořen socket nový. Server tak může obsluhovat víc klientů zároveň a ještě přijímat nová spojení (skutečně zároveň v případě paralelně implementovaného serveru, např. vícevláknového).
accept(2) |
#include <sys/types.h> #include <sys/socket.h>
|
int accept(int sockfd, struct sockaddr *addr, socklen_t *addrlen);
SOCKET accept(int SOCKET, struct sockaddr *addr, int *addrlen);
|
|
TCP spojení ukončíme (na klientu nebo serveru) jednoduše zrušením/zavřením socketu, tj. na unixových systémech funkcí close, na MS Windows funkcí closesocket.
Odesílání a přijímání dat
Po navázání TCP spojení se serverem (odeslání požadavku na klientu funkcí connect a přijmutí na serveru funkcí accept) můžeme v daném čase zároveň odesílat a přijímat data, na klientu i na serveru. Propojení klienta se serverem je tedy plně obousměrné (full duplex) a navíc automaticky synchronizované. Pro daný socket může na klientu existovat nejvýše jedno spojení vytvořené funkcí connect a na serveru nejvýše jedna přiřazená adresa (a port) funkcí bind. U datagramového socketu můžeme funkcí connect měnit (výchozí) adresu příjemce a funkcí bind měnit (výchozí) adresu odesílatele datagramu opakovaně a po nastavení adresy odesílat a přijímat datagramy. Nastavení adresy lze zrušit nastavením adresy s položkou sa_family struktury sockaddr_in rovnou makru AF_UNSPEC.
Pro odeslání a příjem dat na streamovaném socketu se používají funkce send a recv, podobné funkcím sendto a recvfrom, které se spíše používají pro datagramový socket. Obecně, funkce send a recv lze použít i pro datagramový socket (použije se výchozí adresa zadaná funkci connect) a funkce sendto a recvfrom lze použít i pro streamovaný socket (pak jsou parametry to, tolen, from a fromlen ignorovány, měly by být NULL a 0). Data jsou přijata (a u TCP potvrzena), jestliže jsou uložena do vstupního bufferu socketu, tj. nemusí být přečtena funkcí recv nebo recvfrom.
Jelikož se socket chová jako file descriptor nebo file handle (pro Winsock 2 API), můžeme jej zároveň použít ve všech funkcích očekávajících jako parametr file descriptor nebo file handle (pouze pro typ SOCK_STREAM), tedy např. pro odeslání dat použít funkci write/WriteFile pro zápis do a pro příjem dat funkci read/ReadFile pro čtení z file descriptoru nebo socket/file handlu. Na unixových systémech také můžeme descriptor socketu převést na strukturu FILE pomocí funkce fdopen a používat ve funkcích fprintf, fscanf apod.
send(2) |
#include <sys/types.h> #include <sys/socket.h>
|
ssize_t send(int s, const void *buf, size_t len, int flags);
int send(SOCKET s, char* buf, int len, int flags);
|
|
recv(2) |
#include <sys/types.h> #include <sys/socket.h>
|
ssize_t recv(int s, void *buf, size_t len, int flags);
int recv(SOCKET s, char *buf, int len, int flags);
|
|
Pro přenos dat pomocí funkcí send a recv člověk nepotřebuje vědět nic o fungování počítačové sítě, snad až na odlišnost protokolů TCP a UDP. V případě protokolu TCP jsou data přenášena jako datový proud podobně jako ze/do souboru na disku, v případě UDP jsou vyměňovány kousky dat (datagramy). Všechny detaily fungování protokolu TCP (pořadí segmentů, potvrzování přijetí, opakování přenosu, řízení toku apod.) za nás zařídí knihovna implementující Socket API.
Př. Implementujte jednoduchý síťový chatovací program. Server bude očekávat spojení (datagramy) na nějakém neprivilegovaném portu zadaném jako parametr programu (na všech lokálních adresách či rozhraních). Klient naváže spojení se serverem (bude přijímat datagramy ze serveru) zadaném IP adresou jako první parametr programu a portem zadaným jako druhý parametr. Program bude implementovat klienta i server, podle zadaného počtu parametrů se bude chovat buď jako klient nebo jako server. Po navázání spojení (u TCP) bude program (jako klient i jako server) zobrazovat a odesílat řádky zadané na vstup programu protější straně, která je zobrazí jako text z druhé strany. Implementujte komunikaci nejdříve nad protokolem TCP a poté nad protokolem UDP a pozorujte (případné) rozdíly při komunikaci.
Pro komunikaci pomocí socketů potřebujeme znát IP adresu příjemce. Pro člověka (uživatele) je ale snáze zapamatovatelné doménové jméno příjemce. Toto jména je potřeba přeložit (pomocí systému DNS) na IP adresu, popř. i obráceně.
gethostbyname(3) |
#include <netdb.h> |
struct hostent *gethostbyname(const char *name);
|
|
gethostbyname(3) |
#include <netdb.h> |
struct hostent *gethostbyaddr(const void *addr, int len, int type);
struct hostent* gethostbyaddr(const char *addr, int len, int type);
|
|
IP adresa (adresy) je ve struktuře hostent uložena v (binárním) síťovém tvaru. Pro převod do tečkového tvaru lze použít funkci inet_ntoa. Pro opačný převod, např. do parametru addr funkce gethostbyaddr, je funkce inet_aton. Případné využití systému (a aplikačního protokolu) DNS je při překladu jména zcela transparentní a je implementováno Socket API.
Př. Doplňte do příkladů implementace zjednodušené verze programu ping a jednoduchého síťového chatovacího programu (popř. jiného TCP/UDP klienta a serveru) překlad doménového jména na IP adresu a obráceně. Parametrem programů bude místo IP adresy doménové jméno, které se před odesláním dat přeloží na IP adresu. Jestliže program vypisuje IP adresu (např. adresa odesílatele každé ICMP echo odpovědi), bude vypisovat doménové jméno přeložené z IP adresy.