Gestione socket per server

di il
3 risposte

Gestione socket per server

Salve ragazzi! E' arrivato il giorno in cui devo cestinare il prototipo e fare una versiona "alpha" delle mie funzioni di networking.
Fin'ora per il prototipo non mi è interessata la stabilità, volevo giusto qualcosa che funzionasse per poco. Ora mi serve una stabilità indipendente dal client, che sia veloce o meno (il port tra windows e linux lo so già fare).
Se potete, aiutatemi con questo problema:
Ho conoscenze basse del TCP e UDP. Per ora, utilizzerò il tcp, sia come sfida personale (sembra più difficile) sia perché non posso permettermi di perdere qualità dei dati con il videogame, probabilmente in futuro mischierò l'utilizzo dei due protocolli. Ciò che so fare è solo spedire e mandare, infatti nella mia funzione per la communicazione tra il client e il server (in entrambe le applicazioni) si alterna tra un "ricevi" e "manda", e quando riceve crea un vettore con tutti i dati elaborati dal buffer ricevuto. Non utilizzo, attualmente, neanche un invio di quanti byte è lunga la stringa (sembra anche funzionare più del dovuto, regge decine di minuti senza provocare problemi: ogni fine di ricezione, la stringa è completa e non è mischiata con nessun'altra). Utilizzo anche i socket blocking, e credo che questo possa creare dei deadlock. Ciò che mi servirebbe sarebbe una funzione che manda e riceve in modo asincrono (SENZA usare boost: dovrei rifare completamente il server, senza contare che è una questione di principio: voglio capire cosa sto usando).
Attualmente utilizzo i socket SFML, ma so usare anche i socket winsock e i socket linux (i socket sfml sono più facili e uguali tra gli OS, ho comunque appreso anche i socket classici perché, come ho detto prima, è una questione di principio, devo sapere cosa sto usando e come funziona qualcosa prima di usare la sua versione facile), e questa è la mia funzione, che lancio come thread per ogni client (ATTENZIONE: ALCUNE VARIABILI SONO GLOBALI, COME I CLIENT CONTENENTI I SOCKET):

//SERVER

void communicationThread(int id) //Id del client, i client sono un array di client
{
    Socket::Status status = Socket::Done; //Le SFML hanno una classe status, può essere "Done" per "ricevuto tutto correttamente", "Not Ready", "Partial" (con i socket non-blocking), "Error" o "Disconnected".
    string inputString, outputString; //La stringa inputString è la stringa che verrà caricata con il buffer e successivamente elaborata, l'outputString la stringa che verrà caricata con i dati del server e trasformata in buffer
    string data[50]; //Il vettore che conterrà i dati ricevuti elaborati
    stringstream ss; 
    list<ServerStatus>::iterator sStatus; //Iterator per gli snapshot del server, il sistema snapshot è il mio metodo di gestione del multithreading
    char buffer[MAX_BUFFER]; //Non ho sinceramente idea di che valore devo dare a buffer, quindi dò 50 000 e sembra funzionare... Con 5000 ogni tanto dà SIGSEGV
    size_t received; //La variabile per i bytes ricevuti
    client[id].socket.setBlocking(true); // <- si spiega da sola
    while(status == Socket::Done || status == Socket::NotReady) //Finchè non ci sono errori (NotReady non lo considero un errore)
    {
        /*RECEIVE PHASE******************************/
        memset(buffer, '\0', MAX_BUFFER);
        status = client[id].socket.receive(buffer, MAX_BUFFER, received); //Ricezione socket SFML con assegnazione dello status alla variabile status
        if (status == Socket::Done || status == Socket::NotReady) //Se non ci sono errori
        {
            if (!client[id].input.readable && status == Socket::Done && buffer[0] != '\0') //Se il buffer non è vuoto e posso leggere l'input dal file (anche altri thread leggono questi dati, quindi si alternano con la variabile "readable")
            {
                inputString = buffer;
                fragmentString(inputString, "|", data); //E' simile a un explode, l'ho fatta io, frammenta la stringa in un vettore di stringhe, spezzandola con la stringa (e quindi non un solo carattere) specificata, in questo caso "|" (una sola barra)
                if(!(data[0].empty()) && !(data[1].empty()) && !(data[2].empty()) && !(data[3].empty()) && !(data[4].empty())) //Controllo per evitare SIGSEGV in caso di ricezione non completa o corretta della stringa
                {
			//Elaborazione dati...
                }
            }

            /*SEND PHASE******************************/


            if(!serverStatus.empty()) //Se c'è almeno uno snapshot da leggere
            {
                outputString.clear();
                sStatus = serverStatus.begin(); //Prendo lo snapshot più recente
                while() //Questo while elabora tutti i dati dello snapshot e li carica in una stringa con una struttura precisa separata da "|" (che verrà poi frammentata dal client usando la stringa "|"
                {
			//Elaborazione
                }
            }

            memset(buffer, '\0', MAX_BUFFER);
            if (outputString.size() < MAX_BUFFER - 100) //Giusto per assicurarmi che il buffer non vada in overflow
            strcpy(buffer, outputString.c_str());
            else
                cout <<"String too big"<<endl;
            status = client[id].socket.send(buffer, MAX_BUFFER); //Invio
        }
    }
    if (status != Socket::Done && status != Socket::NotReady) //Se ci sono stati errori, tutti i cicli terminano e la funzione giunge alla fine, terminando il thread.
    {
        cout <<"Client disconnected: " <<id <<"." <<endl;
        client[id].clean();
    }
}



