TCP klient v MS Windows - Builder.cz - Informacni server o programovani

Odběr fotomagazínu

Fotografický magazín "iZIN IDIF" každý týden ve Vašem e-mailu.
Co nového ve světě fotografie!

 

Zadejte Vaši e-mailovou adresu:

Kamarád fotí rád?

Přihlas ho k odběru fotomagazínu!

 

Zadejte e-mailovou adresu kamaráda:



C/C++

TCP klient v MS Windows

cpp net

3. ledna 2003, 00.00 | Dnes se naučíme vytvářet jednoduchého TCP klienta v operačním systému MS Windows. Naučíme se jak vytvářet soket, jak navázat spojení se serverem, jak přijmout a odeslat data. Vysvětlíme si pojem soket a ukážeme si funkce socket, connetc, send, recv, closesocket.

Dnes se naučíme vytvářet jednoduchého klienta v operačním systému MS Windows®. Naučíme se jak vytvářet soket, jak navázat spojení se serverem, jak přijmout a odeslat data. Komunikace pomocí TCP/IP probíhá na principu klient - server. Server je program, který čeká na připojení. Klient je program, který spojení navazuje. Klient se mi zdá jednodušší, proto jím začneme.

V minulém díle jsme si velice zjednodušeně vysvětlili co je to soket. Pro ty, kteří nečetli minulý článek věnovaný Linuxu to zopakuji.

V úvodním díle jsme si popsali službu TCP. Chceme-li komunikovat se vzdáleným počítačem, je třeba vytvořit soket. V úvodním díle jsem napsal, že soket je obecný nástroj pro komunikaci. Lze jej mimo jiné použít i pro komunikaci po síti pomocí protokolů TCP/IP. Co si ale pod pojmem soket blíže představit? Překlad slova socket je objímka, zásuvka, hrdlo(trubky). Máme za úkol pomocí soketu spojit dva počítače. Představme si (samozřejmě velice obrazně :-), že mezi počítačem u kterého sedíme a vzdáleným počítačem, se kterým chceme komunikovat je natažené potrubí. V každém počítači je jeden konec trubky - hrdlo trubky - anglicky socket. Nejprve musíme vytvořit hrdlo trubky (vytvořit soket - funkce socket). Poté musíme druhý konec nasměrovat správným směrem. Tedy zasunout druhý konec roury do vzdáleného počítače (navázat spojení - funkce connect). Máme-li takto vytvořené potrubí, můžeme do našeho konce potrubí (do soketu) něco vložit (poslat data - funkce send). Ono to na druhém konci potrubí "vypadne". Také může něco vypadnout nám na našem konci potrubí (příjem dat - funkce recv). Až nás přestane práce s potrubím bavit, tak náš konec potrubí zahodíme (uzavřeme spojení - funkce close). Na druhé straně potrubí sice bude k dispozici druhý konec trubky, ale nebude už na nic. Protože z něj už nic nevypadne (my tam nemůžeme nic vložit, když nemáme svůj konec) a pokud do něj něco vhodí druhá strana, tak to nikam nedojde (náš konec už není). Je zřejmé, že když navazujeme spojení (dáváme druhý konec potrubí do vzdáleného počítače), musí být na vzdáleném počítači něco, co potrubí uchopí a připraví svůj konec potrubí pro komunikaci. Tedy vzdálený počítač musí být TCP server. TCP serveru se budeme věnovat příští článek.

Tolik pro představu pro lidi, kteří o soketech ještě nikdy neslyšeli. Nyní se podrobněji podívejme na jednotlivé funkce.

Vytvoření soketu

