Vyšlo v týdeníku Computerworld č. 15/93 v roce 1993
Vytištěno z adresy: http://www.earchiv.cz/a93/a315c110.php3

Transportní rozhraní - BSD Sockets

V minulém dílu jsme dospěli k tomu, že implementace protokolů transportní vrstvy, stejně tak jako všech nižších vrstev, je součástí operačního systému, a služby těchto vrstev jsou jednotlivým aplikacím poskytovány pomocí takových mechanismů, které jsou v daném operačním systému k dispozici - a které samozřejmě mohou být v různých operačních systémech různé. Pak jsme se již zaměřili na prostředí operačního systému Unix a řekli si, že ten se zpočátku snažil uplatnit i na síť svůj jednotný pohled na všechna zařízení, která chápe jako speciální soubory, a jako se soubory s nimi také pracuje. Řekli jsme si však také, že právě v případě síťových služeb se původní prostředky pro práci se soubory záhy ukázaly být nedostačující, a proto musely být vyvinuty nové, obecnější mechanismy. Závěrem jsme si pak také naznačili, že v každé z obou hlavních větví Unixu byly za tímto účelem vytvořeny jiné mechanismy. Dnes si podrobněji ukážeme, jak je tomu v tzv. BSD Unixu.

Lidé, kteří dostali za úkol vyvinout nové mechanismy pro zpřístupnění protokolů TCP/IP v prostředí BSD Unixu, si svůj původní úkol poněkud rozšířili: rozhodli se vytvořit takový prostředek, který by nebyl "šit na míru" jedné konkrétní soustavě protokolů, tj. TCP/IP, ale byl použitelný obecně pro jakoukoli soustavu protokolů, a dokonce i pro více soustav protokolů současně. V době, kdy si protokoly TCP/IP v prostředí Unixu teprve hledaly své místo, to bylo rozhodnutí vcelku logické. V současné době, kdy TCP/IP protokoly Unix zcela ovládly, není tato možnost příliš využívána. Do budoucna by se ale zmíněné rozhodnutí mohlo ukázat jako velmi prozíravé. Pokud se totiž referenční model ISO/OSI opravdu prosadí do života, nebo se alespoň stane skutečnou konkurencí pro síťový model TCP/IP, bude začlenění ISO protokolů do BSD Unixu velmi jednoduché.

Větší obecnost nově vytvořeného mechanismu, umožňující zpřístupnit aplikacím přenosové služby různých soustav protokolů, však není zcela zadarmo. Cenou za tuto větší obecnost je samozřejmě poněkud větší komplikovanost použití. Kdyby šlo o mechanismus, určený pouze pro protokoly TCP/IP, mohl by například vždy sám předpokládat, že kdykoli se mu zadá nějaká adresa, jde o tzv. IP adresu (viz 44. díl seriálu). Takto je nutné mu to explicitně sdělit.

Co je vlastně socket?