//CLIENT

//La struttura è molto simile:
void communicationThread()
{
    Socket::Status status = Socket::Done;
    string data[50];
    int i = 0;
    char buffer[MAX_BUFFER];
    size_t received;
    stringstream ss;
    string outputString;
    bool connected = true;
    client.socket.setBlocking(true);

    while(status == Socket::Done || status == Socket::NotReady)
    {

        /*SEND PHASE******************************/

        // Loading outputString with data
        outputString.clear();
       //Elaborazione dati

        outputString = ss.str();
        //Sending
        memset(buffer, '\0', MAX_BUFFER);
        strcpy(buffer, outputString.c_str());

        sleep(milliseconds(3));  //Ecco un errore che non capisco perchè avviene: se non metto uno sleep in questa funzione (nel client) il client inizia a laggare come non mai, probabilmente per una desincronizzazione delle funzioni, fino a dare un SIGSEGV
            status = client.socket.send(buffer, MAX_BUFFER);
            
        /*RECEIVE PHASE******************************/
        if (status == Socket::Done)
        {
            //Receiving
            memset(buffer, '\0', MAX_BUFFER);
            i = 0;
                status = client.socket.receive(buffer, MAX_BUFFER, received);
            if (status == Socket::Done && buffer[0] != '\0' && ISReadable == false) //Se ho ricevuto senza errori
            {
                inputString = buffer;
                ISReadable = true; //Il main può leggere ed elaborare la stringa
            }
        }
    }
    if (status != Socket::Done && status != Socket::NotReady) //Uguale al server
    {
        cout <<"Disconnected." <<endl;
        connected = false; //Variabile per il client
    }
}


Dal momento che era solo un prototipo mi perdono per non essermi preoccupato di rendere gli attributi privati, e di altri eventuali errori di stile classico. Se ne notate altri, sarebbe carino farmeli notare ora così posso scrivere del bel codice.
Ovviamente se capisco davvero bene come funziona l'i/o asincrono posso permettermi di fare anche più client per ogni thread, tipo 500 a thread, in modo da bilanciare bene il carico tra di essi e rendere il multithreading efficiente anche da parte della computazione.

Ah, il server deve essere strutturato per mantenere tanti client, da 0 a... Finchè il pc li regge, anche migliaia. Non so se questa cosa comporta differenze nella mia funzione.
(Che, a proposito, poi diventerà un metodo della classe "Server"... Credo sia un metodo stantard nel C++)

Grazie dell'eventuale supporto <3