Soket je reprezentován svým identifikátorem, který je typu SOCKET. První malý rozdíl oproti Linuxu. V Linuxu to bylo celé číslo. Je to ale jen takový formální rozdíl, protože typ SOCKET je definován jako u_int. K vytvoření soketu slouží funkce socket. Hlavička funkce:

  • SOCKET socket(int af, int type, int protocol); - prvním parametrem je rodina protokolů. My budeme sokety používat pro komunikaci po síti pomocí protokolu IP. Proto budeme jako první parametr zadávat vždy hodnotu makra AF_INET. Druhým parametrem je typ soketu. Chceme-li používat protokol TCP (spojová služba, "proud" dat), předáme jako druhý parametr hodnotu makra SOCK_STREAM. Později se v seriálu budeme věnovat také protokolu UDP, potom budeme používat makro SOCK_DGRAM. Makro SOCK_STREAM budeme používat v případě, že chceme vytvořit dvoubodové spojení. Bude se jednat o spojovou službu. Odeslaná data budou potvrzována a určitě přijdou v pořadí, v jakém jsme je poslali. Naopak makro SOCK_DGRAM udává, že se bude jednat o nespojovou datagramovou službu. Tím se budeme zabývat v budoucnu. Existují i další možnosti, kterými se v seriálu pravděpodobně zabývat nebudeme. Posledním parametrem je identifikátor protokolu. Chceme-li používat protokol TCP předáme jako parametr hodnotu makra IPPROTO_TCP. Prozatím budeme tedy vždy vytvářet soket takto: socket(AF_INET, SOCK_STREAM, IPPROTO_TCP); funkce vrací identifikátor soketu. V případě, že došlo k chybě (soket není možné vytvořit), funkce vrací INVALID_SOCKET.
Struktura sockaddr_in

Než začneme navazovat spojení, seznámíme se se strukturou sockaddr_in. Opět si můžeme všimnout drobných rozdílů oproti linuxové variantě. Instance této struktury přesně charakterizuje místo, kam chceme naši rouru zasunout. Je v ní jak IP adresa (adresa vzdáleného počítače), tak i číslo portu (adresa aplikace na vzdáleném počítači). Pojmy IP adresa a TCP port jsem vysvětlil v úvodním díle seriálu. Atributy struktury:

  • short sin_family - typ adresy. Vždy bude odpovídat makru AF_INETP. K IP adrese vzdáleného počítače si vytvoříme instanci struktury hostent. To jsme si již ukazovali v článku Překlad doménových jmen v OS Windows. Struktura hostent má atribut int h_addr_type. Atributu sin_family přiřadíme hodnotu, která bude v atributu h_addr_type.
  • struct in_addr sin_addr - adresa vzdáleného počítače. Se strukturou in_addr jsme se setkali při překladu doménových jmen.
  • u_short sin_port - číslo portu na vzdáleném počítači, ke kterému se chceme připojit. Nedoporučuji vám ale přiřazovat tomuto atributu přímo číslo. Musíme si uvědomit, že internet je síť složená z různých platforem. Mohou zde nastat problémy s reprezentací čísla (big endian vs. little endian). Pro vyřešení těchto problémů použijeme funkci htons.

Další atributy nás nemusí zajímat. Můžeme se setkat s identifikátorem SOCKADDR_IN. Jedná se o další pojmenování (pomocí typedef) struktury sockaddr_in. Strukturu sockaddr_in budeme přetypovávat na strukturu sockaddr. Struktura sockaddr by se nám vyplňovala špatně.

Navázání spojení

Máte tedy vytvořen náš konec "hadice" (pomocí funkce socket). Teď by to chtělo vložit druhý konec hadice do nějakého počítače. K tomu slouží funkce conncet. Její hlavička je:

  • int connect(SOCKET s, const struct sockaddr *name, int namelen); - prvním parametrem je identifikátor soketu. Druhým je adresa TCP serveru. Vytvoříme instanci struktury sockaddr_in a ukazatel na ní přetypujeme na ukazatel na strukturu sockaddr. Posledním parametrem je velikost adresy. V případě, že spojení bylo navázáno, funkce vrací 0. Jinak SOCKET_ERROR. Nyní můžeme začít posílat data nebo data přijímat.
Odeslání dat

