Version 1.5.5
(13-Jan-1999)
[http://www.ecst.csuchico.edu/~beej/guide/net]
Ai probleme cu programarea socket-urilor? Este aceasta problema cam greu de rezolvat doar cu ajutorul paginilor man? Ai dori sa poti scrie programe pentru Internet, insa nu ai timp sa inveti sa folosesti toate acele functii bind(), connect(), etc.
Ei bine, eu deja am invatat aceste lucruri, si vreau neaparat sa impartasesc aceste informatii cu toata lumea! Ai ajuns la locul potrivit. Acest document ar trebui sa ofere unui bun programator de C partea de inceput despre programarea in retea.
Acest document a fost conceput ca un tutorial si nu ca o referire. Cei care abia invata sa foloseasca socket-uri probabil vor aprecia mai mult acest document. Cu siguranta ca nu e un ghid complet al programarii socket-urilor.
Sper ca documentul va fi suficient de detaliat pentru ca paginile da man sa aiba sens.
Tot auzi vorbindu-se despre socketu-ri si te intrebi ce sunt alea. Iata raspunsul: o modalitate de a comunica cu alte programe folosind descriptorii de fisiere din Unix
Se poate sa fi auzit pe cineva ce se pricepe la Unix ca "totul in Unix se comporta ca un fisier!" La ce se referea persoana e ca atunci cand orice program in Unix face o operatie de I/O, foloseste pentru citire sau sciere un descriptor de fisiere. un descriptor de fisiere este un numar intreg ce e asociat unui fisier deschis pentru o operatie de I/O. Insa partea mai importanta e ca acest fisier poate fi o conexiune la retea, un FIFO, un pipe, un terminal, un fisier de pe disk, pana la urma orice. Totul in Unix e un fisier! Deci cand vrei dori sa comunici cu un alt program folosind Internetul vei folosi un descriptor de fisiere.
O intrebare care totusi cred ca nu-ti trece prin minte ar
fi:
"De unde obtin acest descriptor de fisier pentru comunicatie in
retea?" Raspunsul: Faci un apel catre rutina de sistem socket().
Va returna descriptorul de socket, si il vei folosi pentru
comunicatie folosind functiile pentru socket-uri send() si
recv()("
man
send", "man
recv")
Alta intrebare ar fi: "Daca e vorba de un descriptor de fisier de ce nu folosesc functiile standard read() si write pentru a comunica cu socket-ul?" Raspunsul scurt e "Poti!". Varianta lunga e ca send() si recv() ofera un mai mare control asupra transmisiei de date."
In continuare: sunt mai multe tipuri de socket-uri. Sunt socket-uri pentru Internet (care folosesc adresele), socket-uri pentru Unix (folosind o adresa/cale locala), mai putin importante acum X.25 socket-uri si multealtele ce depind de tipul de Unix pe care il folosesti. Documnentul trateaza doar primul tip de socket-uri: cele de Internet.
Sunt mai multe tipuri de socket-uri de Internet dar aici vom vorbi doar despre doua dintre ele. Insa "Raw Sockets" sunt si ele puternice si ar trebui investigate.
Celelalte doua tipuri sunt "Stream Sockets" si "Datgram Sockets", ce pot fi referite si ca "SOCK_STREAM" si respectiv "SOCK_DGRAM". Socket-urile Datagram sunt uneori numite "connectionless sockets" (socket-uri fara conectare) desi daca vrei se poate folosi functia connect(). Detalii: connect().
Socket-urile de tip Stream sunt de incredere in ceea ce prevede fluxul de date. Daca vei trimite catre socket doua lucruri in ordinea "1, 2" vor ajunge in aceeasi ordine la destinatie. Deasemenea nu vor aparea erori. Daca vor aparea erori inseamna ca tu singur le-ai provocat!
Cine foloseste socket-uri stream? Un exemplu concludent ar fi aplicatia telnet. Toate caracterele pe care le tastezi trebuie sa ajunga la destinatie in aceeasi ordine. Deasemenea browserele web folosesc protocolul HTTP care la randul lui foloseste socket-uri stream pentru a obtine paginile. Daca folosesti telnet-ul pentru a te conecta la un site web portul 80 si tastezi "GET pagename" vei obtine pagina HTML
Cum reusesc socket-urile stream sa atinga un asa ridicat nivel de transmisie a datelor? Folosesc protocolul numit "The Transmission Control Protocol" (Protocolul de Control al Transmisiei", cunoscut sub numele de "TCP" (vezi RFC-793 pentru mai multe detalii despre TCP.) TCP are grija ca datele tale sa ajunga in aceeasi ordine si fara erori. Se poate sa fi auzit de TCP in contextul de "TCP/IP" unde IP inseamna "Internet Protocol" (vezi RFC-791. pentru mai multe detalii)
Acum despre socket-uri de tip Datagram. De ce li se spune "connectionless"? Care ar fi ideea aici? De ce sunt nesigure? Raspunsul: daca trimiti un datagram poate sa ajunga. Poate sa ajunga in alta ordine decat cea initiala si astfel vor rezulta erori.
Socket-urile datagram folosesc si ele IP pentru a gasi calea, dar nu folosesc TCP-ul; folosesc "User Datagram Protocol" sau "UDP" (detalii la RFC-768.)
De ce sunt "connectionless"? Raspuns: nu trebuie sa mentii o conexiune deschisa ca pentru socket-uri de tip stream. Doar faci un pachet, ii atasezi un header IP cu destinatia lui si il trimiti. Nu necesita o conexiune. Sunt in general folosite pentru transferuri de informatii de tip packet-by-packet. Aplicatii ca: tftp, bootp, etc.
Intrebarea ar fi: "Cum functioneaza aceste programe daca datele trimise se pot pierde ?!" Raspunsul ar fi ca fiecare mai are un protocol peste UDP. De exemplu, protocolul tftp foloseste faptul ca pentru fiecare pachet trimis, cel ce il primeste trebui sa trimita un pachet care sa spuna ca fost primit(un pachet "ACK".) Daca expeditorul nu primeste acest ACK intr-un interval de timp va retransmite pachetul pana cand primeste ACK-ul. Acest lucru e foarte important la implementarea unei aplicatii SOCK_DGRAM.
E timpul sa vedem cum fundtioneaza o retea, si sa dam si cateva exemple despre cum sunt facute pachetele SOCK_DGRAM. Aceasta parte poate fi sarita.
Vom discuta acum despre incapsularea datelor. Un simplu curs de retele ar spune: un pachet se "naste", este invelit ("encapsulated") intr-un antet("header") si poate si un subsol("footer") cu primul protocol (sa spunem protocolul TFTP), iar dupa aceea intregul rezultat (cu tot cu header-ul TFTP) este incapsulat iarasi cu urmatorul protocol (sa spunem UDP-ul), apoi cu urmatorul protocol (IP), iar la urma cu protocolul final pe suport fizic (Ethernet).
Cand alt calculator primeste pachetul, suportul fizic desface headerul Ethernet, kernelul desface headerele IP si UDP, programul TFTP desface headerul TFTP, si el in cele din urma are datele.
In sfarsit vom putea vorbi despre Layered Network Model(Modelul de stratificare a retelei). Acest Network Model descrie un sistem de functionare a retelei care are multe avantaje asupra altor modele. De exemplu, dvs puteti sa scrieti programe socket fara a avea grija cum sunt transmise datele la nivel fizic(serial, Ethernet, AUI, mai oricare) pentru ca programele au ele grija de nivelele cele mai de jos. Astfel suportul fizic al acestui tip de retea cat si studiul topografic se refera doar la programarea socket.
Iata care sunt straturile unei astfel de retele:
Stratul fizic este hard-ul (serial, Ethernet, etc.). Stratul Aplicatie este de la startul fizic pana unde va puteti voi imagina--este locul unde utilizatorii interactioneaza cu reteaua.
Acum acest model este asa de complex ca ar putea fi folosit ca un ghid de reparare a automobilului. Un alt model de retea stratificata al Uix-ului ar putea fi:
In acest moment puteti constata cum aceste starturi corespund incapsularii datelor originale.
Observati cat munca e in construirea unui singur pachet. Tot ce trebuie sa faceti la socket-urile de tip stream este sa trimiteti pachetul: send. Tot ce trebuie sa faceti la socket-uri de tip datagram este sa incapsulati datele cu ce metoda vreti si sa le trimiteti sendto. Kernelul construieste Startul Transport si Internet iar partea de hard se ocupa de Startul de acces la retea. Tehnologie moderna!
Aici se termina scurta noastra incursiune in functionarea retelei. Am uitat sa va zic ceva despre router: desface pachetul de header-ul IP, consulta itinerariul tabelului, etc. Pentru mai multe detalii incercati:IP RFC
Iata-ne ajunsi la partea unde trebuie sa vorbim despre programare. In aceasta sectiune am de gand sa acopar diverse tipuri de date folosite de interfetele socket, deoarece unele sunt greu de priceput.
La inceput un tip usor: un socket descriptor. Un socket descriptor este un
int
Un simplu numar intreg.
Lucrurile devin mai grele de acum incolo asa ca citit cu atentie in continuare. De stiut: sunt doua tipuri de oranduiri: cel mai semnificativ octet primul sau cel mai nesemnificativ octet primul. Prima orinduire se numeste "Network Octet Order" deoarece primul byte mai este numit si "octet". Unele calculatoare memoraza numerele in "Network Byte Order" unele nu. Daca trebuie sa fie in NBO trebuie folosite o functie (cum ar fi htons() pentru a schimba din "Host Byte Order". Daca nu se precizeaza NBO, atunci totul ramane in Host Byte Order.
My First Struct(TM)--struct sockaddr. Aceasta structura retine informatii despre adrese de socket pentru multe tipuri de socket-uri.
struct sockaddr {
unsigned short sa_family; /* address family, AF_xxx */
char sa_data[14]; /* 14 bytes of protocol address */
};
sa_family poate insemna o mare varietate de date dar pentru ce facem in acest document va insemna "AF_NET". sa_data contine o adresa si un numar de port pentru socket. Aceasta este greu de manuit.
Pentru a putea folosi struct sockaddr, a fos t creata o structura paralela struct sockaddr_in ("in" de la internet).
struct sockaddr_in {
short int sin_family; /* Address family */
unsigned short int sin_port; /* Port number */
struct in_addr sin_addr; /* Internet address */
unsigned char sin_zero[8]; /* Same size as struct sockaddr */
};
Aceasta structura face simpla folosirea referintelor la adresa socket. Notati sin_zero (care este inclus sa umple structura la lungimea unei struct sockaddr) trebuie setat la zero folosind functia bzero() sau memset().Partea importanta e ca un pointer catre o struct sockaddr_in poate fi folosit de un pointer la struct sockaddr. Deci chiar daca socket() vrea struct sockaddr *, puteti folosi un struct sockaddr_in si transforma in ultima clipa! Totodata observati ca sin_family corespunde lui sa_family intr-o struct socket_addr si trebuie setat la "AF_NET". sin_port si sin_addr trebuie sa respecte Network Byte Order!
Cum poate o intreaga structura sa fie in Network Byte Order? Structura struct in_addr trebuie examinata atent:
struct in_addr{
unsigned long s_addr;
};
A fost un union dar acele zile s-au scurs. Daca declari "ina" sa fie de tipul sturct sock_addr_in, atunci "ina.sin_addr.s_addr face referire la adresa IP de 4 bytes (in Network Byte Order). Deci chiar daca sistemul foloseste union pentru a declara structura tot poti face referire la o adresa IP de 4 bytes exact ca adineaori.
E timpul sa vorbim in amanunt despre Network Byte Order!
Sunt doua tipuri care pot fi convertite: short (pe 2 bytes) si long (pe 4 bytes). Aceste functii merg si pentru unsigned. De exemplu vrei sa faci o conversie un short de la Host Byte Order catre un Network Byte Order. Functia e htons (h de la "host", urmat de cuvantul to (catre) apoi n de la "network" iar in final s de la "short": Host to Network Short).
Astea ar fi toate functiile:
Un lucru foarte important: converteste bytes in Network Order inainte de a incerca sa te conectezi la retea.
La final: de ce trebuie ca sin_addr si sin_port sa fie in Network Byte Order in structura struct sockaddr_in, iar in schimb sin_family nu trebuie? Raspunsul e ca sin_addr si sin_port vor fi incapsulate in stratele IP si UDP pe cand sin_family este folosit de kernel ca sa determine ce tip de adresa contine structura, si deci trebuie sa fie in Host Byte Order. Totdata sin_family nu e transmisa in retea si poate fi in Host Byte Order.
Din fericire sunt multe functii care iti permit sa manipulezi adresele IP. Nu trebuie sa le configurezi manual si le introduci intr-un long folosind operatorul << .
Pentru inceput sa spunem ca aveti o struct sockaddr_in ina, si o adresa IP "132.241.5.10" pe care vreti sa o retineti cu ajutorul structurii. Functia pe care o veti folosi inet_addr() converteste o adresa IP cu notatia specifica de numere-si-puncte intr-un unsigned long. Operatia se face dupa cum urmeaza:
ina.sin_addr.s_addr = inet_addr("132.241.5.10");
Notice that inet_addr() returneaza adresa in format Network Byte Order --si nu mai trebuie sa folositi htonl().
Codul de mai sus nu e prea performant deoarece nu se verifica daca sunt eventuale erori. Se observa, inet_addr() returneaza -1 in caz de eroare. Problema e ca (unsigned)-1 se intampla sa corespunda adresei IP 255.255.255.255! Deci este foarte important sa faci o verificare daca apar eventuale erori.
Acum poti sa convertesti un string ce reprezinta o adresa IP intr-un longs. Intrebarea ar fi daca se poate si invers? Daca ai o struct in_addr si vrei sa afisezi in format numere-si puncte? In acest caz vei folosi functia inet_ntoa() ("ntoa" means "network to ascii") in modul urmator
printf("%s",inet_ntoa(ina.sin_addr));
Adresa IP va fi tiparita. Observati cainet_ntoa() are ca argument struct in_addr ,si nu un long. Totodata mai observati ca returneaza un pointer la char. Acesa pointeaza catre un tablou de char memorat static inet_ntoa() si de fiecare data cand vei apela functia inet_ntoa() va scrie peste ultima adresa IP care ai foosit-o. De exemplu:
char *a1, *a2;
.
.
a1 = inet_ntoa(ina1.sin_addr); /* care e 198.92.129.1 */
a2 = inet_ntoa(ina2.sin_addr); /* care e 132.241.5.10 */
printf("address 1: %s\n",a1);
printf("address 2: %s\n",a2);
will print:
adresa 1: 132.241.5.10
adresa 2: 132.241.5.10
Daca vrei sa salvezi adresa folosestestrcpy() catre un tablou de char creat de tine.
MAi tarziu vom invata sa convertim un string ca like "whitehouse.gov" catre adresa lui corespunzatoare (detalii la DNS, below.)
Vom vorbi despre apelul sistem socket() :
#include <sys/types.h>
#include <sys/socket.h>
int socket(int domain, int type, int protocol);
Sa ne uitam la argumente. In primul rand, domain ar trebui sa fie setat la "AF_INET", la fel ca in struct sockaddr_in (mai sus.) In continuare, argumentul arata kernelului ce fel de socket e: SOCK_STREAM sau SOCK_DGRAM. In sfarsit setati protocol la "0". (Sunt mai multe tipuri de domains decat cele mentionate. Sunt mai multe types decat cele enumerate. Detalii la socket() pagina de man . Totodataeste o cale mai "buna" de a obtine variabila protocol. Detalii la getprotobyname() pagina de man.)
socket() returneaza un descriptor de socket care poate fi folosit in urmatoarele apeluri sistem , sau -1 la aparitia unor erori. Variabila globala errno este setata sa arate valoarea erorii (detalii la perror() pagina de man .)
Iata o descriere pentru apelul sistem bind()
#include <sys/types.h>
#include <sys/socket.h>
int bind(int sockfd, struct sockaddr *my_addr, int addrlen);
sockfd este descriptorul de fisier returnat de
socket(). my_addr este un pointer catre o
struct sockaddr ce contine informatii despre adresa anume
portul si adresa IP.addrlen poate fi obtina
sizeof(struct sockaddr).
Pentru o mai buna intelegere iata un exemplu:
#include <string.h>
#include <sys/types.h>
#include <sys/socket.h>
#define MYPORT 3490
main()
{
int sockfd;
struct sockaddr_in my_addr;
sockfd = socket(AF_INET, SOCK_STREAM, 0); /* verifica daca sunt erori */
my_addr.sin_family = AF_INET; /* host byte order */
my_addr.sin_port = htons(MYPORT); /* network byte order */
my_addr.sin_addr.s_addr = inet_addr("132.241.5.10");
bzero(&(my_addr.sin_zero), 8); /* zero restul structurii */
/* verificati erorile si cand folositi bind(): */
bind(sockfd, (struct sockaddr *)&my_addr, sizeof(struct sockaddr));
.
.
.
Iata cateva lucruri de retinut. my_addr.sin_port este in
Network Byte Order. Deasemenea my_addr.sin_addr.s_addr.
Alt lucru de retinut e ca headerele ar putea diferi de la un sistem
la altul. Pentru siguranta verificati paginile de man de pe
calculator.
In cele din urma la bind(), ar trebui sa mentionez ca o parte din procesul de obtinere a propriei adrese IP si/sau a portului poate sa fie automatizat.
my_addr.sin_port = 0; /* alege un port neutilizat
aleatoriu */
my_addr.sin_addr.s_addr = INADDR_ANY; /* imi foloseste adresa IP*/
Observati, configurandmy_addr.sin_port la zero, lasati ca
bind() sa aleaga portul pentru dvs. Deasemenea configurand
my_addr.sin_addr.s_addr to INADDR_ANY,dvs ii
spuneti ca sa completeze automat adresa IP a calculatorului pe care
ruleaza procesul.
Daca observati detaliile, puteti vedea ca nu am pus
INADDR_ANY in Network Byte Order! . Iata explicatia
INADDR_ANY este zero! Zero ramane zero pe bits chiar daca
rearanjati bytes. Oricum se poate mentiona daca INADDR_ANY
este 12 codul nu va functiona .
my_addr.sin_port = htons(0); /* alege un port neutilizat aleator */sockfd este descriptorul de fisier de tip socket returnat de socket() serv_addr este de tipstruct sockaddr contine portul destinatiei atat cat si adresa IP a destinatiei addrlen poate fi setat pentru sizeof(struct sockaddr).
my_addr.sin_addr.s_addr = htonl(INADDR_ANY); /* imi foloseste adresa IP */
bind() de asemenea intoarce -1 in caz de eroare si errno arata valoarea erorii.
Alt lucru de urmarit cand folositi bind(): toate porturile sub 1024 sunt deja folosite deci rezervate. Celelalte pana la 65535 pot fi folosite(daca nu sunt deja folosite de catre alt program).
Inca o nota aditionala despre bind(): sunt situatii cand nu trebuie apelat. Daca folositi connect()(conectare) la un calculator departat si nu va pasa ce port folositi (exemplu telnet) folositi connect(), si el verifica sa vada daca socketul nu e "legat" si va folosi bind()(legare)la un port local nefolosit. <hr>
<h2>connect()--Conectarea!</h2> Sa luam ca exemplu o aplicatie de tip telnet. Prima data se va obtine un descriptor de fisier tip socket folosind socket(). In continuare utilizatorul vrea sa se conecteze la "132.241.5.10" folosind portul 23 (standard pentru telnet). Sa vedem cum:
The connect() call is as follows: <pre> #include <sys/types.h>
#include <sys/socket.h>
int connect(int sockfd, struct sockaddr *serv_addr, int addrlen);
Sa luam un exemplu:
#include <string.h>
#include <sys/types.h>
#include <sys/socket.h>
#define DEST_IP "132.241.5.10"
#define DEST_PORT 23
main()
{
int sockfd;
struct sockaddr_in dest_addr; /* will hold the destination addr */
sockfd = socket(AF_INET, SOCK_STREAM, 0); /* verifica posibilele erori! */
dest_addr.sin_family = AF_INET; /* host byte order */
dest_addr.sin_port = htons(DEST_PORT); /* network byte order */
dest_addr.sin_addr.s_addr = inet_addr(DEST_IP);
bzero(&(dest_addr.sin_zero), 8); /* zero restul structurii */
/* faceti verificarea erorilor cand apelati connect()! */
connect(sockfd, (struct sockaddr *)&dest_addr, sizeof(struct sockaddr));
.
.
.
Aveti grija sa verificati ce valoare returneaza connect() -1 in caz de eroare si variabila errno care arata eroarea.
Observati ca nu am folosit bind. Nu ne intereseaza portul local ci doar destinatia. Kernelul va alege un port local si locul unde ne conectam va primi aceasta informatie de la noi. Fara grija.
Apelul lui listen e simplu, si iata-i explicatia:
int listen(int sockfd, int backlog);
sockfd descriptorul de fisier socket returnat de apelul
sistem socket(). backlog este numarul de
conexiuni ce pot incaoe in coada. Adica toate conexiunile ce au
aceasta destinatie vor astepta intr-o coada pana se face apelul
sistem accept() si asta e limitarea cozii. Majooritatea
sistemelor limiteaza acest numar la 20 dar ar merge chiar si cu o
valoare de 5 sau 10.
In caz de eroare listen() returneaza -1 iar variabila errno eroarea aparuta.
Trebuie sa apelam la bind() pentru ca kernelul sa nu asculte la un port oarecare si apoi abia sa apelam listen() .Deci pentru a asculta conexiunile ce au aceasta destinatie trebuie sa folosim:
socket();
bind();
listen();
/* accept() aici */
Un cod mai detaliat in sectiunea pentru accept().
Cod:
#include <sys/socket.h>
int accept(int sockfd, void *addr, int *addrlen);
sockfd este descriptorul de socket pentru
listen(). Easy addr va fi un pointercatre o
structura locala struct sockaddr_in. Aici va ajunge
informatia despre noua conexiune ce va veni(astfel se va putea afla
ce host a initiat conexiunea si la ce port). addrlen este
o variabila integer locala si va trebui initializata cu
sizeof(struct sockaddr_in) inainte ca adresa sa fie
trimisa catre accept().
accept() intoarce -1 si initializeaza sa afiseze ce eroare e errno daca a survenit o eroare
Iata o bucata de cod care ajuta sa intelegeti cele mentionate mai sus
#include <string.h>
#include <sys/types.h>
#include <sys/socket.h>
#define MYPORT 3490 /* portul unde se vor conecta utilizatorii */
#define BACKLOG 10 /* cate conexiuni in asteptare se vor tine in coada */
main()
{
int sockfd, new_fd; /* ascultacu sock_fd, conexiunile la new_fd */
struct sockaddr_in my_addr; /* informatiile despre adresa mea */
struct sockaddr_in their_addr; /* adresa celui care se conecteaza */
int sin_size;
sockfd = socket(AF_INET, SOCK_STREAM, 0); /* verificati erorile */
my_addr.sin_family = AF_INET; /* host byte order */
my_addr.sin_port = htons(MYPORT); /* short, network byte order */
my_addr.sin_addr.s_addr = INADDR_ANY; /* IP-ul meu */
bzero(&(my_addr.sin_zero), 8); /* zero restul structurii */
/* verificati erorile care pot surveni: */
bind(sockfd, (struct sockaddr *)&my_addr, sizeof(struct sockaddr));
listen(sockfd, BACKLOG);
sin_size = sizeof(struct sockaddr_in);
new_fd = accept(sockfd, &their_addr, &sin_size);
.
.
.
Se foloseste descriptorul de fisier de tip socket new_fd
pentru send() si recv() . Daca te astepti la o
singura conexiune poti inchide cu close() descriptorul
sockfd pentru a preveni alte conexiuni ce ar putea veni pe
acelasi port.
Apelul send() :
int send(int sockfd, const void *msg, int len, int flags);
sockfd este descritorul de socket catre care vrei sa
trimiti datele (chiar daca este returnat de apelul sistem
socket() sau accept().)msg este un
pointer catre datele care trebuie trimise si len este
lungimea datelor in bytes. Setati flags la 0. (
send() pagina de man pentru mai multe informatii despre
flaguri.
Exemplul aici:
char *msg = "Am fost aici!";
int len, bytes_sent;
.
.
len = strlen(msg);
bytes_sent = send(sockfd, msg, len, 0);
.
.
.
send() returneaza numarul de bytes trimisi--care poate
fi mai mic decat numarul de bytes care ai vrut sa-l trimiti
Daca valoarea returnata send() nu e aceeasi cu
len, depinde de tine daca vrei sa trimiti si restul
stringului. Din nou, -1 este returnat in caz de eroare, si
errno arata eroarea aparuta(numarul ei).
Apelul recv() e similar in multe aspecte:
int recv(int sockfd, void *buf, int len, unsigned int flags);
sockfd e descriptorul de unde se citeste si buf
este bufferul unde se citesc informatiile, len este
lungimea maxima a bufferului si flags din nou setat la
0. (
recv() pagina de man pentru detalii despre flaguri .)
recv() intoarce numarul de bytes cititi in buffer sau -1 in caz de eroare (cu errno aratand eroarea.)
Deoarece aceste socketuri nu se conecteaza la un calculator lor le trebuie adresa destinatiei. Iata de ce:
int sendto(int sockfd, const void *msg, int len, unsigned int flags,
const struct sockaddr *to, int tolen);
Apelul este asemanator cu cel de la send() cu adaugarea a
inca 2 informatii to e un pointer catre structura
struct sockaddr ce contine adresa IP a destinatiei si
portul respectiv. tolen va fi setat catre
sizeof(struct sockaddr).
Ca si la send(), sendto() intoarce numarul de bytes ce au fost trimisi sau -1 in caz de eroare.
Aceeasi asemanare si intre recv() si recvfrom(). Prototipul functiei: recvfrom():
int recvfrom(int sockfd, void *buf, int len, unsigned int flags
struct sockaddr *from, int *fromlen);
Se aseamana cu recv() insa se adauga inca niste
informatii.from e un pointercatre structura struct
sockaddr care va detine adresa IP si portul calculatorului de
unde vine conexiunea. fromlen e un pointer catre un int
local int ce trebuie initializat cu sizeof(struct
sockaddr). Cand functia returneaza o valoare, fromlen
va contine lungimea adresei care e in variabila from.
recvfrom() intoarce numarul de bytes primiti,sau -1 in caz de eroare (iar errno arata eroarea.)
Daca folositi connect() la un socket datagram, vei putea folosi send() si recv() .Socketul ramane de tip datagram pachetele folosesc tot UDP-ul dar socket va intoarce destinatia si sursa informatiei
close(sockfd);
Astfel nu se va mai putea scrie si citi din socket. Oricine va
incerca va primi eroare.
Pentru un mai mare control asupra functiei de inchidere a socketului putem folosi functia shutdown(). Va inchide comunicatia intr-o anumita directie sau chiar in ambele sensuri(la fel ca si close)
int shutdown(int sockfd, int how);
sockfd e descriptorul ce trebuie inchis, si how
poate fi una din urmatoarele:
shutdown() intoarce 0 in caz de succes, si -1 in caz de eroare (cu errno aratand eroarea aparuta.)
In caz ca shutdown() e folosit in cazul socketurilor datagram neconectate nu se va mai putea trimite si primi date
E destul de usor.
Functia getpeername() iti va spune cine e la celalalt capat al socketului de tip stream.Iata functia:
#include <sys/socket.h>
int getpeername(int sockfd, struct sockaddr *addr, int *addrlen);
sockfd e descriptorul folosit pentru socketaddr e
un pointer catre structura struct sockaddr (sau catre
structura struct sockaddr_in) ce detine informatii despre
celalalt capat al conexiunii. addrlen e un pointer catre
un int, ce ar trebui initializat cu sizeof(struct
sockaddr).
Functia returneaza-1 in caz de eroare, iar errno numarul erorii.
Cand ai obtinut adresa, poti folosi inet_ntoa() sau gethostbyaddr() pentru afisare sau obtinere de mai multe informatii . Mai multe despre acest subiect la RFC-1413 .)
#include <unistd.h>
int gethostname(char *hostname, size_t size);
Argumentele sunt simple hostname e un pointer catre o matrice de caractere care contine numele calculatorului , si size este lungimea in bytes a matricii respective
Functia returneaza 0 incaz de succces, si -1 in caz de eroare, iar errno ca de obicei.
Iata cum functioneaza acest lucru gethostbyname():
#include <netdb.h>
struct hostent *gethostbyname(const char *name);
Dupa cum se poate observa va returna un pointer catre o structura
struct hostent, care are urmatoarea definitie:
struct hostent {
char *h_name;
char **h_aliases;
int h_addrtype;
int h_length;
char **h_addr_list;
};
#define h_addr h_addr_list[0]
Iata si descrierea acestor campuri struct hostent:
gethostbyname() returneaza un pointer catre struct hostent, sau NULL in caz de eroare. (Insa errno nu e not setatsa arate eroarea--ci h_errno- Functia herror de mai jos.)
Iata un exemplu:
#include <stdio.h>
#include <stdlib.h>
#include <errno.h>
#include <netdb.h>
#include <sys/types.h>
#include <netinet/in.h>
int main(int argc, char *argv[])
{
struct hostent *h;
if (argc != 2) { /* verifica argumentele din linia de comanda */
fprintf(stderr,"usage: getip address\n");
exit(1);
}
if ((h=gethostbyname(argv[1])) == NULL) { /* obtine informatii despre host */
herror("gethostbyname");
exit(1);
}
printf("Host name : %s\n", h->h_name);
printf("IP Address : %s\n",inet_ntoa(*((struct in_addr *)h->h_addr)));
return 0;
}
Cu functia gethostbyname(), puteti folosi
perror() pentru a afisa mesajele de eroare ( deoarece
errno nu e folosit ). In schimb apelati herror().
Sirul de caractere care contine numele masinii ("whitehouse.gov") este trimis catre gethostbyname(), si apoi folosim informatia din struct hostent.
Singura ciudatenie e in afisarea adresei IP. h->h_addr e de tip char *, dar inet_ntoa() accepta argument de tip struct in_addr. Folosim operatorul castde la h->h_addr catre struct in_addr *, .
Implementare cea mai des folosita e categoric client-server.Exemplu telnet. Cand are loc conectarea la un host indepartat pe portul 23 folosind clientul de telnet un program de pe acel host numit telnetd serverul intra in actiune. Are grija de conexiunea ce vine din afara afiseaza un prompt de login etc.
O aplicatie client-server poate folosi SOCK_STREAM, SOCK_DGRAM, sau aproape orice numai sa fie acelasi lucru de ambele parti Exemple de astfel de aplicatii: telnet/telnetd, ftp/ftpd, sau bootp/bootpd. De fiecare data cand folosesti ftp-ul e un program ce ruleaza pe alt calculator ftpd care iti raspunde.
De cele mai multe ori va fi un singur server pe calculator iar acesta va trata cu mai multi clienti. Iata cum: serverul asteapta o conexiune apoi o accepta cu accept() si apoi cu fork() face un proces fiu care sa aiba grija de conexiune. Asta face si serverul nostru in urmatorul exemplu:
$ telnet remotehostname 3490
unde remotehostname numele masinii pe care ruleaza .
Codul pentru server:
#include <stdio.h>
#include <stdlib.h>
#include <errno.h>
#include <string.h>
#include <sys/types.h>
#include <netinet/in.h>
#include <sys/socket.h>
#include <sys/wait.h>
#define MYPORT 3490 /* portul folosit pentru conectare */
#define BACKLOG 10 /* cate conexiuni in asteptare va tine coada*/
main()
{
int sockfd, new_fd; /* listen cu sock_fd, o conexiune noua cu new_fd */
struct sockaddr_in my_addr; /* informatii despre adresa mea */
struct sockaddr_in their_addr; /* adresa celui care se conecteaza */
int sin_size;
if ((sockfd = socket(AF_INET, SOCK_STREAM, 0)) == -1) {
perror("socket");
exit(1);
}
my_addr.sin_family = AF_INET; /* host byte order */
my_addr.sin_port = htons(MYPORT); /* short, network byte order */
my_addr.sin_addr.s_addr = INADDR_ANY; /* IP-ul */
bzero(&(my_addr.sin_zero), 8); /* zero restul structurii */
if (bind(sockfd, (struct sockaddr *)&my_addr, sizeof(struct sockaddr)) \
== -1) {
perror("bind");
exit(1);
}
if (listen(sockfd, BACKLOG) == -1) {
perror("listen");
exit(1);
}
while(1) {
sin_size = sizeof(struct sockaddr_in);
if ((new_fd = accept(sockfd, (struct sockaddr *)&their_addr, \
&sin_size)) == -1) {
perror("accept");
continue;
}
printf("server: got connection from %s\n", \
inet_ntoa(their_addr.sin_addr));
if (!fork()) { /* this is the child process */
if (send(new_fd, "Hello, world!\n", 14, 0) == -1)
perror("send");
close(new_fd);
exit(0);
}
close(new_fd); /* parintele n-are nevoie de asta */
while(waitpid(-1,NULL,WNOHANG) > 0); /* se termina fiul */
}
}
Poti obtine sirul de caractere de la server folosind clientul listat in sectiunea urmatoare
Codul pentru client:
#include <stdio.h>
#include <stdlib.h>
#include <errno.h>
#include <string.h>
#include <netdb.h>
#include <sys/types.h>
#include <netinet/in.h>
#include <sys/socket.h>
#define PORT 3490 /* portul pe care se conecteaza clientul */
#define MAXDATASIZE 100 /* cati bytes poate sa primeasca odata*/
int main(int argc, char *argv[])
{
int sockfd, numbytes;
char buf[MAXDATASIZE];
struct hostent *he;
struct sockaddr_in their_addr;
if (argc != 2) {
fprintf(stderr,"usage: client hostname\n");
exit(1);
}
if ((he=gethostbyname(argv[1])) == NULL) { /* obtine informatii despre host */
herror("gethostbyname");
exit(1);
}
if ((sockfd = socket(AF_INET, SOCK_STREAM, 0)) == -1) {
perror("socket");
exit(1);
}
their_addr.sin_family = AF_INET; /* host byte order */
their_addr.sin_port = htons(PORT); /* short, network byte order */
their_addr.sin_addr = *((struct in_addr *)he->h_addr);
bzero(&(their_addr.sin_zero), 8); /* zero restul structurii */
if (connect(sockfd, (struct sockaddr *)&their_addr, \
sizeof(struct sockaddr)) == -1) {
perror("connect");
exit(1);
}
if ((numbytes=recv(sockfd, buf, MAXDATASIZE, 0)) == -1) {
perror("recv");
exit(1);
}
buf[numbytes] = '\0';
printf("Received: %s",buf);
close(sockfd);
return 0;
}
Daca serverul nu ruleaza clientul intoarce prin connect() "Connection refused".
listener asteapta un pachet la un anumit port 4950. talker trimite un pachet catre hostul specificat pe portul specificat si care contine ce a scris user-ul in linia de comanda.
Iata codul listener.c:
#include <stdio.h>
#include <stdlib.h>
#include <errno.h>
#include <string.h>
#include <sys/types.h>
#include <netinet/in.h>
#include <sys/socket.h>
#include <sys/wait.h>
#define MYPORT 4950 /* portul*/
#define MAXBUFLEN 100
main()
{
int sockfd;
struct sockaddr_in my_addr; /* adresa mea */
struct sockaddr_in their_addr; /* adresa celui ce se conecteaza */
int addr_len, numbytes;
char buf[MAXBUFLEN];
if ((sockfd = socket(AF_INET, SOCK_DGRAM, 0)) == -1) {
perror("socket");
exit(1);
}
my_addr.sin_family = AF_INET; /* host byte order */
my_addr.sin_port = htons(MYPORT); /* short, network byte order */
my_addr.sin_addr.s_addr = INADDR_ANY; /* IP */
bzero(&(my_addr.sin_zero), 8); /* zero restulstructurii */
if (bind(sockfd, (struct sockaddr *)&my_addr, sizeof(struct sockaddr)) \
== -1) {
perror("bind");
exit(1);
}
addr_len = sizeof(struct sockaddr);
if ((numbytes=recvfrom(sockfd, buf, MAXBUFLEN, 0, \
(struct sockaddr *)&their_addr, &addr_len)) == -1) {
perror("recvfrom");
exit(1);
}
printf("got packet from %s\n",inet_ntoa(their_addr.sin_addr));
printf("packet is %d bytes long\n",numbytes);
buf[numbytes] = '\0';
printf("packet contains \"%s\"\n",buf);
close(sockfd);
}
.Observati ca nu e nevoie de listen() sau de
accept().
Sursa pentru talker.c:
#include <stdio.h>
#include <stdlib.h>
#include <errno.h>
#include <string.h>
#include <sys/types.h>
#include <netinet/in.h>
#include <netdb.h>
#include <sys/socket.h>
#include <sys/wait.h>
#define MYPORT 4950 /* portul*/
int main(int argc, char *argv[])
{
int sockfd;
struct sockaddr_in their_addr; /* adresa celui care se conecteaza*/
struct hostent *he;
int numbytes;
if (argc != 3) {
fprintf(stderr,"usage: talker hostname message\n");
exit(1);
}
if ((he=gethostbyname(argv[1])) == NULL) { /* informatii despre host*/
herror("gethostbyname");
exit(1);
}
if ((sockfd = socket(AF_INET, SOCK_DGRAM, 0)) == -1) {
perror("socket");
exit(1);
}
their_addr.sin_family = AF_INET; /* host byte order */
their_addr.sin_port = htons(MYPORT); /* short, network byte order */
their_addr.sin_addr = *((struct in_addr *)he->h_addr);
bzero(&(their_addr.sin_zero), 8); /* zero restul structurii */
if ((numbytes=sendto(sockfd, argv[2], strlen(argv[2]), 0, \
(struct sockaddr *)&their_addr, sizeof(struct sockaddr))) == -1) {
perror("sendto");
exit(1);
}
printf("sent %d bytes to %s\n",numbytes,inet_ntoa(their_addr.sin_addr));
close(sockfd);
return 0;
}
Acesta-i tot codul. Rulati listener pe un calculator si
talker pe altul si vedeti cum comunica.
O mica omisiune: despre socketuri de tip datagram conectate: sa spunem ca talker apeleaza connect() si specifica adresa celui care ruleaza listener-ul. Din acel moment talker-ul poate sa trimita doar adresei precizate apelului lui connect() si nu trebuie sa foloseasca sendto() si recvfrom() ci send() si recv().
Toate functiile se blocheaza adica asteapta: accept(), recv*(). Cand se creeaza descriptorul de socket apeland socket(), kernelul il blocheaza. Daca nu se doreste blocarea ar trebui sa se foloseasca fcntl():
#include <unistd.h>
#include <fcntl.h>
.
.
sockfd = socket(AF_INET, SOCK_STREAM, 0);
fcntl(sockfd, F_SETFL, O_NONBLOCK);
.
.
Daca incerci sa citesti dintr-un descriptor de tip socket care nu a fost blocat si este gol va returna -1 si errno va avea valoarea EWOULDBLOCK.
Ideea de a nu bloca descriptorul nu e prea buna deoarece se foloseste din timpul procesorului si nu e solutie prea eleganta. O solutie mai evoluata este prezentata in sectiunea urmatoare: select().
Ideea e ca daca la accept() blochezi celelalte conexiuni, iar socketuri care nu se blocheaza de catre kernel nu poti folosi deoarece nu vrei sa folosesti multe dintre resursele sistemului, mai laes procesorul.
Cu select() poti monitoriza mai multe socketuri in acelasi timp. Trebuie sa stii care sunt gata sa primeasca date care sa trimita si care au dat erori.
Asa arata select():
#include <sys/time.h>
#include <sys/types.h>
#include <unistd.h>
int select(int numfds, fd_set *readfds, fd_set *writefds,
fd_set *exceptfds, struct timeval *timeout);
Functia monitorizeaza mai multi descriptori de fapt readfds, writefds, si exceptfds. Parametrul numfds trebuie sa fie setat cu cea mai mare valoare a unui descriptor plus 1.
Cand select() ajunge la return, readfds va arata ce descriptor dintre cei selectati e gata pentru citire. Testarea se poate face cu FD_ISSET(), mai jos.
Iata cum puteti folosi aceste seturi de descriptori. Fiecare set e de tipul fd_set.
Daca nu vrei sa astepti tot timpul ca sa trimita cineva date si ca la fiecare de exemplu 96 de secunde sa afisezi un mesaj la terminal chiar daca nu s-a intamplat nimic. Structura struct timeval iti permite sa precizezi aceasta perioada de timp.
Iata structura struct timeval :
struct timeval {
int tv_sec; /* seconds */
int tv_usec; /* microseconds */
};
Doar setati tv_sec la numarul de secunde pe care trebuie
sa-l asteptati numar care trebuie introdus in microsecunde(=1/1.000
milisecunde=1/1.000.000 secunde). Functia intoarce timeout
si poate fi folosit pentru a arata cat timp a mai ramas(depinde de
UNIX-ul folosit)
Cea mai mica unitate de masurare a timpului in UNIX e 100 millisecunde deci nu va hazardati la setarea ceasului cu o valoare foarte mica.
Setata la 0 select() timpul va expira instantaneu, iar daca e setata NULL nu va expira niciodata si va astepta pana cand primul descriptor e pregatit.
Urmatorul program asteapta 2.5 secunde pentru ca ceva sa apara pe terminal:
#include <sys/time.h>
#include <sys/types.h>
#include <unistd.h>
#define STDIN 0 /* standard input */
main()
{
struct timeval tv;
fd_set readfds;
tv.tv_sec = 2;
tv.tv_usec = 500000;
FD_ZERO(&readfds);
FD_SET(STDIN, &readfds);
select(STDIN+1, &readfds, NULL, NULL, &tv);
if (FD_ISSET(STDIN, &readfds))
printf("A key was pressed!\n");
else
printf("Timed out.\n");
}
Daca avem un socket care asculat listen() la conexiunile care ar putea veni putem verifica daca e vreo conexiune noua punind socketul in setul readfds.
Daca apar erori imi pare rau. Si imi pare rau daca nu ati inteles ce am vrut sa spun si incercati sa nu ma trageti la raspundere. Totul poate sa-ti para doar niste prostii.
Dar probabil ca nu e asa. Am stat o gramada de timp ca sa scriu acest document si am implementat cateva utilitati de retea pentru Windows (incluzand Telnet-ul). Nu stapanesc socketurile la perfectie. Sunt doar un tip obisnuit si as putea gresi.
Orice fel de critica sau multumire poate fi trimisa pe adresa beej@ecst.csuchico.edu si voi incerca sa raspund.
Bineinteles ca nu am facut acest lucru pentru bani ci pentr ca o gramada de oameni m-au intrebat despre socketuri si cand le-am spus ca o sa scriu ceva despre acest subiect la toti le-a placut ideea. Si toate aceste cunostiinte trebuie impartite si la ceilalti ca sa nu se piarda si ii incurajez si pe altii sa faca acelasi lucru.
Gata cu asta--inapoi la lucru! ;-)