TCP server v MS Windows
Zdroj: SOOM.cz [ISSN 1804-7270]
Autor: Josifekk
Datum: 12.10.2009
Hodnocení/Hlasovalo: 0/0
TCP server v MS Windows
Dnes si ukážeme, jak vytvořit jednoduchý TCP server. Pod pojmem server mám na mysli program, který bude schopen přijímat požadavky na spojení od klientů. Překlad slova server je sluha. Server obsluhuje požadavky klientů.
V předminulém díle jsem vysvětlil pojem soket. Použil jsem přirovnání soketu ke konci potrubí (hrdlo trubky).
V minulém díle, který je věnován TCP serveru v Linuxu, jsem použil opět přirovnání soketů k potrubí nebo hadici. Nebudu tuto část článku zde zbytečně opisovat.
Postup při vytváření serveru v MS Windows® je v podstatě stejný jako v Linuxu. Na straně serveru vytvoříme hrdlo trubky (funkce socket). Nyní ale na rozdíl od klienta musíme nejprve přiřadit (funkce bind) soketu jméno (instance struktury sockaddr_in). Poté musíme vytvořit frontu, ve které budou uloženy požadavky na spojení (funkce listen). Překlad slova "listen" je poslouchat. Vytvořením fronty jsme vlastně vydali povel k tomu, aby operační systém na daném portu poslouchal, jestli nepřišel požadavek na spojení. Požadavky na spojení můžeme z fronty požadavků (kterou jsme vytvořili pomocí listen) postupně vybírat (funkce accept). Není-li ve frontě žádný požadavek a my se pomocí accept snažíme nějaký vybrat, program počká, dokud nějaký nedojde. Běh programu se "zablokuje". (Stále hovoříme o tak zvaném blokovacím módu.) Funkce accept nám vrátí nový soket, pomocí kterého budeme komunikovat s klientem. Starý soket bude dále sloužit pouze k navazování spojení. Zní to možná zvláštně, ale je to vlastně logické. Jeden soket slouží k navazování spojení a pro každého klienta, který se připojí, máme k dispozici nový soket. Tak může server obsluhovat více klientů najednou a navíc ještě při jejich obsluze přijímat spojení od klientů nových.
Funkce
Se strukturou sockaddr_in jsme se již setkali. Také jsme se již setkali s funkcí socket, která vytváří soket. Pro nás nové funkce jsou:
Pojmenováni soketu
int bind(SOCKET s, const struct sockaddr* name, int namelen); - funkce "pojmenuje" soket. Prvním parametrem je identifikátor soketu. Druhým parametrem je ukazatel na strukturu sockaddr. Ve struktuře je obsažená adresa i s portem. My budeme jako parametr této funkce předávat ukazatel na instanci struktury sockaddr_in. Posledním parametrem je délka struktury předávané v druhém parametru. Tedy délka instance struktury sockaddr_in.
Atributy struktury sockaddr_in vyplníme takto:
Atribut sin_family nastavíme na hodnotu makra AF_INET (jako obvykle).
Do atributu sin_port vložíme číslo portu, na kterém má server čekat na spojení. Nesmíme zapomenout na funkci htons.
Atributu sin_addr přiřadíme adresu síťového rozhraní, na kterém budeme očekávat spojení. Vždy zadáváme adresu lokálního stroje. Můžeme zadat například adresu 127.0.0.1. Poté bude možno se k serveru připojit pouze z lokálního počítače. Můžeme zadat IP adresu síťového rozhraní (síťové karty), potom bude možné se připojit jen z ní. Má-li počítač více síťových karet, můžeme vybrat jednu z nich. A z té bude se bude možno připojit. Taková omezení jsou hlavně z důvodů bezpečnosti. Chceme-li očekávat spojení z libovolného síťového rozhraní (asi téměř vždy), vložíme hodnotu makra INADDR_ANY.
Funkce v případě selhání vrací hodnotu makra SOCKET_ERROR. V opačném případě vrací 0.
Vytvoření fronty
int listen(SOCKET s, int backlog); - vytvoří frontu požadavků na připojení. Prvním parametrem je identifikátor soketu, druhým parametrem je maximální délka fronty. Jestliže je fronta plná a nějaký klient se pokusí k serveru připojit, bude spojení odmítnuto. Funkce v případě selhání vrací SOCKET_ERROR. V opačném případě vrací 0.
Přijmutí spojení - vyzvednutí požadavku z fronty
SOCKET accept(int SOCKET, struct sockaddr *addr, int *addrlen); - vybere požadavek na spojení z fronty požadavků a potvrdí ho. My zatím používáme pouze blokovací mód soketů, proto v případě, že ve frontě žádný požadavek není, funkce accept zablokuje provádění programu, dokud nepřijde nějaký požadavek na spojení. Pro každé přijaté spojení se vytvoří nový soket. Prvním parametrem je identifikátor soketu. Druhým parametrem je ukazatel na nám již známou strukturu sockaddr, která obsahuje adresu vzdáleného počítače, který se připojil k serveru. Třetím parametrem je ukazatel na proměnnou udávající velikost struktury, která je předána jako druhý parametr. Struktura, na kterou se odkazuje druhý parametr bude při volání funkce zaplněna. Před zavoláním funkce nemusí obsahovat žádné smysluplné hodnoty. Musí ale být alokována. Třetí parametr se musí odkazovat na proměnnou obsahující velikost struktury předávané jako 2. parametr. Po zavolání funkce bude obsahovat skutečnou velikost zaplněné struktury. Je jasné, že funkce accept nezaplní větší část paměti, než kolik jsme předali pomocí 3 parametru. Funkce accept vrací identifikátor nového soketu. Komunikaci s klientem provádíme pomocí tohoto nového soketu. "Starý" soket slouží pouze pro navazování spojení. V případě chyby funkce accept vrací hodnotu makra INVALID_SOCKET.
Dále s klientem komunikujeme pomocí stejných funkcí, které používáme při komunikaci klienta se serverem. Těmito funkcemi jsme se již zabývali.
Příklad velice jednoduchého serveru:
#include
#include
#include
#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ě;
std::string text; // Přijímaný text
sockaddr_in sockname; // "Jméno" soketu a číslo portu
sockaddr_in clientInfo; // Klient, který se připojil
SOCKET mainSocket; // Soket
int port; // Číslo portu
char buf[BUFSIZE]; // Přijímací buffer
int size; // Počet přijatých a odeslaných bytů
int addrlen; // Velikost adresy vzdáleného počítače
int count = 0; // Počet připojení
if (argc != 2)
{
cerr << "Syntaxe:\n\t" << argv[0]
<< " " << "port" << endl;
return -1;
}
// Připravíme sokety na práci
if (WSAStartup(wVersionRequested, &data) != 0)
{
cout << "Nepodařilo se inicializovat sokety" << endl;
return -1;
}
port = atoi(argv[1]);
// Vytvoříme soket - viz minulý díl
if ((mainSocket = socket(AF_INET, SOCK_STREAM, IPPROTO_TCP))
== INVALID_SOCKET)
{
cerr << "Nelze vytvořit soket" << endl;
WSACleanup();
return -1;
}
// Zaplníme strukturu sockaddr_in
// 1) Rodina protokolů;
sockName.sin_family = AF_INET;
// 2) Číslo portu, na kterém čekáme
sockName.sin_port = htons(port);
// 3) Nastavení IP adresy lokální síťové karty, přes kterou
// je možno se připojit.
// Nastavíme možnost připojit se odkudkoliv.
sockName.sin_addr.s_addr = INADDR_ANY;
//přiřadíme soketu jméno
if (bind(mainSocket, (sockaddr *)&sockName, sizeof(sockName))
== SOCKET_ERROR)
{
cerr << "Problém s pojmenováním soketu." << endl;
WSACleanup();
return -1;
}
// Vytvoříme frontu požadavků na spojení.
// Vytvoříme frontu maximální velikosti 10 požadavků.
if (listen(mainSocket, 10) == SOCKET_ERROR)
{
cerr << "Problém s vytvořením fronty" << endl;
WSACleanup();
return -1;
}
do
{
// Poznačím si velikost struktury clientInfo.
// Předám to funkci accept.
addrlen = sizeof(clientInfo);
// Vyberu z fronty požadavek na spojení.
// "client" je nový soket spojující klienta se serverem.
SOCKET client = accept(mainSocket, (sockaddr*)&clientInfo,
&addrlen);
int totalSize = 0;
if (client == INVALID_SOCKET)
{
cerr << "Problém s přijetím spojeni" < WSACleanup();
return -1;
}
// Zjistím IP adresu klienta.
cout << "Někdo se připojil z adresy: "
<< inet_ntoa((in_addr)clientInfo.sin_addr) << endl;
// Přijmu data. Ke komunikaci s klientem používám soket
"client"
text = "";
// Přijmeme maximálně 6 bytový pozdrav.
while (totalSize != 6)
{
if ((size = recv(client, buf, BUFSIZE - 1, 0))
== SOCKET_ERROR)
{
cerr << "Problém s přijetím dat." << endl;
WSACleanup();
return -1;
}
cout << "Přijato: " << size << endl;
totalSize += size;
text += buf;
}
cout << text;
// Odešlu pozdrav
if ((size = send(client, "Nazdar\n", 8, 0))==SOCKET_ERROR)
{
cerr << "Problém s odesláním dat" << endl;
WSACleanup();
return -1;
}
cout << "Odesláno: " << size << endl;
// Uzavřu spojení s klientem
closesocket(client);
}while (++count != 3);
cout << "Končím" << endl;
closesocket(mainSocket);
WSACleanup();
return 0;
}
Program má jako svůj parametr číslo portu, na kterém bude očekávat spojení. Pomocí klientů z jednoho z předchozích dílů se můžete připojovat k tomuto serveru. Server obslouží 3 klienty a ukončí se. Nejste-li připojeni k síti, můžete přesto spustit tento program. Na stejném počítači poté spusťte klienta a jako adresu serveru předejte řetězec "localhost". Máte-li možnost, můžete si také vyzkoušet se k tomu programu připojit pomocí klienta z Linuxu. Klienta pro Linux jsem vytvořil v jednom z minulých dílů. Bohužel v něm byla drobná chyba. Opravená verze Linuxového klienta je v článku Tcp server v Linuxu.
Nedoporučuji vám používat čísla portů, které jsou vyšší než 50000 nebo menší než 1024. Čisla portů nižší než 1024 jsou vyhrazeny standardním službám (i když nejsou všechny obsazené).
Všimněte si na tomto programu jedné zvláštnosti, nebo spíše nedokonalosti. Program přijme jedno spojení a poté ho obsluhuje. V momentě, kdy obsluhuje klienta, nepřijímá žádné další požadavky na spojení. Navíc je schopen komunikovat v jednom okamžiku pouze s jedním klientem. Jedná se o jednovláknový server. Tohle si můžeme dovolit pouze v našem banální příkladě, ve kterém dojde k přijetí spojení a odeslání krátkých textů. Nedostatek lze tolerovat zvláště v případě, kdy server nebude příliš zatížen. Jinak by ale každý server měl být schopen obsluhovat více klientů najednou. Jak toto řešit si povíme v budoucnu.