Esempio Client/Server gestione Impianti e Schede in C++

Un esempio abbastanza complesso dal punto di vista didattico sull’utilizzo della programmazione client/server in C++, linguaggio sicuramente molto performante ma piuttosto complesso in questo genere di applicazioni per uno studente di scuola superiore.

Vogliamo simulare un server che gestisce un certo numero di impianti, i client, che possono collegarsi indicando il proprio nominativo attraverso una lettera alfabetica e possono simulare la produzione/invio di certe schede inviando un codice seriale. Il server immagazzina queste informazioni in opportune strutture dati.

Partiamo ovviamente da una libreria OOP in C++ che ci consente di semplificare la gestione dei socket e delle varie connessioni.

Server

Cominciamo col dichiarare le strutture dati necessarie. Ne creiamo una per gestire i dati degli impianti e una delle schede. Creiamo i vector che ne permetteranno la conservazione.

Veniamo al programma main. In questo caso fa operazioni molto semplici necessarie all’avvio del socket e al loop per la ricezione delle richieste client. In fase di collegamento di un ipotetico client non eseguimo ne un saluto ne altre operazioni particolari. Viene semplicemente richiamato il metodo answer che gestisce, in pratica, tutta la strategia risolutiva del problema.

#include "./socketLibrary.h"
#include <vector>
#include <iostream>
#include <string>

using namespace std;

struct Impianto
{
  char id_impianto;
  int qtaProdotta; 
  SocketConnection* client;    
};

struct Scheda
{
  string id;
  char id_impianto;
};


vector<Impianto> vettoreImpianti;
vector<Scheda> vettoreSchede; 


/**
 *  FUNZIONI NECESSARIE A FAR AGGIORNARE/INSERIRE/GETIRE
 *  IL VETTORE CONDIVISO
*/
void aggiungiImpianto(SocketConnection *client, string message)
{

  char nomeimpianto = message.substr(3,string::npos)[0];
  Impianto impianto;

  impianto.client = client;
  impianto.id_impianto = nomeimpianto;
  impianto.qtaProdotta = 0;

  vettoreImpianti.push_back(impianto);
  cout <<"Impianto "<< nomeimpianto <<" aggiunto"<<endl;

}

/**
 * Se il client ha già dichiarato il nome impianto
*/
Impianto* prendiImpianto(SocketConnection *client)
{
  for(auto &c: vettoreImpianti)
  { 
    if (c.client->socket == client->socket)
    {
      return &c;
    }
  }

  return nullptr;
}

/*
 * QUESTA E' OBBLIGATORIA PERCHE' CHIAMATA DAL POLLCLIENT DEL MAIN
*/
void answer(SocketConnection *client, string message)
{
  //FAI QUALCOSA DA TORNARE AI CLIENT
  //cout << message <<endl; //per debug per vedere che stringa arriva dal client e decidere come gestire eventuali caratteri speciali

  /**
   * IN QUESTO ESERCIZO, LA PRIMA CONNESSIONE DA AL SERVER UN CARATTERE OPER IDENTIFICARE
   * L'IMPIANTO CHE SI COLLEGA. TO DO: CONTROLLA SE GIA STATO INSERITO 
   */  
  if (message.substr(2,1) == "?")   // /u è andata a capo/invio
  {
      aggiungiImpianto(client, message);
      client->sendMessage("Benvenuto impianto. Aggiungi seriali schede (Termina con !) \n");
      return;
  }  

  /**
   * ADESSO ASPETTO CHE IL CLIENT MI MANDI LE SCHEDE CHE PRODUCE
   * IDEALMENTE MI MANDA UNA STRINGA DI TESTO PER SIMULARE IL SERIALE
  */
    Impianto* impianto = prendiImpianto(client);
    if (impianto != nullptr)
    { 
      Scheda s;
      s.id = message;
      s.id_impianto = impianto->id_impianto;  
      vettoreSchede.push_back(s);

      impianto->qtaProdotta++;

      cout << "Scheda aggiunta -> S/N " <<s.id_impianto <<"-"<< s.id<<endl;
      cout << "Prodotte " <<impianto->qtaProdotta << " schede" << endl;
    }
}

