Ghidul Beej pentru Programarea in Retea
Folosind socket de internet
Introducere
Salut! Programarea cu socket va darama? E chestia asta destul de dificila ca sa va dati seama de cum se rezolva din paginile man? Vreti sa creati programe internet tari, dar nu aveti timp de pierdut printr-o gramada de structuri incercand sa vadati seama daca trebuie sa apelati bind() inainte de connect() etc.?Ei bine, ia ghiciti! Eu deja am trecut prin treburile astea buclucase, si mor de nerabdare s impart informatia cu toata lumea! Ai venit in locul potrivit. Acest document ar trebui sa dea unui programator mediu de C ceea ce ii trebuie.
Audienta
Acest document a fost scris ca tutorial, nu ca o referinta. Este probabil
cel mai bine sa fie citit de cei care de abia au inceput sa programeze
cu socket si cauta un drum de urmat. Cu siguranta nu este un ghid complet
pentru programarea socket.
Sper totusi, ca va fi indeajuns ca acele pagini man sa inceapa sa aibe sens...
Compilator si platforma
Cea mai mare parte de cod continuta in acest document a fost compilata
pe un PC Linux utilizand compilatorul de la GNU gcc. De asemenea a fost
compilat pe HPUX utilizand gcc. Atentie, fiecare bucatica de cod nu a fost
testata individual.
Cuprins:
- Ce este un socket?
- Doua tipuri de socket Internet
- Structuri
- Conversii Native!
- Adrese IP si cum sa lucram cu ele
- socket() - Obtinerea descriptorului de fisier!
- bind() - Pe ce port sunt?
- connect() - Hei, tu!
- listen() - Va rog, ma suna si pe mine cineva?
- accept() - Va multumesc pentru apelul portului 3490
- send() si recv() - Spune-mi ceva, draga!
- sendto() si recvfrom() - Vorbeste-mi, in stilul DGRAM
- close() si shutdown() - Dispari din fata mea!
- getpeername() - Cine esti?
- gethostname() - Cine sunt?
- DNS - Tu spui whitehouse.gov, eu spun 198.137.240.100
- Esenta despre Client-Server
- Un server stream simplu
- Un client stream simplu
- Datagram Socket
- Blocare
- select() - Multiplexare Sincrona I/O
- Mai multe Referinte
- Eu renunt, va rog ajutati-ma
Ce este un socket?
Ati auzit tot timpul vorbindu-se de "socket", si probabil va intrebati
exact ce reprezinta. Ei bine iata ce: un mod de comunicare catre alte programe
utilizand descriptori Unix standard de fisiere.
Ce?
Ok - poate ati auzit de expresiile unor hackeri Unix, "Mai sa fie, totul in Unix e un fisier!" Poate despre ce vorbea acea persoana este faptul ca atunci cand un program Unix efectueaza orice fel de operatie I/O, o face citind sau scriind la un descriptor de fisier. Un descriptor de fisier este un simplu intreg asociat unui fisier deschis. Dar (si aici e partea importanta), acel fisier poate fi o conexiune de retea, un FIFO, un pipe, un terminal, un fisier real de pe disc, sau aproape orice altceva.
Totul in Unix este un fisier! Asa ca atunci cand doriti sa comunicati cu un alt program prin internet o veti face printr-un descriptor de fisier, ar fi bine sa credeti asta.
"De unde fac eu rost de acest descriptor de fisiere, domnule Pantaloni Destepti?" este probabil ultima intrebare care va vine in minte acum, dar voi raspunde oricum: Faci un apel catre rutina de sistem socket(). Ea intoarce un descriptor de socket, si poti comunica prin el utilizand functiile speciale socket send() si recv() (man send, man recv).
"Dar stai!" poti exclama chiar acum. "Daca este un descriptor de fisier de ce sa nu pot utiliza apelurile normale read() si write() pentru a comunica printr-un socket?" Raspunsul scurt este "Poti!" Raspunsul lung este "Poti, dar send() si recv() iti ofera un control mult mai mare asupra datelor transmise."
Ce urmeaza? Ce spuneti de asta: sunt tot felul de socketi. Exista adrese Internet DARPA (socketi de internet), nume de cale catre un nod local (socketi unix), adrese CCITT X.25 (socketi X.25 pe care puteti sa-i ignorati), si probabil multi altii depinzand de aroma de unix pe care o rulati. Acest document ii include doar pe primii: socketi de internet.
Doua tipuri
de socket Internet
Ce e asta? Exista doua tipuri de socket Internet? Da. Adica nu. Mint. Exista
mai multe, dar nu vreau sa va sperii. Voi vorbi doar de doua tipuri aici.
Exceptand aceasta propozitie, unde va voi spune ca "Raw Sockets" sunt de
asemenea foarte puternici si ca ar trebui sa cautati sa invatati si despre
ei.
E bine pana acum. Care sunt cele doua tipuri de socket? Unul este "Stram Socket"; celalalt este "Datagram Socket", care vor fi referiti de aici in colo ca "SOCK_STREAM" si "SOCK_DGRAM". Datagram Socket se mai numeste uneori "socket fara conectare" (desi pot fi connect()-ati daca asta doriti). Vezi connect() mai jos.
Stream Socket sun conexiuni sigure conectate in ambele sensuri de comunicare. Daca trimiteti la un socket doua articole "1,2", ele vor ajunge in ordinea "1,2" la capatul opus. Vor fi de asemenea fara eroare. Orice eroare o intalniti este emanatia propriei voastre minti, si nu va fi discutata aici.
Ce utilizeaza stream socket? Pai, poate ati auzit de telnet, asa-i? El foloseste stream socket. Toate caracterele tastate trebuie sa ajunga in aceeasi ordine, nu? De asemenea, browser-ele WWW utilizeaza protocolul HTTP care foloseste stream socket pentru a aduce pagini. Intr-adevar, daca dati telnet pe un site WWW pe portul 80, si tastati "GET nume_pagina", va va descarca pagina HTML!
Cum reuseste stream socket sa atinga un asa inalt nivel al calitatii transmisiei de date? Utilizeaza un protocol numit "Transmission Control Protocol" (protocolul de control al transmisiei), cunoscut altfel ca "TCP" (vezi RFC-793 pentru informatii extrem de detaliate asupra TCP). TCP va asigura ca datele ajung secvential si fara erori. Poate ati auzit "TCP" inainte si ca jumatatea mai buna a "TCP/IP", unde "IP" reprezinta "Internet Protocol" (protocolul internet, vezi RFC-791). IP e legat doar de rutarea in Internet.
E tare! Ce se poate spune despre datagram socket? De ce e numit fara conexiune? Care e smecheria aici? De ce este nesigur? Ei bine, iata cateva idei: daca trimiteti o datagrama, ea poate ajunge. Poate ajunge in alta ordine. Daca ajunge, datele din pachet vor fi fara eroare.
Datagram socket de asemenea utilizeaza IP pentru rutare, dar nu folosesc TCP; ci "User Datagram Protocol" sau "UDP" (vezi RFC-768).
De ce sunt fara conexiune? Pai, in esenta, pentru ca nu au nevoie sa mentina deschisa o conexiune asa ca stream socket. Trebuie doar construit un pachet, trantit un header IP peste el cu informatia de destinatie, si trimis. Nu e nevoie de conexiune. In general se foloseste pentru transferuri de informatie de tip pachet dupa pachet. Exemple de aplicatii: tftp, bootp etc.
"Ajunge!" s-ar putea sa tipi. "Cum pot aceste programe sa functioneze daca datagramele se pot pierde?!" Pai, prietenul meu uman, fiecare are un protocol al sau deasupra celui UDP. De exemplu, protocolul tftp spune ca pentru fiecare pachet care este trimis, recipientul (cel care il primeste) trebuie sa trimita inapoi un pachet care zice "L-am primit!" (un pachet "ACK"). Daca expeditorul pachetului original nu primeste nici un raspuns in ,sa zicem, cinci secunde, el va retransmite pachetul pana cand in final primeste un ACK. Aceasta procedura de recunoastere este foarte importanta cand se implementeaza aplicatii SOCK_DGRAM.
Aiureli pe Nivel de Baza si Teoria Retelei
Deoarece de abia am mentionat stratificarea protocoalelor, este timpul sa vorbim despre cum lucreaza retelele cu adevarat, si sa vedem cateva exemple de cum sunt construite pachetele SOCK_DGRAM. Practic, e probabil sa poti sarii aceasta sectiune. Totusi e buna pentru pregatirea de baza.

