1. Softwarová rozhraní systémů UNIX pro přístup k síťovým službám

1.1. Užívaná síťová aplikační rozhraní a jejich obecné vlastnosti

V pokročilejších verzích operačního Unix systému bývá zpravidla jeho součástí protokolový stack TCP/IP. Ačkoliv jsou standardy protokolů rodiny TCP/IP definované v příslušných RFC, nepraví se v těchto dokumentech nic o tom, jakým způsobem mají k protokolovému stacku přistupovat uživatelské aplikace. Důvod pro absenci této specifikace je nasnadě - jednotlivé protokoly mohou totiž být implementovány na tak rozličných systémových architekturách, že exaktní definice společného softwarového rozhraní by byla patrně nemožná.

Jednotlivé systémy tedy poskytují svá vlastní aplikační rozhraní. Volba rozhraní tak závisí jednak na vybavení konkrétního systému, jednak na rozhodnutí programátora. To samozřejmě vede ke ztrátě přenositelnosti kódu. Během vývoje se však ustálila některá rozhraní, která se stala de facto standardem a jsou přebírána většinou nově vznikajících operačních systémů, dokonce i těch, jejichž vývoj neměl s Unixem nic společného. V současné době můžeme považovat za nejrozšířenější rozhraní Unix Sockets, představené v systému 4.1-BSD Unix Berkeleyské univerzity a rozhraní TLI (Transport Layer Interface), které najdeme v systémech Unix vycházejících z verze System V firmy AT&T. Řada novějších systémů podporuje obě tato rozhraní.

Všechna aplikační síťová rozhraní, podporující rodinu protokolů TCP/IP, musí implementovat jistou sadu funkcí, které můžeme shrnout do následujících bodů:

Jak je z uvedených bodů patrné, orientují se rozhraní obvykle na architekturu klient-server. To je způsobeno nutností podpory virtuálního spojení. Při zřizování virtuálního kanálu totiž vždy existuje stanice volaná (server), která očekává požadavky na spojení od stanic volajících (klientů). Distribuovaná aplikace vybudovaná nad virtuálním kanálem již samozřejmě může využívat jak symetrického tak asymetrického modelu. Z hladiska softwarového protokolu je příkladem symetrického modelu TELNET, z asymetrických jmenujme FTP.

Protože TCP/IP neumí při příchodu požadavku o spojení samostatně vytvořit nový proces (což je dáno vrstvenou strukturou protokolového stacku), musí proces serveru pasivně čekat na požadavek o zřízení virtuálního spojení. Servery bývají zpravidla navrhovány tak, aby byly schopny zpracovávat současně požadavky z více spojení. V operačním systému Unix je k tomuto účelu zpravidla s výhodou využíváno volání fork(), které umožňuje proces rozštěpit a vytvořit tak pro každé spojení zvláštní instanci procesu, která bude za obsluhu tohoto spojení zodpovědná. Původní proces zůstává ve stavu očekávání dalších žádostí ze strany klientů.

V prostředí Internetu se během vývoje prosadila řada typů serverů, poskytujících klientům širokou škálu služeb. Protože na jedné stanici může pochopitelně běžet celá řada různých serverů, ustálila se pro jednotlivé služby sada tzv well-known portů, jejichž hodnoty identifikují proces příslušného typu serveru, ať už běží kdekoli a na jakémkoli počítači. Přiřazení well-known portu je zpravidla součástí specifikace softwarového protokolu pro komunikaci se serverem poskytujícím požadovanou službu.

1.2 Aplikační rozhraní BSD Sockets

1.2.1 Historie a paradigma

Rozhraní BSD Sockets poskytuje uživatelským aplikacím jednotný přístup k transportní a případně nižším vrstvám protokolového stacku. Poprvé se objevilo v produktu BSD Unix v.4.1, vyvinutém na University of California, Berkeley, kolem roku 1982 pro počítač DEC VAX-11. Od té doby doznalo značného rozšíření a obliby a stalo se de facto standardním aplikačním rozhraním k protokolům rodiny TCP/IP. Koncepty tohoto rozhraní byly dokonce přejaty aplikačními rozhraními řady dalších platforem, zejména ve světě PC. V dalším textu se budeme držet verzí rozhraní společně označovaných jako BSD Sockets 4.x.

Rozhraní Sockets je navrženo tak, aby se jevilo jako rozšíření Unixovského souborového systému. Tímto je možné využívat sady funkcí určených pro práci se soubory i pro komunikaci prostřednictvím sítě. Protože však je síťová komunikace značně obsáhlejší co do množství nestandardních stavů, které mohou nastat, obsahuje Sockets i řadu přídavných funkcí, kterých není v souvislosti s běžnými souborovými operacemi zapotřebí.

Autoři rozhraní Sockets zvolili pro přístup k vrstvám protokolového stacku poměrně vysokého stupně abstrakce. Rozhraní je takto použitelné nejen pro protokoly rodiny TCP/IP, ale může být implementováno i pro protokoly zcela odlišné, při zachování společné sémantiky. Toho využili například implementátoři rozhraní Windows Sockets pro prostředí Windows NT, kde jsou mimo protokolů TCP/IP podporovány i protokoly IPX/SPX a systém je otevřený pro možnost doplnění dalších rodin protokolů. Za tuto obecnost je samozřejmě zaplaceno nutností specifikovat větší počet parametrů při volání jednotlivých služeb rozhraní.

Pro umožnění implementace rozličných skupin protokolů definuje rozhraní Sockets pojem rodiny protokolů (protocol family). Rodina protokolů zahrnuje skupinu protokolů používaných v jistém komunikačním prostředí - například rodina PF_INET pro použití v Internetu zahrnuje protokoly IP, TCP a UDP. Typ použitého protokolu je zpravidla dán charakterem služby, i když lze protokol specifikovat i explicitně. V současné době Unixovské implementace Sockets podporují mimo Internetu značnou sadu komunikačních domén, jako IBM SNA, Digital DECnet, Novell IPX/SPX, Appletalk a řadu dalších.

Každá rodina protokolů může využívat jednu nebo více rodin adres (address family). Pro prostředí Internetu existuje pouze jediný typ adresy, jeho rodina je označována konstantou AF_INET. Rodina protokolů a rodina adres bývá často uživateli rozhraní zaměňována. Při použití v doméně Internetu však díky tomu, že číselné hodnoty konstant AF_INET a PF_INET jsou shodné, plynou z této nepřesnosti potíže pouze symbolického rázu. Je pravděpodobné, že v okamžiku zavedení 16-bajtových adres při brzy očekávaném rozšíření protokolu IP na verzi 6 bude zavedena v rámci PF_INET další rodina adres.

Kvůli používání adres různých rodin byl zaveden obecný typ sockaddr, což je struktura sestávající ze dvou položek (definice viz netinet/inet.h). Položka sa_family je typu integer a nese číselnou identifikaci rodiny adres, která implikuje způsob interpretace obsahu položky sa_data. Pro jednotlivé rodiny adres jsou zavedeny konkretizující typy, které vycházejí z typu sockaddr a dále člení položku sa_data podle zvyklostí reprezentace adresy v té které komunikační doméně. Například pro prostředí Internetu je definován typ sockaddr_in, který položku sa_data rozděluje na sin_addr a sin_port, představující IP adresu stanice a číslo portu identifikující proces v rámci této stanice. Zavedení takovéto struktury však nepřináší nic jiného než odlišný pohled na stejná data.

