Hogyan működik az Internet?

Czirkos Zoltán · 2021.05.31.

Az Internet működéséről röviden, és a Pythonban használható hálózatprogramozás alapjairól.

Ez az írás röviden elmagyarázza az Internet működését biztosító technológiákat – kiemelve közben néhány olyan dolgot, amely a Programozás alapjai tárgyban is előkerült.

1. Az Internetre kötött számítógépek címei

Az Internet egy globális hálózat, ezért minden egyes számítógépnek egyedi címmel kell rendelkeznie, ha a többivel kommunikálni szeretne. Az Interneten használt címek az nnn.nnn.nnn.nnn formát öltik, ahol minden nnn szám egy 8 bites, előjel nélküli egész, azaz a 0…255 értéket veheti fel. Ezt IP címnek (IP address) nevezzük.

Egészen pontosan ezek IPv4 címnek, az Internet Protocol 4-es verziója szerinti azonosítók. Egyre több helyen használják már az IPv6-ot, amelynél a címek nem 32, hanem 128 bitesek. A 32 bites címek egészen egyszerűen mostanra elfogytak, és már évek óta különféle trükköket használnak, hogy több számítógép közösen használhasson egy címet.

IP címek

Az Internethez csatlakozva a géped valószínűleg automatikusan kapott egy ideiglenes IP címet, pl. a BME WiFi szolgáltatásán keresztül. Az infopy.eet.bme.hu szervernek pedig fix, dedikált címe van (úgyis 24 órában be van kapcsolva). Az IP cím látható parancssorból, a ping programmal, amely amúgy két gép közötti kapcsolat meglétét hivatott ellenőrizni:

rockford:~$ ping infopy.eet.bme.hu
PING infopy.eet.bme.hu (152.66.72.57) 56(84) bytes of data.
64 bytes from infopy.eet.bme.hu (152.66.72.57): icmp_req=1 ttl=55 time=38.0 ms

A ping program először feloldja (resolve) a szerver nevét (host name), hogy megkapja az IP címet (IP address). Ehhez a hálózaton elérhető DNS (Domain Name System) szolgáltatást használja. Így tudja meg, hogy az infopy.eet.bme.hu címhez a 152.66.72.57 cím tartozik. Ezután annak a gépnek egy PING üzenetet küld, amelyre a szerver válaszol is. A ping 152.66.72.57 parancsnak ugyanez lenne az eredménye.

2. Csomagok és protokollok

Mi történik akkor, amikor egy számítógép „beszélni” szeretne egy másikkal; például a te számítógéped, amelynek az IP címe 35.175.180.255, a „hello” üzenetet szeretné küldeni az infopy-nek, amelynek a címe 152.66.72.57?

A szövegtől előbb el kellene jutni valahogy oda, hogy elektromos jelek jelenjenek meg a vezetéken, vagy rádióhullámokat bocsájtson ki a géped antennája, amiket – haladjanak bármilyen eszközökön keresztül – végül vissza kellene alakítani az üzenetté. Ebben segítenek az egymásra épülő üzenetküldési protokollok (protocol stack), ugyanis még csak nem is egyről van szó. Ezek a protokollok határozzák meg, hogyan kell az üzenet betűit kódolni, hogyan lesznek abból a hálózaton közlekedő adatcsomagok, és hogy azok hogyan válnak végül elektromos jelekké. Ezeket minden számítógép operációs rendszere és hardvere tartalmazza. Nyilvánvalóan a két számítógépnek közös nyelvet kell beszélnie. A használt protokollok azonban eltérnek, attól függően, hogy milyen céljaink vannak az üzenetekkel.

Tekintsünk most el a fizikai szinttől (vezetékek, rádióhullámok), és nézzük csak azokat a protokollokat, amelyeknek jelentősége van a programjaink szempontjából. Lássunk egy példát! Indíts egy PuTTY-ot: ez nem csak arra jó, hogy más gépekre bejelentkezni, azokon dolgozni lehessen, hanem egy olyan segédprogramnak is használható, amellyel egy tetszőleges hálózati kapcsolat létrehozható.