int main() 
{
  string welcome = "Magazzino Centrale\r\n";
  SocketServer socketServer;
  SocketConnection *client;
  
  // attiva sul server l'ascolto sulla porta 5500/tcp
  socketServer.init();


  //QUI FAI QUALCOSA DA AGGIUNGERE AL SOLO SERVER
  //E AI SUOI BUFFER CONDIVISI
  
  while (true)
  {
    // verifica la presenza di nuove connessioni in arrivo. Nel caso viene registrato in clien il numero di socket, -1 se
    // non vi è alcuna nuova connessione
    client = socketServer.pollSocketConnection();

    if (client->socket > 0) // si è attivata una nuova connessione
    {
      // mostra a video sul server i dettagli della nuova connessione

     cout << "Nuova connessione - socket " <<  client->socket 
                << " - IP "<< inet_ntoa(client->adr.sin_addr) 
                << " - porta "  << ntohs(client->adr.sin_port) <<endl;
            
      /**
       * QUI SI E' COLLEGATO UN CLIENT
       * E' il SERVER CHE GLI DA UN PESO SPECIFICO
       * QUI DO PER SCONTATO CHE SIA UN IMPIANTO
       * LO ASSOCIO AL CLIENT/SOCKET APPENA CREATO
       * 
      */

      //Impianto  impianto;
      //impianto.client = client;
      //impianto.push_bach(impianto);

      // Invia un messaggio di benvenuto iniziale alla connessione
      //if (!client->sendMessage("Benvenuto! Che impianto sei? Indica la tua"))   //to_string(impianto.id_impianto)
      //{
      //  perror("Send error!!");
      //  socketServer.closeSocket(client);
      //}      
    }
    else
    {
      delete(client);
    }
    // controlla tutti i client collegati per verificare la presenza di nuovi messaggi.
    // Nel caso ci siano nuovi messaggi viene eseguita la funzione answer (che riceve il client e il messaggio ricevuto 
    // come parametri)
    socketServer.pollClients(answer);
  }
  return 0;
}

Client

Il client può inviare come primo messaggio la lettera che lo identifica o da parametro in fase di esecuzione da console o attendere il messaggio di saluto del server. Qui abbiamo emulato di dover inserire un carattere jolly ? davanti alla lettera per consentire al server di identificare e distinguere diversi comandi e messaggi. Spesso in linux siamo abituati ad una sintassi comando parametro valore, qui simuliamo qualcosa di simile in fondo.

#include "./socketLibrary.h"

using namespace std;

int main(int argc, char *argv[])
{
    SocketClient* client = new SocketClient;
    client->setPort(5500);
    client->setServer("127.0.0.1");
    client->setBlocking(true);
    if(client->init()) 
    { 
        /**
         * QUI LE OPERAZIONI UNA TANTUM DA RIGA DI COMANDO/AVVIO    
         * */
        string word;
        if(argc > 1) 
        {
            word = string(argv[1]);
        } 
        else 
        { 
            cout << "Inserisci un carattere identificativo impianto preceduto da ?: ";
            cin >> word;
        }

        client->sendMessage("? "+ word);
        word = client->readMessage();
        cout << word;
        
        /**
         * QUI LE OPERAZIONI DA FARE CICLICAMENTE
        */
        //for(int i = 0;i<=5;i++){
        //    word = client->readMessage();
        //    cout << word;
        //    cin >> word;
        //    client->sendMessage(word);
        //}
        //word = client->readMessage();
        //cout << word;
        while(word != "!")
        {
            cin >> word;
            client->sendMessage(word);
        }

    }
}

La libreria per la gestione dei socket

I socket in C++ non sono una scelta molto agevole per lo studene di scuola superiore che potrebbe perdersi facilmente nei meandri del tanto codice necessario. Avere una libreria che semplifica le chiamate principali risulta per tanto fondamentale. Ne esistono diverse, anche in giro per la rete. Qui ne è proposta una molto didattica e commentata.

#include <iostream>
#include <unistd.h>    // comando read
#include <arpa/inet.h> // funzioni inet_ntoa, ntohs + socket
#include <sys/time.h>
#include <vector>
#include <fcntl.h>

using namespace std;

class SocketConnection;


class SocketServer{
  private:
    int master_sock;
    int addrlen;
    char buff[1025];
    struct sockaddr_in adr;
    int port = 5500;
    vector<SocketConnection *> clients;

  public:
    void setPort(int port);
    void init();
    SocketConnection *pollSocketConnection();
    void pollClients(void manageAnswer(SocketConnection *client, string msg));
    void closeSocket(SocketConnection *client);
    vector<SocketConnection *> getClients() { return clients; }
  };



class SocketConnection
{
public:
  struct sockaddr_in adr;
  int socket;
  SocketServer *socketServer;


  void setSocketServer(SocketServer *server);
  bool sendMessage(string message);
  string readMessage();
  void closeSocket();
};