Rozhraní Sockets poskytuje dva základní způsoby komunikace - virtuální spojení a datagramovou službu. Virtuální spojení (stream service) poskytuje obousměrný, spolehlivý, sekvencovaný, a neduplikovaný tok dat, bez rozlišování hranic záznamů, tedy klasický stream. Oproti tomu datagramová služba dává možnost obousměrného přenosu datagramů, který však nemusí být spolehlivý, sekvencovaný ani neduplikovaný. Hranice záznamů jsou u datagramové služby zachovány (tedy příjemce rozliší, kde končí jeden datagram a začíná jiný). Virtuální spojení řeší za programátora řadu problému spojených se zabezpečením přenosu v reálném poruchovém prostředí, oproti tomu datagramová služba přímo koresponduje s paketovým charakterem většiny sítí. Výhodou datagramové služby je větší pružnost a snížení režie za předpokladu zabezpečení spolehlivosti komunikace jiným způsobem. V prostředí Internetu využívá datagramová služba protokolu UDP, virtuální kanál je vybudován nad protokolem TCP.

Ve většině implementací je umožněn i přístup k nižším vrstvám protokolového stacku, než je vrstva transportní. Slouží k tomu tzv. sockety typu "raw" (syrové). Jejich prostřednictvím se mohou programátoři dostat přímo k protokolu IP a s ním spojenému ICMP nebo dokonce na úroveň rámců linkové vrstvy. To je užitečné zejména při implementací takových aplikací, jako jsou routery nebo monitory síťového provozu. Přístup k raw socketům však bývá obvykle umožňován pouze procesům superuživatele.

Pod pojmem socket rozumíme koncový komunikační uzel spojení. Z hlediska software se vlastně jedná o datovou strukturu, popisující stav, ve kterém se komunikující uzel nachází. To zahrnuje mimo jiné adresu lokálního (a příp. vzdáleného) účastníka spojení a u virtuálního kanálu rovněž jeho stavovou informaci. Pojem socket se objevuje i ve specifikacích protokolů TCP a UDP, kde je definován jako dvojice tvořená IP adresou a číslem portu, jednoznačně identifikuje proces na příslušné stanici. Spojení je pak jednoznačně určeno dvojicí takovýchto socketů, někdy označovanou jako socket pair.

Komunikace probíhá téměř výhradně mezi sockety stejné komunikační domény, avšak z hlediska sémantiky rozhraní Sockets teoreticky nic nebrání tomu, aby se pokusily nižší vrstvy protokolového stacku toto omezení překlenout. Jedná se ovšem spíše o teoretickou možnost, jejíž využití je nanejvýš diskutabilní.

1.2.2 Návaznost na souborový systém

Zajímavé je povšimnout si způsobu, jakým na sockety pohlíží operační systém. Jak bude později podrobně vysvětleno, vzniká socket voláním služby operačního systému socket(). Během tohoto volání vytváří operační systém novou strukturu popisující atributy a stav socketu a odkaz na tuto strukturu ukládá do tabulky deskriptorů souborů (viz obr. 1.1). Aplikaci je pak vrácen "běžný" deskriptor souboru, který je indexem do tabulky deskriptorů. Naprosto tentýž postup využívá Unix při vytváření odkazů na diskové soubory. Jediný rozdíl spočívá v tom, že vytvoření struktury popisující stav přístupu k souboru a uložení odkazu na tuto strukturu do tabulky deskriptorů je zajišťováno voláním open(). Mezi deskriptorem souboru a socketu tedy není z hlediska použití žádný rozdíl. Tato skutečnost má dalekosáhlé důsledky - aplikace může volat stejné funkce pro transfer dat prostřednictvím socketů, jakých používá pro práci s běžnými diskovými soubory. Díky tomu můžeme všechny mechanismy Unixu, uzpůsobené pro použití se soubory (přesměrovávání standardního vstupu a výstupu, tvorbu rour, kolon a další) použít bez význačných změn software i v distribuovaném prostředí. Samozřejmě zde existují i jisté odlišnosti, spojené s nutností zřizování a rušení virtuálního kanálu před zahájením skutečného přenosu dat. Taktéž množina chybových stavů, které mohou v síťovém prostředí nastat v důsledku dočasných i trvalých výpadku spojení, je oproti diskovým operacím podstatně širší.

Obr. 1.1 - Návaznost socketů na souborový systém

Sloučení socketů se souborovým systémem přináší pro síťové rozhraní možnost využití řady možností, které Unix poskytuje pro operace se souborovým systémem. Patrně nejvýznamnější se jeví možnost registrovat požadavek o aktivaci signálu v případě, že na socketu dojde k asynchronní události, která by mohla být pro aplikaci zajímavá. Může se kupříkladu jednat o příchod nových dat nebo o připravenost socketu převzít další data určená k vyslání. Registrace požadavku o signál se provádí pro každý socket zvlášť použitím volání jádra fcntl() v souvislosti s volbou FASYNC.

Poznamenejme, že narozdíl od funkce open() vytváří volání socket() strukturu, ve které ještě řada položek není inicializována a před použitím do ní musejí být vneseny doplňující informace. Jedná se zejména o přiřazení lokální a případně i vzdálené adresy, což se provede s využitím dále popisovaných volání bind() a connect().

1.2.3 Režimy práce socketů

Z hlediska funkce při otevírání virtuálního spojení rozlišujeme dva typy socketů - pasivní a aktivní. Jejich názvy jsou analogické k názvům způsobů otevření TCP spojení použitými v [RFC793] - active & passive open. Pasivním socketem rozumíme socket, který sleduje žádosti o zřízení spojení a je schopen na ně odpovídat. Do stavu sledování žádostí (listening) se socket převádí voláním funkce listen(). Naopak socket aktivní je tím, kdo žádost o zřízení virtuálního spojení iniciuje, což ve vrstvě TCP představuje vyslání prvního SYN-segmentu na adresu pasivního socketu.

Každý jednotlivý socket může být nastaven do dvou módů - blokujícího a neblokujícího. Toto nastavení ovlivňuje chování funkcí, které jsou nad tímto socketem prováděny. Jádro věci spočívá v tom, že ne všechny funkce rozhraní mohou být provedeny okamžitě a poté vrátit řízení aplikaci. Uvažme například funkci connect() pro zřizování virtuálního spojení. Pokud zřizujeme virtuální spojení v síti WAN, může tato operace trvat značně dlouhou dobu, řádově i desítky sekund. Stačí si uvědomit, že se zřizováním spojení je spojena výměna několika paketů mezi volajícím a volaným, což může na síti s malou přenosovou rychlostí trvat určitý čas. Volaný rovněž může být zaneprázdněn zpracováním jiných požadavků a žádost o spojení po nějakou dobu pozdržet ve frontě. Podobný problém vyvstává také například tehdy, když aplikace požaduje převzetí určitého množství dat ze socketu, ale tato data na socket dosud nedorazila. Obdobných příkladů bychom mohli uvést ještě mnoho. Protože se některé aplikace pravděpodobně nebudou chtít vzdát řízení na tak dlouhou a někdy v podstatě neomezenou dobu, umožňuje rozhraní Sockets uvést kterýkoli socket do tzv. neblokujícího režimu. Funkce, která nemůže být provedena okamžitě a je aplikovaná na socket v neblokujícím režimu, vrátí řízení aplikaci s indikací zvláštního chybového kódu (EWOULDBLOCK). Ve většině případů se v takovéto situaci neprovede žádná činnost. Výjimkou je volání funkce connect(), při kterém dochází k nastartování paralelní operace, která se pokouší spojení i nadále navázat. V rozhraní existují pomocné mechanismy, které dovolují aplikaci určit, zda již byla takto dříve nastartována operace na neblokujícím socketu dokončena, ať již úspěšně nebo detekcí chybového stavu. Implicitně jsou všechny sockety nastaveny do blokujícího režimu, což znamená, že některé operace nad těmito sockety mohou trvat prakticky neomezenou dobu.