PuTTY

A host name (or IP address) mezőbe írd be, hogy infopy.eet.bme.hu, a portszámhoz a 80-at. A portokra azért van szükség, hogy egy gépen egyszerre több hálózati kapcsolat is létrejöhessen. Ha a cél gépen több szolgáltatás is fut (pl. webszerver és levelezés), akkor a beérkező adatokról tudnia kell a gépnek, hogy azokat mely programoknak kell megkapniuk. A 80-as számú porton a webszerver szokott lenni. A connection type-nak add meg a „raw”-t. Az Open megnyomása után elvileg csatlakoztál a szerverhez – amely egyelőre szótlan. (Linuxosok: mindez megoldható az nc infopy.eet.bme.hu 80 parancssor begépelésével.) Írd be az alábbi két sort, és végül nyomj két entert! Erre letöltődik egy üzenet, amely a szervertől jön (és átirányít a titkosított oldalra).

GET / HTTP/1.1
Host: infopy.eet.bme.hu

HTTP/1.1 301 Moved Permanently
Server: nginx/1.14.0 (Ubuntu)
Content-Type: text/html
Content-Length: 194
Connection: keep-alive
Location: https://infopy.eet.bme.hu/

Mi történt? Úgy csináltunk, mintha egy webböngésző lennénk, és arra kértük az InfoPy szervert, hogy küldje el a főoldalt. Ehhez először alkalmazási szinten (mint egy böngészőprogram szintje) megfogalmaztuk a kérést, a HTTP (HyperText Transfer Protocol) nyelvén. Ezt a szöveget a PuTTY odaadta az operácios rendszerbe beépített TCP-nek (Transmission Control Protocol), amely a két alkalmazás (a PuTTY és a webszerver) közötti kapcsolat felépítéséért felel. Ez csomagokra bontja az üzenet szövegét, és továbbadja az IP-nek (Internet Protocol), amely a csomagok továbbításáért felel. Innen pedig a csomagok eljutnak a hardver eszközhöz.

A másik oldalon ugyanez történik, csak fordítva: az elektromos jeleket fogadó hardver eszköz az érkező csomagokat az IP-nek adja, amely továbbadja azt a TCP-nek; a TCP előállítja belőle az adatfolyamot, és végül az alkalmazás (amely a webszerver programja) értelmezi az üzenetet: GET / HTTP/1.1.

Egymásra utalt protokollok

Az egyes protokoll rétegek az üzenethez járulékos információt tesznek, például a címzett IP címét vagy a portszámot. Mire az üzenet a webszerverhez jut, ezeket a fogadó oldalon a protokollegyedek újra eltávolítják. Bár minden protokoll a felette és az alatta lévővel kommunikál közvetlenül, mégis az egyes rétegek számára úgy tűnik, mintha a velük egy szinten lévő társukkal beszélnének: a böngésző közvetlenül a webszerverrel. Amikor telefonon beszélünk, akkor is ehhez hasonló dolog történik: a hangunkból elektromos jel lesz, abból adatcsomagok, amelyeket a telefon egy központba küld. Onnan a csomagok eljutnak a másik telefonhoz, amely értelmezi azokat, és előállítja az eredetihez hasonló elektromos jelet, amiből végül újra hang lesz. Nekünk mégis olyan, mintha közvetlenül a hívottal beszélnénk.

3. Csomagkapcsolt és vonalkapcsolt átvitel

Az Interneten minden kommunikáció adatcsomagokból épül fel. Ezen csomagok mérete néhány tíz bájttól néhány tíz kilobájtig terjedhet. A csomagok továbbításáért az IP (Internet Protocol) felelős. A csomagban a tartalma mellett szerepel annak feladója és címzettje is: a két IP cím.

