Beej's Guide to Network Programming

Folosirea Socketurilor pentru Internet

Version 1.5.5 (13-Jan-1999)
[http://www.ecst.csuchico.edu/~beej/guide/net]


Intro

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.

Cui i se adreseaza?

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.


Ce e un socket?

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.


Doua tipuri de socket-uri pentru 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.


Despre functionarea retelei

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.

P>

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


structs

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.


Despre conversia Network Byte Order!

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.


Adresele IP si cum sa le folositi

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.)


socket()--Obtine descriptor de fisier!

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 .)


bind()--Ce port folosesc?

Cand folosit un socket ar trebui sa asociezi socket-ul cu un port al calculatorului. (Acest lucru se foloseste cand vrei sa folosesti listen()(a asculta) conexiunile la un port precizat--MUDurile fac acest lucru atunci cand scriu "telnet to x.y.z portul 6969"). Daca veti folosi doar connect(), aceasta parte nu e chiar necesara, dar cititi-o pentru mai multe informatii.

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 */
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);
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).

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.


listen()In asteptarea unei conexiuni?

E timpl pentru o schimbare de ritm. Daca se asteapta diferite conexiuni si vreti sa fie tratate ca atare sunt doua lucruri de facut: folositi listen()(a asculta) si apoi accept()(a accepta vezi mai jos).

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().

accept()--"Thank you for calling port 3490."

Apelul sistem accept() este destul de dificil! Ce se intampla de fapt e ca: cineva de departeva incerca sa se conecteze folosind connect() la calculatorul tau la un port unde astepti conexiunea cu listen(). Vei folosi accept() ca sa accepti conexiunea. Va returna un nou descriptor de fisier care sa-l folosesti pentru aceasta singura conexiune. Deci in acest moment avem 2 descriptori de fisier primul asculta in continuare pe portul unde a fost pus iar cel de-al doilea va fi folosit pentru apelurile la send() si recv().

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.

send() si recv()--Discutia!

Aceste doua functii sunt folosite pentru comunicare cu socketuri de tip stream sau datagram conectate. Pentru socketurile datagram neconectate vizitati pagina sendto() si recvfrom(), de mai jos.

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.)


sendto() si recvfrom()--Folosind Datagrame

Iata rezolvarea pentru socketuri de tip datagram.

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() si shutdown()--Sfarsitul conexiunii

Dupa ce transferul a fost efectuat suntem gata sa inchidem conexiunea catre descriptorul de fisier. Lucrul acesta se realizeaza usor ca si cum am inchide un fisier normal in Unix cu close() :
    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.


getpeername()--Cine esti?

Apelul functiei e simplu

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 .)


gethostname()--Cine sunt eu?

La fel de usoara ca si getpeername() e functia gethostname(). Intoarce numele calculatorului pe care ruleaza programul. Rezultatul poate fi dat ca parametru catre gethostbyname() pentru a afla adresa IP a calculatorului pe care ruleaza programul. Asa arata functia:
    #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.


DNS--Din "whitehouse.gov" catre "198.137.240.100"

Iata de la ce vine DNS: "Domain Name Service". Daca cineva tasteaza intr-un shell $ telnet whitehouse.gov telnet va stii ca va trebuisa foloseasca connect() catre "198.137.240.100".

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 *, .


Functionarea Client-Server

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:


Un Simplu Server de tip Stream

Tot ce face acest server e sa trimita sirul de caractere "Hello, World!\n" printr-o conexiune de tip stream. Testarea serverului se face astfel: se ruleaza serverul dintr-o fereastra, iar din alta se foloseste telnetul astfel:
    $ 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


Un Client de tip Stream

E mai usor de implementat decat serverul. Ce face clientul e sa se conecteze la hostul precizat pe portul 3490. Primeste apoi sirul de caractere pe care il trimite serverul.

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".


Socketuri Datagram

Nu sunt multe de spus aici doar prezentarea catorva programe: talker.c si listener.c.

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().


Blocarea

Cand rulati listener, asteapta pana cand vine un pachet.

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().


select()--Sincronizarea I/O

Functie oarecum ciudata dar foarte utila. De exemplu situatia: rulezi serverul si vrei sa astepti conexiunile care vin dar sa si citesti din cele curente.

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.


Mai multe detalii si pe Internet:

BSD Sockets: A Quick And Dirty Primer
(http://www.cs.umn.edu/~bentlema/unix/--mai multe informatii despre programarea in UNIX)

Client-Server
(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/)
RFCs:

RFC-768 -- The User Datagram Protocol (UDP)
(ftp://nic.ddn.mil/rfc/rfc768.txt)

RFC-791 -- The Internet Protocol (IP)
(ftp://nic.ddn.mil/rfc/rfc791.txt)

RFC-793 -- The Transmission Control Protocol (TCP)
(ftp://nic.ddn.mil/rfc/rfc793.txt)

RFC-854 -- The Telnet Protocol
(ftp://nic.ddn.mil/rfc/rfc854.txt)

RFC-951 -- The Bootstrap Protocol (BOOTP)
(ftp://nic.ddn.mil/rfc/rfc951.txt)

RFC-1350 -- The Trivial File Transfer Protocol (TFTP)
(ftp://nic.ddn.mil/rfc/rfc1350.txt)

Disclaimer and Call for Help

Deci asta-i tot. Sper ca macar cateva din informatiile prezentate aici sunt prezentate intr-un asa mod incat sa fie intelese si sper ca nu au erori desi mai aproape tot timpul apar.

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! ;-)


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.