Newsgroup: it.comp.lang.c++
Titolo: Classe per comunicazioni asincrone (lungo) (può sembrare ma non è OT)

Sto cercando di realizzare una classe per un programma server di rete, e
tale classe deve incapsulare la gestione dei socket che utilizzerò, fornendo
un'interfaccia verso i suoi "utenti".
La trattazione dell'utilizzo dei socket è ovviamente piuttosto complessa e
platform-dependant, ma non è quello il cuore del problema: so come
utilizzare le API appropriate, anche sotto più di un sistema operativo. Il
problema è come gestire le cose internamente alla classe.
Per questo scopo, è sufficiente sapere che una volta che un socket sia
aperto, esistono due funzioni (send() e recv()) che permettono
rispettivamente di scrivere e leggere singoli byte, e che esistono modi per
controllare quando ci sono dati da leggere. Si può quindi assumere che in un
dato istante si possano leggere con certezza N byte, e che sia possibile
stabilire quando non ce ne sono altri pronti. Allo stesso modo, è possibile
assicurarsi di spedire byte solamente quando il socket è pronto a
trasmetterli. Anche costruzione e distruzione dell'oggetto sono realizzabili
senza problemi, e si può assumere che l'oggetto sia relativo a una
connessione pienamente operativa (ho già realizzato questa parte).
La comunicazione deve avvenire tramite singoli byte che rappresentano un
flusso bidirezionale di caratteri, e i problemi nascono principalmente dal
fatto che il flusso in input deve essere spezzato in stringhe contenenti
singole linee, ma non è garantito che queste si presentino intere sul
socket: potrebbero arrivare incomplete, e il testo arrivato deve essere
immagazzinato. Per rendere le cose più complicate, non ci sono limiti
compile-time sulla lunghezza massima di una singola linea, né sul numero
totale di caratteri che possono arrivare contemporaneamente. L'output (che
può essere di vario genere e quindi si presta particolarmente ad
un'interfaccia simil-ostream) viene invece immagazzinato in un buffer e
spedito interamente quando possibile.
Il comportamento dell'oggetto può essere schematizzato come segue (pseudo
codice, dichiarazione e implementazione contemporanee):
class MySocket
{
    // costruttore, distruttore
    // parte di input
    // membri
    in_buffer        // tipo da stabilire
    coda_input    // contenitore (da stabilire) di std::string*
    // metodi
    bool read_input(void)
    {
        // funzione chiamata ad intervalli regolari per leggere
        // l'input pronto e dividerlo
        while(ci sono caratteri disponibili sul socket)
        {
            - legge uno o più caratteri in in_buffer
            - if(c'è un newline)
            {
                - alloca una std::string contenente i caratteri letti
                - pone il puntatore a questa stringa in coda_input
                - toglie da in_buffer i caratteri utilizzati
            }
            - if(si è verificato qualche errore sul socket)
                return(false)
        }
        // possono essere avanzati caratteri
        // questi devono rimanere in in_buffer
        return(true)
    }
    std::string* get_next_line(void)
    {
        // ritorna la prima linea di testo se disponibile
        -if(c'è almeno una std::string in coda_input)
        {
            -prende la prima std::string da coda_input
            return(stringa);
        }
        else
            return(NULL);
    }
    // parte di output
    // membri
    out_buffer    // ignoro totalmente cosa possa essere ma deve
                        // avere l'interfaccia di un ostream
    // metodi
    template<class T> MySocket& operator << (const T& data)
    {
        // immagazzina dati per la spedizione
        out_buffer << data;
        return(*this);
    }
    bool send_all_data(void)
    {
        // spedisce tutti i dati sul socket
        -prende i dati da out_buffer e li mette in un array di char
        -calcola il numero di char da spedire
        -invia il tutto
        -elimina il contenuto di out_buffer
        -if(si è verificato qualche errore sul socket)
            return(false);
        else
            return(true);
    }
};
Come si vede, c'è un'ampia scelta su quali algoritmi e contenitori
utilizzare,  e purtroppo non ho sottomano una documentazione esauriente e
approfondita su iostreams e container vari (per non parlare di algoritmi e
iteratori). Gradirei qualche consiglio, sono ben accette anche revisioni
totali di questa implementazione, solo l'interfaccia (per interagire con
altre parti del programma) dovrebbe rimanere quella descritta. Non ho
problemi eventualmente a ereditare da una o più classi standard (o a usare
classi intermedie), se questo può migliorare le cose. I punti cruciali sono
la compattezza (di questi oggetti ce ne saranno un bel po' in giro),
l'occupazione di memoria (ci sono parecchi buffer dinamici da ingrandire al
bisogno ma soprattutto rimpicciolire appena si svuotano) e le prestazioni
(invio, ricezione e prelevamento stringhe avvengono alcune volte al secondo,
l'immagazzinamento dell'output anche alcune decine).
Grazie a chi ha avuto la pazienza di leggere fin qui :-)
Massimo

 l'immagazzinamento dell'output anche alcune decine).