Fontos tudni azt, hogy az Internet kommunikációja csomagkapcsolt. Ez azt jelenti, hogy az IP számára a csomagok teljesen függetlenek, és az IP protokollt kezelő eszközök semmilyen információt nem jegyeznek meg a csomagokról. Beérkezik, fogadják, továbbküldik, és el is van felejtve. A csomagok viselkedését leginkább úgy lehet elképzelni, mint egy borítékba tett levél életét: megcímezzük, feladjuk, és a címzett egyszercsak megkapja. Nem garantált sem az, hogy megérkezik, sem az, ha több levelet adunk fel, akkor azok a feladás sorrendjében fognak megérkezni. Nem létezik kapcsolat felépítése és kapcsolat bontása művelet sem: minden levél egymástól független.

Egy fájl továbbításánál megengedhetetlen lenne a darabjainak elveszése vagy összekeveredése. Ilyenkor a vonalkapcsolt átvitel kellene, ami leginkább a telefonáláshoz hasonlít: a kapcsolatnak van állapota, mivel azt a kommunikáció előtt felépítjük, és a kommunikáció után lebontjuk. Egy vonalkapcsolt csatornán a küldött adatok garantáltan eredeti sorrendjükben, hiánytalanul érkeznek. Mivel az Internet természetét tekintve csomagkapcsolt, a vonalkapcsolt (-nak tűnő) kommunikációt egy, az IP fölé helyezett újabb protokollréteggel szokták biztosítani: éppen ez a TCP (transmission control protocol). A TCP a küldendő adatfolyamot csomagokra bontja, a csomagokat megszámozza, és így adja át az IP-nek. Minden csomagra nyugtát vár a címzettől. A címzett pedig a beérkező, számozott csomagokat sorba állítja, hogy abból az eredeti adatfolyamot rekonstruálhassa. Szükség esetén az elvesző csomagok újraküldéséről gondoskodik a TCP. Mindezt a TCP felett kommunikáló alkalmazások számára láthatatlanul történik.

Bizonyos feladatokhoz azonban ezek a szolgáltatások feleslegesek. Ha hangot továbbítunk, nem baj, ha elveszik vagy összekeveredik egy-két csomag. Sokkal nagyobb gond, ha egy kimaradó csomagra várni kezdünk, mert akkor megakad a hang. A TCP által biztosított megbízhatóságnak ára van, amit a sebességben és a késleltetésben fizetünk meg. Ezért ha a programoknak gyors üzenetküldésre van szükségük, amelynél azonban nem fontos a 100%-os megbízhatóság, akkor UDP-t (User Datagram Protocol) szokás használni. Ez egy nagyon egyszerű protokoll, amely alig biztosít többlet szolgáltatásokat az IP-hez képest: lényegében csak annyit, hogy ellenőrző összeget tesz a csomagba – legalább a hibás átvitelt detektálni lehessen.

Érdekesség: a mobiltelefonoknál a hang 4,6 ezredmásodperces darabokra van bontva. Minden csomag egy hangdarabkát tartalmaz. A fogadó oldal, ha azt érzékeli, hogy egy csomag kimaradt, egyszerűen az előtte lévőt duplázza. Elsőre talán meglepő, de ezt az emberi fül észre sem veszi, mert ennél sokkal lassabban változnak a beszédben képzett hangok. Ha a kimaradó csomagok helyét a telefon csönddel helyettesítené, az sokkal zavaróbb lenne!

4. A portok és kapcsolatok

Azért, hogy egy számítógépen ne csak egy hálózati program vagy szolgáltatás futhasson, minden TCP és UDP csomagot egy ún. portszámmal látnak el. Ez egy 16 bites, előjel nélküli egész szám (0…65535). Ilyen portszám tartozik az üzenet küldőjéhez és az üzenet címzettjéhez is.

UDP-nél ennek a működése a következő. Ha egy program adatot szeretne fogadni UDP-n, akkor választ egy portszámot, pl. az 1234-et. Ezt a választását jelzi az operációs rendszernek. Ilyenkor a megfelelő IP címre ÉS portszámra küldött UDP csomagokat az a program fogja megkapni. A küldő oldal (másik gép) is választ egy portszámot (vagy megkéri az operációs rendszert, hogy válasszon találomra egy szabad portot), és úgy küldi el az üzenetet. Így kapcsolódik össze a két program: IP1:port1 és IP2:port2 között mennek a csomagok. Az egyik irányban IP1:port1 a feladó, IP2:port2 a címzett (ezek az adatok szerepelnek az UDP fejlécével ellátott csomagban), a másik irányban pedig fordítva.