V souvislosti s blokujícími sockety můžeme také narazit na výraz blokující funkce. Jedná se o funkce, které mohou v případě použití na blokujícím socketu zablokovat proces na neurčitou dobu. Funkce označované jako neblokující vracejí řízení aplikaci okamžitě, bez ohledu na to, zda je socket blokující či nikoli. Mezi neblokující funkce patří zejména funkce pro zjišťování a nastavování parametrů, zjišťování adresy lokálního a vzdáleného účastníka spojení a vůbec všechny funkce, u kterých je informace potřebná pro jejich provedení dostupná kdykoli.

Každý socket je po vytvoření nastaven do blokujícího režimu. Tento režim může být změněn vhodným nastavením volby FNDELAY při volání funkce fcntl().

1.2.4 Urgentní data

Rozhraní Sockets podporuje koncept tzv. urgentních dat, která bývají v definici rozhraní ne zcela správně ozačována jako "out-of-band data". Model urgentních dat spočívá v tom, že ke každému virtuálnímu spojení může být přiřazen zvláštní nezávislý logický přenosový kanál, který poskytuje možnost přednostního předávání významných dat spolupracující aplikaci.

Koncept urgentních dat byl zaveden již při definici protokolu TCP [RFC793], avšak jeho sémantika zde nebyla příliš přesně specifikována. V hlavičce segmentu TCP můžeme najít položku URG PTR (Urgent Pointer), která odkazuje na urgentní data, jsou-li v segmentu obsažena. Platnost ukazatele je indikována nastavením příznaku URG pole FLAGS. Vlivem nepřesnosti definice však není interpretace mechanismu urgentních dat jednoznačná - liší se mimo jiné ve výkladu ukazatele URG PTR. Upřesnění přineslo až [RFC1122] , podle něhož se jedná o ukazatel na poslední bajt urgentních dat. Bohužel, řada implementací, mezi nimi i Berkeleyská, interpretuje Urgent Pointer nesprávně jako ukazatel na bajt, který následuje bezprostředně za urgentními daty. Tento způsob výkladu je však v praxi patrně nejrozšířenější. Umístění začátku urgentních dat rovněž závisí na výkladu aplikace, který často vychází pouze z definice softwarového protokolu implementované služby.

Nejasně bývá vykládána i situace, která nastane, když transportní vrstva přijme TCP segment s urgentními daty a ve frontě pro aplikaci ještě čekají urgentní data z dřívějška. Často bývá tato situace ošetřena jednoduše tak, že se udržuje jen jediný ukazatel urgentních dat. V případě příchodu urgentních dat v době, kdy nebyla ještě předchozí urgentní data přečtena, se informace o urgentnosti dřívějších dat ztratí a urgentními se stanou data nově doručená.

Z důvodů uvedených nejasností se doporučuje urgentních dat nepoužívat, pokud to není nutné pro implementaci protokolu některé standardní aplikace. Mezi aplikace, které konceptu urgentních dat využívají, patří například RLOGIN , kde jsou jako urgentní data přenášeny povely jako žádost o přerušení procesu, zrušení výpisu nebo dočasné zablokování a odblokování vysílání.

Rozhraní Sockets dovoluje aplikacím zvolit rozdílné metody pro přístup k urgentním datům. Urgentní data mohou být ukládána zvlášť nebo připojena do datového toku s tím, že jejich konec bude označen a aplikace může testovat, zda se aktuální pozice shoduje s pozicí této značky. Režim zpracování urgentních dat lze ovlivňovat funkcí setsockopt() v souvislosti s volbou SO_OOBINLINE. Urgentní data mohou být vyslána stejně jako data obyčejná funkcemi send() a sendto() s tím, že se nastaví příznak MSG_OOB. Podobně je tomu s extrakcí urgentních dat - v případě nastavení příznaku MSG_OOB při volání funkcí recv(), resp. recvfrom() dodá funkce data označená jako urgentní, pokud jsou taková k dispozici. Je třeba zdůraznit, že funkce pro čtení z virtuálního kanálu nikdy při jediném volání nesměšují obyčejná data s urgentnímu.

Jestliže je socket nakonfigurován pro zařazování urgentních dat do datového toku (in-band), je možné ověřit urgentnost právě čtených dat použitím funkce ioctl() s volbou SIOCATMARK.

1.2.5 Broadcasting

Rozhraní Sockets neposkytuje žádnou simulaci vysílání typu broadcast ani multicast. Broadcastingu lze použít pouze v případě, že jej podporuje síťová vrstva. Broadcast datagramy mohou být samozřejmě vysílány pouze ze socketů určených pro datagramovu službu. Jelikož broadcasting představuje zvýšené zatížení pro všechny stanice připojení k síti, vnáší Sockets pro jeho použití ještě dodatečné omezení: datagramy pro broadcast mohou být vysílány pouze ze socketů, které byly explicitně označeny jako umožňující broadcast. Toto označení se uskuteční nastavením volby SO_BROADCAST voláním funkce setsockopt().

V prostředí Internetu můžeme při směřování broadcastu na lokální síť zadat jako adresu příjemce konstantu INETADDR_BROADCAST. Adresu pro sítě nelokální je nutné vytvořit v souladu s konvencemi pro IP adresy - pole určující stanici v rámci sítě bude obsahovat samé jedničky. Je však třeba upozornit, že směřování broadcast datagramů na sítě jiné než lokální se nedoporučuje, protože routery zpravidla tento typ paketů (directed broadcast) filtrují.

1.2.6 Volby nastavitelné pro sockety

Každému socketu samostatně mohou být explicitně nastavovány různé parametry, definující rozličné aspekty jeho chování. Mezi takovéto parametry patří například již zmíněné nastavení blokujícího, resp. neblokujícho režimu, možnost použití broadcastingu a dále kupříkladu povolení aplikace Nagleova algoritmu pro TCP. Zajímavá je rovněž i možnost periodického zasílání "keep-alive" paketů pro udržování spojení v platnosti, nebo možnost ovlivnit, zda může být lokální adresa přidělená socketu využita současně znovu. Všechny takovéto parametry lze nastavovat funkcemi setsockopt(), fcntl() a ioctl(). Hodnoty nastavené funkcí setsockopt() je možné zpětně testovat využitím funkce getsockopt(). Funkce fcntl() a ioctl() byly původně zamýšleny pro použití v souvislosti s descriptory souborů, ale vzhledem k návaznosti rozhraní Sockets na souborový systém je řada jejich voleb použitelná i na sockety.

1.2.7 Mechanismus signálů

Jak již bylo naznačeno v předchozím textu, umí operační systém Unix zaregistrovat požadavek aplikace o vydání softwarového přerušení (signálu) v případě výskytu určité asynchronní události. V souvislosti se sockety bývá používána trojice signálů SIGIO, SIGURG a SIGPIPE. Signál SIGIO je vydáván vždy, kldyž je některý ze socketů přiřazených procesu (a explicitně označených pro takovéto chování) připraven pro asynchronní I/O operaci. Podobně signálem SIGURG systém informuje o výskytu zvláštní události na socketu - zpravidla o příchodu urgentních dat. Signál SIGPIPE může být vydán při I/O operaci v případě, že kanál není připraven pro transfer dat - obvykle je aktivován při požadavku o přenos po zrušení spojení. Pokud má každý jednotlivý socket indikovat zvláštní stavy signálem SIGIO, resp. SIGURG, musí k tomu být nejprve připraven funkcemi fcntl() a ioctl().