Vediamo di spezzare il problema.
Primo ti serve un meccanismo che ti nasconda le API di accesso alle socket. Io
ti consiglio di usare una delle classi commerciali gia' esistenti oppure di
studiarti bene, ma molto bene, come funzionano gli iostream ed implementarti di
conseguenza la tua versione di streambuf e della socket.
Supponiamo che tu abbia gia' un IO stream specifico per le socket o che tu abbia
una delle librerie esistenti. La parte di output dovrebbe essere gia a posto a
meno che tu non debba aggiungerci un protocollo tuo (perche' calcolavi il numero
di byte da spedire??).
Nella parte di input il tuo problema e' quello di spezzare le stringhe in
ingresso. Tanto per cominciare io non farei come dici tu una funzione "da
chiamare ogni tanto". Quello e' di fatto un poll. Il SO dovrebbe già di per se
darti un minimo di bufferizzazione intelligente. Ergo hai due casi: lettura
sincrona o lettura asincrona.
Se vuoi lavorare in maniera sincrona stai ragionando come per uno stream
standard. Ergo quando esegui la lettura della stringa non hai altro da fare (nel
tuo thread) e pertanto la lettura puo' essere bloccante. In questo caso ti
bastano i meccanismi di IO forniti da istream per leggerti la stringa.
Se vuoi lavorare in maniera asincrona allora stai per definizione in un ambiente
multithreaded e puo' avere senso separare il meccanismo di "stringhizzazione"
dal resto. Lo puoi fare in vari modi a seconda del supporto che il sistema
operativo o la tua astrazione delle socket ti danno per l'accesso asincrono. Se
ti permettono un accesso asincrono a te basta definire la relativa callback del
comando di input in maniera che elabori i dati in ingresso e ti prepari
l'eventuale stringa pervenuta nella lista. Praticamente quello che tu facevi con
il tuo poll read_input ma eseguito in maniera concorrente al tuo processo
primario, senza che tu debba chiamarlo periodicamente. Se invece il SO o la
libreria non ti da direttamente supporto all'IO asincrono, lo puoi sempre
emulare creando un apposito thread il cui corpo non e' altro che un loop di
lettura sincrona sulla socket.
Nota che se non specifichi meglio il problema che devi risolvere e' difficile
dire cosa conviene fare. Dici che non c'e' limite alla dimensione di una
stringa. Ma questo e' virtualmente impossibile. Se qualcuno di spedisce una
stringa di dimensioni spropositate come minimo ti impalla il sistema. Un limite
deve esistere, altrimenti non stai processando stringhe ma streams. E gli
streams li processi a blocchi. Dici che ti serve spezzare le stringhe prima di
leggerle e dall'interfaccia che proponi sembra che tu intenda consumare l'input
con un poll e testare la presenza di input in maniera non bloccante. Ma in un
sistema multithreaded ti conviene usare thread separati se hai lavoro da fare
mentre l'input e' ancora in corso. Ergo non ti conviene un meccanismo di poll.
Per quale motivo la classe e' disegnata cosi'? Non e' chiaro. Ergo e' difficile
dirti in che modo e' meglio implementarla.
Per finire, se esistono solidi motivi per fare le cose nel modo in cui dici tu,
non vedo particolari difficolta'. Usa una lista come contenitore di stringhe (
std::list<std::string> coda_input; ). Per i buffer ... studiati gli iostream.
--
Andrea Ferro
---------
Brainbench C++ Master. Scored higher than 97% of previous takers
Scores: Overall 4.46, Conceptual 5.0, Problem-Solving 5.0
More info 
http://www.brainbench.com/transcript.jsp?pid=2522556

 std::list<std::string> coda_input; ). Per i buffer ... studiati gli
iostream.
Ho realizzato una prima implementazione che usa (internamente) gli
stringstream: per l'input uso uno std::ostringstream, aggiungo un carattere
per volta leggendolo dal socket e quando trovo un newline salvo la stringa
in una std::queue e svuoto il buffer, per l'output uso uno std::stringstream
bidirezionale dove inserisco l'output man mano che si genera e poi lo leggo
un carattere per volta (o più, devo vedere cos'è più conveniente a livello
di rete) spedendolo sul socket.
L'unica cosa che mi preoccupa sono le prestazioni, temo che gli stringstream
siano terrificanti sotto questo profilo... per questo volevo sapere quali
contenitori e buffer sarebbero migliori, secondo il parere di gente che li
usa abitualmente.
Massimo

 single-thread
Volevo scrivere programma, non so perché mi sia uscito "problema"...
E il motivo principale di questo è che il supporto multithread potrebbe
benissimo non essere presente a livello utente nel sistema operativo di
destinazione (il mio programma non deve girare solo sotto Windows ma anche
sotto uno UNIX qualunque con un minimo di porting, tra l'altro questo è il
motivo per cui voglio basarmi sui socket standard BSD e non su una qualche
classe proprietaria).
Massimo

 wrote in message
news:9cc5fh$486$1...@news.mclink.it...

All'altro mega messaggio rispondero' con calma. Per questo ti faccio notare che
qualunque sistema operativo che abbia senso usare come server ti permette di
lavorare in multithread.
Secondo te che grosso problema di porting avresti ?


 Secondo te che grosso problema di porting avresti ?