K odeslání dat slouží funkce send. Její hlavička je:

  • int send(SOCKET s, char* buf, int len, int flags); - funkce odešle data. Oproti linuxové verzi se deklarace liší v typech parametrů. Jejich význam ale zůstává. Prvním parametrem je identifikátor soketu, druhým parametrem je ukazatel na blok dat určený k odeslání. Třetím parametrem je velikost bloku dat v bytech. Posledním parametrem jsou flagy. My budeme prozatím používat 0. Funkce vrací počet odeslaných bytů, nebo SOCKET_ERROR v případě chyby. Existuje podobná funkce sendto. Je možné ji také použít.
Příjem dat

K příjmu dat slouží funkce recv. Její hlavička je:

  • int recv(SOCKET s, char *buf, int len, int flags); - funkce přijme data. Oproti linuxové verzi se deklarace liší v typech parametrů. Jejich význam ale zůstává. Prvním parametrem je identifikátor soketu, druhým parametrem je ukazatel na souvislý blok paměti, který je určen k zaplnění příchozími daty. Třetím parametrem je maximální počet dat, které je možné do bufferu přijmout. Typicky je to číslo, které udává velikost na kterou je alokován buffer (v bytech). Posledním parametrem jsou flagy. My budeme prozatím používat 0. Funkce vrací počet skutečně přijatých bytů, nebo SOCKET_ERROR v případě chyby. Je-li spojení uzavřeno, funkce vrací 0.

Když jsme vytvářeli soket, použili jsme makro SOCK_STREAM a rozhodli se komunikovat pomocí TCP. Tím jsme hlavně rozhodli o charakteru spojení. Máme garantováno, že data budou přijímána v pořadí, ve kterém byla odeslána. Data nebudou poškozena. Budeme je přijímat jako proud bytů. Jestliže nám druhá strana pomocí jednoho soketu pošle vícekrát data, nebudeme na naší straně schopni nijak rozpoznat, kde končí jeden odeslaný blok dat a začíná druhý. Musíme jej sami rozpoznat řídicích bytů, nebo mít pevně danou délku jednoho záznamu. V tomto smyslu lze přirovnat práci se soketem typu SOCK_STREAM jako s binárním souborem (posloupnost bytů nebo proud bytů).
Jestliže druhá strana (server) uzavře spojení, funkce recv vrací jako počet přečtených bytů číslo 0. Toho využiji v jednoduchém příkladu.

Zatím jen pro úplnost poznamenám, že zde mluvíme o tak zvaném blokovacím módu. Funkce recv čeká na data. Tedy zablokuje se běh programu, dokud nějaká data nepřijdou. O neblokovacím módu, jak jej udělat a o věcech s tím souvisejících si povíme něco v budoucnu. Blokovací mód nám bude prozatím stačit. Nyní několik článků budu mluvit o blokovacím módu a nebudu to zdůrazňovat.

Uzavření spojení

Soket uzavřeme funkcí closesocket. Musím jen zdůraznit, že jej neuzavřeme funkcí close, jak jsme zvyklí z Unixových systémů. Této chyby se lehce dopustí někdo, kdo je zvyklý používat sokety v Linuxu a přechází na MS Windows ®. Je to taková zákeřnost, protože close očekává jako parametr int, takže nás překladač neupozorní na chybu. Je-li někdo zvyklý z Unixových systémů na close, může být velice překvapen. Překladač nic nenahlásí, spojení se neuzavře, ale programátor při ladění intuitivně očekává, že close spojení uzavře. Například při komunikaci pomocí protokolů HTTP má uzavření spojení velký význam.

Ukázkový příklad - TCP klient ve Windows

Nyní si vytvoříme jednoduchý příklad. Vytvoříme program, který naváže spojení se serverem, odešle mu textový řetězec a přijme jiný textový řetězec. Poté server spojení ukončí. Parametry programu budou adresa vzdáleného počítače (IP adresa nebo doménové jméno) a číslo portu. V příkladu používám šablonu string. Umožňuje v C++ pohodlnější práci s řetězci než pole znaků. Více informací o šabloně je v článku Řetezce v C++.

#include <iostream>
#include <string>
#include <windows.h>

#define BUFSIZE 1000

using namespace std;