1.2.8 Jednotné řazení bajtů číselných položek

Protože protokol IP (a z něj vycházející TCP a UDP) je implementován řadou platforem s často velmi rozdílnou architekturou, bylo třeba pro číselné položky zadefinovat formát, ve kterém budou řazeny jednotlivé bajty jejich binární reprezentace. Toto řazení bývá označováno jako Network Byte Ordering (NBO). Jedná se o formát vlastní například procesorům řady Motorola 68000, kdy významnější bajt je ukládán vždy jako první. Tato reprezentace se liší od konvencí přijatých na procesorech Intel nebo na počítačích DEC VAX, kde je standardně jako první ukládán bajt méně významný. Z pohledu uživatele Sockets v prostředí Internetu je významné především dbát na formát položky port ve specifikaci socketu - tato položka je v NBO. V souvislosti s tím je třeba upozornit, že identifikace rodiny protokolů je ve formátu vlastnímu platformě, na které se program provádí - to proto, že tento identifikátor se nikdy nevkládá do paketů směřovaných na síť. IP adresa je ukládána vždy ve směru jejího čtení, tedy bajt umístěný nejvíce vlevo se ukládá na nejnižší adresu.

Aby aplikace mohla být na úrovni zdrojového kódu přenositelná na různé platformy, je třeba se vyhnout vlastní implementaci konverzních rutin. Místo toho se má používat souboru konverzních funkcí, poskytovaných k tomuto účelu rozhraním Sockets. Jedná se především o funkce provádějící vzájemný převod čísel small a long mezi formátem platformy a NBO. Symbolicky jsou tyto funkce pojmenovány ntohl (network-to-host-long), resp. htonl (host-to-network-long) a podle stejného schématu ntohs a htons pro čísla typu short. Knihovny rozhraní Sockets poskytované platformou, na níž je program překládán, implementují konverzní funkce s ohledem na to, jaký formát je používán pro ukládání čísel na dané platformě.

1.2.9 Databázové funkce

Pro pohodlí uživatelů poskytuje rozhraní Sockets mechanismy, pomocí nichž je možné vzájemně mapovat číselné konstanty na symbolické názvy služeb, sítí, protokolů a stanic. Jedná se o funkce typu getXbyY(), které byly původně v Unixu koncipovány univerzálně pro vyhledávání v databázových souborech textového formátu. Rozhraní Sockets předpokládá přítomnost sady informačních souborů v adresáři /etc : jedná se o soubory hosts, protocols, services a networks. Soubor hosts obsahuje dvojice, přiřazující každému symbolickému jménu stanice její IP adresu. V současnosti je mechanismus vzájemného mapování jména na IP adresu nahrazen využitím služeb DNS serveru. Soubor protocols přiřazuje jednotlivým symbolickým jménům přenosových protokolů konstanty, kterých lze použít jako parametry při volání funkcí rozhraní Sockets. Podobně soubor services přiřazuje jednotlivým službám poskytovaným různými servery na Internetu čísla "well-known" portů (TCP i UDP), určených pro komunikaci s procesy těchto služeb. Poněkud méně využívaným je dnes soubor networks, jehož položky definují přiřazení symbolických jmen sítí jejich IP adresám.

Pro každý z těchto souborů existuje dvojice funkcí, která dovoluje vyhledávat položku podle zadaného klíče. Takto můžeme vyhledávat například číslo portu služby podle jména funkcí getservbyname() a opačně název funkce podle portu funkcí getservbyport(). Podobné funkce jsou zavedeny i pro vyhledávání v ostatních souborech. Navíc existuje mechanismus, který dovoluje procházet položky jednotlivých souborů sekvenčně, bez nutnosti neustálého otevírání a zavírání souboru.

Soubor hosts bývá bez dalších rozšíření používán jen ve velmi omezených implementacích. Bez použití jiného mechanismu totiž musejí být všechna používaná symbolická jména a jim odpovídající IP adresy do souboru vložena ručně. Protože v tak rozlehlé síti, jako je Internet, je něco takového nemyslitelné, využívají funkce pro vyhledávání jména a IP adresy (gethostbyname(), resp. gethostbyaddr()) služeb tzv. Domain Name Serveru, poskytujícího přístup k distribuované databázi systému doménových jmen, používanému na Internetu a specifikovanému v [RFC1034] a [RFC1035]. Soubor /etc/hosts však bývá konzultován přednostně.

1.2.10 Detekce chyb

Každá z funkcí rozhraní Sockets může z rozličných důvodů skončit neúspěšně. Většina funkcí vrací hodnotu int, která určuje, zda byla funkce úspěšná. Nulová hodnota indikuje úspěch, hodnota -1 neúspěch. Funkce, jejichž návratová hodnota je typu char*, vracejí při neúspěšném ukončení nulový pointer (NULL).

Jestliže aplikace detekuje, že posledně volaná funkce Sockets nebyla úspěšná, může se o příčině chyby dozvědět více analýzou chybového kódu uloženého v globální proměnné errno. Jedná se o stejný mechanismus, jakým jsou indikovány chybové stavy u ostatních volání jádra Unixu.

Určitou zvláštností je indikace chyb funkcí, které pracují nad databázemi. Jestliže je takovouto funkcí indikována chyba, lze chybový kód získat z (globální) proměnné h_errno, namísto z proměnné errno. Je třeba zdůraznit, že stav těchto proměnných se mění vždy po volání funkce jádra, proto je třeba příčiny chyby analyzovat vždy bezprostředně za voláním funkce, která vedla k chybovému stavu.

1.2.11 Funkce rozhraní

Funkce rozhraní Sockets můžeme rozdělit do třech skupin:

Funkce hlavní skupiny budou rozebrány poněkud podrobněji, protože je to nutné k pochopení sémantiky rozhraní Sockets a typických sekvencí pro obvyklé komunikační úlohy. U ostatních funkcí bude vždy uvedena jen jejich hlavička a stručný popis určení, detaily mohou být zjištěny z tištěné dokumentace nebo manuálových stránek kteréhokoli systému Unix, který rozhraní Sockets podporuje. Deklarace hlaviček funkcí a pomocných konstant použitých v rozhraní Sockets se standardně nalézají v následujících hlavičkových souborech:

	netdb.h
	arpa/inet.h
	sys/time.h
	sys/socket.h
	netinet/in.h

1.2.11.1 Hlavní funkce

int socket ( int pf, int type, int protocol );

Funkce socket() bývá volána jako první v pořadí a slouží k vytvoření nového socketu požadovaného typu. Jako parametr se předává požadovaná rodina protokolů implikující doménu, typ socketu specifikující požadovanou službu a protokol v rámci rodiny protokolů. Za poslední parametr se obvykle dosazuje nula, což zapříčiní automatický výběr vhodného protokolu pro požadovaný typ služby. Výjimkou je explicitní specifikace protokolu pro sockety typu "raw". Výsledkem funkce je deskriptor odkazující na nově vytvořený socket nebo hodnota -1, jestliže socket nebylo možné vytvořit

Většina implementací dovoluje specifikovat alespoň rodiny protokolů PF_UNIX pro lokální meziprocesovou komunikaci prostřednictvím souborového systému a PF_INET pro komunikační doménu Internetu. Z typů služeb je vždy k dispozici datagramová služba (SOCK_DGRAM) a virtuální spojení (SOCK_STREAM) a pro procesy superuživatele také SOCK_RAW, poskytující přístup k síťové a nižším vrstvám protokolového stacku.

int bind ( int sock, struct sockaddr* name, int namelen);

K přiřazení lokální adresy socketu, který dosud tuto adresu přiřazenu nemá, může být použito funkce bind(). Socket pro přiřazení adresy je identifikován deskriptorem sock a adresa je uložena ve struktuře odkazované pointrem name. Parametr namelen definuje délku struktury name. Například pro prostředí Internetu se použije výrazu sizeof(sockaddr_in).

Pro stanice, kterým je přidělen větší počet IP adres (multihomed hosts) je možné použít pro adresu konstanty INETADDR_ANY. Ačkoli se implementace této možnosti poněkud odlišují, nejčastěji je výběr skutečné IP adresy použité jako adresa odesílatele odložen až na dobu, kdy je zřizován virtuální kanál voláním funkce connect(). To té doby zpravidla také nemá význam se na konkrétní adresu dotazovat voláním funkce getsockname(). Co se týče datagramové služby, předávají se aplikaci veškeré datagramy, u kterých se adresa příjemce shoduje s kteroukoli adresou "multihomed" stanice.

Je třeba upozornit, že některé implementace neumožní přiřadit socketu jinou lokální adresu, než je skutečně vyhrazena pro danou stanici. Ve výjimečných situacích pak nelze přiřadit ani adresu typu loopback, podle konvence zapisovanou jako 127.0.0.1. V takovémto případě je třeba buď skutečnou IP adresu stanice nejprve explicitně zjistit a pak ji přiřadit příslušnému socketu, nebo použít konstanty INETADDR_ANY.

int connect ( int sock, struct sockaddr* name, int namelen );

Funkce connect() se významově liší podle typu socketu, na který je aplikována. V případě socketu typu SOCK_STREAM zřizuje virtuální spojení se vzdálenou stanicí identifikovanou adresou name. Pokud je aplikována na socket typu SOCK_DGRAM, specifikuje se tím adresa, na kterou budou všechny další datagramy z tohoto socketu odesílány a ze které budou (výhradně) přijímány.

Zajímavé je chování funkce connect() na neblokujícím socketu typu SOCK_DGRAM. Volání connect() totiž obecně vede na posloupnost časově dosti náročných akcí, spojených s navazováním virtuálního spojení. To ovšem téměř jistě nebude možno provést v takovém čase, aby neblokující funkce mohla na navázání spojení počkat. Bude tedy indikována chyba EWOULDBLOCK, která oznamuje, že funkce nemůže být bez čekání dokončena. Narozdíl od jiných funkcí (jako např. recv()) však zde tento chybový kód neznamená, že operace byla zrušena. Proces navazování spojení naopak pokračuje paralelně a je na zodpovědnosti aplikace detekovat okamžik, kdy spojení bylo navázáno, nebo kdy transportní vrstva rozhodla o nedostupnosti partnera. Navázání spojení, resp. informaci o nedostupnosti volaného, lze detekovat jednak jako asynchronní událost s využitím signálů a jednak pollingem. Při použití pollingu voláme select() tak dlouho, až testovaný socket projde podmínkou pro přítomnost v množině socketů, na které lze zapisovat (při úspěšním spojení) nebo v množině socketů nacházejících se v chybovém stavu. Druhá možnost nastane tehdy, když pokus o navázání spojení nebyl úspěšný.

int read( int fd, char* buf, unsigned buflen);
int write( int fd, char* buf, unsigned buflen);

Ve spojení se sockety je možno použít stejných I/O funkcí používaných běžně pro souborové operace. Protože tyto funkce neumožňují specifikovat adresáta ani odesílatele, předpokládá se, že socketu bude před jejich použitím přiřazena lokální a vzdálená adresa funkcemi bind() a connect(). Namísto obvyklého deskriptoru souboru lze těmto funkcím předat i deskriptor socketu.

Ke stejnému účelu jako write() a read() slouží také funkce writev(), resp. readv(). Jediný rozdíl spočívá v tom, že uvedené funkce dovoluje specifikovat pro odesílaná/čtená data více bufferů, čímž se obejde vícenásobné volání jádra pro každý buffer zvlášť. To může přinést jistou časovou úsporu, protože přepnutí do privilegované úrovně při každém volání jádra je na některých architekturách časově dosti náročnou operací.

K diskutované skupině funkcí bývá často zařazována i sendmsg(), která mimo samotných dat navíc přenáší i identifikaci přístupových práv. To je však použitelné pouze v komunikační doméně lokálního prostředí Unixu, věšinou v souvislosti s rourami. Párovou funkcí k sendmsg() je funkce recvmsg().

int send( int sock, char* buf, int buflen, int flags);
int recv( int sock, char* buf, int buflen, int flags);

Dvojice funkcí send() a recv() je obdobou read() a write() s tím, že dovolují specifikovat přídavný parametr, který má význam jen v souvislosti s rozhraním Sockets. Tento parametr zavádí možnost předání příznaků, které v současné verzi (1) identifikují urgentní data a (2) umožňují obejít standardní mechanismus routingu. Použitelný je ještě (3) příznak MSG_PEEK, který dovoluje předečtení určitého množství dat, aniž by při tom byla data odstraněna ze vstupní fronty.

V případě, že adresa vzdálené a lokální stanice byla dříve socketu přiřazená explicitně, lze diskutované funkce použít i nad sockety typu SOCK_DGRAM.

int sendto ( int sock, char* buf, int len, int flags, struct sockaddr* to, int tolen );
int recvfrom ( int sock, char* buf, int len, int flags, struct sockaddr* from, int* fromlen );

Pro použití v souvislosti s datagramovou službou je k dispozici pár funkcí recvfrom() a sendto(). Jejich využití odpovídá výše popsaným funkcím send() a recv(), avšak s tím rozdílem, že adresa vzdálené stanice je při volání vždy zadávána explicitně.

Při vysílání datagramů ze socketu typu SOCK_DGRAM nesmí být překročena maximální délka IP paketu podporovaná síťovou vrstvou pro použitý interface. Jestliže k překročení dojde, žádná data vyslána nebudou a funkce se vrací s chybovým stavem EMSGSIZE.

Výjimečně je možné popisované dvojice funkcí použít i ve spojitosti se sockety typu SOCK_STREAM s tím, že parametry to, resp. from jsou ignorovány a namísto nich je použito adres, které byly pro socket dříve nastaveny příslušnými funkcemi.

int listen( int sock, int backlog );

Funkce listen() je určena pro uvedení socketu do stavu listening, tedy k jeho přípravě k přebírání žádosti o zřízení spojení (passive open). Parametr backlog specifikuje délku fronty, do níž se budou ukládat žádosti, které není možné vyřídit okamžitě (z historických důvodů je maximální hodnotou parametru backlog zpravidla 5). Případné další žádosti, které již nebude možné do fronty umístit, budou odmítány.

Řízení je aplikaci z funkce listen() vráceno okamžitě - pouze se vytvoří vstupní fronta a o úmyslu přijímat spojení se informují nižší vrstvy.

int accept ( int sock, struct sockaddr* addr, int* addrlen );

Funkce accept() slouží pro extrakci požadavků o spojení z fronty socketu, který byl předtím uveden do stavu listening. Pokud je takový požadavek k dispozici, vytvoří funkce nový socket se stejnými parametry, jako má socket, z jehož fronty byl požadavek na spojení extrahován. Jako výsledek vrátí deskriptor tohoto socketu. Volitelný parametr addr umožňuje aplikaci zachytit adresu stanice, která zřízení spojení požaduje. Parametr addrlen specifikuje délku bufferu, do něhož má být adresa volajícího uložena. Jestliže je buffer kratší, než by bylo třeba pro uložení adresy určitého typu, končí funkce chybovým stavem. Při úspěšném akceptování požadavku o spojení je buffer pro adresu naplněn a hodnota odkazovaná parametrem addlen upravena na skutečnou délku dat (adresy) obsažených v bufferu. Jestliže si aplikace nepřeje adresu volající stanice uchovat, může přiřadit parametru addrlen hodnotu 0.