3 Risposte

  • Re: Gestione socket per server

    La domanda qual é?
  • Re: Gestione socket per server

    Come scrivo una funzione/metodo descritta sopra in maniera corretta e crash_proof? Come funziona l'i/o asincrono e come mando messaggi in modo sicuro (non intendo ssl ecc) con il tcp?
    Ho cercato entrambi su google ma l'unica cosa che ho trovato è select/poll ma non sono sicuro siano ciò che sto cercando... E tra l'altro non li saprei usare con le SFML e il loro socket.
  • Re: Gestione socket per server

    Ok, ho provato a trasformare la mia funzione in "non blocking":
    
    //SERVER
    void communicationThread(int id) 
    {
        Socket::Status status = Socket::Done;
        string inputString, outputString;
        string data[50];
        stringstream ss;
        list<ServerStatus>::iterator sStatus; //Server status
        list<Ship>::iterator shipsItr;
        char buffer[MAX_BUFFER];
        char partialBuffer[MAX_BUFFER];
        size_t received;
        size_t sent;
        int i = 0;
        client[id].socket.setBlocking(false); //Ora non è blocking
        while(status == Socket::Done || status == Socket::NotReady || status == Socket::Partial)
        {
            /*RECEIVE PHASE******************************/
            memset(buffer, '\0', MAX_BUFFER);
            i = 0;
            do
            {
                status = client[id].socket.receive(buffer, MAX_BUFFER, received); //Finchè il socket a cui sto inviando non è pronto provo a inviarlo, per 10 volte.
                i++;
            }
            while (status == Socket::NotReady && i < 10);
    
            while (status == Socket::Partial) //Questo lo spiego dopo
            {
                status = client[id].socket.receive(partialBuffer, MAX_BUFFER, received);
                strcat(buffer, partialBuffer);
            }
                //Elaborazione input
                /*SEND PHASE******************************/
    
                if(!serverStatus.empty()) //Uguale alla vecchia funzione
                {
                    outputString.clear();
    
                    sStatus = serverStatus.begin();
                    sStatus->us++; //Incremento la variabile atomica intera per la sincronizzazione dei thread
                    //***********************
    	//Elaborazione dati
                    sStatus->us--; //Ora la sottraggo di 1 (quando è 0 il game può rimuovere gli snapshot in eccesso)
                    			//Questo sistema è più efficente del precedente "push_back" e "pop_back" di prima. 
                }
    
                memset(buffer, '\0', MAX_BUFFER);
                if (outputString.size() < MAX_BUFFER - 100)
                    strcpy(buffer, outputString.c_str());
                else
                    cout <<"String is too big"<<endl;
    
                i = 0;
                do
                {
                    status = client[id].socket.send(buffer, MAX_BUFFER, sent); 
                    i++;
                }
                while (status == Socket::NotReady && i < 10); //Stessa cosa della ricezione, 10 tentativi finchè il socket non è pronto a ricevere
                while(status == Socket::Partial) //Anche questo lo spiego dopo
                {
                    cout <<"Partial" <<endl;
                    memmove(buffer, buffer + sent, sizeof(buffer));
                    status = client[id].socket.send(buffer, MAX_BUFFER, sent);
                }
        }
        if (status != Socket::Done && status != Socket::NotReady)
        {
            cout <<"Client disconnected: " <<id <<"." <<endl;
            client[id].clean();
        }
    }
    
    //Il client è uguale a prima, ma non blocking, con lo stesso principio sopra citato.
    
    

    Con mia sorpresa ho scoperto che con i socket delle SFML non è necessario, nel tcp, inviare il numero di byte della stringa precedentemente all'invio della stringa stessa. I socket restituiscono "Done" quando la stringa è arrivata esattamente come nel "send", e restituiscono "Partial" quando si è ricevuta o inviata solo una parte della stringa. Nella funzione gestisco questo sistema così:
    
    //SEND
                while(status == Socket::Partial)
                {
                    cout <<"Partial" <<endl;
                    memmove(buffer, buffer + sent, sizeof(buffer)); //Un memmove rimuove la parte già mandata al client 
                    status = client[id].socket.send(buffer, MAX_BUFFER, sent);
                }
    
    //RECEIVE
            while (status == Socket::Partial) 
            {
                status = client[id].socket.receive(partialBuffer, MAX_BUFFER, received);
                strcat(buffer, partialBuffer); //Uno strcat aggiunge al buffer già ottenuto la nuova parte
            }
    
    
    Questa funzione ora sembra che vada un pò meglio dal punto di vista della differenza di velocità di esecuzione tra client/server, proprio per questo anche senza lo sleep nella funzione del client (vedi la funzione nel primo messaggio) riesce a collegarsi bene.
    Ma, dal punto di vista del risultato visibile, lagga tutto:
    Inanzitutto l'input arriva con +- 600 ms di ritardo, in locale questa cosa non dovrebbe succedere. Le stringhe tra l'altro contano massimo 40 caratteri.
    La ricezione, invece, viene fatta a singhiozzi: in un secondo di lavoro, ci sono momenti in cui l'output viene ricevuto liscio, svariate decine di volte ogni +- 100 ms, e altri momenti in cui c'è un blackout di ricezione in cui si ricevono soltanto "Not Ready". Non so come risolvere questo problema. Credo che il client e il server, a periodi, si sincronizzino e poi desincronizzino causando un i/o instabile.
    Per finire, ho il sospetto che questo sistema sia una sorta di "attacco DoS" a me stesso: senza le applicazioni attive, il ping in locale è 0 (fatto dal CMD), appena connetto i clients al server il ping schizza a circa 630 ms (0% packet loss). Un lag del genere non lo vedo neanche quando scarico a velocità massima, un videogame non dovrebbe mai provocare una cosa del genere.
    Se non fosse chiara, la domanda è: come risolvo i problemi sopra citati? Se la risoluzione è ovvia mi basta anche solo il "perché succede tutto questo?".
    Grazie in anticipo
Devi accedere o registrarti per scrivere nel forum
3 risposte