Anche se più sistemi la mettono a disposizione, la gestione dei thread
cambia *considerevolmente* secondo il sistema utilizzato. Va bene che un
programma di questo tipo non può essere *totalmente* portabile, ma vorrei
che lo fosse senza dover scavare troppo a fondo nelle API. Altrimenti usavo
direttamente una CAsyncSocket... no, in effetti credo che non l'avrei usata
lo stesso ;-)
Massimo

 usa abitualmente.
Un vettore di caratteri di lunghezza corrispondente alla dimensione massima
della stringa.
Ti spiego perche' secondo me non e' vero che non c'e' una dimensione massima
della stringa e nemmeno che non devi usare un timeout.
Per prima cosa mi dici che la stringa corrisponde all'input utente. Secondo me
definire una lunghezza massima di, diciamo, qualche centinaio di caratteri e'
piu' che abbondante. Nel caso non lo sia allora diciamo che un lunghezza di
16Kbyte e' certamente sufficiente. Nessun umano batterà righe di comando di
oltre 16Kbyte. Se lo fa non e' un umano ma un programma che cerca di creare un
denial of service. E se il tuo server non pone un limite, dopo un dato numero di
megabyte o di gigabyte (dipende dal tuo disco di swap e dalla tua ram) il
sistema si impalla per out of memory o, molto prima di questo, praticamente si
ferma per eccesso di swap. Indipendentemente dalla granularità dell'estensione
del buffer, arrivi ad un punto che per aggiungere un carattere devi ricopiarlo.
E rileggere e poi riscrivere qualche centinaio di gigabyte da HD richiede
parecchio tempo, anche se nel tuo programma stai solo aggiungendo un carattere
ad una stringa (comunque sia implementata). E questo, oltre a essere un
nonsense, contraddice il tuo desiderio di non avere IO bloccante con timeout.
Secondo, devi definire un numero massimo di connessioni gestibili e definire un
timeout per ogni connessione. Se un utente (o qualcuno che cerca il denial of
service) ti apre una connessione e poi la tiene aperta senza mai scriverci (o ci
scrive una mezza stringa senza newline e poi si ferma) tu devi per forza ad un
certo punto abbattere la connessione o ti trovi un'altra volta passibile di
denial of service.


 cambia *considerevolmente* secondo il sistema utilizzato.
Centralizza il codice thread dependent. Praticamente ti servono la spawn e un
meccanismo di sincronizzazione (per esempio un mutex).


 denial of service.
Questo del timeout è un suggerimento interessante, ne terrò conto :-)
Massimo

 anche prestazioni ragionevoli, però...
class DynaInputBuffer {
 std::vector<char> v;
public:
 bool stringReady() const
 {
  return v.size()>0 && v.back()=='\n';
 }
    bool push(char c)
    {
  // if (stringReady())
  //    throw MyException;
  v.push_back( c );
        return stringReady();
    }
 std::string getString()
 {
  //if ( !stringReady() )
   // throw MyException;
  std::string s(v.begin(),v.end());
  v.clear();
  return s;
 }
};
Nota una sola cosa: non e' garantito che v.clear() liberi la memoria. Ma la cosa
non dovrebbe infastidirti troppo. Puoi provare a sostituire il vector con una
deque o con una lista. Ma tutto dipende dall'implementazione.


una lista. Ma tutto dipende dall'implementazione.
Credo che per come e' scritto lo standard vieti a vector
di liberare la memoria con la chiamata clear().
Il motivo e' che viene detto che se e' stata chiamata
reserve() allora e' garantito che non ci saranno
riallocazioni sino a quando il numero di elementi
non supera la capacity assegnata.
Da nessuna parte nei documenti in mio possesso si
accenna un "a meno che sia stata chiamata clear()"...
io deduco che se scrivo:
   v.reserve(100);
   v.clear();
   for (int i=0; i<100; i++)
     v.push_back(i);
e' *garantito* che non avvengano riallocazioni all'interno
del loop.
clear() per come viene detto in "The C++ Programming
Language" e' un sinonimo di erase(begin(),end()) e
come tale non rilascia lo spazio.
Sempre "The C++ Programming Language" consiglia come sistema
per liberare la memoria questa cosa...
   {
     std::vector<int> tmp;
     v.swap(tmp);
   }
Anche se a dire il vero questo sarebbe comunque in contrasto
con le garanzie che deve fornire reserve() (non ho visto
citare clear() come non ho visto citare swap()).
Andrea
NOTA:
  Credo che scrivere "v.swap(std::vector<int>());" non sarebbe
  corretto per il problema del passaggio di un rvalue come
  reference non const.
  Pero' "std::vector<int>().swap(v);" invece dovrebbe essere
  perfettamente lecito vero ?
  A volte il C++ e' sorprendente :)
-- 
Posted from [151.99.250.40] 
via Mailgate.ORG Server - 
http://www.Mailgate.ORG

   A volte il C++ e' sorprendente :)
Solo "a volte"?  E` tale la sua complessita`, che c'e` da stupirsi
quando non ci stupisce:-).
Alex (MVP di Brainbench per il C++)