Pokud je accept() volán v situaci, že žádný požadavek o spojení není k dispozici, bude podle režimu socketu proces zablokován, nebo se funkce vrátí s indikací chyby EWOULDBLOCK.

int getsockopt ( int sock, int level, int optname, char* optval, int* optlen );
int setsockopt ( int sock, int level, int optname, char* optval, int optlen );

Dvojice funkcí getsockopt() a setsockopt() slouží k nastavování parametrů jednotlivých socketů a zpětnému zjišťování jejich hodnot. Parametry jsou rozděleny do několika úrovní podle toho, kterou vrstvu protokolového stacku ovlivňují. Úroveň je dána hodnotou parametru level; mezi používané úrovně patří zejména SOL_SOCKET a IPPROTO_TCP. Parametr optname specifikuje konkrétní parametr v rámci dané úrovně. Protože hodnoty nastavovaných parametrů mohou být obecně různého typu, předávají se v bufferu odkazovaném parametrem optval s délkou optlen. U funkce getsockopt() dochází navíc k aktualizaci délky bufferu podle skutečného množství vrácených dat.

Na úrovni SOL_SOCKET jsou definovány následující volby:

Z voleb úrovně IPPROTO_TCP je podstatná volba TCP_NODELAY, která na daném socketu vyřazuje použití Nagleova algoritmu. Nagleův algoritmus slouží ke snížení zatížení sítě. Spočívá v tom, že data předávána jednotlivými voláními k vyslání na virtuální kanál nejsou vysílána okamžitě, ale shromažďována v bufferu a vyslána až tehdy, když se nasbírá takové množství dat, které zaplní TCP segment s určitou délkou (nebo vyprší timeout). Tím se omezí posílání krátkých paketů, které by jednak měly nevyhovující poměr významové informace k informaci servisní (hlavičce) a jednak by zcela zbytečně vyžadovaly potvrzování každého krátkého segmentu samostatně. Tím by celkový objem režijních dat přenášených sítí vzrostl. Aplikace by za normálních okolností neměly mít důvod Nagleův algoritmus zablokovávat, pokud nepracují v podmínkách, kdy je doručení dat spolupracující aplikaci časově kritické. Vzhledem k časovým poměrům na Internetu však takováto možnost prakticky nepřipadá v úvahu .

Další volby je možné nastavovat a zjišťovat funkcemi fcntl() a ioctl().

int close (int sock);

Jestliže již socket nebude dále využíván, je třeba jej uzavřít voláním funkce close(). Pokud je rušen poslední odkaz na socket, vede úspěšné dokončení této oprace k uvolnění přiřazených systémových prostředků. V případě socketu typu SOCK_STREAM mu samozřejmě předchází soubor akcí vedoucích ke korektnímu zrušení virtuálního spojení.

V okamžiku uzavírání socketu se ještě ve frontě mohou vyskytovat data, která doposud transportní vrstva nestačila vyslat. Způsob obsluhy této situace lze pro každý socket volit pomocí nastavení volby SO_LINGER funkcí setsockopt(). U parametru LINGER (otálení) se specifikuje jednak zapnutí či vypnutí a v případě zapnutí i timeout, po který se bude transportní vrstva pokoušet operaci ukončení spojení dokončit korektním způsobem.

Jestliže je volba LINGER nastavena a doba timeoutu je nulová, funkce close() nebude nikdy proces blokovat. Případná nevyslaná data budou ztracena a virtuální spojení resetováno. Aplikace na vzdáleném konci bude o násilném (hard) uzavření spojení informována při volání funkce pro čtení ze socketu chybovým stavem ECONNRESET.

V případě nastavení volby LINGER s nenulovým timeoutem se funkce bude po dobu tohoto timeoutu pokoušet vyslat všechna data z fronty a spojení ukončit korektním způsobem (tzv. graceful disconnect). Tohoto způsobu uzavření však není možné použít v situaci, kdy je socket nastaven do neblokujícího režimu.

Poslední možností je situace, že volba LINGER nastavena není. Funkce close() tehdy vrátí řízení aplikaci okamžitě. Rozhraní Sockets se však bude i nadále (paralelně) pokoušet data z fronty vyslat a spojení uzavřít korektním způsobem. Systémové prostředky přiřazené socketu budou uvolněny až v okamžiku, kdy je korektního ukončení komunikace dosaženo. Protože tato operace může v kritických situacích trvat velmi dlouho, aplikace musí při tomto způsobu uzavírání počítat s tím, že až do doby dealokace systémových prostředků spojených se socketem nebude moci vytvořit nový socket s parametry, jako měl socket právě zrušený.

int getpeername( int sock, struct sockaddr* name, int* namelen);

Jestliže aplikace potřebuje zjistit adresu vzdáleného účastníka spojení, může k tomu využít funkce getpeername(). Parametrem funkce je jednak buffer určený pro uložení adresy a jednak ukazatel na hodnotu, která na počátku udává počet bajtů, který je pro buffer k dispozici. V případě úspěchu volání je tato hodnota modifikována na skutečný počet bajtů, který adresa uložená v bufferu zaujímá. Funkce může být aplikována jednak pro zpětné zjištění vzdálené adresy u socketu, kterému tato adresa byla dříve přiřazena funkcí connect(), jednak na socket, jenž byl získán při volání funkce accept() a požadavek na zjištění adresy vzdálené stanice vyvstane dodatečně.

int getsockname( int sock, struct sockaddr* name, int* namelen);

Funkce getsockname() může být použita pro zjištění lokální adresy přiřazené socketu (toto přiřazení může být důsledkem volání bind() nebo accept()). Parametr name definuje buffer pro uložení adresy, namelen definuje počáteční délku bufferu, která je po vykonání funkce aktualizována podle skutečného počet bajtů, který adresa zaujímá.

int shutdown ( int sock, int how );

Pro částečné uzavření virtuálního spojení (tzv. half-close) je možné využít funkce shutdown(). Plně duplexní TCP spojení totiž nemusí být uzavřeno úplně, ale pouze jeden ze směrů, pokud je známo, že tímto směrem již nebudou přenášená žádná data. Způsob uzavření spojení je specifikován parametrem how. Hodnotou 0 se oznamuje, že na socketu již nebudou přijímána další data. Nižší vrstvy protokolového stacku nebudou touto informací ovlivněny. Volání s parametrem 1 informuje, že již nebudou vysílána žádná data - v případě TCP spojení dojde k vyslání FIN segmentu a přechodu do stavu half-close. Poslední možnou hodnotou parametru how je 2, která způsobí zákaz dalšího příjmu i vysílání na daném socketu - RESET spojení.

Ačkoli užíváno velmi zřídka, je uzavření jednoho směru spojení vhodné pro indikaci konce vysílaných dat při zachování spojení schopného přijímat data. Toho lze výhodně použít například v souvislosti s aplikací remote shell (rsh), která dovoluje provést příkaz na vzdáleném počítači, v situaci, že je požadováno přesměrování standardního vstupu vzdálené aplikace na některý ze souborů lokálního souborového systému. Uzavřením jednoho směru spojení je vzdálené aplikaci indikován konec vstupních dat s tím, že spojení může být dále použito pro přečtení výsledků ze vzdálené aplikace.

long select ( int nfds, fd_set* readfds, fd_set* writefds, fd_set* exceptfds, struct timeval* timeout );

