Il progetto che si propone di sviluppare è suddiviso in tre parti:
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.
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.
Un gioco in rete.
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.
Aggiungere un wxBoxSizer e modificare la proprietà Orientation in wxVertical.
Aggiungere un componente wxMenu. Aggiungere un menu File contenente le voci &Imposta, &Attesa Connessione e, dopo aver inserito un separatore, la voce &Esci.
Per completare il menu come descritto seguire i seguenti passi:
Nella scheda Componenti aprire la cartella MenuBar, nell'elenco che si apre fare clic su wxMenuBar e poi clic in un punto qualsiasi del form.
Aprire la scheda proprietà del componente wxMenuBar, clic sulla riga Menu Items, poi fare clic sul pulsante con i 3 puntini accanto alla voce Edit MenuItems.
Si apre il riquadro di dialogo: Menu Item Editor. Premere il pulsante Add Item. Nella casella Caption scrivere l'intestazione che si vuole dare al menu: &File e premere il pulsante Apply.
Premere il pulsante Create Submenu e, nella proprietà Caption, scrivere &Imposta . Nella sezione nella proprietà Events dell'editor di menu, sulla riga onMenu premere il pulsante Create, accettare di salvare il file e poi accettare il nome proposto per il gestore dell'evento, quindi premere il pulsante Apply.
Premere nuovamente il pulsante Create Submenu, per restare sempre nell'ambito dello stesso menu File, e, nella proprietà Caption, scrivere &Ascolta. Nella sezione Events della scheda, sulla riga onMenu premere il pulsante Create, accettare di salvare il file e poi accettare il nome proposto per il gestore dell'evento quindi premere il pulsante Apply.
Premere il pulsante Create Submenu. Nella casella Type scegliere Separator e premere il pulsante Apply.
Premere il pulsante Create SubMenu e nella casella Caption scrivere: &Esci. Associare un gestore di evento, premere Apply e poi Ok.
L'aggiunta del menu Aiuto è lasciata come esercizio.
Aggiungere una Status Bar.
Aprire la scheda Componenti e nella cartella Common Controls fare clic su wxStatusBar e poi clic sul form.
Nella scheda proprietà della barra di stato osservare il valore riportato nella riga Larghezza. Siccome la barra di stato verrà divisa in due parti, una per i messaggi di stato dell'applicazione e l'altra per notificare il numero di connessioni, questo valore diviso per 2 costituirà la larghezza di ciascun campo in cui viene divisa la barra di stato.
Fare clic sulla riga Fields e poi fare clic sul pulsante con 3 puntini che compare accanto a Edit Fields. Nel riquadro di editor assegnare alla casella Width il valore 150, fare clic sul pulsante Add due volte per inserire 2 campi di uguale larghezza nella barra di stato.
Aggiungere un wxStaticText. Nella scheda delle proprietà modificare la Label in Messaggi ricevuti:.
Aggiungere un componente wxMemo e ridimensionarlo affinchè, in orizzontale, copra tutta l'area del frame e, in verticale, metà area.
Nella scheda proprietà del componente wxMemo fare clic sulla riga Strings e poi clic sul pulsante con i tre puntini accanto alla voce Edit Strings. Cancellare il testo di default e sostituirlo con: "Server:".
Aggiungere un wxStaticText. Nella scheda delle proprietà modificare la Label in Utenti Connessi:.
Aggiungere un componente wxMemo e ridimensionarlo affinchè, copra tutta la restante area del frame.
Nella scheda proprietà del componente wxMemo fare clic sulla riga Strings e poi clic sul pulsante con i tre puntini accanto alla voce Edit Strings. Cancellare il testo di default.
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:
Hostname() - imposta il nome o l'indirizzo IP (in notazione decimale) del computer destinatario. Se usato senza parametri restituisce il nome del computer.
IPAddress() - restituisce una stringa contenente l'indirizzo IP.
Service() - imposta la porta di ascolto, senza parametri restituisce la porta su cui il socket è in ascolto.
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();
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.
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.
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.)
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,
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)
Definire il gestore dell'evento.
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:
wxSOCKET_INPUT_FLAG: per ricevere wxSOCKET_INPUT.
wxSOCKET_OUTPUT_FLAG: per ricevere wxSOCKET_OUTPUT.
wxSOCKET_CONNECTION_FLAG: per ricevere wxSOCKET_CONNECTION.
wxSOCKET_LOST_FLAG: per ricevere wxSOCKET_LOST.
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);
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:
Close() - chiude la connessione, impedisce la trasmissione, disabilita gli eventi e rilascia le risorse.
Read(stringa, uint) - legge i dati contenuti nel pacchetto. Il primo parametro è la stringa destinazione dei dati contenuti nel pacchetto ricevuto e il secondo parametro è il numero di byte da leggere dal pacchetto.
Write(stringa, uint) - scrive i dati nel pacchetto.
GetPeer - riceve il riferimento ad un oggetto in cui scriverà l'indirizzo del mittente del pacchetto.
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.
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:
un wxBoxSizer con Orientamento verticale.
un wxStaticBoxSizer con proprietà Caption = "Parametri di Connessione"
All'interno del quale inserire un wxFlexGridSizer. Questo sizer, per default è formato da due righe e due colonne. In esso disporre i seguenti componenti:
un wxStaticText con proprietà Label = "Indirizzo del Server"
un wxEdit con proprietà Text = "127.0.0.1"
un wxStaticText con proprietà Label = "Porta di ascolto del Server"
un wxEdit con proprietà Text = "3000"
All'interno del primo sizer aggiungere un wxStaticBoxSizer con Caption="Messaggio da inviare"
All'interno di questo sizer Aggiungere un wxEdit con proprietà Text = "Ciao, mi senti?"
un wxButton con proprietà Label="Invia"
Nel sizer principale aggiungere un wxStaticText con Label="Messaggi ricevuti"
Aggiungere un componente wxMemo e ridimensionarlo per coprire la parte restante del frame.
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)
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 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; } } }
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.
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.