UDP

TCP-nél egy kicsit bonyolultabb, hiszen ott minden kapcsolatot meg kell különböztetni a vonalkapcsolt jelleg miatt. Ezért ott nem csak adatcsomagok vannak, hanem vezérlőcsomagok is. Itt is először az egyik gép (nevezzük szervernek) nyit egy portot, ami azt jelenti, hogy azon a porton fogadni tudja a bejövő kapcsolatokat. Ezután a hozzá csatlakozó kliens elindítja a kapcsolódási folyamatot egy olyan adatcsomaggal, amelyben a SYN jelzésű bit (ez a csomag 13. bájtjának 6. bitje) 1-be van állítva: synchronize. Erre a szerver válaszol neki egy SYN-ACK (synchronize+acknowledge) csomaggal, amelyben a 13. bájt 3. bitje is 1-be van állítva; mire a kliens egy újabb ACK csomaggal válaszol. Amint ez a 3-way handshake nevű folyamat megtörtént, a kapcsolat él, a kommunikáció bármelyik irányban történhet. A kapcsolat lebontása hasonlóan történik, csak FIN (finish) jelzésű csomagokkal. A TCP kapcsolatok vezérléséhez használt állapotgép állapotátmeneti gráfja megtalálható az erről szóló Wikipedia oldalon is.

TCP kapcsolat megnyitása
TCP kapcsolat megnyitása
TCP kapcsolat bezárása
TCP kapcsolat bezárása

A programunkban ezekkel a vezérlő üzenetekkel nem kell foglalkozni, kezeli őket az operációs rendszer. Egy kapcsolat felépítése szinte annyiból áll, mint egy fájl megnyitása. Egy connect() függvényhívás után, ha az sikerült, kapunk egy ún. socket-et, amelybe írhatunk, és amelyből olvashatunk. Amint a kapcsolat létrejött, a kliens/szerver megkülönböztetésnek sincsen már semmi jelentősége. A különbség a kettő között csak abban áll, hogy melyik fél kezdeményezi a kapcsolatot. Azért szokás a két felet kliensnek és szervernek nevezni, mert általában a kliens kapcsolódik a szerverhez azért, hogy annak valamilyen szolgáltatásait elérje, például az ott tárolt fájlokat letöltse.

5. Hálózatprogramozás

A hálózat programozását lehetővé tevő API (Application Programming Interface) először a BSD operációs rendszerben jelent meg. Ezeket a függvényeket mindegyik Unix típusú rendszer tartalmazza, és a Windows is átvette. Bár a kétféle rendszer függvényeit szinte ugyanúgy kell használni, apró különbségek azért vannak: hogyan jelzik a hibát, hogyan kell lekérdezni, pontosan milyen hiba történt, stb. A Python beépítve tartalmaz egy modult, amely megoldja ezeket. Ebben a hibákat (pl. sikertelen csatlakozás) kivételek jelzik.

Egy egyszerű szerver

Alább egy példaprogram látható egy szerver létrehozására. Ez a szerver egyetlen egy bejövő kapcsolatot fogad (többet nem), aztán vár egy üzenetet, aminek a fogadása után maga is küld egyet. Ez kipróbálható a PuTTY programmal is: ha fut a lenti szerver, akkor a localhost-ra (127.0.0.1) kell csatlakozni, a 2000-es portra.

A hálózatkezelő függvények bájttömböket várnak, amelyeket elküldenek és fogadnak. Ha sztringeket küldünk át, akkor azokat kódolni kell – lásd a karakterkódolásokról szóló írást.

import socket