Funkce select() se používá především při současné komunikaci s více vzdálenými stanicemi. Je určena k ověření, které ze zadaných socketů jsou připraveny pro příjem dat (resp. obsahují požadavek o spojení), zápis dat (resp. úspěšně navázaly spojení) nebo se nacházejí v chybovém stavu. Socket se ověřuje na výskyt některé z uvedené události zařazením do odpovídající množiny určované jedním z parametrů readfds, writefds nebo exceptfds. Pro manipulaci s těmito množinami jsou definovaná pomocná makra (FD_ZERO, FD_SET, FD_CLR a FD_ISSET). Ve většině případů bývají tyto množiny implementovány jako bitová pole, ve kterých každá pozice odpovídá jednomu deskriptoru socketu. Před voláním funkce se zařadí sockety, u nichž se mají jednotlivé typy události ověřovat, do odpovídajících množin. Po návratu jsou z množin odstraněny ty deskriptory, které podmínkám příslušnosti v té které množině nevyhovují. Funkce vrací celkový počet socketů (ze všech množin), které splnily ověřovanou podmínku.

Funkce vrací řízení v okamžiku, kdy je podmínka splněna alespoň pro jeden socket nebo vyprší časový limit daný parametrem timeout. Pokud timeout ukazuje na strukturu timeval, obsahující nulové hodnoty, vrací se řízení aplikaci okamžitě po ověření stavů všech zadaných socketů. Tototo postupu může být využito pro polling. Jestliže si jako hodnota parametru timeout použije NULL, časový limit se považuje za neomezený a aplikace je zablokována až do okamžiku, kdy se splní podmínka pro některý ze zadaných socketů.

int socketpair( int pf, int type, int proto, int sockets[2]);

Funkce vrátí dvojici propojených nepojmenovaných socketů pro zadanou doménu, typ služby a protokol. Její nejčastější využití je pro ladící účely.

1.2.11.2 Funkce pro převod čísel mezi formátem platformy a NBO

u_long htonl (u_long hostlong);
- převádí číslo long z formátu platformy do NBO

u_short htons (u_short hostshort);
- převádí číslo short z formátu platformy do NBO

unsigned long inet_addr (char* cp);
- převádí IP adresu v textové "tečkové" notaci na hodnotu long v NBO. V případě syntaktické chyby vrací hodnotu -1, která neodpovídá žádné regulérní IP adrese.

char* inet_ntoa (struct in_addr in);
- převádí IP adresu v binární podobě (4 bajty) do čitelné textové "tečkové" notace.

u_long ntohl (u_long netlong);
- převádí číslo long z NBO do formátu platformy

u_short ntohs (u_short netshort);
- převádí číslo short z NBO do formátu platformy

1.2.11.3 Funkce pro zacházení s IP adresou

int inet lnaof(struct in_addr inetaddr);
- z Internetovské adresy extrahuje adresu lokální sítě (s ohledem na masku subsítě)

int inet_netof(truct in_addr inetaddr);
- z Internetovské adresy extrahuje číslo sítě

u_long inet_network(char* dotstring);
- převede textový zápis čísla sítě na číslo sítě podle konvencí pro Internetovské adresy

struct in_addr inet_makeaddr(int net, int localaddr);

- z čísla sítě a adresy stanice vytvoří Internetovskou adresu

1.2.11.4 Funkce pro identifikaci lokální stanice

int gethostname (char* name, int namelen);

Nakopíruje do zadaného bufferu jméno přiřazené stanici, na které je program prováděn.

int sethostname(char* name, int namelen);
Dovoluje procesům superuživatele nastavit jméno přiřazené dané stanici. Funkce se volá během zavádění operačního systému.

long gethostid()

Vrací unikátní 32-bitový identifikátor daného procesoru. Zpravidla bývá k tomuto účelu užívána IP adresa.

int sethostid(long hostid)
Dovoluje nastavit identifikaci daného procesoru. Obvykle se jako identifikátor používá IP adresy. Funkce bývá k dispozici pouze procesům superuživatele.

1.2.11.5 Funkce pro vyhledávání v databázích a DNS

struct hostent* gethostbyaddr(char* addr, int len, int type);

Funkce vyhledá k zadané adrese doménové jméno stanice. Ve struktuře hostent odkazované vraceným ukazatelem je navíc uveden seznam alias jmen pro zadanou stanici. Jestliže vyhledávání nebylo úspěšné, funkce vrací nulový pointer. Způsob práce funkce je závislý na šíři Implementace rozhraní - buď se informace vyhledává v souboru /etc/hosts, nebo je k tomu účelu využito služeb DNS serveru.

struct hostent* gethostbyname(char* name);

Dohledá k zadanému doménovému jménu stanice seznam adres, které jsou této stanici přiřazeny. Další charakteristika je shodná s funkcí gethostbyaddr().

struct netent* getnetbyaddr(int netnumber, int type);

Ze souboru /etc/networks na základě čísla sítě vyhledá informaci o oficiálním a alias jménech dané sítě.

struct netent* getnetbyname(char* name);

Ze souboru /etc/networks se na základě symbolického jména sítě dohledá informace o této síti.

struct servent* getservbyport(int port, char* proto);

Funkce nalezne k zadanému číslu portu a symbolickému jménu protokolu informace o službě, které jsou zadané parametry přiřazeny. Vyhledává se v databázi /etc/services. Jestliže je namísto odkazu na jméno protokolu zadán nulový pointer, vyhledá se první záznam o službě s odpovídajícím číslem portu bez ohledu na užívaný protokol.

struct servent* getservbyname(char* name, char* proto);

Nalezne k zadanému symbolickému jménu služby a užívaného protokolu číslo portu, který je pro provozování dané služby na daném protokolu vyhrazen. Opak služby getservbyport().

struct protoent* getprotobynumber(int proto);

K zadanému číslu protokolu dohledá jeho oficiální název a množinu alias jmen.

struct protoent* getprotobyname(char* name);

K zadanému názvu protokolu dohledá jeho číslo a množinu alias jmen.

1.2.11.6 Funkce pro skupinový a sekvenční přístup k záznamům databází

Jestliže si aplikace přeje postupně zpracovat větší počet záznamů z databází hosts, protocols, services nebo networks, může k tomu využít funkcí, jejichž použitím se vyhne potřebě neustálého otevírání a zavírání souboru, čímž se operace podstatně urychlí. Ke každé z databází existuje trojice funkcí setXXXent(), entXXXent() a getXXXent(), jejichž deklarace vypadají následovně:

	sethostent(int stayopen);
	servent(int stayopen);
	setprotoent(int stayopen);
	setnetent(int stayopen);
	struct hostent* gethostent();
	struct servent* getservent();
	struct protoent* getprotoent();
	struct netent* getnetent();
	endhostent();
	endservent();
	endprotoent();
	endnetent();
 

Funkce skupiny setXXXent() specifikují, zda má být soubor mezi jednotlivými voláními příslušných funkcí getXbyY() vždy znovy otevírán a uzavírán. Nenulovou hodnotou parametru stayopen je možné docílit toho, že soubor bude až do explicitního uzavření pomocí funkce třídy endXXXent() ponechán otevřený. Je třeba upozornit, že v implementacích Sockets, které zajišťují vzájemné mapování doménových jmen na IP adresy prostřednictvím DNS, je význam parametru stayopen u funkce sethostent() poněkud odlišný - vztahuje se k udržování TCP spojení s DNS serverem. Protože v případě zatíženého DNS serveru může být navázání spojení časově značně náročné, je udržování takovéhoto spojení pro zrychlení běhu aplikace ještě daleko větším přínosem, než udržování otevřené lokální databáze.

