V minulém čísle mě zaujal článek o IPX síťování. Ačkoliv je IPX protokol použitelný na velké části lokálních sítí (Novell NetWare a Windows 95), na větší vzdálenost s ním neprorazíte. Dnes však spousta programů musí pracovat po internetu a pak je potřeba používat TCP/IP protokoly, o kterých jsem se rozhodl sepsat nějaké informace. Nejdříve trochu teorie. Adresy Každý počítač, no vlastně každé síťové zařízení (routery jich mají víc), musí dostat svoji IP adresu. IP adresa je 4-bytová a je zvykem ji zapisovat jako čtveřici čísel oddělených tečkami. Například 192.168.82.13. Ke každé IP adrese náleží síťová maska (netmask) (např 255.255.255.0), která adresu rozděluje na síťovou (net) část (v našem případě 192.168.82) a hostitelskou (host) část (v našem případě .13). Z toho se pak odvodí takzvaná síťová adresa (používaná při směrování -- v našem případě 192.168.82.0 -- adresa & netmask) a vysílací (broadcast) adresa (buď nejvyšší možná adresa v síti -- tedy 192.168.82.255, nebo síťová adresa (teoreticky může být i jiná)), což je adresa, ke které se budou znát všechny stanice v síti. Směrování (Routing) Každý počítač v TCP/IP síti má takzvanou routovací tabulku, která říká, co se má dělat s jednotlivými pakety. Ta vypadá nějak takhle: address flag mask device 127.0.0.1 up 255.0.0.0 lo 192.168.81.1 up 255.255.255.0 plip0 192.168.82.0 net, up 255.255.255.0 eth0 default net, up 0.0.0.0 slip0 Tohle nám říká zhruba následující: předpokládejme, že náš počítač má tři síťová zařízení. Přes paralelní linku je na něj připojen počítač s adresou 192.168.81.1, přes síťovou kartu několik počítačů se společnou síťovou adresou 192.168.82.0 a přes sériovou linku (řekněme modem) je připojen k internetu, kde se nalézají všechny ostatní možné adresy. Že jsem neřekl nic o prvním řádku? No to je přece speciální loopback device, tedy zařízení pro komunikaci sama se sebou (například s X-serverem aplikace komunikují přes TCP/IP protokol, aby bylo jedno, jestli běží vzdáleně, nebo ne). Když nějaký program odešle paket, tak se kernel (jádro OS) našeho milého počítače podívá do routovací tabulky a postupně porovnává. Nejdříve nejpřesnější definice (1. a 2. řádek). Pokud tyto neodpovídají, zkusí méně konkrétní 3. řádek. A když ani tento nepasuje, tak ze zoufalství vezme za vděk položkou default a pošle paket přes modem internetovému serveru, který pomocí o něco sofistikovanějších metod rozhodne, kam s ním dál. (Velké routery používají dynamické routovací tabulky -- říkají si navzájem, co pod sebou mají za adresy a upravují si tabulky za běhu -- a různé další zběsilosti.) Navazování spojení Pod IP protokolem (který se stará o směrování -- nese cílovou IP, odesilatelskou IP a hardwarovou adresu routeru) existuje několik protokolů pro výměnu informace a jim odpovídajících druhů přenosu. V souboru /etc/protocols na mém počítači je následující seznam: ip 0 IP # internet protocol, pseudo protocol number icmp 1 ICMP # internet control message protocol igmp 2 IGMP # internet group multicast protocol ggp 3 GGP # gateway-gateway protocol tcp 6 TCP # transmission control protocol pup 12 PUP # PARC universal packet protocol udp 17 UDP # user datagram protocol idp 22 IDP # WhatsThis? raw 255 RAW # RAW IP interface Pokud správně rozumím tomuto popisu, tak IP znamená automatický výběr protokolu (obvykle požadovaný typ spojení pracuje jen s jedním protokolem). ICMP je protokol, kterým si počítače vyměňují provozní informace, jako "zahodil jsem ti paket --", když dojde time-to-live, nebo "pakety pro X posílej Y a ne mě --", když je potřeba upravit routovací tabulky; IGMP mi vážně nic neříká a totéž platí o GGP. TCP je nejpoužívanější protokol pro obecná data, který má podobné vlastnosti jako SPX, tedy dodržuje pořadí paketů a kontroluje přijetí. Naproti tomu UDP je protokol, který podobně jako IPX prostě posílá pakety, takže ani přijetí, ani pořadí nejsou zaručeny, ale zase se nevyměňuje ověřovací informace. U PUP opět nevím, o co se jedná a u IDP to zjevně nevěděl ani autor tohoto seznamu (Fred N. van Kempen). Konečně RAW znamená, že si můžete posílat co chcete (a dělat bordel) a mnoho systémů vyžaduje rootí práva k otevření spojení v RAW módu (ne jako v IPX, kde hlavičku vyplňujete sami a můžete tam naflákat co chcete). Pokud se nebudete zrovna chtít hrabat v routovacím systému (v takovém případě Vás odkazuji na zdrojáky Linuxu:-), budou vás zajímat zejména protokoly TCP/IP a UDP/IP. Nejdříve k TCP/IP. První věc, kterou musíte udělat, je získat socketu. Za tímto účelem použijete volání socket: #include<sys/types.h> #include<sys/socket.h> int socket(int domain, int type, int protocol); Této funkci předáte v domain typ sítě (tzv. adresová rodina), který bude AF_INET, ale například můj linux zná také hodnoty AF_UNIX (UUCP -- unix-to-unix copy), AF_AX25 (AX.25 amatérské rádio), AF_IPX (Novell IPX), AF_APPLETALK (Appletalk DDF), AF_NETROM (NetROM amatérské rádio), AF_BRIDGE (přenos mezi protokoly), AF_AAL5 (rezervováno pro Werner's ATM), AF_X25 (rezervováno pro X.25) a kernely 2.1.x už znají AF_INET6, což je IP protokol s adresami nataženými na 6 byte (internet na ně má postupně přecházet, aby bylo dost místa na přidělování adres). Nicméně zůstaňme u AF_INET. Pro AF_INET přichází v úvahu tři hodnoty parametru type, a sice SOCK_STREAM a SOCK_DGRAM. SOCK_STREAM je typ odpovídající právě a pouze tcp protokolu, a tak můžete jako třetí argument uvést 0. Obdobně SOCK_DGRAM odpovídá právě udp protokolu, a tak můžete jako protocol opět klidně uvést 0. Funkce vrací file descriptor (i socket je soubor:-) otevřené sockety. A co třetí typ? SOCK_RAW. TCP/IP client Tcp spojení je asymetrické. Jedna strana (server) musí otevřít poslouchací socket, na kterém klient požádá o spojení, což způsobí otevření obousměrného komunikačního kanálu. Nejdříve k jednodušší práci klienta. Ten si otevře socketu (výše uvedeným voláním socket), případně nastaví nějaké parametry (podrobnosti viz manuálová stránka funkce setsockopt) a naváže spojení voláním connect: #include<sys/types.h> #include<sys/socket.h> int connect(int sockfd, struct sockaddr *serv_addr, int addrlen); Je snad zřejmé, že sockfd je file handle vaší pracně vytvořené sockety. Naproti tomu, pokud Vám následující parametr připadá divný, tak nejspíš nejste sami. Jedná se totiž opravdu o pěknou čuňárnu. V sys/socket.h je struct sockaddr definována následovně: struct sockaddr { unsigned short sa_family; /* adresová rodina AF_xxx */ char sa_data[14]; /* cosi:-) */ }; To je nádhera, co? Když budete pátrat po tom, co patří do toho cosi, tak se pěkně zapotíte, a pak zjistíte, že v souboru netinet/in.h (tam je tak akorát #include<linux/in.h>:-) je definovaná následující struktura: struct sockaddr_in { short int sin_family; /* stejné definice :-) */ unsigned short int sin_port; /* číslo portu */ struct in_addr sin_addr; /* inetová adresa */ }; A teď se podržte: struct in_addr je výše v tomtéž souboru definovná konstrukcí: struct in_addr { __u32 s_addr; }; Co že to proboha je __u32. No to je obyčejný unsigned int! Takže si vytvoříte strukturu typu struct sockaddr_in (řekněme foo), do sin_family dáte AF_INET, do sin_port dáte port hledaného serveru (ten si musíte předem domluvit -- obvykle bývá konstantní) a do sin_addr.s_addr dáte adresu počítače, kde server běží, no a můžete vesele zavolat: connect(fd, &foo, sizeof(sockaddr_in)); což vám odpoví 0, pokud se dovoláte a -1 pokud se něco po.... Jenže ono se po! Connect totiž příslušná čísla chce v big endianess (vyšší byte první), kdežto vy nevíte, jestli máte vyšší, nebo nižší byte první! (nesnažte se mě přesvědčit, že víte. Unixy běhají na počítačích obou typů!) Takže musíte přiřazovat přes funkce ushort htons(ushort) a ulong htonl(ulong) a číst přes ushort ntohs(ushort) a ulong ntohl(ulong) (jsou definované v netinet/in.h), případně převést adresu funkcí int inet_addr(char *host) nebo int inet_aton(char *host, struct in_addr *inp), které převádí řetězec s tečkovanou decimální a (snad) nebo symbolickým jménem na adresu (v síťovém (big endianess)) tvaru. No a teď už si můžete vesele povídat pomocí read a write (případně send a recv) a nakonec voláním shutdown a close spojení ukončíte: shutdown(fd, 2); /* viz man 2 shutdown */ close(fd); /* viz man 2 close */ TCP/IP server Server má trochu víc práce. Nejdříve si vytvoří socketu a případně nastaví parametry voláním setsockopt. Pak musí nastavit port, což udělá voláním funkce bind: #include<sys/types.h> #include<sys/socket.h> int bind(int sockfd, struct sockaddr *my_addr, int addr_len); Kde parametry mají obdobný význam, jako u connect, ale do my_addr->sin_addr.s_addr můžete dát konstantu INADDR_ANY (== 0; v netinet/in.h); Tím jste si zabrali port. Nyní musíte systému říct, že posloucháte, což uděláte voláním listen: #include<sys/socket.h> int listen(int s, int backlog); Argument s je handler sockety a backlog je délka fronty pro requesty (když přijdou moc rychle za sebou, tak tam počkají). Nespoléhejte nicméně na její hodnotu, protože OS má interní limit (Linux má 128, ale některé UNIXy taky jenom 5), který vám nedovolí překročit. Funkce vrací 0 při úspěchu a -1 při chybě. Poslední Funkcí, kterou musíte zavolat, je accept: #include<sys/types.h> #include<sys/socket.h> int accept(int s, struct sockaddr *addr, int *addrlen); Této funkci řeknete handle sockety (s) a v addrlen délku vyhrazeného místa a accept vám naoplátku vrátí handle nové sockety (-1 při chybě), přes kterou budete komunikovat, zpáteční adresu v addr a její délku v addrlen. Socketa s zůstane otevřená pro poslouchání. Pokud není žádný požadavek na spojení, funkce buď čeká, až nějaké přijde, nebo skončí chybou podle toho, zda socketa je "non-blocking." To je ovšem malý problém. Z jednoho zdrojáku (v manuálu jsem to nenašel) jsem zjistil, že se to dělá na některých systémech přes setsockopt, ale s argumenty, které nejsou v manuálu a na jiných přes ioctl, kterých existuje několik dalších variant. Fuj:-(. Podle manuálové stránky však lze otestovat přítomnost požadavku voláním select na čtení této sockety. Na socketu vytvořenou tímto voláním nyní můžete používat read, write, send a recv stejně jako klient a (doufám) ji stejným způsobem (shutdown, close) uzavřít (v AF_UNIX musíte ještě zavolat unlink (podle man)). UDP/IP spojení Když otevřete udp socketu, je situace trochu jiná. Vytvoříte ji voláním socket a nastavíte port voláním bind stejně, jako u tcp sockety, ale uděláte to tentokrát na obou stanách. Vlastně máte socketu, která je schopná komunikace s libovolným množstvím partnerů pomocí funkcí sendto, kterou můžete poslat packet na libovolnou adresu a recvfrom, která přečte další paket a vyplní adresu jeho odesilatele (adresové argumenty se chovají jako u accept). #include<sys/types.h> #include<sys/socket.h> int sendto(int s, void *msg, int len, unsigned int flags, const struct sockaddr *to, int tolen); int recvfrom(int s, void *buf, int len, unsigned int flags, struct sockaddr *from, int *fromlen); Většina argumentů je doufám zcela jasná. Trochu nejasný asi bude argument flag, který u send(to) může mít hodnoty: MSG_OOB /* out-of-band data. Paket předbíhá, nebo jde do * zvláštní fronty (podle systému) */ MSG_NOROUTE /* obejití routování -- pouze pro diagnostiku */ a u recv(from) hodnotu: MSG_OOB /* zpracovat out-of-band data (mají-li zvláštní frontu)*/ MSG_PEEK /* paket se nevybere z fronty */ MSG_WAITALL /* čeká se na zaplnění bufferu (len bytů) */ Jedná se o klasické flagy, tedy hodnoty je možné kombinovat pomocí or. UDP/IP connect I u udp má smysl volání connect, i když není povinné. Když na udp socketu pustíte connect, tak tím systému řeknete, že vaše socketa bude posílat všechna data (od teď přes send místo sendto) a přijímat data (může přes recv) pouze od specifikovaného partnera (peer). Udp socketu (asi) zavřete stejně jako každou jinou (shutdown, close). Jestli jste se opravdu prokousali až sem, na 283. řádek, tak Vás opravdu obdivuji a doufám, že se Vám výše uvedené informace budou hodit, až se někdy budete prokousávat nepořádnou dokumentací síťového rozhraní (třeba na ten byte ordering jsem narazil náhodou při prolézání jednoho zdrojáku). Měl bych ještě poznamenat, že výše uvedené informace se vztahují ke standartní UNIXové knihovně (jak je popsána v dokumentaci na mém Linuxu). Vzhledem k tomu, že na DOSu není TCP/IP součástí systému, je zde rozhraní do určité míry záležitostí konkrétní nadstavby. A na W95 si nemůžete byt jisti ničím (Microsoft si prý (viz článek ve Výhni #8 (Programování pro Microsoft Windows/Shakul)) přejmenovávají i funkci main na WinMain:-|). O těchto systémech nemám přehled (V DOSu už akorát hraju Heroesy:-), ale pokud jste se někdo zabýval, nebo hodláte zabývat komunikací po internetu z DOSu nebo Windows95/8, byl bych rád, kdybyste o TCP/IP v těchto systémech něco napsali. - Bulb - Poznámka redakce: Ve Windows 9x/NT se s TCP/IP pracuje skoro stejně. Jediný rozdíl spočívá v tom, že program musí na začátku zavolat funkci pro inicializaci WinSocku WSAStartup a na konci WSACleanup. Poté je možné používat snad všechny Bulbem popsané funkce. výheň