Cand alt calculator primeste pachetul, componenta hardware inlatura antetul Ethernet, kernel-ul inlatura antelele IP si UDP, programul TFTP inlatura antetul TFTP, si in final obtine datele.
Acum pot vorbi in sfarsit despre infamul Model de Retea Stratificat. Acest Model de Retea descrie un sistem de functionare a retelei care are numeroase avantaje asupra altor modele. De exemplu, poti scrie programe socket care sunt la fel fara sa-ti pese cum sunt transmise fizic datele (cablu serial, cablu subtire Ethernet, AUI, sau orice altceva) pentru ca programe la nivel de baza le gestioneaza in locul tau. In realitate partea hardware si topologia retelei este transparenta pentru programatorul socket.
Fara alte adaugiri, voi prezenta straturile intregului model. Amintiti-va de asta pentru examenele cursului de retea:
- Aplicatie;
- Prezentare;
- Sesiune;
- Transport;
- Retea;
- Legatura de date;
- Fizic.
Acum, acest model este atat de general incat daca vrei cu adevarat, probabil il poti utiliza ca un ghid de reparatii al automobilului. Un model stratificat mai consistent pentru Unix poate fi:
- Stratul Aplicatie (telnet, ftp etc.);
- Stratul de Transport de la o masina la alta (TCP, UDP);
- Stratul Internet (IP si rutare);
- Stratul de Acces la Retea (ceea ce a fost Retea, Legatura de Date si Fizic).
Vezi cata munca se depune pentru a construi un pachet? Mai sa fie! Si trebuie sa tastezi antetele de unul singur utilizand "cat"! Glumeam. Tot ce trebuie sa faci pentru o stream socket este sa trimiti (send()) datele. Tot ce trebuie sa faci pentru datagram socket este sa incapsulezi pachetul intr-o metoda la alegere si din nou sa il trimiti (send()). Kernel-ul construieste stratul de transport si stratul internet pentru tine, iar componenta hardware construieste antetul stratului de acces la retea. Ah, tehnologia moderna.
Aici inchei scurta noastra incursiune in teoria retelei. A da, am uitat sa va spun tot ce doream despre rutare: nimic! Asa e, nu voi vorbi despre ea de loc. Ruterul inlatura antetele pachetului pana la IP, consulta tabela de rutare, bla, bla, bla. Verifica IP RFC daca intr-adevar ai chef. Daca nu vei invata niciodata despre el, ei bine, vei trai.
Structuri
Suntem in sfarsit aici. Este timpul sa vorbim despre programare. In aceasta
sectiune, voi acoperi numeroase tipuri de date utilizate de interfata socket,
pentru ca e chiar nasol sa te dumiresti singur.
Primul este cel mai usor: un descriptor de socket. Un descriptor de socket este de tipul urmator:
intDoar un intreg obisnuit.
Lucrurile incep sa se complice de aici, asa ca citestele si alaturate mie. Trebuie stiut: exista doua tipuri de ordonare a octetilor: byte-ul cel mai important (uneori numit "octet") primul, sau cel mai putin important octet primul. Cel dintai se numeste "Network Byte Order". Unele masini pastreaza numerele intern in aceasta ordine (Network Byte Order), altele nu. Cand spun ceva trebuie sa fie in NBO, tu va trebui sa aplezi o functie (cum ar fi htons()) pentru a schimba acel ceva in "Host Byte Order". Daca nu spun NBO, atunci trebuie sa lasi valoarea in Host Byte Order.
Prima mea structura (TM) - struct sockaddr. Aceasta structura pastreaza informatia desprea adresele socket ale multor tipuri de socket:
struct sockaddr {sa_family poate fi o varietate de lucruri, dar va fi "AF_INET" pentru tot ce vom face in acest document. sa_data contine adresa de destinatie si numarul de port pentru socket. Mai de graba asta este de nestapanit.
unsigned short sa_family; /* address family, AF_xxx */
char sa_data[14]; /* 14 bytes of protocol address */
};
Pentru a lucra cu structura sockaddr, programatorii au crea o structura paralela: struct sockaddr_in ("in" de la "Internet").
struct sockaddr_in {Aceasta structura usureaza referirea elementelor adresei socket. Notati ca sin_zero( care este inclus pentru a completa lungimea structurii pana la sockaddr) ar trebui umplut cu zero cu functia bzero() sau memset(). De asemenea, si aceasta e cea mai important observatie, un pointer la o structura sockaddr_in poate fi convertit catre un pointer la o structura sockaddr si invers. Asadar chiar daca socket() vrea o structura sockaddr*, poti in continuare sa folosesti structura sockaddr_in si sa o convertesit in ultimul minut! De asemenea, e de notat ca sin_family corespunde lui sa_family intr-o structura sockaddr si trebuie pusa pe "AF_INET". In sfarsit, sin_port si sin_addr trebuie sa fie NBO!
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 */
};
"Dar", obiectezi tu, "cum poate intreaga structura, struct in_addr sin_addr, sa fie NBO?" Aceasta intrebare cere o examinare atenta a structurii struct in_addr, una din cele mai groaznice variabile de tip union in viata:
/* Internet address (a structure for historical reasons) */Ei bine, era folosita ca union, dar acum acele vremuri par de mult apuse. Paguba-n ciuperci. Asa ca daca ai declarat "ina" de tipul struct sockaddr_in, atunci "ina.sin_addr.s_addr" reprezinta o adresa IP de 4 byte (in NBO). Atentie, chiar daca sistemul dumneavoastra utilizeaza in continuare ingrozitorul union pentru struct in_addr, poti referi in acelasi mod cei 4 byte ai adresei IP, in exact acelasi mod ca mai inainte (asta datorita directivei #define).
struct in_addr {
unsigned long s_addr;
};
Conversii Native!
In acest moment suntem condusi in urmatoarea sectiune. A fost prea multa
vorbarie despre aceasta conversie NBO->HBO, acum este momentul pentru actiune!
In regula. Exista doua tipuri pe care le puteti converti: short (doi byte) si long (patru byte). Aceste functii lucreaza la fel si pentru variantele unsigned. Sa spunem ca vrei sa convertesti un short din HBO in NBO. Incepi cu "h" de la "host", urmeaza "to", apoi "n" de la "network", si "s" de la "short": h-to-n-s, sau htons() (se citeste "Host to Network Short").
E chiar prea usor...
Poti utiliza fiecare combinatie vrei "n", "h", "s" si "l", fara sa punem la socoteala pe cele prostesti. De exemplu, nu exista o functie stolh() ("short to long host"), nu la aceasta petrecere in orice caz. Dar exista:
- htons()--"Host to Network Short";
- htonl()--"Host to Network Long";
- ntohs()--"Network to Host Short";
- ntohl()--"Network to Host Long".
Un punct final: de ce intr-o structura sockaddr_in, sin_addr si sin_port au nevoie sa fie in NBO, iar sin_family nu? Raspunsul: sin_addr si sin_port sunt incapsulate in pachet in cadrul stratului IP si UDP. De aceea, ele trebuie sa fie in NBO. Pe de alta parte, campul sin_family este folosit doar de kernel pentru a determina ce tip de adresa contine structura, deci trebuie sa fie in HBO. De aesmenea, din moment ce sin_family nu iese pe retea, poate fi in HBO.
Adrese
IP si cum sa lucram cu ele
Din fericire pentru tine, exista o gramada de functii care permit manipularea
adreselor IP.
Mai intai, sa spunem ca ai o structura struct sockaddr_in ina, si ai o adresa IP "123.241.5.10" pe care vrei s-o pui in structura. Functia pe care vrei s-o utilizezi este inet_addr(), care converteste o adresa IP din numere si puncte intr-un unsigned long. Asta se face dupa cum urmeaza:
ina.sin_addr.s_addr = inet_addr("132.241.5.10");Atentie ca inet_addr() intoarce adresa deja in NBO - nu va trebui sa apelezi htonl(). Tare!
Acum, bucatica de cod de deasupra nu e foarte robusta pentru ca nu exista nici o verificare de eroare. Functia inet_addr() intoarce -1 in caz de eroare. Iti aduci aminte de numrele binare? (unsigned) -1 intamplator corespunde adresei IP 255.255.255.255! Asta e adresa de broadcast! Nasulie. Adu-ti aminte sa faci corect o verificare a erorii.
Bun, acum poti converti adrese IP din char* in long. Ce e cu calea inversa> Ce se intampla daca ai o strucura struct in_addr si vrei sa o afisezi notatia cu punct? In acest caz, vei folosi functia inet_ntoa() ("ntoa" vine de la "network to ascii") cam asa:
printf("%s",inet_ntoa(ina.sin_addr));Asta va afisa adresa IP. De retinut ca inet_ntoa() ia ca argument struct in_addr, nu un long. De asemenea intoarce un pointer catre un char. Acesta pointeaza catre un sir de caractere pastrat static in functia inet_ntoa() deci de fiecare data cand apelezi inet_ntoa() ea va suprascrie adresa IP pe care ai cerut-o anterior. De exemplu:
char *a1, *a2;va afisa:
.
.
a1 = inet_ntoa(ina1.sin_addr); /* this is 198.92.129.1 */
a2 = inet_ntoa(ina2.sin_addr); /* this is 132.241.5.10 */
printf("address 1: %s\n",a1);
printf("address 2: %s\n",a2);
address 1: 132.241.5.10Daca ai nevoie de adrese trebuie sa le salvezi cu strcpy() in propriul tau sir de caractere.
address 2: 132.241.5.10
Asta e tot despre aceasta problema pentru moment. Mai tarziu, vei invata sa convertesti siruri ca "witehouse.gov" in corespondentul sau: adresa IP (vezi DNS mai jos).
socket()
- Obtinerea descriptorului de fisier!
Banuiesc ca nu pot s-o mai lungesc. Trebuie sa vorbesc despre functia de
sistem socket(). Iata ruperea:
#include <sys/types.h>Dar ce sunt aceste argumente? Mai intai, domain trebuie pus pe "AF_INET", ca in struct sockaddr_in (mai sus). Urmeaza type care spune kernel-ului ce tip de socket este: SOCK_STREAM sau SOCK_DGRAM. In sfarsit, trebuie doar sa pui protocolul pe "0". (Atentie: exista mult mai multe domenii decat am listat. Exista mult mai multe tipuri decat am listat. Vezi pagina man pentru functia socket(). De asemenea, exista o modalitate mai buna de a obtine protocolul. Vezi pagina man pentru functia getprotobyname().)
#include <sys/socket.h>int socket(int domain, int type, int protocol);
Functia socket() returneaza simplu un descriptor de socket pe care il poti folosi mai tarziu in apeluri de sistem, sau -1 in caz de eroare. Variabila globala errno este setata pe valoarea erorii (vezi pagina de man pentru functia perror()).
bind() - Pe ce
port sunt?
Odata ce ai un socket, ar trebui sa asociezi acel socket cu un port de
pe masina ta, local. (Asta se face in mod normal daca urmeaza sa asculti,
listen(), pentru conexiunile ce vin pe un anumit port - MUD face asta cand
iti spune "telnet to x.y.z port 6969".) Daca ai de gand sa faci doar un
connect(), aceasta asociere nu mai e necesara. Citeste oricum doar de frumusete.
Aici sintaxa pentru functia de sistem bind():
#include <sys/types.h>sockfd este descriptorul de fisier socket intors de socket(). my_addr este un pointer catre struct sockaddr care contine informatii despre adresa ta, mai exact, portul si adresa IP. addrlen se poate fixa la sizeof(struct sockaddr).
#include <sys/socket.h>int bind(int sockfd, struct sockaddr *my_addr, int addrlen);
Uau. E ceva sa sorbi asta dintr-o singura inghititura. Sa luam un exemplu:
#include <string.h>Exista cateva lucruri de notat aici. my_addr.sin_port este in NBO. Deci este my_addr.sin_addr.s_addr. Alt lucru la care trebuie sa fim atenti este ca fisierele header pot diferi de la un sistem la altul. Pentru a fi sigur, verifica paginile locale man.
#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); /* do some error checking! */
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 = inet_addr("132.241.5.10");
bzero(&(my_addr.sin_zero), 8); /* zero the rest of the struct *//* don't forget your error checking for bind(): */
bind(sockfd, (struct sockaddr *)&my_addr, sizeof(struct sockaddr));
.
.
.
Ultimul lucru in paragraful cu bind(), trebuie sa mentionez ca o parte din procesul obtinerii propriei adrese IP si/sau al portului pot fi automatizate:
my_addr.sin_port = 0; /* choose an unused port at random */Fixand my_addr.sin_port la 0, spui functiei bind() sa aleaga portul pentru tine. De asemenea, fixand my_addr.sin_addr.s_addr a INADDR_ANY, ii spui functiei sa umple variabila automat cu adresa IP a masinii pe care ruleaza procesul.
my_addr.sin_addr.s_addr = INADDR_ANY; /* use my IP address */
Daca ai spirit de observatie, s-ar putea sa fi vazut ca nu am pus INADDR_ANY in NBO. Ce obraznic sunt! Oricum, am informatii din interior: INADDR_ANY este defapt zero! Zero ramane zero chiar daca ii rearanjezi octetii. Oricum, puristii vor obiecta ca se poate intampla ca intr-o dimensiune paralela unde INADDR_ANY este, sa spunem, 12, codul meu sa nu functioneze. Din partea mea nu e nici o problema:
my_addr.sin_port = htons(0); /* choose an unused port at random */Acum suntem asa portabili ca nu-ti vine sa crezi. Am vrut doar sa elimin obiectiile, pentru ca cea mai mare parte a codului cu care te vei intalni nu se va obosi sa ruleze INADDR_ANY prin htonl().
my_addr.sin_addr.s_addr = htonl(INADDR_ANY); /* use my IP address */
Functia bind() de asemenea intoarce -1 in caz de eroare si fixeaza errno la valoarea erorii.
Alt lucru care merita atentia in momentul apelului bind(): nu te duce sub masa cu numerele de port. Toate porturile sub 1024 sunt REZERVATE! Poti avea orice port deasupra acestuia, pana la 65535 (furnizate de sistem daca nu sunt deja utilizate de alt program).
Inca o mica extra nota de final despre bind(): exista momente cand nu vei avea nevoie sa o apelezi. Daca te conectezi cu connect() la o masina aflata la distanta si nu te intereseaza ce port local ai (cum este in cazul telnet), poti sa apelezi simplu connect(), ea va verifica daca socket-ul este neatasat, si ii va atasa cu bind() un port local neutilizat.
connect() - Hei, tu!
Haide sa pretindem pentru cateva minute ca esti o aplicatie telnet. Utilizatorul
tau iti comanda (ca in filmul TRON) sa obtii un descriptor de fisier socket.
Tu te supui si apelezi socket(). Apoi, utilizatorul iti spune sa te conectezi
la "132.241.10" pe portul "23" (portul standard telnet). O doamne! Ce faci
acum?
Din fericire pentru tine, programule, esti chiar in momentul in care citesti sectiunea connect() - cum sa te conectezi la o masina din retea. Citesit cu furie mai departe, nedorind sa-l dezamagesti pe utilizator...
Apelul functiei connect() urmeaza:
#include <sys/types.h>sockfd este prietenosul nostru vecin descriptor de fisier socket, cel furnizat de apelul socket(), serv_addr este struct sockaddr continand portul destinatie si adresa IP, iar addrlen poate fi setat la dimensiunea sizeof(struct sockaddr).
#include <sys/socket.h>int connect(int sockfd, struct sockaddr *serv_addr, int addrlen);
Nu te face asta sa simti ca are mai mult sens? Ia sa luam un exemplu:
#include <string.h>Din nou asigurate ca ai verificat valoarea intoarsa de connect() - va returna -1 in caz de eroare si va seta variabila errno.
#include <sys/types.h>
#include <sys/socket.h>#define DEST_IP "132.241.5.10"
#define DEST_PORT 23main()
{
int sockfd;
struct sockaddr_in dest_addr; /* will hold the destination addr */sockfd = socket(AF_INET, SOCK_STREAM, 0); /* do some error checking! */
dest_addr.sin_family = AF_INET; /* host byte order */
dest_addr.sin_port = htons(DEST_PORT); /* short, network byte order */
dest_addr.sin_addr.s_addr = inet_addr(DEST_IP);
bzero(&(dest_addr.sin_zero), 8); /* zero the rest of the struct *//* don't forget to error check the connect()! */
connect(sockfd, (struct sockaddr *)&dest_addr, sizeof(struct sockaddr));
.
.
.
De asemenea, observa ca nu am apelat functia bind(). In esenta, nu ne pasa de numarul de port local; ne intereseaza doar unde mergem. Kernel-ul va alege un port local pentru noi, iar site-ul unde ne conectam va lua automat aceasta informatie de la noi. Nici o grija.
listen()
- Va rog, ma suna si pe mine cineva?
Ok, e timpul pentru o schimbare. Ce se intampla daca nu vrei sa te conectezi
la o masina in retea. Sa zicem, de frumusete, ca vrei sa astepti cereri
de conectare din afara si sa le gestionezi intr-un fel. Procesul e format
din doi pasi: primul asculti listen(), apoi accepti accept() (vezi mai
jos).
Apelul pentru ascultare este chiar simplu, dar cere o mica explicatie:
int listen(int sockfd, int backlog);sockfd este de obicei un descriptor de fisier socket obtinut cu functia de sistem socket(). backlog este numarul de conexiuni permise in coada de intrare. Ce inseamna asta? Pai, cererile de conectare care sosesc vor astepta in aceasta coada pana le vei accepta cu accept() (vezi mai jos) si aceasta este limita maxima a numarului de cereri care vor fi puse in coada. Cele mai multe sisteme limiteaza in tacere acest numar undeva in jurul lui 20; tu poti scapa fixand-ul la 5 sau 10.
Din nou, ca de obicei, listen() intoarce -1 in caz de eroare si seteaza errno.
Ei bine, dupa cum probabil iti imaginezi, avem nevoie sa apelam bind() inainte de apelul listen() sau kernel-ul va pune programul sa asculte pe un port ales la intamplare. Bleah! Asa ca daca ai de gand sa asculti cereri de conectare, secventa de apeluri de sistem este urmatoarea:
socket();Am pus exemplul de cod, pentru ca se explica singur. (Codul din sectiunea accept() de mai jos este mai complet). Partea cea mai dificila a acestei chestii este apelul accept().
bind();
listen();
/* accept() goes here */
accept()
- "Va multumesc pentru apelul portului 3490"
Sunteti gata - apelul accept() este un pic ciudat! Ce se intampla este:
cineva foarte foarte departe va incerca sa se conecteze cu connect() la
masina dumneavoastra pe un port pe care tu asculti cu listen(). Conexiunea
lor va fi pusa in coada de asteptare pentru a fi acceptata cu accept().
Tu apelezi accept() si ii spui sa preia conexiunea in curs. Functia iti
returneaza un nou descriptor de fisier socket pentru a-l utiliza numai
cu aceasta conexiune! Asa e, subit ai doi descriptori de fisier socket
pentru pretul unuia! Cel original asculta in continuare pe port, iar cel
nou creat este in sfarsit gata pentru send() si recv(). Am ajuns!
Sintaxa e urmatoarea:
#include <sys/socket.h>sockfd este descriptorul de fisier socket pe care se asculta. Destul de simplu. addr va fi de obicei un pointer catre o structura locala struct sockaddr_in. Acesta este locul unde va merge informatia despre conexiunea care tocmai a fost acceptata (si poti determina ce masina se conecteaza si pe ce port). addrlen este o variabila intreaga locala care ar trebui sa fie setata la dimensiunea sizeof(struct sockaddr_in) inainte ca adresa sa sa fie pasata functiei accept(). Accept nu va pune mai mult de atatia octeti in addr. Daca pune mai putini, va schimba valoarea lui addrlen ca s-o reflecte.int accept(int sockfd, void *addr, int *addrlen);
Ia ghici? Functia accept() returneaza -1 si seteaza errno in caz de eroare. Pun pariu ca nu te-ai gandit la asta.
Ca mai inainte, asta este o gramada care trebuie asimilata dintr-o bucata, asa ca iata un exemplu de cod fragmentat pentru examinare atenta:
#include <string.h>Din nou, observa ca vom utiliza descriptorul de socket new_fd pentru toate apelurile send() si recv(). Daca intotdeauna vei avea numai o singura conexiune, poti inchide cu close() socketul original sockfd ca sa nu mai primesti si alte cereri de conectare pe acelasi port, daca doresti.
#include <sys/types.h>
#include <sys/socket.h>#define MYPORT 3490 /* the port users will be connecting to */
#define BACKLOG 10 /* how many pending connections queue will hold */
main()
{
int sockfd, new_fd; /* listen on sock_fd, new connection on new_fd */
struct sockaddr_in my_addr; /* my address information */
struct sockaddr_in their_addr; /* connector's address information */
int sin_size;sockfd = socket(AF_INET, SOCK_STREAM, 0); /* do some error checking! */
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; /* auto-fill with my IP */
bzero(&(my_addr.sin_zero), 8); /* zero the rest of the struct *//* don't forget your error checking for these calls: */
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);
.
.
.
send()
si recv() - Spune-mi ceva, draga!
Aceste doua functii sunt pentru comunicare printr-un stream socket sau
datagram socket dar conectat. Daca vrei sa folosesti datagram socket obisnuit
(fara conexiune), ai nevoie sa vezi sectiunea sendto() si recvfrom() de
mai jos.
Apelul send():
int send(int sockfd, const void *msg, int len, int flags);sockfd este descriptorul de socket pe care vrei sa trimiti date (cel obtinut cu socket() sau cu accept()). msg este un pointer catre datele pe care vrei sa le trimiti, iar len este lungimea acelor date in octeti. flags poti sa-i setezi la 0. (vezi pagina man a functiei send() pentru mai multe informatii referitoare la flags)
Ceva cod de exemplu poate fi:
char *msg = "Beej was here!";send() intoarce numarul de octeti care au fost trimisi - acesta poate fi mai mic decat numarul pe care i-ati spus sa-i trimita! Vezi, uneori ii spui sa trimita o gramada de date, iar el nu le poate gestiona. Va transmite atat de mult cat va putea, si va avea incredere in tine ca vei trimite restul mai tarziu. Nu uita, daca valoarea returnata de send() nu se potriveste cu valoarea din len, depinde de tine sa trimiti restul sirului. Vestea buna este asta: daca pachetul este mic (mai putin de 1K sau asa ceva) atunci probabil va reusi sa trimita toate datele intr-o singura transa. Din nou, -1 este returnat in caz de eroare, iar errno este setata pe numarul erorii.
int len, bytes_sent;
.
.
len = strlen(msg);
bytes_sent = send(sockfd, msg, len, 0);
.
.
.
Apelul recv() este similar in multe aspecte:
int recv(int sockfd, void *buf, int len, unsigned int flags);sockfd este descriptorul de socket de la care se citeste, buf este zona tampon in care se pun informatiile, len este lungimea maxima a zonei tampon, iar flags din nou poate fi setat la 0. (vezi pagina de man a functiei recv() pentru mai multe informatii legate de flag)
recv() intoarce numarul de octeti care au fost cititi in buffer, sau -1 in caz de eroare (cu errno setat in consecinta).
A fost usor, nu-i asa? Acum poti trimite date inainte si inapoi prin stram socket! Uraa! Esti un Programator de Retea in Unix!
sendto()
si recvfrom() - Vorbeste-mi, in stilul DGRAM
"Toate bune si frumoase", te aud zicand, "dar unde ma lasa asta pe mine
cu datagram socket fara conexiune?" No problemo, amigo. Avem chiar ce ne
trebuie.
Din moment ce datagram socket nu se conecteaza la o masina in retea, ghici ce bucata de informatie avem nevoie sa-i dam inainte de a trimite un pachet? Asa e! Adresa de destinatie!
Iata ce scoatem:
int sendto(int sockfd, const void *msg, int len, unsigned int flags,Dupa cum poti vedea, aceasta functie este in esenta la fel ca send() cu adaugarea a doua informatii. to este un pointer catre o structura struct sockaddr (in care probabil vei avea struct sockaddr_in si convertita in ultimul minut) care contine adresa IP de destinatie si portul. tolen poate fi setat simplu la sizeof(struct sockaddr).
const struct sockaddr *to, int tolen);
La fel ca la send(), sendto() intoarce numarul de octeti care au fost trimisi (care din nou poate fi mai mic decat numarul de octeti pe care i-ai spus sa-i trimita!), sau -1 in caz de eroare.
Similar este cu functia recvfrom(). Sintaxa este:
int recvfrom(int sockfd, void *buf, int len, unsigned int flagsDin nou, aceasta este asemanatoare cu recv(), la care se adauga doua campuri. from este un pointer catre o structura locala struct sockaddr care va fi completata cu adresa IP si portul masinii originale. fromlen este un pointer catre un intreg local int care trebuie initializat la sizeof(struct sockaddr). Cand functia se intoarce, fromlen va contine lungimea adresei pe care o stocheaza.
struct sockaddr *from, int *fromlen);
recvfrom() intoarce numarul de octeti primit, sau -1 in caz de eroare (cu errno setat in consecinta).
Nu uita, daca conectezi un datagram socket cu connect(), poti apoi sa folosesti simplu send() si recv() pentru toate tranzactiile. Socket-ul in sine ramane un datagram socket iar pachetele utilizeaza in continuare UDP, dar interfata socket va adauga automat informatia de destinatia si sursa pentru tine.
close()
si shutdown() - Dispari din fata mea!
Uau! Ai trimis si ai primit date toata ziua, si te-ai saturat. Esti gata
sa inchizi conexiunea cu descriptorul socket. Asta e usor. Poti sa folosesti
functia Unix obisnuita pentru un descriptor de fisier:
close(sockfd);Asta va impiedica alte citiri sau scrieri prin socket. Oricine incearca sa citeasca sau sa scrie pe socket la capatul celalalt va primi o eroare.
Doar in cazul in care vrei ceva mai mult control asupra modului in care se inchide socket-ul, poti folosi functia shutdown(). Ea permite sa tai comunicatia intr-o anumita directie sau in ambele directii (exact cum face close()). Sintaxa:
int shutdown(int sockfd, int how);sockfd este descriptorul de fisier socket pe care vrei sa-l inchizi, iar how ia una din valorile:
- 0 nu se mai admite receptia mesajelor
- 1 nu se mai admite trimiterea mesajelor
- 2 nu se mai admite transmiterea si receptia datelor (la fel ca close())
Daca binevoiesti sa folosesti shutdown() cu datagram socket fara conectare, functia va face socket-ul indisponibil pentru apeluri de trimitere send() sau receptie recv() (aminteste-ti o ca poti utiliza daca ai conectat datagram socket-ul).
Nimic in plus.
getpeername()
- Cine esti?
Aceasta functie este atat de simpla.
Este atat de simpla, ca aproape nu i-am dat o sectiune proprie. Dar iat-o oricum.
Functia getpeername() iti va spune cine este conectat la celalalt capat al unui stream socket. Sintaxa:
#include <sys/socket.h>sockfd este descriptorul stream socket-ului conectat, addr este un pointer catre o struct sockaddr (sau struct sockaddr_in) care va pastra informatia despre celalat capat al conexiunii, iar addrlen este un pointer catre un int ce trebuie initializat cu sizeof(struct sockaddr).int getpeername(int sockfd, struct sockaddr *addr, int *addrlen);
Functia intoarce -1 in caz de eroare si seteaza errno corespunzator.
Odata ce ai adresa lor, poti folosi inet_ntoa() sau gethostbyaddr() ca as afisezi mai multe informatii. Nu nu poti sa afli numele de login. (Ok, ok. Daca celalalt capat ruleaza un demon ident, asta e posibil. Asta, oricum, este in afara scopului acestui document. Verifica RFC-1413 pentru mai multe informatii.)
gethostname()
- Cine sunt?
Chiar mai usoara decat getpeername() este functia gethostname(). Ea intoarce
numele calculatorului pe care ruleaza programul tau. Numele poate fi apoi
utilizat de gethostbyname(), mai jos, pentru a determina adresa IP a masinii
locale.
Ce poate fi mai amuzant? Ma pot gandi la cateva lucruri, dar ele nu se preteaza la programarea socket. Oricum, aici e ruperea:
#include <unistd.h>Argumentele sunt simple: hostname este un pointer care un sir de caractere care va contine numele masinii dupa ce functia se intoarce, iar size este lungimea in octeti a sirului.int gethostname(char *hostname, size_t size);
Functia intoarce 0 in caz de succes, si -1 in caz de eroare, setand errno ca de obicei.
DNS
- Tu spui "whitehouse.gov", eu spun "198.137.240.100"
In cazul in care nu stiti ce este un DNS, el vine de la "Domain Name Service".
Mai pe scurt, ii spui adresa pe intelesul oamenilor pentru un site, iar
el iti da adresa IP (asa incat sa o poti folosi cu bind(), connect(), sendto(),
sau orice altceva care are nevoie de ea ). In acest fel cand cineva introduce:
$ telnet whitehouse.govtelnet poate afla ceea ce are nevoie pentru connect() la "198.137.240.100".
Dar cum functioneaza? Vei folosi functia gethostbyname():
#include <netdb.h>Dupa cum poti vedea returneaza un pointer catre un struct hostent, a carui schema este:struct hostent *gethostbyname(const char *name);
struct hostent {Si iata descrierea campurilor din 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]
- h_name numele oficial al masinii
- h_aliases un sir de caractere terminat cu NULL, pentru numele alternativ al masinii
- h_addrtpye tipul de adresa returnata; de obicei AF_INET
- h_length lungimea adresei in octeti
- h_addr_list un sir de adrese de retea terminat cu 0, pentru masina. Adresele masinii sunt in NBO
- h_addr prima adresa din h_addr_list
Dar cum se foloseste? Uneori (asa cum aflam cand citim manuale de calculatoare), doar varsarea informatiei catre cititor nu este de ajuns. Aceasta functie este cu siguranta mai usor de utilizat decat pare.
Iata un exemplu de program (hostname.c):
#include <stdio.h>Cu gethostbyname(), nu poti utiliza perror() pentru a afisa mesaje de eroare (din moment ce errno nu este utilizat). In loc, apeleaza herror().
#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) { /* error check the command line */
fprintf(stderr,"usage: getip address\n");
exit(1);
}if ((h=gethostbyname(argv[1])) == NULL) { /* get the host info */
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;
}
Este destul de simplu. Ii dai pur si simplu sirul care contine numele masinii ("whitehouse.gov") functiei gethostbyname(), iar apoi iei informatia din structura returnata struct hostent.
Singura ciudatenie posibila poate fi in afisarea adresei IP, mai sus h->h_addr este char*, dar inet_ntoa() vrea struct in_addr. Asa ca convertesc h->h_addr in struct in_addr*, apoi dereferentiez pentru a obtien datele.
Esenta despre
Client-Server
Este o lume cliet-server, draga. Aproape totul in retea lucreaza cu procese
client care discuta cu procese server si invers. Ia de exemplu telnet.
Cand te conectezi la o masina in retea pe portul 23 cu telnet (clientul),
un program pe acea masina (numit telnetd, serverul) vine la viata. El gestioneaza
cererile de conectare telent, iti furnizeaza un prompt de login etc.

The Client-Server Relationship
De notat ca perechea client-server pot vorbi SOCK_STREAM, SOCK_DGRAM, sau orice altceva (atat timp cat vrobesc despre acelasi lucru). Unele exemple bune de perechi client-server sunt telnet/telnetd, ftp/ftpd, sau bootp/bootpd. De fiecare data cand utilizezi ftp, exista un program in retea, ftpd, care te serveste.
Deseori, va fi doar un server pe o masina, si acel server va gestiona clienti multipli folosind fork(). Rutina de baza este: serverul va astepta cereri de conectare cu listen(), le va accepta cu accept(), apoi va genera cu fork() procese copil pentru a le gestiona. Aceasta este ceea ce exemplul nostru de server face in urmatoarea sectiune.
Un server stream
simplu
Tot ceea ce face acest server este sa trimita sirul "Hello, World!\n" pe
o conexiune stream. Tot ce aveti nevoie pentru a test acest server este
sa-l rulati intr-o fereastra, iar cu telnet sa va conectati la el din alta
fereastra:
$ telnet remotehostname 3490unde remotehostename este numele masinii pe care ruleaza serverul.
Codul serverului (tcpserver.c): (Atentie: un backslash la sfarsitul unei linii inseamna ca acea linie se continua pe urmatoarea)
#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 /* the port users will be connecting to */
#define BACKLOG 10 /* how many pending connections queue will hold */
main()
{
int sockfd, new_fd; /* listen on sock_fd, new connection on new_fd */}
struct sockaddr_in my_addr; /* my address information */
struct sockaddr_in their_addr; /* connector's address information */
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; /* auto-fill with my IP */
bzero(&(my_addr.sin_zero), 8); /* zero the rest of the struct */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) { /* main accept() loop */
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); /* parent doesn't need this */while(waitpid(-1,NULL,WNOHANG) > 0); /* clean up child processes */
In caz ca esti curios, am tot codul intr-o singura, mare functie main() pentru (simt eu) claritate sintactica. Esti liber sa il spargi in functii mai mici daca te face sa te simti mai bine.
De asemenea poti sa obtii sirul de caractere de la acest server utilizand clientul listat in secitunea urmatoare.
Un client stream simplu
Tipul asta e chiar mai simplu decat serverul. tot ceea ce face acest client este sa se conecteze la masina gazda pe care o specifici in linia de comanda, pe portul 3490. Preia sirul de caractere pe care acel server il trimite. Sursa clientului (tcpclient.c):#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 /* the port client will be connecting to */
#define MAXDATASIZE 100 /* max number of bytes we can get at once */
int main(int argc, char *argv[])
{
int sockfd, numbytes;
char buf[MAXDATASIZE];
struct hostent *he;
struct sockaddr_in their_addr; /* connector's address information */if (argc != 2) {
fprintf(stderr,"usage: client hostname\n");}
exit(1);
if ((he=gethostbyname(argv[1])) == NULL) { /* get the host info */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 the rest of the struct */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;
De notat ca daca nu rulezi serverul inainte de client, connect() retruneaza "Connection refused". Very useful.
Datagram Socket
Nu prea am multe de spus aici, asa ca voi prezenta doua programe: talker.c
si listener.c.
listener stat pe o masina asteptand sa vina pachete pe portul 4950. talker trimite pachete pe acel port, pe masina specificata, care contin orice introduce utilizatorul in linia de comanda.
Aici este sursa pentru listener.c:
#include <stdio.h>De retinut ca in apelul functiei socket() folosim in final SOCK_DGRAM. De asemenea, de retinut ca nu este nevoie de listen() sau accept(). Acesta este unul din dichisurile utilizarii datagram socket fara conectare.
#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 /* the port users will be sending to */
#define MAXBUFLEN 100
main()
{int sockfd;}
struct sockaddr_in my_addr; /* my address information */
struct sockaddr_in their_addr; /* connector's address information */
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; /* auto-fill with my IP */
bzero(&(my_addr.sin_zero), 8); /* zero the rest of the struct */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);
Urmeaza sursa pentru talker.c:
#include <stdio.h>Si asta este tot! Ruleaza listener pe o masina, apoi ruleaza talker pe alta. Uita-te la ele cum comunica! Extraordinar, incantare pentru intreaga familie nucleara!
#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 /* the port users will be sending to */
int main(int argc, char *argv[])
{int sockfd;}
struct sockaddr_in their_addr; /* connector's address information */
struct hostent *he;
int numbytes;if (argc != 3) {
fprintf(stderr,"usage: talker hostname message\n");}
exit(1);if ((he=gethostbyname(argv[1])) == NULL) { /* get the host info */
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 the rest of the struct */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;
Exceptand un singur detaliu mic pe care l-am mentionat de mai multe ori in trecut: conectarea datagram socket. Am nevoie sa discut despre asta aici, din moment ce suntem la sectiunea despre datagrame a documentului. Sa spunem ca talker apeleaza connect() si specifica adresa lui listener. Din acest punct, talker poate trimite si primi mesaje doar la adresa specificata prin connect(). Din acest motiv, nu ai nevoie sa folosesti sendto() si recvfrom(); poti folosi simplu send() si recv().
Blocare
Blocare. Ai auzit despre ea - acum ce naiba este? Pe scurt, "block" este
un jargon tehnic pentru "sleep". Ai observat probabil ca atunci cand ruleaza
listener, mai sus, el sta acolo pana cand ajunge un pachet. Ce se intampla
este ca el a apelat recvfrom(), nu era nici o data, asa ca recvfrom() a
spus "block" (adica, dormi aici) pana cand ajung niste date.
Multe functii blocheaza. accept() blocheaza. Toate functiile recv*() blocheaza. Motivul pentru care pot face asta este ca li se permite. Cand creezi prima oara un descriptor de socket cu socket(), kernel-ul il seteaza sa poata bloca. Daca nu vrei un socket blocabil, trebuie sa apelezi functia fcntl():
#include <unistd.h>Setand un socket sa fie non-blocabil, tai efectiv socket-ul de la informatie. Daca incerci sa citesti de la un socket non-blocabil si nu exista date acolo, nu ii este permis sa se blocheze si va intoarce -1 iar errno va fi setat pe EWOULDBLOCK.
#include <fcntl.h>
.
.
sockfd = socket(AF_INET, SOCK_STREAM, 0);
fcntl(sockfd, F_SETFL, O_NONBLOCK);
.
.
In general, acest tip de taiere este o idee proasta. Daca pui programul intr-o cautare de date de tip busy-wait pe un socket, vei suge timp de lucru de la CPU de parca si-ar fi iesit din mana. O solutie mult mai eleganta pentru verificare este sa vezi daca exista date asteptand sa fie citite in sectiunea urmatoare cu select().
select()
- Multiplexare Sincrona I/O
Aceasta functie este oarecum ciudata, dar foarte utila. Ia de exemplu urmatoarea
situatie: esti un server si vrei sa asculti cereri de conectare si sa citesti
in continuare de pe conexiunile pe care deja le ai.
Nici o problema, poti sa spui, doar un accept() si cateva recv()-uri. Nu chiar asa de repede, mestere! Ce faci daca esti blocat pe un apel accept()? Cum vei primi date cu recv() in acelasi timp? "Foloseste un socket non-blocabil!" Nici vorba! Doar nu vrei sa te porti ca un porc cu CPU-ul. Atunci cum?
select() iti da puterea de a controla mai multi socketi in acelasi timp. Iti va spune care este gata de citire, care este gata de scriere, si care socket a intalnit exceptii, daca vrei cu adevarat sa stii ce e aia.
Fara alte adaugiri, iti voi oferi sintaxa functiei select():
#include <sys/time.h>Functia controleaza seturi de descriptori de fisiere; in particular readfds, weitefds, si exceptfds. Daca vrei sa vezi daca poti citi de la standard input si un descriptor de socket, sockfd, trebuie doar sa adaugi descriptorii de fisiere 0 si sockfd la setul de readfds. Parametrul numfds va trebui setat la valoarea celui mai mare descriptor plus unu. In acest exemplu, ar trebui setat la sockfd+1, din moment ce cu siguranta este mai mare decat standard input (0).
#include <sys/types.h>
#include <unistd.h>int select(int numfds, fd_set *readfds, fd_set *writefds,
fd_set *exceptfds, struct timeval *timeout);
Cand select() returneaza, readfds va fi modificat pentru a reflecta care dintre descriptorii de fisiere pe care i-ai selectat este gata de citire. Poti sa-i testezi cu macroul FD_ISSET(), mai jos.
Inainte de progresa mult mai departe, voi vorbi despre cu sa manipulezi aceste seturi. Fiecare set este de tipul fd_set. Urmatoarele macrouri opereaza cu acest tip:
- FD_ZERO(fd_set *set) - curata setul de descriptori de fisiere
- FD_SET(int fd, fd_set *set) - adauga fd la set
- FD_CLR(int fd, fd_set *set) - sterge fd din set
- FD_ISSET(int fd, fd_set *set) - testeaza sa vada daca fd este in set
Structura struct timeval are urmatoarele campuri:
struct timeval {Trebuie doar sa setezi tv_sec la numarul de secunde pe care trebuie sa le astepte, iar tv_usec la numarul de microsecunde pe care trebuie sa le astepte. Da, sunt microsecunde nu milisecunde. Sunt 1000 de microsecunde intr-o milisecunda, si 1000 de milisecunde intr-o secunda. De aceea,sunt 1000000 de microsecunde intr-o secunda. De ce este "usec"? Litera "u" se presupune ca arata ca litera greceasca Mu utilizata pentru "micro". De asemenea, cand functia iese, timeout poate fi improspatata pentru a arata timpul care a mai ramas. Aceasta depinde de aroma de Unix pe care rulezi.
int tv_sec; /* seconds */
int tv_usec; /* microseconds */
};
Uau! Avem un timer cu o rezolutie de o microsecunda! Ei bine, nu conta pe asta. Fractiunile de timp standard cu care lucreaza Unix sunt de 100 de milisecunde, asa ca va trebui probabil sa astepti un timp atat de lung, indiferent de cat de mic ai setat structura struct timeval.
Alt lucru de interes: daca setezi campurile in struct timeval la 0, select() va iesi imediat, taind toti descriptorii din set. Daca setezi parametrul timeout la NULL, functia nu va mai iesi niciodata, ci va astepta pana cand primul descriptor este gata. In sfarsit, daca nu-ti pasa de asteptarea unui set, poti sa setezi la NULL apelul select().
Urmatoarea bucatica de cod asteapta 2.5 secunde ca ceva sa se intample la standard input (select.c):
#include <sys/time.h>Daca te afli la un terminal de tip line buffered, tasta pe care ar trebui sa o apesi este RETURN sau va iesi oricum.
#include <sys/types.h>
#include <unistd.h>#define STDIN 0 /* file descriptor for standard input */
main()
{struct timeval tv;}
fd_set readfds;tv.tv_sec = 2;
tv.tv_usec = 500000;FD_ZERO(&readfds);
FD_SET(STDIN, &readfds);/* don't care about writefds and exceptfds: */
select(STDIN+1, &readfds, NULL, NULL, &tv);if (FD_ISSET(STDIN, &readfds))
printf("A key was pressed!\n");elseprintf("Timed out.\n");
Acum, cativa dintre voi pot spune ca asta e o cale minunata pentru date de pe un datagram socket - si aveti dreptate: poate fi. Unii unix-isti pot folosi select() in aceasta maniera, iar alti nu. Va trebui sa vezi ce spun paginile man locale despre aceasta problema daca vrei sa o incerci.
O ultima nota de intere despre select(): daca aveti un socket care e pus cu listen() pe ascultare, poti verifica daca exista o noua cerere de conectare punand acel descriptor de fisier in setul readfds.
Si asta, prietenii mei, este un vedere rapida asupra atotputernicei functii select().
Mai multe Referinte
Ai ajuns atat de departe, si acum tipi dupa mai mult! Unde altundeva sa
mergi sa inveti mai mult despre toate chestiile astea?
Incearca urmatoarele pagini man, pentru inceput:
- socket()
- bind()
- connect()
- listen()
- accept()
- send()
- recv()
- sendto()
- recvfrom()
- close()
- shutdown()
- getpeername()
- getsockname()
- gethostbyname()
- gethostbyaddr()
- getprotobyname()
- fcntl()
- select()
- perror()
- Internetworking with TCP/IP, volumes I-III by Douglas E. Comer and David L. Stevens. Published by Prentice Hall. Second edition ISBNs: 0-13-468505-9,0-13-472242-6, 0-13-474222-2. There is a third edition of this set which covers IPv6 and IP over ATM.
- Using C on the UNIX System by David A. Curry. Published by O'Reilly & Associates, Inc. ISBN 0-937175-23-4.
- TCP/IP Network Administration by Craig Hunt. Published by O'Reilly & Associates, Inc. ISBN 0-937175-82-X.
- TCP/IP Illustrated, volumes 1-3 by W. Richard Stevens and Gary R. Wright. Published by Addison Wesley. ISBNs: 0-201-63346-9, 0-201-63354-X, 0-201-63495-3.
- Unix Network Programming by W. Richard Stevens. Published by Prentice Hall. ISBN 0-13-949876-1.
- BSD Sockets: A Quick And Dirty Primer (http://www.cs.umn.edu/~bentlema/unix/--has other great Unix system programming info, too!)
- Client-Server Computing (http://pandonia.canberra.edu.au/ClientServer/socket.html)
- Intro to TCP/IP (gopher)(gopher://gopher-chem.ucdavis.edu/11/Index/Internet_aw/Intro_the_Internet/intro.to.ip/)
- Internet Protocol Frequently Asked Questions (France)(http://web.cnam.fr/Network/TCP-IP/)
- The Unix Socket FAQ (http://www.ibrado.com/sock-faq/)
- RFC-768 -- The User Datagram Protocol (UDP)(http://www.freesoft.org/CIE/RFC/768/index.htm)
- RFC-791 -- The Internet Protocol (IP)(http://www.freesoft.org/CIE/RFC/791/index.htm)
- RFC-793 -- The Transmission Control Protocol (TCP)(http://www.freesoft.org/CIE/RFC/793/index.htm)
- RFC-854 -- The Telnet Protocol (http://www.freesoft.org/CIE/RFC/854/index.htm)
- RFC-951 -- The Bootstrap Protocol (BOOTP) (http://www.freesoft.org/CIE/RFC/951/index.htm)
- RFC-1350 -- The Trivial File Transfer Protocol (TFTP)(http://www.freesoft.org/CIE/RFC/1350/index.htm)
Eu renunt,
va rog ajutati-ma
Ei bine, asta e cea mai mare parte. Sa speram ca cel putin cateva din informatiile
continute in acest document au fost precise si sincer sper ca nu sunt erori
batatoare la ochi. Adica, bineinteles, din astea apar intotdeauna.
Asa ca, daca exista, sunt dure pentru tine. Imi cer scuze daca vreo eroare continuta aici ti-a cauzat vreun necaz, dar nu ma poti trage la raspundere pentru asta. Vezi tu, din punct de vedere legal, eu nu stau in spatele nici unui singur cuvant din acest document. Asta e avertismentul meu pentru tine: toata chestia poate fi doar o incarcatura de prostii.
Dar probabil ca nu e asa. Mai presus de toate, am cheltuit multe, multe ore lucrand la material, si implementand cateva utilitare de retea TCP/IP pentru Windows (inclusiv Telnet) ca lucru pentru vacanta de vara. Nu sunt dumnezeul socket; sunt doar un tip oarecare.
Apropo, daca cineva are orice critica distructiva sau constructiva asupra acestui document, va rog sa-mi trimiteti un mesaj la beej@ecst.csuchico.edu iar eu voi incerca sa fac un efort ca sa indrept lucrurile.
In caz ca te intrebi de ce am facut asta, pai, am facut-o pentru bani. Ha! Nu e adevarat, am facut-o pentru ca o gramada de oameni mi-au pus intrebari despre probleme legate de socket, si cand le-am spus ca m-am gandit sa le pun cap la cap intr-o pagina, ei au zis "tare!". In afara de asta, simt ca toate aceste cunostinte dobandite cu greu se vor pierde daca nu le voi imarti cu altii. S-a intamplat ca WWW sa fie mijlocul ideal de transport. II incurajez pe altii sa furnizeze informatii similare oricand au posibilitatea.
Ajunge cu asta - inapoi la programare!
Copyright © 1995, 1996 by Brian "Beej" Hall. This guide may be reprinted in any medium provided that its content is not altered, it is presented in its entirety, and this copyright notice remains intact. Contact beej@ecst.csuchico.edu for more information.
Pentru imbunatatirea traducerii puteti sa ma contactati: dmoroian@rol.ro.