Nový mechanismus pro zpřístupnění služeb transportních protokolů, vytvořený pro tzv. BSD Unix (tj. pro Unix, vyvíjený ve středisku Berkeley Software Distribution při University of Berkeley), je označován jako socket interface, nebo zkráceně jen jako sockets (doslova: objímky, zásuvky. V prvním přiblížení lze říci, že jde o zobecnění původního mechanismu Unixu pro práci se soubory, a že každý "socket" je vlastně koncovým bodem komunikace.

Abychom si ale mohli ukázat, v čem toto zobecnění spočívá, musíme se ještě vrátit k souborům a způsobu práce s nimi.

Když se nějaký proces rozhodne pracovat s určitým souborem, musí jej nejprve otevřít. Vlastní otevření však zajišťuje operační systém, a proto si provedení potřebné operace (operace open) proces vyžádá na operačním systému. Konkrétní forma je taková, že proces použije tzv. systémové volání (system call), neboli spustí podprogram, který je součástí operačního systému. Svůj konkrétní požadavek (který soubor má být otevřen, jakým způsobem atd.) proces specifikuje prostřednictvím parametrů tohoto volání. Vlastní otevření souboru pak probíhá plně v režii operačního systému, který si za tímto účelem založí vhodnou datovou strukturu, a v ní si udržuje vše, co k práci se souborem potřebuje.

Proces, který si otevření souboru vyžádal, pak ještě musí mít k dispozici prostředek, pomocí kterého při všech následných operacích nad již otevřeným souborem (tj. operacích read, write a close atd.) určí, kterého souboru se požadované operace týkají. Nejlépe by se k tomuto účelu hodila zmíněná datová struktura, ale ta je zcela ve správě operačního systému, který ji aplikačním procesům nezveřejňuje. Přesto ale umožňuje aplikačním procesům odkazovat se na tuto strukturu, a to prostřednictvím nepřímého ukazatele (který je ve skutečnosti celým číslem bez znaménka, a lze si jej představit jako index do tabulky, obsahující ukazatele na zmíněné datové struktury). Operace open proto vrací jako svůj výsledek celé číslo (tzv. deskriptor souboru, file descriptor), a ten pak používají operace read, write a close k určení souboru, ze kterého se má číst, do kterého se má zapisovat, resp. který má být uzavřen.

Nyní si již můžeme snadno naznačit, co je podstatou nového mechanismu "socket interface". Samotný socket není nic jiného, než jiná varianta výše popsané datové struktury, ve které si operační systém uchovává různé údaje (tentokráte pro potřeby komunikace v síti). Socket opět není přímo přístupný, ale aplikační procesy se na něj mohou odkazovat přesně stejným způsobem, jako na zmíněnou datovou strukturu při práci se soubory - tedy nepřímo, prostřednictvím celého čísla. To se nyní označuje jako deskriptor socketu (socket descriptor), ale svým datovým typem je totožné s deskriptorem souboru (jde o celé číslo bez znaménka).

Stejný je také způsob práce se sockety - prostřednictvím systémových volání. Samotný repertoár systémových volání pro práci se sockety je však samozřejmě jiný, než pro práci se soubory. BSD Unix se však snaží zachovávat maximální možnou slučitelnost obou mechanismů, tak aby se lišily jen tam, kde je to skutečně nutné. Proto například umožňuje používat operace read a write jak pro čtení a zápis dat do souborů, tak i pro jejich vlastní přenos po síti prostřednictvím socketů. Za tímto účelem mj. vybírá celá čísla, která přiděluje jako deskriptory, z jediné množiny. Nemůže se tudíž stát, aby se číselná hodnota deskriptoru souboru a deskriptoru socketu rovnala.

Vytváření socketů

Společnou vlastností obou mechanismů je skutečnost, že příslušné datové struktury vznikají až v okamžiku jejich skutečné potřeby - při otevírání souboru, resp. při řádosti o vytvoření socketu, a vytváří je operační systém na základě bezprostředního požadavku aplikačního procesu. Významný rozdíl je ale v tom, co všechny musí být v okamžiku vytváření příslušných datových struktur zadáno: v případě otevírání souborů to musí být explicitní údaj o konkrétním souboru, který má být otevřen. Naproti tomu při zřizování socketu se ještě neuvádí, s kým se bude jeho prostřednictvím komunikovat.

Tím se vychází vstříc potřebám nejrůznějších procesů, které vystupují v roli serverů, potřebují si nechat vytvořit socket pro komunikaci se svými klienty, ale teprve následně se dozvídají, kteří klienti s nimi budou chtít komunikovat.

Žádost o vytvoření nového socketu, kterou aplikační proces předává operačnímu systému formou systémového volání (s příznačným názvem socket) však ve svých parametrech obsahuje údaj o tom, s jakou soustavou protokolů bude socket pracovat (což mj. definuje význam následně zadávaných adres), dále druh spojení (spolehlivé spojované či nespolehlivé nespojované, případně spojení na nižší úrovni, než je transportní), a také konkrétní protokol z příslušné rodiny protokolů, který bude přenos zajišťovat (což pamatuje na případ, kdy požadovaný druh spojení může být v rámci zvolené soustavy protokolů realizován více alternativními protokoly).

Vazba socketu na lokální port

Jestliže jsme si již dříve uvedli, že na socket se můžeme dívat jako na koncový bod komunikace na daném uzlovém počítači, jaký je v tomto případě jeho vztah k portům? Jak jsme si uvedli v 55. dílu, je právě port koncovým bodem komunikace na rozhraní mezi transportní vrstvou a vrstvou aplikační.

Zde je ovšem třeba si uvědomit, že porty jsou jednotným konstruktem v rámci celého síťového modelu TCP/IP, zatímco sockety jsou jen jedním z možných prostředků pro zpřístupnění transportních služeb aplikačním procesům (tím, který je používán v BSD Unixu). Jednotnost portů ve všech uzlech umožňuje jednotné adresování entit aplikační vrstvy, bez ohledu na to, jaký konkrétní mechanismus používají tyto entity pro přístup ke službám transportní vrstvy. Jsou-li tímto mechanismem sockety, existuje mezi nimi a porty jednoznačná korespondence: každý socket odpovídá jednomu portu, resp. je svázán s právě jedním portem.

Tato vazba mezi portem a socketem však není automatická. Přesněji: nevzniká při vytvoření socketu (systémovým voláním socket), ale musí být vytvořena až následně, na explicitní příkaz (systémovým voláním bind).

Volba lokálního portu, na který má být socket navázán, může být pro některé aoplikační procesy irrelevantní, a tyto procesy pak mohou nechat volbu konkrétního portu na operačním systému. Jinak je tomu ale u procesů, které vystupují v roli serverů, a své služby poskytují na tzv. dobře známých portech (se kterými předem počítají potenciální klienti těchto služeb), viz 55. díl seriálu.

Vazba socketu na vzdálený port

Vytvoření vazby socketu na některý z lokálních portů na daném uzlovém počítači stále ještě nic neříká o tom, s kým se bude prostřednictvím daného socketu komunikovat - tj. s kterým jiným uzlovým počítačem a kterou aplikační entitou na tomto počítači. Za tímto účelem je BSD Unix vybaven dalším systémovým voláním (connect), kterému se jako parametr zadá adresa vzdáleného počítače a číslo portu na tomto počítači. Výsledný efekt systémového volání connect pak ovšem závisí na tom, jaký druh spojení byl specifikován při vytváření socketu (pomocí volání socket). Pokud šlo o spojení spojovaného charakteru, je v rámci volání connect spojení skutečně vytvořeno (tj. navázáno). Pokud jde o komunikaci nespojovaného charakteru, je volání connect nepovinné, a má vlastně jen vedlejší efekt v tom, že specifikuje adresu, na kterou budou následně odesílány jednotlivé datagramy - tak aby tato adresa nemusela být s každým datagramem zadávána vždy znovu.

Čekání na žádost o spojení

Aplikační proces, který sám iniciuje přenos dat, si tedy nejprve nechá vytvořit socket (voláním socket), pak jej sváže s určitým místním portem (voláním bind), a posléze i s portem na vzdáleném počítači (voláním connect), jde-li o spojovanou komunikaci. Pak již může přistoupit k vysílání či příjmu vlastních dat. Za tímto účelem má k dispozici hned několik variant operací read a write (realizovaných opět formou systémových volání), které jsou v principu shodné se čtením a zápisem dat do souborů.

Jak má ale postupovat proces, který sám komunikaci iniciovat nechce? Tedy například proces, který chce vystupovat v roli serveru, a chce přitom používat spojovaný charakter komunikace?

Také si musí nejprve nechat vytvořit potřebný socket (voláním socket), a pak jej navázat na vhodný lokální port (voláním bind). Pak ale musí uvést socket do takového stavu, který odpovídá pasivnímu otevření - tedy kdy socket (resp. na něj navázaný port) pouze pasivně čeká na přicházející žádosti o navázání spojení. Za tímto účelem je v BSD Unixu k dispozici další systémové volání listen, kterému se navíc jako parametr zadává, jak velkou frontu si má vytvořit pro přicházející volání či data.

Když je takto socket připraven přijímat žádosti o navázání spojení, musí začít čekat i samotný aplikační proces. K tomu má k dispozici další systémové volání accept.

Obrázek 62.1.
Obr. 62.1.: Představa práce se sockety při spojované komunikaci
Jakmile na port, na kterém proces "čeká", přijde nějaká žádost o navázání spojení, operační systém tuto žádost přijme, a vytvoří nový socket, který "naváže" na port volajícího - čímž vlastně naváže spojení mezi serverem a jeho klientem. Přes tento nový socket bude probíhat vlastní komunikace, zatímco na původním socketu budou dále očekávány požadavky na navázání spojení. Údaj o nově vytvořeném socketu přitom operační systém předává jako výstupní parametr volání accept. Aplikační proces v roli serveru pak buďto zajistí následnou komunikaci s klientem sám, a po jejím skončení nově vytvořený socket zase nechá zrušit, nebo si ke zpracování požadavku vytvoří samostatný podproces, a sám se věnuje jen čekání na další žádosti (volá accept).

Celý postup komunikace spojovaného charakteru ze strany serveru i klienta ukazuje obr. 62.1., včetně zrušení socketu voláním close. V případě nespojované komunikace je celý postup poněkud jednodušší - na straně serveru pak odpadají volání listen a accept, která se týkají navazování spojení, a na straně klienta nepovinné volání connect.