Jestliže aplikace potřebuje zpracovat postupně všechny záznamy databáze, může k tomu použít funkcí skupiny getXXXent(). Před použitím takovéto funkce je třeba soubor otevřít a nastavit na začátek odpovídajícím voláním setXXXent(). Od této chvíle každé další volání funkce getXXXent() přečte následující řádek v databázi, z jeho informací vytvoří standardní strukturu a vrátí ukazatel na ni. Jestliže již není žádný další záznam k dispozici, vrací se nulový ukazatel. Po ukončení zpracování záznamů je třeba databázi explictně uzavřít voláním služby ze skupiny endXXXent().

1.2.12 Typické sekvence volání

V praxi se velmi často opakují typické úlohy, charakteristické pro různá používaná uspořádání komunikujících aplikací. Zkušenosti ukazují, že například při sestavování aplikací pro komunikaci s různými službami rozšířenými na Internetu zůstává kostra spodních vrstev programu vždy zachována a nový obsah se vnáší pouze implementací specifického softwarového protokolu. Proto zde budou ukázány typické sekvence volání pro aplikaci s funkcí serveru i klienta a to pro virtuální spojení i datagramovou službu.

Nejprve se věnujme vytvoření dvojice klient-server s použitím virtuálního spojení (SOCK_STREAM).

Program serveru sestává z následujících kroků:

  1. Voláním socket() se vytvoří socket typu SOCK_STREAM, na kterém budou přijímány žádosti o spojení ze strany klientů
  2. Funkcí bind() se tomuto socketu přiřadí lokální adresa
  3. Socket se převede pomocí funkce listen() do stavu listening. Od této chvíle se budou požadavky o spojení ukládat do fronty příslušející socketu
  4. Další postup je závislý na tom, zda aplikace hodlá použít blokujících nebo neblokujících socketů a zda předpokládá obsluhu více klientů paralelně. Vždy však v tomto místě následuje volání accept()

    V každém případě je při úspěšné extrakci požadavku o spojení funkcí accept() vrácen deskriptor nově vytvořeného socketu, pomocí něhož je již možné přímo zahájit komunikaci se vzdálenou stanicí. Nový socket přebírá parametry od socketu, z jehož fronty byl extrahován požadavek na zřízení spojení.

    Na tomto místě bývá často praktikováno rozštěpení procesu funkcí fork() na dva, z nichž starý pokračuje v očekávání dalších žádostí o spojení a nový zajistí další obsluhu klienta, jehož požadavek byl právě akceptován. Kód je pro oba procesy společný, proto musí být aplikace připravena ubírat se oběma cestami. Další chování procesu po rozštěpení se určuje z návratové hodnoty funkce fork() - je-li tato nulová, jedná se o proces potomka, zatímco jiná hodnota (ve skutečnosti nový pid) identifikuje původní proces. Rodičovský proces se vždy vrací k dalšímu volání accept(), zatímco proces potomka se začne věnovat komunikaci s klientem.

    Pokud je server koncipován jen k uspokojování požadavků určité skupiny klientů dané výčtem jejich IP adres, může po návratu z accept() prostřednictvím vrácené identifikace klienta ověřit, kdo vlastně o spojení požádal. Jestliže volající stanice není na seznamu podporovaných klientů, může server spojení okamžitě zrušit voláním funkce close().

  5. Na socketu získaném z funkce accept() nyní může po neomezenou dobu probíhat datová komunikace. Nejlépe se k tomu hodí služby send() a recv(), popřípadě read() a write().
  6. Uzavření spojení může být iniciováno jak serverem, tak klientem. Druhý případ je v praktických aplikacích rozšířenější. Stanice se o rozpojení spojení druhou stranou dozví při volání funkce recv() nebo některé se shodnou funkčností tím, že je vrácen nulový počet přečtených bajtů. V takovémto případě se volá funkce close() pro zrušení socketu a uvolnění systémových zdrojů.

Server může rovněž volat funkci close() jako první, pokud se rozhodne spojení aktivně ukončit.

Implementace klienta pro virtuální spojení je podstatně snazší. Je třeba provést následující operace:

  1. Funkcí socket() vytvořit nový socket typu SOCK_STREAM určený pro napojení na server
  2. Přidělit tomuto adresu lokální adresu funkcí bind()
  3. Voláním connect() navázat virtuální spojení se serverem. I zde je možné volit použití neblokujícího socketu a okamžik zřízení spojení nebo jeho odmítnutí testovat ve vlastní režii, ale protože doba, po kterou se transportní vrstva o zřízení spojení pokouší, je omezená rozumným timeoutem, je zpravidla jednodušší použít socketu blokujícího.
  4. Po úspěšném zřízení spojení může po neomezenou dobu probíhat datová komunikace prostřednictvím funkcí send() a recv(), resp. některé jiné dvojice sloužící ke stejnému účelu.
  5. Pro zrušení virtuálního spojení volá klient funkci close()

V případě použití datagramové služby je sekvence funkcí pro server i klienta víceméně shodná. Datagramová služba je totiž v podstatě symetrická a mezi chováním serveru a klienta není z jejího pohledu žádný rozdíl.

  1. Volání socket() se vytvoří nový socket typu SOCK_DGRAM.
  2. Socketu se přidělí lokální adresa voláním funkce bind()., Tato operace je nutná k tomu, aby vzdálená stanice byla schopna správně identifikovat odesílatele datagramu.
  3. Pokud budou datagramy z vytvořeného socketu směrovány vždy na tutéž vzdálenou stanici, je možné adresu této stanice jednou provždy určit voláním funkce connect(). Při další komunikaci již nemusí být adresa vzdálené stanice zadávána a může být využito i dvojic I/O funkcí, které specifikaci adresáta ve svých parametrech nemají. Pro příjem dat na socketu má volání connect() ten význam, že aplikace bude dostávat pouze datagramy, jejichž zdrojem je stanice určená tímto voláním jako vzdálená.
  4. Dále může po neomezenou dobu probíhat výměna datagramů. Jestliže nebylo použito volání connect() podle předchozího kroku, musí aplikace vždy znovu specifikovat adresáta, resp. odesílatele zprávy parametrem funkce sendto(), resp. recvfrom(). Tohoto způsobu se použije především v případě, že se komunikace střídavě účastní větší počet stanic současně.
  5. Jestliže byla komunikace ukončena, uvolní se systémové prostředky svázané se socketem voláním funkce close()

1.2.13 Komunikace mezi procesy v lokálním prostředí

V rozhraná Sockets může být komunikace mezi lokálními procesy zajišťována stejnými mechanismy jako síťová - je jejím zvláštním případem. Proto se všechny úvahy o komunikaci mezi stanicemi vztahují stejně dobře i na komunikaci skupiny procesů pracujících na stejné stanici. Z hlediska aplikací je tento pohled výhodný, protože se nemusejí zajímat o to, kde partnerské procesy ve skutečnosti pracují.

1.2.14 Příklad využití Berkeley Sockets v UNIXu

Jako příklad použití rozhraní Sockets uvádíme dvojici ukázkových aplikací pro přenos textového souboru pomocí virtuálního spojení. Jedná se o Text Server, který na vyhrazeném portu očekává požadavky klienta o přenos souborů a Text Client, jenž komunikuje s uživatelem i s Text Serverem a vypisuje obsah specifikovaných souborů ze souborového systému stanice-Text Serveru na konzole klienta. Program je napsán v jazyce C, pro jeho překlad bylo použito standardního překladače cc.

udpserver.c Simple UDP Server
udpclient.c Simple UDP Client

tcpserver.c Simple TCP Server
tcpclient.c Simple TCP Client

server.c Text Server
client.c Text Client