class SocketClient
{
private:

    int master_sock;
    int addrlen;
    char buff[1025];
    struct sockaddr_in adr;
    int port = 5500;
    string server = "127.0.0.1";
    bool blocking = true;

public:
    void setBlocking(bool blocking);
    void setPort(int port); // imposta la porta di collegamento (default 5500)
    void setServer(string address); // imposta il serve cui collegarsi (default 127.0.0.1)
    bool init(); // attiva il collegamento con il server
    string readMessage(); // verifica se sono arrivati messaggi ("" = nessun messaggio)
    bool sendMessage(string message); // invia un messaggio
    void closeSocket(); // chiude la connessione
};

#include "./socketLibrary.cpp"
#include <iostream>
#include <unistd.h>    // comando read
#include <arpa/inet.h> // funzioni inet_ntoa, ntohs + socket
#include <sys/time.h>
#include <vector>
#include <fcntl.h>

void SocketServer::setPort(int port)
{
  this->port = port;
}


/**
 * @brief Attiva il server sulla porta indicata in port e rimane in ascolto.
 *        Il numero di socket è memorizzato in master_sock
 *
 */
void SocketServer::init()
{
  // la funzione socket attiva un nuovo socket e ne restituisce il numero
  // AF_INET indica un socket su IPV4
  // SOCK_STREAM indica un socket TCP, SOCK_DGRAM un socket UDP
  if ((master_sock = socket(AF_INET, SOCK_STREAM, 0)) == 0) // creating a master socket
  {
    perror("Failed_Socket");
    exit(EXIT_FAILURE);
  }


  /*
  adr contiene i parametri dell'indirizzo ip di ascolto. Se si usa INADDR_ANY il server sarà in ascolto
  su tutte le porte IPV4. Per attivare l'ascolto solo su localhost, usare inet_addr("127.0.0.1").
  */

  adr.sin_family = AF_INET;
  adr.sin_addr.s_addr = INADDR_ANY;
  adr.sin_port = htons(port);

  /*
   bind collega il socket all'indirizzo adr. Restituisce -1 in caso di errore di collegamento
  */
  if (bind(master_sock, (struct sockaddr *)&adr, sizeof(adr)) < 0) // bind the socket to localhost port 5500
  {
    perror("Failed_Bind");
    exit(EXIT_FAILURE);
  }

  printf("Port having listener:  %d \n", port);

  /*
  listen pone il socket in modalità di ascolto sulla porta
  */
  if (listen(master_sock, 3) < 0) 
  // 3 è il massimo numero di connessioni client che possono rimanere in attesa
  {
    cerr << "Listen error" << endl;
    exit(EXIT_FAILURE);
  }


  cout << "Looking For SocketConnections " << master_sock << endl;
  /*
  fcntl imposta il socket in modalità non-blocking. Con questa modalità le successive istruzioni
  di accept e read non si bloccano in attesa di connessioni o messaggi ma restituiscono -1 se non vi sono
  input
  */
  fcntl(master_sock, F_SETFL, fcntl(master_sock, F_GETFL, 0) | O_NONBLOCK);
  cout << "Non blocking enabled\n";

}



/**
 * @brief Controlla se è presente una nuova connessione client e nel caso la aggiunge al vettore
 *        dei client
 *
 * @return SocketConnection*
 */
SocketConnection *SocketServer::pollSocketConnection()
{
  SocketConnection *newClient = new SocketConnection;
  /*
  accept verifica se è presente una nuova richiesta di connessione al server e nel caso memorizza
  in newClient->adr i parametri ip del client e in newClient->socket il numero di socket del client.
  Anche questo va messo in modalità non blocking con fcntl.
  Se non ci sono connessioni in arrivo accept restituisce -1
  */

  int addrlen = sizeof(newClient->adr);
  newClient->socket = accept(master_sock, (struct sockaddr *)&newClient->adr, (socklen_t *)&addrlen);
  if (newClient->socket > 0)
  {
    newClient->setSocketServer(this);
    fcntl(newClient->socket, F_SETFL, fcntl(newClient->socket, F_GETFL, 0) | O_NONBLOCK);
    cout << "Non blocking enabled on socket " << newClient->socket << endl;
    clients.push_back(newClient);
  }

  return newClient;
}


/**
 * @brief Esegue un readMessage su ogni client connesso e, nel caso ci siano messaggi,
 *        richiama la funzione manageAnswer per elaborarli. La funzione può essere definita
 *        esternamente alla classe
 *
 * @param manageAnswer funzione che elabora il messaggio ricevuto
 */