# bejövő kapcsolatokat fogadó socket létrehozása
# AF_INET - address family, IPv4
# SOCK_STREAM - adatfolyam, TCP kapcsolat
listeningsocket = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
listeningsocket.bind(('', 2000))
listeningsocket.listen()

# bejövő kapcsolatra várakozás, és annak fogadása.
# ha megvolt, a bejövőkre várakozó socket már nem kell
# (most csak egy kapcsolatot fogadunk)
clientsocket, address = listeningsocket.accept()
listeningsocket.close()
print("Bejövő kapcsolat innen:", address)

# bejövő kapcsolaton kommunikáció: max 1024
# bájt fogadása, és sztringgé alakítás
bytearr = clientsocket.recv(1024)
message = str(bytearr, encoding="UTF-8")
print("Ezt küldte a kliens:", message)

# válasz küldése
message = "Válasz üzenet"
bytearr = message.encode(encoding="UTF-8")
clientsocket.send(bytearr)
clientsocket.close()

Gyakran pongyolán egy gép IP címéről beszélünk. Igazából ez egy félreértés; nem a számítógépnek van IP címe, hanem a hálózati eszköznek. Ha a vezetékes hálózat is, és a Wi-Fi is aktív egyszerre, akkor a gépünknek épp két IP címe van (vagy még több). Minden operációs rendszer szokott egy 127.0.0.1 IP című, fiktív hálózati eszközt is biztosítani: ezen az ún. loopback eszközön a számítógép saját magát látja. Ez nagyban megkönnyíti a hálózatos programok tesztelését is, hiszen így nincs több számítógépre szükség.

Egyszerű kliens

Egy egyszerű kliens pedig valahogy így nézhet ki. Ez létrehoz egy kapcsolatot, utána kér egy sort a felhasználótól. Ezt elküldi a szervernek, aztán vár egy üzenetre, amit a szerver küld.

import socket

# kapcsolódás a localhost:2000 címre
serversocket = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
serversocket.connect(("localhost", 2000))
print("Kapcsolódva")

# üzenet kérése a billentyűzetről, és küldése
message = input("Írj be valamit, elküldöm a szervernek!")
bytearr = message.encode(encoding="UTF-8")
serversocket.send(bytearr)

# üzenet fogadása
bytearr = serversocket.recv(1024)
message = str(bytearr, encoding="UTF-8")
print("A szerver ezt küldte:", message)
serversocket.close()

A TCP programozás buktatói

Van néhány buktató, amire érdemes figyelni a hálózatprogramozás során.

UDP használata esetén nagyon kényelmes, hogy amit egy üzenetként küldünk: send(), az egy üzenetként is fog megérkezni: recv(). Viszont nem lehetnek túl nagyok a csomagjaink. Elvileg a legnagyobb üzenet 65507 bájtos, azonban ez sem garantált, hogy mindenhol át fog jutni. Jobb egy kilobájt alatt maradni.

Nem így a TCP-nél. Egy nyitott kapcsolaton tetszőlegesen sok adatot küldhetünk. Azonban az nem garantált, hogy amit egy send() hívással küldtünk el az egyik oldalon, azt a túlvégen egyetlen recv() hívással kapjuk meg. Mivel a TCP az adatfolyamot csomagokra bontja, előfordulhat, hogy egy nagy, egészben elküldött blokk kis részletekben érkezik meg. Ha különálló üzeneteket kell küldeni, akkor az üzenetek elválasztását nekünk kell megoldanunk – például úgy, hogy valamelyik karaktert, a \0-t vagy a \n-t használjuk erre, esetleg bináris tartalmú üzenetek esetén előbb elküldjük az üzenet hosszát, utána pedig magát az üzenetet. A függvények nem sztringekkel dolgoznak, hanem bájttömbökkel, és sztringek esetén a kódolásról is gondoskodnunk kell.

Figyelni kell még arra is, hogy a fogadást kezdeményező, recv() függvények blokkolják a programot, ahogyan egy input()-nál is megáll a végrehajtás. Ha ez zavaró, akkor a socket.setblocking() függvényt kell használni – ebbe ez az írás már nem megy bele.

