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ň