C++ és a socketek

C++ és a socketek

Mivel nincs kimondottan C++-os eszköz a socketelésre ezért a meglévő C-s eszközökkel elég sokat lehet szívni, ha épp a sors úgy hozza. És hát miért ne hozná úgy. Történetünk valahogy úgy kezdődik, hogy szeretnénk csatlakozni valami webszerverhez, hogy lekérjünk onnan valami adatot, így adott három változónk, nevezzük őket valahogy így:

std::string host = "www.something.com";
std::string port = "80";
std::string data = "GET / HTTP/1.1\\nHost: www.something.com\\nConnection:close\\n\\n";

Már csak az az aprócska dolog maradt, hogy akkor most hát, innen hogyan tovább. Először is rakjunk össze egy c-szerű megoldást, a jobb érthetőség kedvéért, aztán majd jöhetnek az objektumok, hogy a dolog ki is nézzen valahogy.

Első lépésben szükség lesz még pár változóra, mondjuk egy bufferre, egyre amiben a socketet tároljuk, egyben, amiben a host visszafejtése után nyert struct adatokat és egyben amiben a kapcsolódáshoz szükséges adatokat tároljuk.

/*
 * Ebben találjuk a sockaddr_in struct-ot és a htons()
 * függvényt, amire majd a későbbiekben szükségünk lesz.
 */
#include <netinet/in.h>
/*
 * Ebben van a hostent struct és a gethostbyname() függvény.
 */
#include <netdb.h>

int main()
{
    /* ... */
    char        buffer[32]; // Az olvasásnál használt buffer
    int         s;          // Ez kell a socketnek
    sockaddr_in sin;        // Ez tárolja a kapcsolódó adatokat
    hostent*    hp;         // Ez pedig a host adatokat
    /* ... */
}

Az első ...-nál vannak az elején megadott host, port, data változók, amikhez nem árt egy #include sor a többi #include között. A második ... után fogjuk a kódot folytatni a socket létrehozásával.

/*
 * Erre a header fájlra van szükség a socket() függvényhez
 */
#include <sys/socket.h>
/* ... */
int main ()
{
    /* ... */
    if ((s = socket(AF_INET, SOCK_STREAM, 0)) < 0) {
        return 1;
    }
    /* ... */
}

A socket() függvény első paramétere határozza meg a címcsaládot, azaz hogy a cím hogyan legyen értelmezve. Ez IP esetén az AF_INET konstans, a többi típust a socket.h -ban meg lehet találni. A második paraméter a kapcsolat típusának meghatározásáért felelős, ami TCP esetén SOCK_STREAM, UDP esetén SOCK_DGRAM. Az utolsó a protokoll meghatározása, ami IP esetén 0, egyéb esetek az /etc/protocols-ban találhatóak meg. Most, hogy ezzel megvagyunk, ideje, hogy a hostunkból kapjunk egy használható structot a gethostbyname() függvény segítségével.

/* ... */
int main()
{
    /* ... */
    if ((hp = gethostbyname(host.c_str())) == 0) {
        return 2;
    }
    sin.sin_family = AF_INET;
    bcopy(hp->h_addr, (char*)&sin.sin_addr, hp->h_length);
    sin.sin_port = htons(atoi(port.c_str()));
    /* ... */
}

