Programmare con C++.
By noreply@blogger.com (Ubuntu Software Libero)
Far comunicare due computer è abbastanza facile: esistono molti programmi progettati appositamente per questo scopo. Potremmo però avere bisogno di qualcosa che sia costruito specificatamente attorno alle nostre esigenze. Per fare un esempio, potrebbe essere utile un server che rimanga in attesa sul nostro Raspberry Pi e che, quando ci connettiamo ad esso, ci invii un’immagine scattata in tempo reale dalla webcam: un semplice sistema di sorveglianza remota. Per realizzare il nostro server TCP utilizzeremo soltanto librerie standard C++, ma per semplificare la compilazione possiamo ricorrere a QtCreator, l’ambiente di sviluppo ufficiale delle librerie Qt. Il file del progetto, serverTCP.pro, è il seguente:
(QT += core
QT -= gui
TARGET = serverTCP
CONFIG += console
CONFIG -= app_bundle
TEMPLATE = app
HEADERS = base64.h
SOURCES += serverTCP.cpp
base64.cpp)
Specifichiamo che non ci servono le librerie grafiche, che stiamo realizzando un programma per console, e che il codice sorgente è contenuto nel file serverTCP.cpp. Aggiungiamo anche due file con le funzioni base64, una libreria che sfrutteremo per codificare i file. Ora possiamo cominciare a scrivere il nostro codice sorgente:
#include
#include
…
#include
#include “base64.h”
using namespace std;
Le prime righe del codice, come negli esempi che abbiamo scritto in Python, servono a includere le librerie. Le librerie necessarie sono tante, perché il programma è ovviamente più complesso di quelli che abbiamo visto finora.
int BUFFERSIZE = 4096;
void dostuff (int sock, sockaddr_in cli_addr, pid_t pid);
std::string decToDotIP(long num);
void handleSIGCHLD(int n);
void error(const char *msg)
{
perror(msg);
exit(1);
}
Definiamo tre funzioni che scriveremo tra poco, mentre ne definiamo una semplice immediatamente: serve solo a far apparire un messaggio di errore sullo schermo se qualcosa va storto.
int main(int argc, char *argv[])
{
La funzione principale di un programma C++ è main.
signal(SIGCHLD, handleSIGCHLD);
Il nostro server costruirà dei “figli”: praticamente, il programma si clona ogni volta che un nuovo client lo contatta. E ogni clone deve essere terminato quando il client interrompe la comunicazione: in quel momento il clone invia il segnale SIGCHLD, e dobbiamo assegnargli una funzione per gestirlo correttamente.
int sockfd, newsockfd, portno;
socklen_t clilen;
struct sockaddr_in serv_addr, cli_addr;
sockfd = socket(AF_INET, SOCK_STREAM, 0);
if (sockfd < 0) error("ERROR opening socket");
std::cout << "This is my server.n";
Una connessione TCP avviene tramite un socket, che deve essere aperto tramite la funzione socket(). Il socket che realizziamo è basato sulla famiglia di indirizzi IP AF_INET, ovvero gli indirizzi IPv4.
bzero((char *) &serv_addr, sizeof(serv_addr));
if (argc < 2) {
portno = 1612;
}
if (argc > 1) portno = atoi(argv[1]);
serv_addr.sin_family = AF_INET;
serv_addr.sin_addr.s_addr = INADDR_ANY;
serv_addr.sin_port = htons(portno);
Il socket deve poi essere configurato: il dato più importante è la porta di comunicazione TCP che vogliamo assegnare al nostro server: ne dobbiamo scegliere una non utilizzata da altri programmi, ad esempio la 1612. Possiamo lasciare che la porta venga specificata all’avvio del programma server come argomento del programma (e in tal caso sarà l’elemento 1 dell’array argv, come abbiamo visto in Python). E possiamo impostarne una predefinita nel caso non sia stata indicata come argomento. Visto che stiamo realizzando un server, questo programma deve rimanere in ascolto sul socket che abbiamo aperto: possiamo farlo con la funzione listen.
while (1) {
Cominciamo un ciclo infinito (il ciclo while continua finché la condizione espressa tra parentesi è uguale ad 1, ed 1 è sempre uguale ad 1), nel quale il server attenderà l’arrivo delle connessioni.
newsockfd = accept(sockfd, (struct sockaddr *) &cli_addr, &clilen);
if (newsockfd < 0) error("ERROR on accept");
Appena arriva una richiesta di connessione da parte di un client, la accettiamo.
pid_t pid = fork();
if (pid < 0) error("ERROR on fork");
Creiamo un fork, ovvero un clone del programma server da dedicare esclusivamente alla connessione appena accettata.
if (pid != 0) {
std::cout << "Opened new child process with pid " << pid << "." << std::endl;
dostuff(newsockfd, cli_addr, pid);
close(newsockfd);
}
}
Se la creazione del figlio clonato è andata a buon termine, dobbiamo ovviamente dire al clone che cosa deve fare per gestire correttamente la connessione che gli abbiamo assegnato: lo facciamo lanciando la funzione dostuff, che vedremo tra poco.
close(sockfd);
return 0;
}
Quando il ciclo while viene interrotto, perché è successo un imprevisto, possiamo chiudere il socket e terminare il server.
void handleSIGCHLD(int n)
{
int stat;
while(waitpid(-1, &stat, WNOHANG) > 0);
}
La funzione handleSIGCHLD, che abbiamo già menzionato, si occupa proprio di gestire questo segnale emesso dai figli: la funzione rimane in attesa finché il clone non è definitivamente terminato. Se non lo facessimo, il clone potrebbe diventare un processo “zombie”, che continua a utilizzare risorse del processore senza però essere più controllato dal processo padre.
void dostuff (int sock, sockaddr_in cli_addr, pid_t pid)
{
int n;
char buffer[BUFFERSIZE];
long empty = 0;
int found = 0;
Questa è la funzione che stabilisce il comportamento del server: tutto ciò che abbiamo visto finora sono le routine standard di un server, ma adesso scriveremo il codice che fa funzionare davvero il nostro server.
while (1) {
bzero(buffer,BUFFERSIZE);
n = read(sock,buffer,BUFFERSIZE-1);
Grazie ad un altro ciclo infinito leggiamo continuamente i messaggi inviati sul socket da parte del client. I messaggi vengono letti dalla funzione read, ed inseriti nella variabile buffer che è un array di caratteri inizializzato con una certa dimensione grazie alla funzione bzero.
if (n < 0) error("ERROR reading from socket");
std::string answ(buffer);
if (answ!=””) std::cout << "Message from " << decToDotIP(cli_addr.sin_addr.s_addr) << ": " << answ << std::endl;
Per comodità, trasformiamo il buffer in una stringa, che è molto più gestibile di un array di caratteri.
if (answ==””) empty++;
Se la risposta del client è vuota (quindi non c’è risposta), teniamo il conto sulla variabile empty, la quale incrementa il suo valore di una unità: empty++ è un modo conciso per dire empty+1.
if (empty > 2000000) {
kill(pid, SIGTERM);
std:cout << "Killed process " << pid << "." << std::endl;
return;
}
Se il client non sta rispondendo da molto tempo, terminiamo (inviando il segnale SIGTERM) il processo clone per evitare che la connessione con il client possa rimanere aperta: è il concetto di timeout.
if (answ.substr(0,5) == “HELLO”)
{
found++;
std::cout << "Sending welcome message." << std::endl;
n = write(sock,”Ìm ready to listen to your commands.”,38);
if (n < 0) std::cout << "ERROR writing to socket";
}
In questo momento la stringa answ contiene il completo messaggio inviato dal client al server, quindi possiamo cercare di interpretarla per capire che cosa vuole fare l’utente. Ad esempio, se le prime 5 lettere sono la parola HELLO, probabilmente l’utente vuole solo salutare il server per assicurarsi di essere davvero connesso. Abbiamo appena definito un comando del nostro server, il comando HELLO. Si può rispondere scrivendo sul socket una frase che faccia capire che il server è davvero pronto. La frase che scriviamo è lunga 38 caratteri, quindi possiamo indicare questo numero come terzo parametro della funzione write, che si occupa di scrivere sul socket.
if (answ.substr(0,6) == “WEBCAM”)
{
found++;
Ora definiamo un altro comando: WEBCAM. Questo comando deve far eseguire al server uno scatto tramite la propria webcam e poi inviare l’immagine attraverso il socket.
std::string encoded = “WEBCAM”;
Prepariamo la stringa da inviare come risposta al client: comincerà con la scritta WEBCAM, così il client saprà che questa risposta contiene l’immagine della webcam.
system(“streamer -s 176×144 -f jpeg -o /tmp/image.jpeg”);
È ovviamente necessario ottenere l’immagine della webcam: potremmo produrla con le librerie di Video4Linux, ma sarebbe complicato. Molto meglio ricorrere a un programma a parte, che può essere installato su un sistema Debian-like con il comando sudo apt-get install streamer. Streamer produrrà per noi uno scatto dalla webcam, della risoluzione di 176×144 pixel. L’immagine verrà salvata nel file /tmp/image.jpeg.
std::ifstream infile (“/tmp/image.jpeg”,std::ifstream::binary);
infile.seekg (0,infile.end);
long size = infile.tellg();
infile.seekg (0);
Possiamo quindi leggere il file immagine con la libreria ifstream. Il file è binario, naturalmente, perché è una immagine e non un semplice file di testo. Sfruttando la funzione seekg ci spostiamo all’ultimo byte dell’immagine, per calcolarne la dimensione e memorizzarla nella variabile size.
n = write(sock,encoded.c_str(),encoded.length());
encoded.clear();
if (n < 0) std::cout << "ERROR writing to socket";
}
Adesso possiamo scrivere la stringa encoded sul socket. La funzione write accetta soltanto array di caratteri, ma per fortuna è facile convertire una stringa in array di caratteri: basta usare la sua funzione c_str. Così, il client che ci aveva scritto “WEBCAM” ora riceve in risposta l’immagine della webcam in formato base64.
Il client, che dobbiamo sviluppare per fare coppia con il nostro server, è molto semplice. Il suo file di progetto, clientTCP.pro, è praticamente identico a quello del server, cambia solo il nome del file sorgente. Il suo codice sorgente, contenuto nel file clientTCP.cpp, comincia con l’inclusione delle stesse librerie che abbiamo utilizzato per il server. Poi procede:
int main(int argc, char *argv[])
{
int sockfd, portno, n;
struct sockaddr_in serv_addr;
struct hostent *server;
int BUFFERSIZE = 4096;//256;
char buffer[BUFFERSIZE];
La funzione principale (main) deve definire il buffer, l’array che utilizzeremo per i messaggi: deve avere la stessa dimensione che abbiamo indicato per il buffer del server.
if (argc < 3) {
portno = 1612;
server = gethostbyname(“127.0.0.1”);
}
printf(“This is the client for my server. Please type HELLO or WEBCAM. nn”);
if (argc > 2) {
portno = atoi(argv[2]);
server = gethostbyname(argv[1]);
}
Il server aveva bisogno di conoscere la porta TCP su cui lavorare. Il client ha bisogno anche dell’indirizzo IP del server da contattare (nel caso non si specificato tra gli argomenti del programma client, si da per scontato che sia localhost, ovvero 127.0.0.1).
sockfd = socket(AF_INET, SOCK_STREAM, 0);
if (connect(sockfd,(struct sockaddr *) &serv_addr,sizeof(serv_addr)) < 0)
error(“ERROR connecting”);
Specificati i vari parametri, che abbiamo già visto per il server, possiamo connetterci ad esso tramite la funzione connect. Se tutto va bene, otterremo un socket chiamato sockfd per la comunicazione.
n = write(sockfd,buffer,strlen(buffer));
if (n < 0)
error(“ERROR writing to socket”);
Possiamo scrivere il comando impartito dall’utente sul socket, e dunque inviarlo al server, grazie alla funzione write.
std::string received;
unsigned long long int bytes= 20*1024*1024;
received.resize(bytes);
Il server risponderà qualcosa: dobbiamo preparare una stringa per memorizzarlo. Ne impostiamo la dimensione massima a 20 MB.
int bytes_received = read(sockfd, &received[0], bytes-1);
if (bytes_received<0) {
std::cout << "Failed to read data from socket.n";
}
Utilizzando la funzione read possiamo leggere la risposta del server e memorizzarla nella stringa received. Alla funzione read non passiamo l’intera stringa, ma solo il puntatore al suo primo carattere (con il simbolo &). Questo significa che la funzione read comincerà a copiare la risposta del server in tutte le celle della RAM che il sistema operativo ha assegnato alla variabile received, a partire dalla prima.
std::cout << received.c_str() << "n";
Adesso possiamo far apparire sullo schermo il testo della risposta ricevuta dal server.
if (received.substr(0,6) == “WEBCAM”){
Quando per un qualsiasi motivo il ciclo infinito che permette l’invio dei comandi al server termina, significa che il client non ha più motivo di essere attivo e quindi possiamo chiudere il socket e terminare il programma client con l’istruzione return 0 (che restituisce il valore 0, indicante una corretta chiusura del programma). Server e client sono ora pronti: basta compilarle i due progetti con qmake, avviamo poi entrambe i programmi (prima il server, poi il client) per provarli.
Se vuoi sostenerci, puoi farlo acquistando qualsiasi cosa dai diversi link di affiliazione che abbiamo nel nostro sito o partendo da qui oppure alcune di queste distribuzioni GNU/Linux che sono disponibili sul nostro negozio online, quelle mancanti possono essere comunque richieste, e su cui trovi anche PC, NAS e il ns ServerOne. Se ti senti generoso, puoi anche donarmi solo 1€ o più se vuoi con PayPal e aiutarmi a continuare a pubblicare più contenuti come questo. Grazie!
Hai dubbi o problemi? Ti aiutiamo noi!
Se vuoi rimanere sempre aggiornato, iscriviti al nostro canale Telegram.Se vuoi ricevere supporto per qualsiasi dubbio o problema, iscriviti alla nostra community Facebook o gruppo Telegram.
Cosa ne pensi? Fateci sapere i vostri pensieri nei commenti qui sotto.
Ti piace quello che leggi? Per favore condividilo con gli altri.