Ez a rovat azoknak szól, akik legalább alapszinten már programoznak C-ben, és kezdôk Linux-ban (mint jómagam is). Egy pár alapvetô dolog után majd arra próbálok rátérni, amiben a Unix alapú rendszerek a legjobbak, a hálózati programozásra, természetesen Unix (pontosabban Linux) alatt.
Aki már használja a gcc-t, ugorjon a következô címre.
Egy keveset arról, miért jobb Linux-on C-t programozni. Más operációs rendszerekkel ellentétben a Unix rendszereken a C fordító az operációs rendszer szerves része. Sôt, magát a kernelt, a rendszer magját, és a programok túlnyomó többségét a beépített C fordítóval fordítják le. Arról nem is beszélve, hogy a C nyelv Unix-os környezetbôl nôtt ki, alapvetô elnevezései innen valók. (pl. stdin, stdout, stream-ek stb..) Azért persze hátrányai is vannak. Ha valaki megszokta már a Borland C barátságos fejlesztôi környezetét, hát az hiányozni fog. A debug-oláshoz pedig egy parancs-módos debugger van segítségünkre a megszokott csillogó-villogó helyett. Azért mégis jobb Egy keveset a fordítóról. A fordító, amit Linux alatt használunk, a GNU C. Ha belegondolunk, a "Linux"-ban "csak" a kernel a Linux, minden mást a GNU, és más fejlesztôcsoportok csinálnak. Szóval a fordító a GCC. Ez magában foglalja a linkert, compilert, preprocessort is. A C fejlesztôkörnyezet másik nagyon fontos része a GNU LIBC. Ez pár lib állomány, amely tartalmazza a C könyvtári függvényeket. Mint például a fájl, memória, input, output kezelés stb. ... A GLIBC tartalmazza az ISO C szabványban definiált összes függvényt, de számos fejlesztést is tartalmaz, mint a különbözô POSIX szabványok. A GLIBC a linux rendszerek második legfontosabb része, rögtön a kernel után. Egysoros kalauz a GCC-hez: Egy program lefordítása:

gcc -o név forrás1.c forrás2.c .... A -o után megadott név lesz a futtatható állomány neve.

Bôvebben: "man gcc".

A hálózati kommunikáció alapjai

Azzal, hogy fizikailag hogyan megy, nem foglalkozunk - hisz más cikksorozatunk e témát részletezi -, nézzük azt az oldalát, ami majd hasznosítható számunkra. A továbbiak az IP alapú, az-az az Interneten használt kommunikációra értendôek. Tehát minden protokoll az IP-re épül. A programozás szemszögébôl nézzük a dolgot.
A hálózati kommunikáció socket-ek segítségével történik. Egy socket-et végpontként, csatlakozó pontként foghatunk fel. Egymagában semmire sem jó, a működéshez két socket kell, amik között létrejön egy kommunikációs csatorna. A két socket ennek a csatornának a két végponjta. Egy program a hozzá rendelt socket-en keresztül egy másik socket-tel kommunikál, amely socket egy másik programhoz van rendelve. Így a két program socket-eken keresztül egymással beszélget. A "beszélget" kifejezés lényege, hogy a socket-eken keresztül létrejött kommunikáció kétirányú. Az elôbbiekben a "program" kifejezésen egy program egy futó példányát, azaz process-t értettünk. A socket-ek a process-szek közti kommunikáció (Inter Process Communication = IPC) alapjai. A két process ugyanazon gépen is futhat, de a világ két ellenkezô felén lévô gépeken is lehetnek. Az egyik process szempontjából teljesen mindegy, hogy helyileg hol van a másik, akivel kommunikál, mindegyik csak a saját socket-jére ír illetve olvas. A tényleges adatátvitelt a két socket intézi.
Két socket között létrejött kommunikációs csatorna alapvetôen kétféle lehet. Lehet kapcsolat orientált (connection oriented), mint amilyen egy telefonbeszélgetés is. A két fél kapcsolódik, és miután megtörtént az információcsere, a kapcsolatot lebontják, leteszik a kagylót. Vagy lehet olyan, mint amikor feladunk egy levelet (datagram oriented). Bedobjuk a postaládába, de a posta nem fog minket értesíteni, amikor a címzetthez megérkezik, vagy hogy megérkezett-e egyáltalán. Nem tudhatjuk, hogy a feladott leveleinket ugyanolyan sorrendben fogja-e megkapni a címzett, mint ahogy feladtuk. A kapcsolat orientált kommunikáció során a rendszer elrejti elôlünk az esetleges hibákat, nem kell törôdnünk azzal, mi történik az átvitel során. Például, ha egy csomag nem érkezik meg, arról kér egy ismétlést, garantált, hogy minden feladott csomagot megkap a vevô. Attól függetlenül, hogy a csomagok milyen sorrendben érkeztek meg, a vevô oldalon a rendszer sorrendbe rakja ôket. Az egyik fél pontosan olyan sorrendben olvassa az adatokat, ahogy a másik fél azt elküldte, és fordítva ugyanígy. Ilyen protokoll a TCP. A datagram orientált összeköttetés során a csomagok elveszhetnek, de ha megérkeznek is, nem tudhatjuk, milyen sorrendben voltak feladva. Elônye viszont, hogy jobb a teljesítménye. A kapcsolat orientált kommunikáció kényelmét a teljesítményben kell megfizetni. Datagram orientáltprotokoll az UDP.

Azonosítás a hálózaton

Ahhoz, hogy két socket egymással kommunikálhasson, tudni kell egymás azonosítóit. A telefonnál be kell tárcsázni annak a telefonszámát, akivel beszélni akarunk, a levélnél, pedig ráírjuk a címét. Tehát a telefonszám, vagy a lakcím az azonosító. Az interneten ez egy azonosító párral van megoldva. Egy IP cím, és egy port. Ez a két adat azonosítja a kommunikációban résztvevô process-eket.
Az IP címek 32 bites, azaz négy byte-os egészek. A leggyakrabban használt szabvány a 4-es verziójú IP cím. A négy byte-ot ponttal elválasztva reprezentáljuk. (Pl.: 192.168.17.8) Egy IP cím alapvetôen két részre osztható, az egyik a hálózatot azonosítja, amiben a gép szerepel, a másik a számítógépet a hálózatban. Szigorúan véve ez persze így nem igaz, hiszen egy géphez annyi IP tartozhat, ahány eszközzel csatlakozik a hálózathoz. Például, ha az otthoni kis hálózatot egy modem köti össze az ISP-vel, akkor a modem kap egy IP-t az ISP-tôl, a hálózati kártyának pedig ugyanabban a gépben egy egész más IP-je van. Egy géphez több IP is tartozhat. Az IP cím valójában a hálózati eszközt, és nem a gépet azonosítja, de a továbbiakban tekintsünk el ettôl az apróságtól. Mivel az ember ritkán jegyez meg elsôre egy 12 jegyű számot, nem IP címekre hivatkozunk, hanem nevekre. (Pl.: www.prog.hu ) Ezek IP címeket takarnak, egy name server tudja megmondani a hozzá rendelt IP-t. Léteznek speciális jelentéssel bíró IP címek is. Ilyen a localhost (127.0.0.1) (loopback localhost). Ez azt jelenti, hogy az erre a címre küldött csomagokat a rendszer nem engedi ki, hanem visszahurkolja, tehát saját magának küldi vissza. Így az egy gépen lévô process-ek is kommunikálhatnak egymással. A 0.0.0.0 (INADDR_ANY) a gép minden IP címét jelenti. A 255.255.255.255 minden gépet jelent egy helyi hálózaton.
A port (a másik adat, mely azonosítja a kommunikációban résztvevô processzt) egy 16 bites egész. Az elsô 1024 port fent van tartva az ismertebb szolgáltatások számára. (pl.: http a 80-as port, ftp 21-es pop3 110-es stb. Lásd. /etc/services és /etc/inetd.conf ). Az 1024 feletti portoknál semmi sem biztos. Úgy 49152-tôl 65535-ig bárki bármire használhat portot. Az 1024 és 49151 közti port-ok további szabványos használatra maradtak hátra. A nulladik portnak speciális jelentése van, ekkor a kernel keres egy szabad port-ot.

Klines-szerver modell

A socket alapú kommunikáció általános módja a kliens és szerver közti kommunikáció. A szerver egy process, amely bizonyos szolgáltatást tud nyújtani. Például leküldi a kért leveleket (pop3), vagy fájlokat (ftp), esetleg weblapot (http), vagy névrôl megmondja az IP-t (ns - name server). A kliens kezdeményezi a kapcsolatot, elküldi a kérést, és vár a válaszra. Mivel a kapcsolatot a kliens kezdeményezi, így tudnia kell a host (szerver-gép) IP címét (vagy nevét), ahol a szerver process fut, és a szolgáltatáshoz tartozó portot. Az egyes szolgáltatásokhoz tartozó port-okat a /etc/services fájlban találjuk meg. Persze elôfordulhat, hogy valamilyen okból nem azon a port-on van a szolgáltatás, ahol várjuk. Ilyenkor kell megkérdezni a rendszergazdától. Az IP címet sem kötelezô fejben tartani, jó a név is. A nevet többféleképpen fordíthatja a rendszer IP címre. Vagy megtalálja az /etc/hosts fájlban, vagy egy name server-t kérdez meg. Ehhez persze tudnia kell a name server IP címét, ennek az /etc/resolv.conf állományban kell lennie.
A port száma csak a szervernél fontos. A kliens számára teljesen lényegtelen, hogy a hozzá rendelt socket melyik portra van kötve, így általában a kernel keres számára egy szabad port-ot. A szervernek elôre nem kell tudnia a kliens azonosítóit, maga a kérés tartalmazza az azonosítókat, egyszerűen oda küldi a választ, ahonnan a kérés jött (arra az IP-re és arra a portra). Az IP+port azonosító (továbbiakban cím) alkalmas rá, hogy több kapcsolatban vegyen részt egyszerre. Így egy szerver process egyszerre több klienst is kiszolgálhat, több csatornán is kommunikálhat. A szerver minden egyes csatornához külön socket-et hoz létre. Ezek a socketek, mind ugyanahhoz a címhez tartoznak. Ezzel hoz létre kapcsolatot az újabb kliens [Szerk.: ha elvesztette volna a t. Olvasó a fonalat, bíztatom: a példák, majd segítenek!]. Egy Internet-kapcsolatot két socket segítségével tudunk leírni. [szerver IP+ szerver PORT] - [kliens IP + kliens PORT]

A programozás szemszögébôl

A program számára a socket gyakorlatilag egy fájl leíró (file descriptor). (Egy fájlra a leírójával (~ azonosítójával) tudunk hivatkozni, miután megnyitottuk. Így tudjuk írni, olvasni stb.. Hasonló, mint DOS-ban a file handle.)
Amikor létrehoz a program egy socketet, egy socket leírót (socket descriptor) kap vissza, amit ugyanúgy használhat, mintha fájl leíró volna. Írhatunk, olvashatunk a socket-rôl ugyanazokkal a függvényekkel, amiket egy fájl esetében használnánk (read, write stb.).
Kapcsolat orientált (connection oriented) adatcsere során a socket úgy viselkedik, mint egy egyszerű szövegfájl. A másik oldalon (másik process által a saját socketjére) írt adat akárhány olvasással, (pl. byte-onként, soronként) beolvasható. Biztos, hogy az olvasó process ugyanazt kapja, mint amit a másik írt. Sôt, a socket-re, mintha szövegfájlra hivatkozna, stream-et is húzhatunk (fdopen). Így használhatóak a stream-es függvények. Pl.: fprintf, fgets, fscanf,fputs, stb.. Datagram orientált kommunikáció során egy olvasással kell beolvasni a socket-rôl az adatot. Ha kevesebb byte-ot olvasunk be, mint az üzenet teljes hossza, akkor a maradék el fog veszni. Amit a küldô process egy írással ír ki a socket-jére, azt a fogadónak is egy olvasással kell beolvasni. Így aztán stream-et nem húzhatunk a socket leíróra. Kétféle socket-et lehet létrehozni. Az egyik a helyi unix rendszeren belüli proccess-ek közti kommunikációra való (UNIX domain), a másik az internetes kommunikációra (INET domain). Csak a másodikkal foglakozunk.

TCP alapú kliens működése

Mind a kapcsolat orientált protokollokat (pl. TCP), mind a datagram orientált protokollokat (pl. UDP) máshogyan kell programozni. Ezen kívül a szerver és kliens programok is igen különböznek egymástól. A további számokban igyekszem mindegyikre példát adni - hogy a fenti elmélet is teljesen átlátható legyen -, most kezdjük talán a legegyszerűbbel, egy TCP alapú kliens-el. A TCP alapúság azt jelenti, hogy a program egy a TCP-re épülô szolgáltatást próbál elérni.
Közben sok olyan függvénnyel is megismerkedünk, amelyekre majd a többi esetben is szükség lesz.
A TCP alapú kliens által végrehajtandó lépések

socket létrehozás => [port-hoz kötés =>] kapcsolódás => kommuniáció => bezárás
 

Létre kell hozni egy socketet, összekötni egy port-al, kapcsolódni a szerver process-el, kommunikálni, és aztán bezárni. Ha nem kötjük oda egy porthoz, a kernel fog nekünk keresni egy szabad portot. Lássuk, melyik függvényekkel megy ez. Általában egy kliensnek teljesen mindegy, melyik portot kapja, így most nem fogunk portot kötni. Socket létrehozása Ahhoz, hogy kommunikálni tudjunk, elôször egy socket-et kell létrehozni. A létrehozás teljesen független attól, hogy a továbbiakban hova fogunk csatlakozni vele. Itt még nem kell tudni a szerver process címét. Arra kell vigyázni, hogy az itt megadott protokoll, és domain helyes legyen. Socket létrehozására a socket függvény való.
#include <sys/types.h>
#include <sys/socket.h>
int socket(int domain, int type, int protocol)
 
domain AF_UNIX A Unix doman, ez a helyi unix hálózaton belüli kommunikációra való. AF_INET Az internetes kommunikációra való. Csak ezt fogjuk használni. type SOCK_STREAM "connection oriented", ez alaphelyzetben TCP protokollt jelent. SOCK_DGRAM "datagram orinted", ez alaphelyzetben UDP protokollt jelent. SOCK_RAW IP szint. A program belenyúlhat az IP csomagba. Pl. átírhatja a csomag fejlécét, ahol a küldô (azaz saját) címe van. (csúnya dolgokat lehet csinálni) Az ilyet használó program csak root jogokkal futhat. protocol Itt kell megadni a protokollt. Ha nullát adunk meg, akkor az alapértelmezett protokoll lesz. Azaz TCP, vagy UDP. Most jó lesz a nulla. Visszatérési értéke siker esetén a socket leíró. Egyébként -1. Egy egyszerű példa:

if ((sd = socket(AF_INET, SOCK_STREAM, 0) < 0)
{
  perror("socket");
  exit(1);
}

Kapcsolódás Mivel a kliens a kezdeményezô, neki kell kapcsolódni. Ez a connect függvénnyel történik. Itt kell tudnunk a szerver címét. A kapcsolat orientált protokolloknál (TCP) kötelezô a connect használata. A datagram orientált protokolloknál (pl. UDP) nem kötelezô, de használható. Ettôl persze az UDP még ugyanaz marad.

#include <sys/types.h>
#include <sys/socket.h>
int connect(int sd, const struct sockaddr *addr, int addrlen)
 
sd socket leíró
addr pointer a szerver process címére. (IP+PORT)
addrlen a cím struktúra hossza
Siker esetén nullával tér vissza, egyébként -1.
 
A szerver process címét egy sockaddr struktúrába kell elhelyezni. Ez a struktúra méretben és összeállításban többféle lehet, és a nevek is különböznek. A sockaddr_in struktúrát fogjuk használni.
struct in_addr {
    u_long s_addr;
};
struct sockaddr_in {
    u_short sin_family;
    u_short sin_port;
    struct in_addr sin_addr;
    char sin_zero[8]; //nem használt
};
 
Három mezôjét kell kitöltenünk:
sin_family Azt kell megadni, amit domain-nek adtunk meg a socket létrehozásakor. Tehát AF_INET.
sin_port A szolgáltatás port-ja.
sin_addr A host (szerver gép) IP címe. Ez igazából még egy struktúra, aminek az s_addr tagjába kell tenni az IP-t.
Az IP címet csak a legritkább esetben fogjuk rögtön tudni. Jóval valószínűbb, hogy egy névrôl kell visszakeresni. Erre a gethostbyname függvény való.
#include <netdb.h>
struct hostent *gethostbyname(const char *hostname);
 
A függvény egy hostent struktúrára mutató pointert ad vissza, hiba esetén pedig nullát. Ebben a struktúrában találjuk meg az IP-t:
struct hostent {
    char *h_name; /* host neve */
    char **h_aliases; /* nullával lezárt lista a többi nevérôl*/
    int h_addrtype; /* a cím tipúsa (AF_INET)*/
    int h_length; /* a cím hossza byte-ban */
    char **h_addr_list; /* nullával lezárt lista az IP címekrôl/*
    #define h_addr h_addr_list[0]
    /*a lista elsô eleme. A kompatíbitás miatt*/
};
 
Az IP címek listájáról az elsô elemet (h_addrlist[0]) bemásoljuk a sockaddr_in struktúra sin_addr tagjába, és lehet hívni a connect függvényt.
Egy egyszerű példa:
struct sockaddr_in servaddr; /*Szerver process címe*/
 
struct hostent *hp; /*Ez mutat a host adataira*/
                    /* a gethosbyname-tôl*/
int sd; /* socket leíró*/
.......
/* nullázzuk a szerver címet */
bzero((char *)&servaddr, sizeof(servaddr));
servaddr.sin_family = AF_INET;
 
//a server portja. htons kell, majd jön a magyarázat
servaddr.sin_port = htons(SERVERPORT);
hp = gethostbyname(SERVERNAME);
if (hp == 0) {fprintf(stderr, "gethostbyname hiba"); exit(1);}
 
/*átmásoljuk az IP címet*/
bcopy(hp->h_addr_list[0],
&servaddr.sin_addr.s_addr,
hp->h_length);
 
/*kapcsolódunk*/
if (connect(sd, (struct sockaddr *)&servaddr, sizeof(servaddr)) < 0)
{
  perror("connect");
  exit(1);
}
 

A htons függvényre azért van szükségünk, mert más (lehet) a hálózati byte sorrend, mint amilyen a host számábrázolása. Az Intel processzorokon az alacsonyabb helyi értékeket tartalmazó bájt van elöl (little endian), a hálózati sorrend meg pont fordítva van. A htons függvény hálózati sorrendre fordítja a byte-okat, ha szükséges. Több, evvel a problémakörrel foglalkozó függvény van. A htons short integer-re, azaz 16 bites egészre való. Gyakorlatilag fel kell cserélnie a két byte-ot.

Miután a connect függvény visszatért, felépült a kapcsolat. Innen igen egyszerű a dolgunk. A socket azonosítót fájl azonosítóként kezelve használhatjuk a read, és write függvényeket, mintha csak fájlból olvasnánk, írnánk. A példaprogramban egy socket-et húzunk a leíróra, így még kényelmesebben kezelhetjük. Az adatcsere végeztével a socket-et ugyanúgy, mintha fájl volna, egy sima close-al le tudjuk zárni, illetve fclose-al, ha stream-et használtunk. A példaprogram egy POP3 szerverhez kapcsolódik, bejelentkezik, és lekérdezi hány levelünk jött. A program kódba be kell írni a hozzáférés azonosítóit. Szerver, loginnév, jelszó. A program megértéséhez pár POP3 parancsot ugyan tudni kell, de azon kívül az itt leírt függvényeket használja. Kapcsolódás egy POP3 szerverhez:
int pop3connect(const char *hostname)
{
    int sd; //socket descriptor
    struct hostent *host_entry; //server host adatai a name servertol
    struct sockaddr_in host_addr;//host adatai a connecteleshez
 
    //lookupolas a POP3 szerverre
    printf("Looking up %s ...\n", hostname);
    if ((host_entry = gethostbyname(hostname))==NULL)
    {
        //a herror hibakodot is kiir h_errno bol
        herror("gethostbyname failed");
        exit(1);
    }
    //TCP socket letrehozasa
    if ((sd=socket(AF_INET,SOCK_STREAM,0)) < 0)
    {
        perror("socket err");
        exit(1);
    }
    //host adatainak nullazasa
    bzero(&host_addr,sizeof(host_addr));
 
    //host IP cimet atmasoljuk a host adatok koze
    bcopy(host_entry->h_addr_list[0],
     &host_addr.sin_addr.s_addr,
     host_entry->h_length);
 
    //meg kell adni, hogy milyen tipusu a cim. AF_INET lesz
    host_addr.sin_family = host_entry->h_addrtype;
 
    //megadjuk a POP3 portot.
    //htons -al forditani kell a byte sorrenden
    host_addr.sin_port = htons(POP3_PORT);
 
    //connecteles (kapcsolodas)
    printf("Connecting ...\n");
    if (connect(sd,(struct sockaddr *)&host_addr,
    sizeof(struct sockaddr_in)) < 0)
    {
        perror("connect error");
        exit(1);
    }
    return sd;
}

A cikkben tárgyalt forráskód: mailbox_stat.c