6. Útválasztás az Interneten

Láttuk, hogy az Interneten az összes kommunikáció csomagkapcsolt formában történik. Az egyes végpontok, számítógépek által elküldött csomagok hosszú utat tesznek meg a céljukig. Például a BME WiFi-n küldött csomag előbb eljut a WiFi hálózatot biztosító hozzáférési pontig (access point), onnan tovább különböző hálózati eszközökön válószínűleg eljut az R épületbe, ahol a BME net központja van. Innen továbbküldik az Internet gerinchálózatára (backbone), és így tovább. Ez könnyen látható a tracert (Unixokon: traceroute) programmal:

traceroute to infopy.eet.bme.hu (152.66.72.57), 30 hops max, 60 byte packets
 1  192.168.1.1 (192.168.1.1)  0.851 ms  0.887 ms  0.950 ms
 2  catv-80-98-191-254.catv.broadband.hu (80.98.191.254)  13.115 ms  26.418 ms  41.688 ms
 3  catv-89-135-217-158.catv.broadband.hu (89.135.217.158)  19.073 ms  19.263 ms  19.478 ms
 4  84.116.240.85 (84.116.240.85)  19.619 ms  19.613 ms  19.767 ms
 5  84.116.240.138 (84.116.240.138)  31.514 ms  31.767 ms  31.917 ms
 6  tg0-1-0-6.rtr1.vh.hbone.hu (195.111.97.101)  25.914 ms  14.588 ms  21.918 ms
 7  tg2-1.rtr.bme.hbone.hu (195.111.97.102)  21.796 ms  23.379 ms  23.403 ms
 8  xge10-1.taz.net.bme.hu (152.66.0.125)  33.358 ms  33.594 ms  33.661 ms
 9  xge5-4.ixion.net.bme.hu (152.66.0.122)  32.998 ms  33.029 ms  33.085 ms
10  xge5-4.quark.net.bme.hu (152.66.0.71)  78.614 ms  85.568 ms  85.857 ms

Itt a látható címek nagy része egy hálózati útválasztó (router), amelynek az a dolga, hogy a hozzá beérkező csomagokat valamilyen irányba továbbküldje. Ezek az útválasztók az IP címek tartományaival dolgoznak, méghozzá prefixekkel: egy útválasztási táblázat (routing table) alapján az adott bitekkel kezdődő című csomagokat a meghatározott irányba küldik. Ilyen útválasztást maguk a végpontok, a számítógépek is végeznek, mivel minden számítógépnek akár több hálózati eszköze lehet, és a csomagokról az operációs rendszernek is tudnia kell, hogy azokat hova küldje tovább. A táblázatok minden sora tartalmaz egy cél tartományt, és egy hozzá tartozó maszkot (netmask), amely egy 32 bites, 1-esekkel kezdődő szám:

Destination     Gateway         Genmask         Flags Metric Ref    Use Iface
127.0.0.0       0.0.0.0         255.0.0.0       U     0      0        0 lo
152.66.72.0     0.0.0.0         255.255.255.240 U     0      0        0 eth0

Egy adott irányba küldéshez az szükséges, hogy a csomag cél IP címe az 1-esek helyén megegyezzen a táblázat sora által kiválasztott iránnyal. Vagyis a fenti táblázat szerint azokat a csomagokat, amelyek címe az első 28 bitben (255.255.255.240 = FFFFFFF0) megegyeznek a 152.66.72.0 címmel, az eth0 eszköz felé kell küldeni. Ezt meg lehet fogalmazni Pythonban is:

if csomag_celja & netmask == cel_tartomany & netmask:
    # ...

Ezt a vizsgálatot kell elvégezni minden egyes csomagnál. A fenti táblázat egyébként tartalmazza a fiktív loopback (lo) eszközt is: minden, aminek a címe 127-tel kezdődik (maszk = FF000000, vagyis az első bájtban kell megegyezzen), az ehhez kerül. Ezért látja ezen keresztül a számítógép saját magát.