void SocketServer::pollClients(void manageAnswer(SocketConnection *client, string msg))
{
  string cmd = "";
  for (int i = 0; i < clients.size(); i++)
  {
    SocketConnection *client = clients.at(i);
    cmd = client->readMessage();
    if (cmd.length() > 0)
    {
      manageAnswer(client, cmd);
    }

    if (cmd == "end")
    {
      close(client->socket);
      clients.erase(clients.begin() + i);
    }

    cmd = "";
  }

}


void SocketServer::closeSocket(SocketConnection *client)
{
  client->closeSocket();
  for (int i = 0; i < clients.size(); i++)
  {
    if (clients[i]->socket == client->socket)
    {
      clients.erase(clients.begin() + i);
      break;
    }
  }
}


void SocketConnection::setSocketServer(SocketServer *server) { socketServer = server; }

bool SocketConnection::sendMessage(string message)
{
  return write(socket, message.c_str(), message.length()) == message.length();
}

/**
 * @brief Usato da pollClients. Controlla la ricezione di messaggi da un socket client e
 *        nel caso li restituisce come string
 *
 * @return string
 */

string SocketConnection::readMessage()
{
  char buffer[1024];
  int reader = recv(socket, buffer, 1024, 0);
  if (reader > 0) // messaggio ricevuto
  {
    buffer[reader] = '\0';
    string message = string(buffer);
    int pos;

    /* Rimuovo eventuali \n dalla stringa    */
    for (int i = 0; i < message.length(); i++)
    {
      if (message[i] == '\r' || message[i] == '\n')
      {
        message.erase(message.begin() + i);
        i--;
      }
    }

    return message;
  }
  else if (reader == -1) // Nessun messaggio ricevuto
  {
    return "";
  }
  else // Probabile errore di ricezione
  {
    return "end";
  }
}

void SocketConnection::closeSocket()
{
  close(this->socket);
}

void SocketClient::setPort(int port)
{
    this->port = port;
}

void SocketClient::setBlocking(bool blocking) {this->blocking = blocking;} 

void SocketClient::setServer(string address)
{
    this->server = address;
}

/**
 * @brief Attiva il server sulla porta indicata in port e rimane in ascolto.
 *        Il numero di socket è memorizzato in master_sock
 *
 */
bool SocketClient::init()
{
    // la funzione socket attiva un nuovo socket e ne restituisce il numero
    // AF_INET indica un socket su IPV4
    // SOCK_STREAM indica un socket TCP, SOCK_DGRAM un socket UDP
    if ((master_sock = socket(AF_INET, SOCK_STREAM, 0)) == 0) // creating a master socket
    {
        cerr << "Failed_Socket" << endl;
        return false;
    }

    /*
    adr contiene i parametri dell'indirizzo ip del server. Per collegarsi a localhost, 
    usare inet_addr("127.0.0.1").
    */
    adr.sin_family = AF_INET;
    adr.sin_addr.s_addr = inet_addr(server.c_str());
    adr.sin_port = htons(port);

    if (connect(master_sock, (struct sockaddr *)&adr, sizeof(adr)) < 0)
    {
        cerr << "Connection Failed : Can't establish a connection over this socket !" << endl;
        return false;
    }

    /*
    fcntl imposta il socket in modalità non-blocking. Con questa modalità le successive istruzioni
    di accept e read non si bloccano in attesa di connessioni o messaggi ma restituiscono -1 se non vi sono
    input
    */
    if(!this->blocking) 
    {
     fcntl(master_sock, F_SETFL, fcntl(master_sock, F_GETFL, 0) | O_NONBLOCK);
     cout << "Non blocking enabled\n";
    }

    return true;
}


/**
 * @brief Usato da pollClients. Controlla la ricezione di messaggi da un socket client e
 *        nel caso li restituisce come string
 *
 * @return string
 */
string SocketClient::readMessage()
{
    char buffer[1024];
    int reader = recv(master_sock, buffer, 1024, 0);
    if (reader > 0) // messaggio ricevuto
    {
        buffer[reader] = '\0';
        string message = string(buffer);
        return message;
    }
    else if (reader == -1) // Nessun messaggio ricevuto
    {
        return "";
    }
    else // Probabile errore di ricezione
    {
        return "end";
    }
}


void SocketClient::closeSocket()
{
    //close(master_sock);
    sendMessage("end");
}

bool SocketClient::sendMessage(string message)
{
    return write(master_sock, message.c_str(), message.length()) == message.length();
}

Ultima modifica 22 Marzo 2023