wxWidgets

Socket

Il progetto che si propone di sviluppare è suddiviso in tre parti:

  1. Una comunicazione tra un server e uno o più client, come descritto in questa pagina.
    In questo primo progetto il Server è in grado di inviare messaggi solo all'ultimo client da cui ha ricevuto un pacchetto.

  2. Una gestione di connessioni multiple da parte del server.
    In questo progetto il Server memorizza gli indirizzi dei client, quindi può scegliere a chi inviare un messaggio e può riconoscere il client che si disconnette.

  3. Un gioco in rete.

  4. In questo progetto si realizza il gioco della tombola: il server invia, su richiesta di un client, una cartella, poi comunica di volta in volta i numeri estratti ed aspetta di conoscere eventuali combinazioni vincenti.

La libreria wxWidgets non mette a disposizione un componente network, pertanto per utilizzare i socket bisogna scrivere i gestori di evento e impostare da programma tutte le proprietà della connessione.

Creare una cartella wxSocket nella quale verranno salvati i due progetti (oppure creare due sottocartelle denominate una server e l'altra client e memorizzare i due progetti separatametne).

Avviare wxDev-C++.
Creare un nuovo progetto basato su wxWidgets Frame e denominarlo Server, salvarlo nella cartella wxSocket o nella sottocartella Server, a seconda della scelta adottata.

Progetto dell'interfaccia utente

L'applicazione Server

Aprire la scheda del file ServerFrm.h. Osservare che nella sezione delle direttive al compilatore ci sono delle sezioni riservate, riconoscibili dai commenti che le delimitano, in cui non bisogna inserire righe. Al di fuori di queste sezioni, ad esempio, esattamente prima della dichiarazione della classe aggiungere le righe:


////Dialog Style End

#include <wx/socket.h>
typedef wxIPV4address indirizzoIP;

class ServerFrm : public wxFrame

Il file di intestazione socket.h consente di utilizzare le funzioni della libreria, mentre il nuovo tipo indirizzoIP specifica un nome alternativo per la classe wxIPV4address.

La classe wxIPV4address contiene le seguenti funzioni membro:

Consulta la classe wxIPV4address per conoscere le proprietà e i metodi.

Nella dichiarazione della classe aggiungere una sezione private e dichiarare il campo membro nClient di tipo int e due puntatori, uno ad un oggetto di classe wxSocketServer e l'altro ad un oggetto di classe wxSocketBase:


class ServerFrm : public wxFrame
{
    private:
        int nClient;
        wxSocketServer *server;
        wxSocketBase *client;
        
    private:
        DECLARE_EVENT_TABLE();

Gestione Eventi.

Quando un utente sceglie una voce di menu, preme un pulsante, ridimensiona una finestra ecc. viene generato un evento. Anche i dispositivi periferici possono generare eventi: la ricezione di un pacchetto dalla scheda di rete, il time out generato dal timer, ecc.

In ognuno di questi casi, viene creata un'istanza di classe wxCommandEvent che contiene un identificatore che serve a riconoscere chi ha generato l'evento (nel caso di un pulsante è wxID_OK) e il tipo di evento (per un pulsante è solo wxEVT_BUTTON). L'applicazione ricerca la finestra che contiene il gestore dell'evento, esaminando all'interno della tabella degli eventi se esiste una riga che contiene la corrispondenza EVT_BUTTON(wxID_OK, classeFrm::OnButtonClicked). Di conseguenza viene richiamato il gestore OnButtonClicked(), definito nel file di implementazione della classe derivata da wxFrame.

Riepilogando, per creare il gestore di un evento.

  1. nella dichiarazione della classe derivata da wxFrame, nella sezione privata, deve essere presente la riga:
    DECLARE_EVENT_TABLE();
    Questa dichiarazione viene inserita automaticamente al momento della creazione del progetto basato su wxWidgets, perchè ogni applicazione di questo tipo è orientata agli eventi.

  2. All'interno della dichiarazione della classe si deve specificare il prototipo del gestore dell'evento, ad esempio:
    void OnServerEvent(wxSocketEvent& event);.
    Il parametro event viene creato dal sistema operativo e contiene tutte le informazioni che descrivono completamente l'evento (ad esempio è stato ricevuto un pacchetto, è stata chiusa la connessione, da chi è stato ricevuto il pacchetto, ecc.)

  3. nella stessa dichiarazione di classe, tra le costanti del tipo enum, si devono definire gli identificatori degli elementi che possono generare eventi, ad esempio SOCKET_ID,

  4. Nel file di implementazione, all'interno della tabella degli eventi deve essere presente una riga che associa, al tipo di evento, l'identificatore con il gestore. Ad esempio:
    EVT_SOCKET(SOCKET_ID, ClientFrm::OnSocketEvent)

  5. Definire il gestore dell'evento.

  6. Dopo aver creato un'istanza del socket, bisogna assegnare l'ID all'istanza e indicare quale funzione richiamare quando il socket genera un evento. Questa associazione avviene con: SetEventHandler(&gestore, ID). Il gestore dell'evento verrà richiamato quando si verifica uno degli eventi impostati con SetNotify e abilitati con Notify.

SetNotify specifica quali eventi generati dal socket devono essere inviati al gestore. Il parametro della funzione è una OR di costanti predefinite:

Gli eventi sono abilitati con il metodo Notify(bool) se il parametro è true, o disabilitati se il parametro è false.

Le cinque operazioni da compiere, elencate per gestire gli eventi, vengono svolte nel modo indicato di seguito.

La prima operazione è stata aggiunta al momento della creazione del progetto.

(Seconda operazione). Nella dichiarazione della classe ServerFrm, che si trova nel file ServerFrm.h, raggiungere la sezione public e, dopo le dichiarazioni dei gestori di evento generati dalle voci di menu, aggiungere le righe:


    void Mnuascolta1004Click1(wxCommandEvent& event);
    void Mnuesci1006Click(wxCommandEvent& event);
    
    void OnServerEvent(wxSocketEvent& event);
    void OnSocketEvent(wxSocketEvent& event);

Queste due righe sono i prototipi dei due gestori di evento.

(Terza operazione). Scorrendo la dichiarazione della classe, si trova una sezione private nella quale viene dichiarato un tipo enum con alcune costanti già presenti tra i commenti:
////GUI Enum Control ID Start
e
////GUI Enum Control ID End.
Subito dopo questa sezione inserire le seguenti righe:


  enum
    {
      ////GUI Enum Control ID Start

      ////GUI Enum Control ID End
    
       SERVER_ID,
       SOCKET_ID,

       ID_DUMMY_VALUE_ //don't remove this value unless you have other enum values
       

(Quarta operazione). Aprire la scheda del file ServerFrm.cpp. Nella sezione delle direttive aggiungere le seguenti righe:


  BEGIN_EVENT_TABLE(ServerFrm,wxFrame)
  ////Manual Code Start
  EVT_SOCKET(SERVER_ID,  ServerFrm::OnServerEvent)
  EVT_SOCKET(SOCKET_ID,  ServerFrm::OnSocketEvent)
  ////Manual Code End

  EVT_CLOSE(ServerFrm::OnClose)
  EVT_MENU(ID_MNU_IMPOSTA_1003, ServerFrm::Mnuimposta1003Click1)

(Quinta operazione). Alla fine del file ServerFrm.cpp aggiungere le due definizioni dei gestori di evento (per adesso vuote):

  void ServerFrm::OnServerEvent(wxSocketEvent& event) {
    
  }

  void ServerFrm::OnSocketEvent(wxSocketEvent& event) {
    
  }

A questo punto l'applicazione può essere compilata per correggere eventuali errori.

(Sesta operazione). L'abilitazione degli eventi generati dalla ricezione di un pacchetto potrebbe avvenire nel costruttore, ma in questo esempio si decide di farlo tramite il comando di menu Imposta allo scopo di consentire all'utente di scegliere la porta di ascolto del socket.

Si vuole dare all'utente la possibilità di inserire l'indirizzo IP e la porta di ascolto del socket. Normalmente il server prende come indirizzo di loopback come default, ma in questo modo non sarebbe indirizzabile, quindi l'utente deve specificare l'indirizzo IP della macchina su cui esegue l'applicazione per consentire a computer client di comunicare con esso.

Aprire la scheda ServerFrm.wxform. Nella scheda delle proprietà aprire la cartella Dialogs e fare clic sul componente wxTextEntryDialog, poi fare clic in un punto qualsiasi del form (non ha importanza dove si colloca il componente perchè esso è nascosto e viene aperto come riquadro di dialogo solo quando verrà richiamato.

Per conoscere le proprietà e i metodi di questo componente consultare la pagina >class reference di wxWidgets.

Nella scheda delle proprietà del compionente wxTextEntryDialog sostituire il testo presente nella riga Caption con il seguente: Inserire indirizzo IP:numero porta.
Nella riga Value scrivere: 127.0.0.1:3000. Si può anche aggiungere un'ulteriore indicazione sul significato dei valori da inserire usando la proprietà Message

Cercare il gestore di evento associato al comando del menu e completarlo come segue:


void ServerFrm::Mnuimposta1003Click1(wxCommandEvent& event)
{
  // insert your code here
    wxString indIP =_("127.0.0.1"), Porta = _("3000");
    int scelta = WxTextEntryDialog1->ShowModal();
	
    if (scelta==wxID_OK) {
      wxString Parametri = WxTextEntryDialog1->GetValue();
      indIP = Parametri.BeforeFirst(':');
      Porta = Parametri.AfterFirst(':');
      WxStatusBar1->SetStatusText(_("\nServer pronto ") + indIP + _(":") + Porta, 0);
    }
}

Commenti al segmento di programma inserito nel gestore di evento.
L'istruzione:
int scelta = WxTextEntryDialog1->ShowModal();
richiama il metodo ShowModal del riquadro di dialogo di dialogo.
Un riquadro di dialogo Modale non consente di continuare a usare l'applicazione fintantochè restaaperto. Un riquadro di dialogo non modale consente di passare al documento dell'applicazione tenendo aperto anche il riquadro di dialogo (un esempio è il riquadro "Cerca" che resta aperto anche se si continua a lavorare con l'applicazione).
La funzione ShowModal restituisce il codice del pulsante premuto.

Se si è premuto il pulsante OK si acquisisce la stringa scritta nella casella di testo, poi tramite le due funzioni BeforeFirst e AfterFirst, della classe wxString, vengono separate le due stringhe che rappresentano l'indirizzo IP e la porta. Il carattere due punti viene utilizzato per individuare le due parti.

A questo punto nel gestore di evento si crea un'istanza del socket mettendola in ascolto sulla porta specificata:

    indirizzoIP indirizzo;
    indirizzo.Service(Porta);
    indirizzo.Hostname(indIP);
    server = new wxSocketServer(indirizzo);
    if (!server->Ok()) {
        WxMemo1->AppendText(_("\nErrore: porta occupata"));
        return;
    }
    

Il metodo Service(uInt) imposta il numero della porta su cui è collegato il socket. Il metodo Hostname() imposta l'indirizzo IP del computer nell'istanza del socket.

Il componente wxServerSocket eredita metodi e proprietà della classe wxSocketBase.
Il metodo IsOK() restituisce true se il socket è pronto.
Il metodo GetLocal() restituisce l'indirizzo locale del computer, nel quale sono contenuti l'indirizzo e la porta di ascolto..

Abilitando la gestione degli eventi il server passa in ascolto:

Nel gestore dell'evento associato alla voce di menu Ascolta aggiungere le seguenti righe:


    indirizzoIP indirizzo;
    WxMemo1->AppendText(_("\nServer in ascolto: ") + indirizzo.Hostname() + _(" ") + indirizzo.IPAddress());
    wxString msg;
    msg.Printf(_("%d Client connessi"), nClient);
    WxStatusBar1->SetStatusText(msg, 1);
    
    server->SetEventHandler(*this, SERVER_ID);
    server->SetNotify(wxSOCKET_CONNECTION_FLAG);
    server->Notify(true);

Aprire la scheda del file ServerFrm.cpp e osservare che il costruttore richiama la funzione CrateGuiControls. Scorrere il documento fino a raggiungere la fine di questa funzione ed inserire la seguente riga:


    ////GUI Items Creation End
    nClient=0;
    wxString msg;
    msg.Printf(_("%d client connessi"), nClient);
    WxStatusBar1->SetStatusText(msg, 1);
}

La prima istruzione assegna il valore 0 alla proprietà nClient della classe, la seconda riga crea una stringa. La terza riga scrive il valore della variabile intera nClient nella stringa, e vi accoda del testo. La quarta riga scrive la stringa nel campo 1 della barra di stato (la numerazione dei campi in cui è divisa la barra di stato inizia da 0).

Nel gestore di evento del comando Esci del menu File aggiungere le seguenti righe:

  Close(true);

Gestire gli eventi Call_Request

Completare il gestore dell'evento onServerEvent con le seguenti istruzioni:


void ServerFrm::OnServerEvent(wxSocketEvent& event) {
  switch(event.GetSocketEvent()) {
    case wxSOCKET_CONNECTION: {
      client = server->Accept(false);
      WxMemo2->AppendText(_("\nAccettata connessione da:"));

      wxString msg;
      msg.Printf(_("%d Client Connessi"), ++nClient);
      WxStatusBar1->SetStatusText(msg, 1);
   
      wxSocketBase *client1 = event.GetSocket();
      wxIPV4address ipClient;
      client1->GetPeer(ipClient);
      wxString ipV4Client = ipClient.IPAddress();
      WxMemo2->AppendText(ipV4Client + _("\n"));
      delete client1;
    }
  }
  client->SetEventHandler(*this, SOCKET_ID);
  client->SetNotify(wxSOCKET_INPUT_FLAG | wxSOCKET_LOST_FLAG);
  client->Notify(true);
}

In seguito all'impostazione fatta con setNotify, questo gestore viene richiamato quando si verifica un evento wxSOCKET_CONNECTION. Riceve come parametro il riferimento ad un oggetto di classe wxSocketEvent che contiene le informazioni che descrivono l'evento.

Il metodo Accept(bool) comunica al client che la richiesta di connessione è stata accettata, crea un oggetto di classe wxSocketBase che rappresenta il lato server della connessione e ne restituisce il riferimento. Se il parametro è true l'applicazione si blocca in attesa di ricevere la richiesta, se il parametro è false, per evitare l'attesa, la richiesta viene notificata tramite l'evento wxSOCKET_CONNECTION.

Il metodo GetSocket dell'oggetto event, restituisce un riferimento all'oggetto che ha generato l'evento.

Il metodo GetSocketEvent dell'oggetto event restituisce il tipo di evento.

Un oggetto di classe wxSocketBase, tra i suoi metodi, possiede i seguenti:

Le ultime tre righe abilitano il riconoscimento degli eventi "ricezione di un messaggio" e "chiusura della connessione" da parte del client generati su questo nuovo socket.

Completare il gestore degli eventi onSocketEvent:


void ServerFrm::OnSocketEvent(wxSocketEvent& event) {
    client = event.GetSocket();
    switch(event.GetSocketEvent()) {
        case wxSOCKET_INPUT: {
            char buf[100];
            client->Read(buf, sizeof(buf));
            wxIPV4address ipClient;
            client->GetPeer(ipClient);
            wxString ipV4Client = ipClient.IPAddress();
            WxMemo1->AppendText(_("\nMessaggio da: ") + ipV4Client + _("\n") + _(buf));
            wxString risposta = wxGetTextFromUser(
                "Rispondi al messaggio", "Test", "ciao"
            );
            client->Write(risposta, risposta.Len());
            break;
        }
        case wxSOCKET_LOST: {
            WxMemo1->AppendText(_("\nSocket eliminato"));
            client->Destroy();
            break;
        }
    }
}

Il gestore legge il messaggio contenuto nel pacchetto, legge l'indirizzo di provenienza, e prepara un pacchettodi risposta.

L'evento wxSOCKET_LOST corrisponde alla chiusura della connessione.

Per verificare il funzionamento del programma bisogna scrivere l'applicazione Client.

Chiudere il progetto.

L'applicazione Client

Creare uno nuovo progetto basato su wxWidgets Frame e denominarlo Client.

Aggiungere i seguenti componenti in modo da ottenere una disposizione simile a quella in figura e con le proprietà impostate con i valori presenti nella figura:

Aprire la scheda ClientFrm.h ed inserire le seguenti righe:


////Dialog Style End

#include <wx/socket.h>

class ClientFrm : public wxFrame
{

    private:
        wxSocketClient *client;
        wxSocketBase *socket;
        wxIPV4address *indirizzo;

Nella sezione public, subito dopo il distruttore, aggiungere il prototipo del gestore di evento onSocketEvent:


virtual ~ClientFrm();
		void OnSocketEvent(wxSocketEvent& event);

Nella sezione enum aggiungere la costante:


////GUI Enum Control ID End
  SOCKET_ID,

  ID_DUMMY_VALUE_ //don't remove this value unless you have other enum values
};

Aprire la scheda del file ClientFrm.cpp. Aggiungere la seguente riga nella sezione Manual Code Start:


////Event Table Start
BEGIN_EVENT_TABLE(ClientFrm,wxFrame)
    ////Manual Code Start
    EVT_SOCKET(SOCKET_ID, ClientFrm::OnSocketEvent)
    ////Manual Code End

    EVT_CLOSE(ClientFrm::OnClose)

Gestore dell'evento generato dal pulsante Connetti

Il gestore crea un'istanza di classe wxIPV4address e, tramite i metodi Hostname() e Service() la inizializza con le stringhe acquisite dalle caselle di testo contenenti l'indirizzo e la porta di ascolto del server:

    // insert your code here
    indirizzo = new wxIPV4address();
    wxString host = WxEdit1->GetValue();
    wxString porta = WxEdit2->GetValue();
    indirizzo->Hostname(host);
    indirizzo->Service(porta);

È opportuno verificare che l'indirizzo sia valido. Il controllo di correttezza viene lasciato come esercizio.

Il gestore crea un'istanza di classe wxSocketClient e abilita la gestione dei tre eventi Connesso, pacchetto entrante, chiusura connessione.

    client = new wxSocketClient();

    client->SetEventHandler(*this, SOCKET_ID);
    client->SetNotify(wxSOCKET_CONNECTION_FLAG | wxSOCKET_INPUT_FLAG | wxSOCKET_LOST_FLAG);
    client->Notify(true);

La connessione viene mostrata nella casella accanto al pulsante (che da questo momento dovrebbe essere disabilitato). La richiesta di connessione viene inviata al server con il metodo Connect:

    wxString msg;
    msg.Printf(_("Connesso a ")+host+_(":")+porta);
    WxStaticText3->SetLabel(msg);

    client->Connect(*indirizzo, false);

Il metodo Connect(indirizzo) invia una richiesta di connessione (vedi wxSocketClient Class Reference)

Il gestore di evento onSocketEvent

Il gestore riceve come parametro un oggett event. Lo esamina e decide quale operazione svolgere.

Il messaggio ricevuto viene trasferito dal pacchetto ad un array di caratteri buf.


void ClientFrm::OnSocketEvent(wxSocketEvent& event) {
    socket = event.GetSocket();
    char buf[100];

    switch(event.GetSocketEvent()) {
        case wxSOCKET_CONNECTION: {          
            wxString msg = WxEdit3->GetValue();
            socket->Write(msg, msg.Len());
            break;
        }
        case wxSOCKET_INPUT: {
            socket->Read(buf, sizeof(buf));
            WxMemo1->AppendText(_("\n") + _(buf));
            break;
        }
        case wxSOCKET_LOST: {
            socket->Destroy();
            break;
        }
    }
}

Il gestore dell'evento generato dal pulsante Invia

   wxString msg = WxEdit3->GetValue();
   client->Write(msg, msg.Len());

Mandare in esecuzione sia l'applicazione client che l'applicazione server. Impostare l'indirizzo e la porta di ascolto del server se il client risiede su una macchina diversa. Mettere il server in ascolto. Sul client impostare i parametri per connettersi al server e premere il pulsante Connetti. Inviare un messaggio dal client al server.

Se si eseguono due diverse istanze dell'applicazione client, si può notare che il server risponde solo all'ultimo client da cui ha ricevuto un messaggio.

Esercizi

Aggiornare la barra di stato per ogni nuovo client connesso o disconnesso.

Aggiungere un componente Choice che si aggiorna con l'elenco dei client connessi e disconnessi.

Aggiungere un menu Aiuto con le voci relative alle istruzioni per l'uso del programma, Note sull'autore e sulla versione, ecc.