tecniche Leggermente Avanzate

Queste non sono cose veramente avanzate, ma sono un po' oltre i livelli di base che abbiamo già affrontato. In effetti, se siete arrivati fin qui, dovreste già considerarvi molto preparati nelle basi della programmazione di rete Unix! Congratulazioni!

dunque qui entriamo nel pericoloso e strano mondo di alcune tra le cose più esoteriche che imparerete mai riguardo i socket. Tenetevi pronti!

Blocco

Blocco. Ne avete sentito parlare-- di che diavolo si tratta? In parole povere, "bloccare" è il gergo tecnico per "dormire". Probabilmente avrete notato che quando eseguite listener, qua sopra, quello si metteva seduto e aspettava che arrivasse qualche pacchetto. Ciò che accadeva è che veniva chiamata recvfrom(), non c'erano dati, e quindi si dice che recvfrom() "blocca" (cioè, in effetti, si addormenta li) finchè non arrivano dei dati.

Un sacco di funzioni bloccano. accept() blocca. tutte le recv() bloccano. La ragione per cui lo fanno è che gli viene permesso di farlo. Quando create il primo descrittore di socket con la funzione socket(), il kernel lo imposta in maniera che blocchi. Se non volete che un socket blocchi, dovete effettuare una chiamata a fcntl():

    #include <unistd.h>
    #include <fcntl.h>
    .
    .
    sockfd = socket(AF_INET, SOCK_STREAM, 0);
    fcntl(sockfd, F_SETFL, O_NONBLOCK);
    .
    . 

impostando un socket come non-bloccante, potete praticamente "interrogare" il socket riguardo alla sua informazione. Se cercate di leggere da un socket non-bloccante e non ci sono dati, non gli viene permesso di bloccare-- esso restituirà -1 e errno verrà impostata a EWOULDBLOCK (errore "avrebbe bloccato").

Parlando in generale, comunque, questo tipo di interrogazione è una cattiva idea. Se mettete il vostro programma in un ciclo continuo di ricerca di dati dal socket, succhierete tempo di CPU in modo pazzesco. Una soluzione più elegante per controllare se ci sono dati in attesa di essere letti è nella sezione seguente, con select().

select()--Multiplazione dell' I/O Sincrono

questa funzione è strana in qualche maniera , ma è molto utile. prendiamo la seguente situazione: siete un server e volete restare in ascolto di nuove connessioni ma anche continuare a leggere alle connessioni che già avete.

Nessun problema, direte voi, solo una accept() ed un po di recv(). Non così veloce, amico! Che cosa accade se tu blocchi con una accept()? come fai adavere alo stesso tempo una recv()? "Usi socket non-bloccanti! " Nemmeno per sogno! Non vuoi essere uno sbafatore di CPU. E allora, cosa?

select() vi da il modo per controllare molti socket allo stesso tempo. Essa vi dirà quali sono pronti per essere letti, quali sono pronti per la scrittura, e quali socket hanno causato delle eccezioni, se proprio ve ne importa qualcosa.

Senz'altro indugio, eccovi la sinossi di 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); 

La funzione controlla "sets" (insiemi) di descrittori di file; in particolare readfds, writefds, e exceptfds. Se volete vedere se è possibile leggere dallo standard input e da un descrittore di socket, sockfd, aggiungete semplicemente i descrittori di file 0 e sockfd all'insieme di readfds. il parametro numfds dovrebbe essere impostato al valore del più alto descrittore di file più uno. In questo esempio, esso dovrebbe essere impostato a sockfd+1, poichè è sicuramente più alto dello standard input (0).

Quando select() ritorna, readfds verrà modificato per riflettere il cambiamento dei descrittori di file che avete selezionato pronti per la lettura. Potete verificarli con la macro FD_ISSET(), più avanti.

Prima di andare troppo avanti, Vi parlerò di come manipolare questi set/insiemi. Ogni set è del tipo fd_set. Le seguenti macro operano su questo tipo:

Infine, cos'è questa strana struttura struct timeval? Bene, alle volte non vorrete attendere per sempre che qualcuno vi invii dei dati. Magari ogni 96 secondi vorrete scrivere "Funziona Ancora..." sul terminale anche se non è accaduto niente. Questa struttura vi permette di specificare un periodo di timeout Se il tempo viene superato e select() non ha ancora trovato nessun descrittore di file pronto in lettura, essa terminerà in modo che possiate continuare a lavorare.

La struct timeval ha i seguenti campi:

    struct timeval {
        int tv_sec;     // secondi
        int tv_usec;    // microsecondi
    }; 

impostate semplicemente tv_sec al numero di secondi di attesa, e impostate tv_usec al numero di microsecondi daattendere. Si, sono microsecondi, non milliseconds. ci sono 1,000 microseconds in un millisecond, e 1,000 millisecondi in un secondo. Dunque, ci sono 1,000,000 di microsecondi in un secondo. Perché sono "usec"? L "u" dovrebbe assomigliare alla lettera greca (Mu) che usiamo per designare "micro". Inoltre, quando la funzione termina, timeout potrebbe essere aggiornato per mostrare il tempo rimanente. Questo dipende dalla specie di Unix che state usando.

Yay! Abbiamo un timer con risoluzione di un microsecondo! Bene, non contateci. L'unità di tempo standard di Unix è di circa 100 millisecondi, quindi dovrete aspettare così tanto , non importa quanto piccolo impostatiate il valore nella vostra struct timeval.

altre cose interessanti: se impostate i campi nella vostra struct timeval a 0, select() andrà in timeout immediatamente, interrogando subito tutti i descrittori di file dell'insieme. Se imostate il parametro timeout a NULL, non andrà mai in timeout, e attenderà finchè non sia pronto il primo descrittore di file . Infine, e non vi importa di restare in attesa di un certo insieme, potete semplicemente impostarlo a NULL nella chiamata a select().

il seguente pezzetto di codice attende 2.5 secondi cheaccada qualcosa sullo standard input:

    /*
    ** select.c -- un esempio d'uso  di select() 
    */

    #include <stdio.h>
    #include <sys/time.h>
    #include <sys/types.h>
    #include <unistd.h>

    #define STDIN 0  // descrittore di file per lo standard input

    int main(void)
    {
        struct timeval tv;
        fd_set readfds;

        tv.tv_sec = 2;
        tv.tv_usec = 500000;

        FD_ZERO(&readfds);
        FD_SET(STDIN, &readfds);

        // non ci importa di  writefds e exceptfds:
        select(STDIN+1, &readfds, NULL, NULL, &tv);

        if (FD_ISSET(STDIN, &readfds))
            printf("E' stato premuto un tasto!\n");
        else
            printf("Timeout.\n");

        return 0;
    } 

Se state usandoun terminale con buffer, la chiave dovrete premere INVIO o andrà comunque in timeout .

Ora, alcuni di voi pensaranno che questo potrebbe essere un ottimo metodo per attendere dati su un socket di tipo datagramma--e avete ragione: potrebbe . Alcuni Unix possono usare select in questo modo, altri no. Dovrete vedere cosa ne dice la vostra pagina man locale sull'argomento se volete provarci.