int main(int argc, char *argv[])
{
    WORD wVersionRequested = MAKEWORD(1,1); // Číslo verze
    WSADATA data;                           // Struktura s info. o knihovně
    string text("Ahoj\n");                  // Odesílaný a přijímaný text
    hostent *host;                          // Vzdálený počítač
    sockaddr_in serverSock;                 // Vzdálený "konec potrubí"
    int mySocket;                           // Soket
    int port;                               // Číslo portu
    char buf[BUFSIZE];                      // Přijímací buffer
    int size;                             // Počet přijatých a odeslaných bytů
    if (argc != 3)
    {
        cerr << "Syntaxe:\n\t" << argv[0]
             << " " << "adresa port" << endl;
        return -1;
    }
    // Připravíme sokety na práci
    if (WSAStartup(wVersionRequested, &data) != 0)
    {
        cout << "Nepodařilo se inicializovat sokety" << endl;
        // Podle všeho, zde se WSACleanup volat nemusí.
        return -1;
    }
    port = atoi(argv[2]);
    // Zjistíme info o vzdáleném počítači
    if ((host = gethostbyname(argv[1])) == NULL)
    {
        cerr << "Špatná adresa" << endl;
        WSACleanup();
        return -1;
    }
    // Vytvoříme soket
    if ((mySocket = socket(AF_INET, SOCK_STREAM, IPPROTO_TCP)) == -1)
    {
        cerr << "Nelze vytvořit soket" << endl;
        WSACleanup();
        return -1;
    }
    // Zaplníme strukturu sockaddr_in
    // 1) Rodina protokolů
    serverSock.sin_family = AF_INET;
    // 2) Číslo portu, ke kterému se připojíme
    serverSock.sin_port = htons(port);
    // 3) Nastavení IP adresy, ke které se připojíme
    memcpy(&(serverSock.sin_addr), host->h_addr, host->h_length);
    // Připojení soketu
    if (connect(mySocket, (sockaddr *)&serverSock, sizeof(serverSock)) == -1)
    {
        cerr << "Nelze navázat spojení" << endl;
        WSACleanup();
        return -1;
    }
    // Odeslání dat
    if ((size = send(mySocket, text.c_str(), text.size() + 1, 0)) == -1)
    {
        cerr << "Problém s odesláním dat" << endl;
        WSACleanup();
        return -1;
    }
    cout << "Odesláno " << size << endl;
    // Příjem dat
    text = "";
    while (((size = recv(mySocket, buf, BUFSIZE, 0)) != 0) && (size != -1))
    {
        cout << "Přijato " << size << endl;
        text += buf;
    }
    if (size == -1)
    {
        cout << "Nelze přijmout data" << endl;
    }
    // Uzavřu spojení
    closesocket(mySocket);
    WSACleanup();
    cout << endl << text << endl;
    return 0;
}

Tento příklad je možné si stáhnout jako ukázku. Tím máme hotového Windows klienta. Teď by to pro názornou ukázku chtělo mít k dispozici server, ke kterému se klient bude moci připojit. Server si napíšeme v příštích článcích. Věřím, že je mnoho takových, kteří si chtějí vyzkoušet klienta teď hned. Chtělo by to nějaký server na rychlé vyzkoušení. Proč ale server narychlo psát, když na internetu je jich spousta (například WWW servery). Proto druhý příklad ke stažení je příklad, který stáhne WWW stránku (tento článek) a uloží ji na disk. Používám protokol HTTP, což je textový protokol, který běží nad TCP/IP. WWW servery (pokud není uvedeno jinak) očekávají spojení na portu 80. HTTP protokolem se v seriálu zabývat nebudeme.

Příklady

V příštím článku vytvoříme TCP server v Linuxu. V článku, který bude následovat poté vytvoříme TCP server v MS Windows®.

Obsah seriálu (více o seriálu):

Tématické zařazení:

 » Rubriky  » C/C++  

 

 

 

Nejčtenější články
Nejlépe hodnocené články

 

Přihlášení k mému účtu

Uživatelské jméno:

Heslo: