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.
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.
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.
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
3.146.152.119, 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ó.
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
.
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.
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!
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.
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.
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.
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.
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.