|
|
|
|
|
TCP server v Linuxu
Dnes
si vytvoříme jednoduchý TCP server v Linuxu. Server je program, který
čeká na připojení klienta. Ukážeme si funkce bind, listen, accept. Na
závěr opravíme drobnou chybu z minula.
|
|
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). 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.
Mám-li použít přirovnání k potrubí jako v předminulém díle,
tak lze říci, že na straně serveru vytvoříme objímku potrubí. Do této objímky budou zasouvat potrubí klienti, kteří
se budou chtít připojit. Nejprve ale musíme rozhodnout, na kterém portu budeme spojení očekávat a do kterých síťových karet
je možné potrubí (hadici) zasunout. Poté vytvoříme frontu, do které budeme vkládat požadavky na spojení. Zde trochu selhává
parafráze s potrubím. Představme si to tak, že klienti vlastně vkládají potrubí do fronty. Z fronty budeme vybírat
potrubí vložené klienty a pro každé potrubí vytvoříme novou objímku (soket). Chceme-li spojení s klientem ukončit, zahodíme
konec potrubí, který jsme pro komunikaci s daným klientem získali. Chceme-li přestat nabízet možnost připojit se, uzavřeme
"hlavní" soket, který jsme pojmenovali.
Na straně klienta používáme tak zvané anonymní sokety (nepojmenované
sokety). Na straně serveru používáme sokety pojmenované.
Pomocí pojmenovaného soketu čekáme na vytvoření spojení. Pro každé
spojení poté vytvoříme anonymní soket. Podívejme se na funkce
podrobněji.
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(int sockfd, struct sockaddr *my_addr, socklen_t addrlen);
- 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 bind je deklarována v hlavičkovém souboru sys/socket.h. Funkce v případě selhání vrací -1. V opačném případě vrací 0.
Vytvoření fronty
- int listen(int 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 listen je deklarována v hlavičkovém souboru
sys/socket.h. Funkce v případě selhání vrací -1. V opačném případě vrací 0.
Přijmutí spojení - vyzvednutí požadavku z fronty
- int accept(int s, struct sockaddr *addr, socklen_t *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í -1.
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 <iostream>
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <unistd.h>
#include <string>
#define BUFSIZE 1000
using namespace std;
int main(int argc, char *argv[])
{
std::string text; // Přijímaný text
sockaddr_in sockName; // "Jméno" portu
sockaddr_in clientInfo; // Klient, který se připojil
int mainSocket; // Soket
int port; // Číslo portu
char buf[BUFSIZE]; // Přijímací buffer
int size; // Počet přijatých a odeslaných bytů
socklen_t 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;
}
port = atoi(argv[1]);
// Vytvoříme soket - viz minulý díl
if ((mainSocket = socket(AF_INET, SOCK_STREAM, IPPROTO_TCP)) == -1)
{
cerr << "Nelze vytvořit soket" << endl;
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)) == -1)
{
cerr << "Problém s pojmenováním soketu." << endl;
return -1;
}
// Vytvoříme frontu požadavků na spojení.
// Vytvoříme frontu maximální velikosti 10 požadavků.
if (listen(mainSocket, 10) == -1)
{
cerr << "Problém s vytvořením fronty" << endl;
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.
int client = accept(mainSocket, (sockaddr*)&clientInfo, &addrlen);
int totalSize = 0;
if (client == -1)
{
cerr << "Problém s přijetím spojeni" <<endl;
return -1;
}
// Zjistím IP 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)) == -1)
{
cerr << "Problém s přijetím dat." << endl;
return -1;
}
cout << "Přijato: " << size << endl;
totalSize += size;
text += buf;
}
cout << text;
// Odešlu pozdrav
if ((size = send(client, "Nazdar\n", 8, 0)) == -1)
{
cerr << "Problém s odesláním dat" << endl;
return -1;
}
cout << "Odesláno: " << size << endl;
// Uzavřu spojení s klientem
close(client);
}
while (++count != 3);
cout << "Končím" << endl;
close(mainSocket);
return 0;
}
|
|
Program má jako svůj parametr číslo portu, na kterém bude očekávat
spojení. Pomocí klientů z minulého dílu 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 MS Windows®. Klienta pro MS Windows jsem vytvořil v minulém díle.
Spustit server čekající na portu nižším než 1023 může pouze uživatel
root. Jedná se o bezpečnostní opatření. Na nižších portech
běží standardní služby a nikdo jiný než správce (root) by neměl mít
možnost je spouštět. Nejste-li root, nedávejte tedy programu
jako parametr nižší číslo než 1023. Jen pro zajímavost si můžeme říci,
že toto číslo je o 1 nižší než hodnota makra PROT_SOCK deklarovaném v hlavičkovém
souboru /usr/src/linux/include/net/sock.h jádra operačního systému. Jestliže změníme hodnotu makra PROT_SOCK
a překompilujeme jádro operačního systému, budeme mít jinou
hodnotu maximálního portu, na kterém můžou poslouchat pouze programy
spuštěné rootem. PODOBNÉ EXPERIMENTY VÁM ALE V ŽÁDNÉM PŘÍPADĚ NEDOPORUČUJI.
Omluva
Vyzkoušíte-li tento server a připojíte se k němu pomocí Linuxového
klienta, bude se možná trochu divit. Linuxový klient se totiž nezastaví.
Je v něm chyba. K odeslání a přijetí dat dojde,
ale v momentě, kdy server uzavře spojeni, klient zamrzne v nekonečné
smyčce. Při psaní ukázkového příkladu jsem zapomněl na fakt, že při
ukončení spojení druhou stranou vrací recv 0 a
neblokuje program. V ukázkovém programu je potřeba upravit cyklus while
při přijímání dat. Na chybu jsem přišel až při psaní tohoto článku,
když jsem napsal server. Při psaní článku o
klientovi jsem neměl ještě server a na ničem jsem ukázkový příklad
netestoval. Příště již nebudu ukázkové příklady takto podceňovat a za
svou chybu se všem omlouvám. Nerad měním již vydané články, proto tam
tuto
drobnou chybu ponechám. V příkladech ke stažení je opravená verze
klienta.
Příklady
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. V
příštím díle vytvoříme TCP klienta pro MS Windows®.
Hodnocení článku |
1 |
2 |
3 |
4 |
5 Aktuální známka: 2.62 (Počet známek: 3777)
|
|
|
|
|
|