A host megkapással egyben le is rendeztem a másik struct adatainak feltöltését, mivel minden szükséges adat a rendelkezésünkre állt. Azaz a cím család itt is AF_INET lett, a címet a hp struct-ból másolgattuk át, a portot pedig már a legelején stringként megadtuk, abból csináltunk char*-ot majd int-et és végül a htons() függvény segítségével host byte orderből network byte ordert csináltunk (ami nekem még nem teljesen tiszta, hogy miért is kell ezt így, sz'al ha valakinek valami infója van róla, szivesen venném). Így most már nincs más hátra mint csatlakozni és elküldeni a kérést.

/* ... */
int main()
{
    /* ... */
    if (connect(s, (sockaddr*)&sin, sizeof(sin)) < 0) {
        return 3;
    }

    int num;
    num = write(s, data.c_str(), data.length());
    if (num < 0) {
        return 4;
    }
    /* ... */
}

A connect() első paramétere a megnyitott socket-ünk, a második a kapcsolat adatai a harmadik pedig a kapcsolat adatainak a mérete. A write() első paramétere a socket a második az adat (char* formában), a harmadik pedig a küldendő adat hossza. Most, hogy megvolt a csatlakozás, megvolt a kérés, már csak megfelelő fogadtatásban kell részesíteni az érkező adathalmazt.

/* ... */
int main()
{
    /* ... */
    while ((num = read(s, buffer, sizeof(buffer))) > 0) {
        buffer[num] = '\\0';
        std::cout << buffer;
    }
    close(s);
    return 0;
}

Megkaptuk az adatokat, lezárjuk a kapcsolatot, kilépünk a programból. Persze a kóddal a legnagyobb probléma (azon kívül, hogy ezekkel a /* ... */ kommentekkel talán még kibogózhatatlanabbá tettem), hogy nagyon is egyszer haszálatos, eléggé átláthatatlan meg egyébként is csúnya. Nézzünk akkor egy osztályos megoldást:

#ifndef __SOCKET_H__
#define __SOCKET_H__

#include <sys/socket.h>
#include <netinet/in.h>
#include <netdb.h>

#include <string>

#define E_SOCKET_ERROR  1
#define E_HOST_ERROR    2
#define E_CONNECT_ERROR 3
#define E_WRITE_ERROR   4

class Socket
{
    private:
        char buffer[32];
        int sock;
        hostent* hp;
        sockaddr_in sin;
    public:
        Socket();
        Socket(const std::string& host, const std::string& port)
        {
            this->Open(host, port);
        }
        ~Socket()
        {
            this->Close();
        }
        void Open(const std::string& host, const std::string& port)
        {
            // Socket létrehozása
            if ((this->socket = socket(AF_INET, SOCK_STREAM, 0)) < 0) {
                throw new Socket::SocketException(E_SOCKET_ERROR);
            }

            // Host visszafejtése
            if ((this->hp = gethostbyname(host.c_str())) == 0) {
                throw new Socket::SocketException(E_HOST_ERROR);
            }

            // sockaddr_in struct feltöltése
            this->sin.sin_family = AF_INET;
            bcopy(this->hp->h_addr, (char*)&this->sin.sin_addr, this->hp->h_length);
            this->sin.sin_port = htons(atoi(port.c_str()));

            // Csatlakozás
            if (connect(this->sock, (sockaddr*)&this->sin, sizeof(this->sin)) < 0) {
                throw new Socket::SocketException(E_CONNECT_ERROR);
            }
        }
        void Send(const std::string& data)
        {
            int wb = write(this->sock, data.c_str(), data.length());
            if (wb < 0) {
                throw new Socket::SocketException(E_WRITE_ERROR);
            }
        }
        std::string GetLine()
        {
            int rb;
            std::string ret;
            do {
                rb = read(this->sock, this->buffer, 1);
                if (rb > 0) {
                    if (this->buffer[0] != '\\n') {
                        ret += this->buffer[0];
                    }
                    else {
                        break;
                    }
                }
            } while (rb > 0);

            return ret;
        }
        void Close()
        {
            close(this->sock);
            return;
        }
        class SoketException
        {
            public:
                int errorCode;
                SocketException(const int& code)
                {
                    this->errorCode = code;
                }
        };
};

#endif

Csak a kép teljességéért, egy rövidke példaprogram, ami ezt a Socket.h fájlt használja és ugyanazt eredményezi, mint az osztály nélküli kód.

#include "Socket.h"

#include <string>
#include <iostream>

int main()
{
    std::string host = "www.something.com";
    std::string port = "80";
    std::string data = "GET / HTTP/1.1\\nHost: www.something.com\\nConnection:close\\n\\n";

    Socket* sock;

    try {
        sock->Open(host, port);
        sock->Send(data);

        std::string row;
        while ((row = sock->GetLine()).length() > 0) {
            std::cout << row << std::endl;
        }

        sock->Close();
    }
    catch (Socket::SocketException e) {
        delete sock;
        return e.errorCode;
    }
    delete sock;
    return 0;
}

Egy cseppet szebb lett. Mondjuk nézőpont kérdése. Egy egyszer használatos gyors socketelésre felesleges lenne osztályt írni, de ha már az embernek kéznél van egy ilyen, akkor gyorsabban megírhat egy egyszerhasználatos socketelést, mint egyébként. Bár az igazsághoz hozzátartozik, hogy így a kód mérete picikét nagyobb lett (ha jól emlékszem alapból 16k volt, így meg 20k lett g++-szal fordítva mindenféle extra paraméterezés nélkül).

Hozzáfűznél valamit?

Dobj egy emailt a blog kukac deadlime pont hu címre.

Feliratkoznál?

Az RSS feed-et ajánljuk, ha kedveled a régi jó dolgokat.