Alcuni Unix aggiornano il tempo nella vostra struct timeval per riflettere la quantità di tempo che ancora rimane prima del timeout. Ma altri non lo fanno Non fidatevi ciecamente ddi questo se volete scrivere codice portabile (Usate gettimeofday() se volete tener conto del tempo passato. E' un rompimento di scatole, lo so, ma è così.)

Che succede se un socket in lettura chiude la connessione? Beh, in quel caso, select() restituisce il descrittore di file come "pronto per la lettura". Quando cercherete effettivamente di usare recv() su di esso, recv() restituirà 0. In questa maniera potrete venire a conoscenza che il client ha chiuso la connessione.

Un'ulteriore nota interessante su select(): se avete un socket in attesa con listen(), potete controllare se c'è una nuova connessione mettendo il suo descrittore di file nell'insieme readfds .

E questa, amici miei, è una veloce panoramica sulla onnipotente select().

Ma, a grande richiesta, qui c'è un esempio più approfondito. Sfortunatamente, la differenza tra l'esempio sempliciotto, quello precedente, e questo, è significativa Ma dateci un'occhiata, poi leggete la descrizione che segue.

Questo programma funziona come un semplòice server di chat multi utente. Avviateo in una finestra, poi usate telnet per collegarvici ("telnet hostname 9034") da altre finestre. Quando digiterete qualcosa in una sessione telnet, essa apparirà in tutte le altre.

    /*
    ** selectserver.c -- un gustoso chat server multiutente
    */

    #include <stdio.h>
    #include <stdlib.h>
    #include <string.h>
    #include <unistd.h>
    #include <sys/types.h>
    #include <sys/socket.h>
    #include <netinet/in.h>
    #include <arpa/inet.h>

    #define PORT 9034   // la porta sulla quale stiamo ascoltando

    int main(void)
    {
        fd_set master;   // lista dei descrittori di file principale (master)
        fd_set read_fds; // lista dei secrittori di file temporanea per  select() (temp)
        struct sockaddr_in myaddr;     // indirizzo del server 
        struct sockaddr_in remoteaddr; // indirizzo del client 
        int fdmax;        // massimo numero di descrittori di  file 
        int listener;     // descrittore di socket in ascolto
        int newfd;        // descrittore di socket appena ottenuto  con accept()
        char buf[256];    // buffer per i dati dei  client 
        int nbytes;
        int yes=1;        // per  setsockopt() SO_REUSEADDR, più avanti
        int addrlen;
        int i, j;

        FD_ZERO(&master);    // pulisce gli insiemi master e temp 
        FD_ZERO(&read_fds);

        // ottiene  listener
        if ((listener = socket(AF_INET, SOCK_STREAM, 0)) == -1) {
            perror("socket");
            exit(1);
        }

        // elimina il fastidioso  messaggio d'errore "address already in use" 
        if (setsockopt(listener, SOL_SOCKET, SO_REUSEADDR, &yes,
                                                            sizeof(int)) == -1) {
            perror("setsockopt");
            exit(1);
        }

        // bind
        myaddr.sin_family = AF_INET;
        myaddr.sin_addr.s_addr = INADDR_ANY;
        myaddr.sin_port = htons(PORT);
        memset(&(myaddr.sin_zero), '\0', 8);
        if (bind(listener, (struct sockaddr *)&myaddr, sizeof(myaddr)) == -1) {
            perror("bind");
            exit(1);
        }

        // ascolta
        if (listen(listener, 10) == -1) {
            perror("listen");
            exit(1);
        }

        // aggiunge  listener all'insieme master
        FD_SET(listener, &master);

        // tiene traccia del  descrittore più grande
        fdmax = listener; // fin'ora è questo

        // ciclo  principale
        for(;;) {
            read_fds = master; // lo copia
            if (select(fdmax+1, &read_fds, NULL, NULL, NULL) == -1) {
                perror("select");
                exit(1);
            }

            // cicla tra le connessioni esistenti in attesa di dati da leggere
            for(i = 0; i <= fdmax; i++) {
                if (FD_ISSET(i, &read_fds)) { // eccone un po'!!
                    if (i == listener) {
                        // gestisce le nuove connessioni
                        addrlen = sizeof(remoteaddr);
                        if ((newfd = accept(listener, (struct sockaddr *)&remoteaddr,
                                                                 &addrlen)) == -1) { 
                            perror("accept");
                        } else {
                            FD_SET(newfd, &master); // aggiunge all'insieme  master 
                            if (newfd > fdmax) {    // tiene traccia del più grande descrittore 
                                fdmax = newfd;
                            }
                            printf("selectserver: nuova connessione da %s attiva "
                                "socket %d\n", inet_ntoa(remoteaddr.sin_addr), newfd);
                        }
                    } else {
                        // gestisce i dati da un client
                        if ((nbytes = recv(i, buf, sizeof(buf), 0)) <= 0) {
                            // ottenuto un errore o una connesione chiusa dal  client
                            if (nbytes == 0) {
                                // connessione chiusa
                                printf("selectserver: socket %d hung up\n", i);
                            } else {
                                perror("recv");
                            }
                            close(i); // ciao!
                            FD_CLR(i, &master); // rimuove dall'insieme  master 
                        } else {
                            // abbiamo dei dati da un client
                            for(j = 0; j <= fdmax; j++) {
                                // li inviamo a tutti!
                                if (FD_ISSET(j, &master)) {
                                    // tranne che  quello in ascolto e noi stessi
                                    if (j != listener && j != i) {
                                        if (send(j, buf, nbytes, 0) == -1) {
                                            perror("send");
                                        }
                                    }
                                }
                            }
                        }
                    } // è TERRIBILE!
                }
            }
        }
        
        return 0;
    } 

Notate che abbiamo due insiemi di descrittori di file nel codice: master e read_fds. Il primo , master, contiene tutti i descrittori di file che sono attualmente connessi, e anche il descrittore che è in attesa di nuove connessioni.

La ragione per cui abbiamo master è che select() in effetti cambia l'insieme che gli viene passato per riflettere quali socket sono pronti per la lettura Poichè dobbiamo tenere traccia delle connessioni tra una chiamata di select() e la seguente, dobbiamo immagazzinare tutti i descrittori da qualche parte. All'ultimo minuto, copiamo il master in read_fds, e poi chiamiamo select().

Ma questo significa che ogni volta che abbiamo una nuova connessione, devo aggiungerla all'insieme master set? Già! Eogni volta che una connessione viene chiusa, dobbiamo rimuoverla dall'insieme master ? Si, è così.

Notate che c'è un controllo per scoprire quando listener è pronto per la lettura Quando ciò accade, significa che c'è una nuova connessione in attesa, e la prendiamo con accept() aggiungendola a master. Allo stesso modo, quando una connessione da un client è pronta per la lettura, e recv() restituisce 0, sappiamo che il client ha chiuso la connessione, e dobbiamo rimuoverla dall'insieme master .

Se la recv() dal client restituisce non-zero, sappiamo che sono stati ricevuti dei dati. Qundi ce li prendiamo, e poi passiamo alla lista di master e li reinviamol a tutti gli altri client connessi.

E questa, amici miei, è una panoramica un pomeno semplice sulla onnipotente select() .

Gestione di send() Parziali

Ricordate la sezione su send(), qui sopra, quando vi dissi che send() avrebbe potuto non inviare tutti i byte che gli avevate richiesto di inviare? Ad esempio, volete inviare 512 byte, ma essa restituisce 412. Cos'è successo ai 100 byte rimanenti?

Ebbene, essi sono ancora nel vostro piccolo buffer in attesa di essere inviati. A causa di circostanze al di fuori del vostro controllo, il kernel ha deciso di non inviare tutti i dati in un'unica volta, e ora, amici miei, sta a voi far in modo che quei dati vengano spediti fuori.

Potreste anche scrivere una funzione come questa :

    #include <sys/types.h>
    #include <sys/socket.h>

    int sendall(int s, char *buf, int *len)
    {
        int total = 0;        // quanti  byte abbiamo inviato
        int bytesleft = *len; // quanti byte ci restano da inviare
        int n;

        while(total < *len) {
            n = send(s, buf+total, bytesleft, 0);
            if (n == -1) { break; }
            total += n;
            bytesleft -= n;
        }

        *len = total; // restituisce il numero di byte effettivamente inviati

        return n==-1?-1:0; // restituisce  -1 in caso di fallimento, 0 in caso di successo
    } 

In questo esempio, s è il socket al quale volete inviare dati, buf è il buffer contenente i dati, e len è un puntatore ad un int contenente il numero di byte nel buffer.

La funzione restituisce -1 in caso d'errore (e errno è ancora lo stesso impostato dalla chiamata send().) Inoltre, il numero di byte effettivamente inviati è restituito da len. Questo sarà lo stesso numero di bytes che gli avevate chiesto di inviare, a meno che non ci sia stato un errore. sendall() farà del suo meglio, soffiando e sbuffando, per inviare i dati, ma se c'è un errore ancora una volta la palla tornerà a voi.

Per com,plòetezza qui c'è un esempio di chiamata della funzione:

    char buf[10] = "Beej!"
    int len;

    len = strlen(buf);
    if (sendall(s, buf, &len) == -1) {
        perror("sendall");
        printf("abbiamo inviato solo  %d byte a causa di un errore!\n", len);
    } 

Cosa succede dal lato ricevente quando arriva una parte del pacchetto? Se i pacchetti sono di lunghezza variabile come fa, il ricevente a sapere quando finisce un pacchetto e ne inizia un altro? Già, gli scenari realistici sono un bel casino. Probabilmente dovreste incapsulare (vi ricordate di ciò dalla sezione Incapsulamento dei Dati quasi all'inizio?) Andate avanti per scoprire i dettagli!

Figlio dell'Incapsulamento dei Dati

Ad ogni modo, cosa significa veramente incapsulare? Nel caso più semplice, significa che appiccicate un'intestazione sul pacchetto che identificano l'informazione o la lunghezza del pacchetto, o entrambe.

Come dovrebbe esserefatto il vostro header ? Beh, sono solo un po di dati binari che rappresentano tutto quello che ritenete necessario pr completare il vostro progetto.

Wow. tutto ciò è molto vago.

Okay. Ad eempio, poniamo che voi abbiate un programma di chat multiutente che usa SOCK_STREAM. quando un utente digita ("dice") qualcosa, due pezzi di informazione devono essere trasmessi al server: quello che è stato detto e chi l'ha detto.

Fin qui tutto bene? "Qual'è il problema?" vi starete chiedendo.

il problema è che il mesaggio può essere di lunghezza variabile . Una persona di nome "tom" potrebbe dire , "Ciao", ed un'altra persona di nome "Benjamin" potrebbe dire , "Ciao ragazzi come va?"

Dunque voi inviate con send() tutta questa roba ai client come vi arriva. Il vostro flusso di dati in uscita apparirebbe così:

    t o m c i a o B e n j a m i n C i a o r a g a z z i c o m e v a ?

E così via. Come fa il client a sapere quando inizia un mesaggio e ne finisce un altro? Potreste, volendo, rendere tutti i messaggi della stessa lunghezza e usare la sendall() che abbiamo implementato,prima. Ma ciò sprecherebbe banda! non vogliamo usare send() con 1024 byte solo perchè "tom" posa dire "Hi".

Dunque incapsuliamo i dati in una piccolo intestazione ed in una struttura a pacchetto. Entrambi, client e server sanno come scrivere e leggere un pacchetto ( volte si dice "marshal" e "unmarshal" (NdT:: non sono a conoscenza di una cosa simile in italiano, più o meno è "scortare", credo ) di dati simile . Non guardate adesso, ma stiamo cominciando a definire un protocollo che descriva la comunicazione tra un client ed un server!

In questo caso, assumiamo che il nome utente sia di una lunghezza fissata di 8 caratteri, completata con degli '\0'. Eassumiamo anche che i dati siano di lunghezzavariabile, fino a un massimo di 128 caratteri. diamo un'occhiata ad un esempio di struttura pacchetto che potremmo usare in questa situazione :

  1. len (1 byte, senza segno) -- La lunghezza totale del pacchetto, contando il nome utente da 8-byte e i dati della chat .

  2. name (8 byte) -- il nome utente , completato da NULL se necessario.

  3. chatdata (n-byte) -- i dati veri e proprif, non più di 128 byte. La lunghezza del pacchetto dovrebbe essere calcolata come la lunghezza di questi dati più 8 (la lunghezza del campo name, qui sopra).

Perchè ho scelto limiti di 8-byte e 128-byte per i campi? Li ho presi a caso , immaginando che sarebbero stati abbastanza. Forse, 8 byte è anche troppo restrittivo per i vostri bisogni, e potreste anche avere un campo name da 30-byte , o come vi pare. La scelta sta a voi.

Usando la definizione di pacchetto precedente, il primo pacchetto consisterebbe delle seguenti informazioni (in esadecimale ed ASCII):

      0C     74 6F 6D 00 00 00 00 00      48 69
   (length)  T  o  m    (completamento)         C  i a o

Ed il secondo, similmente:

      18     42 65 6E 6A 61 6D 69 6E      48 65 79 20 67 75 79 73 20 77 ...
   (length)  B  e  n  j  a  m  i  n       C i a o   r a g a z z i  ...

(La lunghezza è immagazzinata in Network Byte Order, ovviamente. In questo caso, si tratta di un solo byte e quindi non è importante ma , in generale dovrete avere tutti gli interi immagazzinati in Network Byte Order nei vostri pacchetti.)

Quando stato inviando questi dati , dovrete stare attenti ad usare un comando simile a sendall(), visto prima , cosicchè potrete sapere che i dati sono stati tutti inviati, anche se ci volesero chiamate multiple a send() per mandarli .

Alla stessa maniera, quando riceverete questi dati, avrete bisogno di fare un po di lavoro extra. Per essere sicuri, dovrete considerare di poter ricevere un pacchetto parziale abbiamo bisogno di chiamare recv() più volte finchè il pacchetto non sia completamente ricevuto.

Ma come? Bene, conosciamo il numero di byte che abbiamo bisogno di ricevere in totale affinchè un pacchetto sia completo, poichè quel numero è appiccicato in cima a pacchetto. Sappiamo anche che la dimensione mssima del pacchetto è 1+8+128, , 137 byte (perchè è così che abbiamo definito il pacchetto)

quello che potete fare è dichiarare un semplice array grande abbastanza per due pachetti. Questo sarà il vostro array di lavoro quando ricostruirete i pacchetti in arrivo.

Ogni volta che usate recv() per ricevere dati, avrete bisogno di metterli nel buffer di lavoro e controllare se il pacchetto è completo. Ciò significa che il numero di byte nel buffer è più grande o uguale alla lunghezza specificata nell'header (+1, poichè la lunghezza nell'header non include il byte per la lunghezza stessa.) Se il numero di byte nel buffer è minore di uno il pacchetto non è completo, ovviamente. Dovrete avere un caso speciale per questo, poichè il primo byte è inutile e non potete fidarvi di quello per stabilire la correttezza del pacchetto.

Una volta che il pacchetto è completo, potrete farci quello che volete. Usatelo, e rimuovetelo dal vostro buffer di lavoro.

Whew! Non siste ancora riusciti a ficcarvi tutto in testa? Bene, qui c'è il secondo pugno di questo destro-sinistro: potreste aver letto oltre la fine del singolo pacchetto e cominciare il secondo con una sola recv() . Ciò significa, voi avete un bufferdi lavoro con un pacchetto completo, ed una parte incompleta del prossimo pacchetto! Porco Mondo. (Ma è oper questo che abbiamo fatto un buffer di lavoro che potesse mantenere due pacchetti--in caso accaddesse questo!)

Poichè conoscete la lunghezza del primo pacchetto dall'header, e avete tenuto traccia del numero di byte nel buffer di lavoro, potete sottrarre e calcolare quanti byte nel buffer di lavoro appartengono al secondo pacchetto (incompleto). Quando avrete gestito il primo, potrete ripulire il buffer di lavoro muovere il pacchetto parziale all'inizio del bufer per essere pronti alla prossima recv().

(alcuni di voi lettorinoteranno che effettivamente spostare il pacchetto parziale all'inizio del buffer richiede troppo tempo, ed iul programma potrebbe essere scritto in modo da non richiedere questo passaggio usando un buffer circolare. Sfortunatamente per il rsto di voi, una discussione sui buffer circolari è aldilà del nosro scopo attuale. Se siete ancora curiosi, prendetevio ulibro di strutture dati e studiatevela li.)

Non ho mai detto che fosse facile. Si, ho detto che era facile, è vero. E lo è; avete solo bisogno di un po' di pratica e poi vi verrà tuto naturale. By Excalibur I swear it! [intraducibile.. "grosso modo giuro per excalibur"]