AngoLinux

L'esperienza di Quinta

- A cura del Prof. Stefano Salvi -


Sommario

  1. Introduzione
  2. L'esercizio proposto
  3. I Berklay Sockets
  4. La funzione select
  5. L'indirizzo di broadcast
  6. L'I file header necessari
  7. Le variabili globali, le costanti e i prototipi
  8. La funzione main
  9. La funzione rxsock
  10. La funzione txsock
  11. La funzioneGetPeer
  12. La funzione DoChat
  13. La funzione GetPeerName
  14. Il sorgente Completo

Introduzione

Il programma Abacus di quinta per sistemi di elaborazione e trasmissione dell'informazione prevede la trattazione delle reti geografiche e locali.

Per quanto riguarda le reti locali si introduce tra l'altro il datagram. Si tratta anche la Internet Protocol Suite.

Anche in questo caso si potrebbero fare scelte diverse per allestire un'esperienza su questi temi. Si può ad esempio utilizzare qualche libreria di funzioni di basso livello per sperimentare la trasmissione datagram oppure l'interfaccia winsock per l'Internet Protocol Suite.

Entrambe le soluzioni non sono ottimali, in quanto le varie librerie difficilmente sono documentate e Winsock richiede l'introduzione della programmazione sotto Windows, che introduce un notevole livello di complicazione.

Anche in questo caso, quindi, la soluzione più immediata è quella di utilizzare Linux come piattaforma.

L'esercizio proposto

L'esercizio proposto consiste nella creazione di un programma di comunicazione peer to peer tra due computer in rete.

Ogni computer può scegliere a chi inviare i suoi messaggi. Una volta scelto il corrispondente, tutti i messaggi inviati raggiungeranno questo corrispondente.

Se un altro corrispondente ci invierà un messaggio, il nostro corrispondente diventerà lui.

Noi potremo in ogni momento cambiare corrispondente. Per questo, dovremo avere una lista degli utenti attivi.

Per ottenere la lista, invieremo un pacchetto di broadcast richiedendo a tutti di presentarsi. Alla ricezione di questo pacchetto, ogni stazione sulla quale sia attivo il nostro programma risponderà con un pacchetto contenente il nome della stazione (non il nome dell'host, ma un nome dichiarato dall'utente all'avvio del programma) a chi ha inviato la richiesta. Noi potremo quindi ottenere una lista di corrispondenti disponibili, dalla quale scegliere il nostro corrispondente.

Visto il carattere sostanzialmente aperto della comunicazione si richiede di utilizzare una comunicazione di tipo datagram, usando quindi il protocollo UDP.

I Berklay Sockets

Precisiamo subito che noi ci occuperemo solo del protocollo UDP (User Datagram Protocol) di Internet Protocol Suite.

Per utilizzare i servizi di rete di Internet Protocol Suite, come per utilizzare i servizi di comunicazione di altri protocolli, è possibile usare, nei sistemi Unix come Linux, l'interfaccia Berklay Sockets.

Questa interfaccia software prevede l'utilizzo di un handle per riferirsi ad un flusso di informazioni, esattamente come si fà con i file (in effetti, molte delle funzioni usabili per i file possono essere usate anche con i socket). L'handle in questione (un intero che identifica univocamente l'oggetto) prende il nome di socket.

Dato che l'interfaccia socket è così flessibile, prima di utilizzare un socket è necessario, tramite la funzione socket dichiarare che protocollo vorremo usare.

Una volta ottenuto il socket, associato al protocollo che ci interessa, dovremo collegarlo ad un indirizzo IP e ad un port, quindi potremo eseguire le nostre operazioni, fino a quando abbandoneremo il socket, con la chiamata closesocket.

Per usare le funzioni relative ai socket dovremo sempre includere gli header file <sys/types.h> e <sys/socket.h>.

Se intendiamo usare gli indirizzi Internet, dovremo anche includere l'header <netinet/in.h>.

Passiamo quindi a vedere le funzioni che dovremo usare per gestire la nostra comunicazione:

int socket(int domain, int type, int protocol);
La funzione socket crea un socket associato ad un protocollo. Il valore di ritorno è l'handle del socket. Questo handle, appena creato, non è ancora utilizzabile ma è associato al protocollo. Vediamo i parametri:
int domain
Questo parametro indica il set di protocolli che si intendono utilizzare. Nel caso di Internet Protocol Suite dovremo utilizzare la costante AF_INET.
int type
Il tipo di protocollo è il protocollo all'interno della suite. Nel nostro caso, volendo utilizzare un protocollo UDP, dovremo indicare un tipo SOCK_DGRAM.
int protocol
Il protocollo è un protocollo specifico tra tutti i protocolli datagram di Internet. Nel nostro caso potremo utilizzare il valore 0
In caso di errore, la funzione ritorna -1 e lascia un codice di errore nella variabile di sistema errno.
int bind(int sockfd, struct sockaddr *my_addr, int addrlen);
La funzione bind associa un indirizzo IP ed un port ad un socket. Un socket senza un indirizzo IP ed un port associato non può nè spedire nè ricevere.
I parametri della funzione sono:
int sockfd
È l'handle del socket cui assegnare l'indirizzo.
struct sockaddr *my_addr
Contiene l'indirizzo cui collegare il socket. Dato che il socket è un'interfaccia valida per più protocolli, con schemi di indirizzamento diversi, struct sockaddr è una union che contiene gli indirizzi per ogni protocollo. Noi useremo quello di Internet.
int addrlen
Dato che il parametro my_addr può puntare ad indirizzi di tipo diverso, questo parametro indica la dimensione dell'effettivo indirizzo utilizzato.
Come al solito, la funzione ritorna -1 in caso di errore, lasciando il codice di errore nella variabile di sistema errno. In caso di successo, la funzione ritornerà invece 0.
struct sockaddr
Nella funzione bind, vista precedentemente viene utilizzata la struttura sockaddr, per indicare un indirizzo Internet. La struttura sockaddr in realtà è un contenitore vuoto, che contiene un campo unsigned short sa_family ed un buffer di 14 caratteri per un'eventuale indirizzo.
In realtà al posto del generico struct sockaddr si userà la struct specifico per il protocollo che si chiama, nel nostro caso struct sockaddr_in. Vediamone i campi:
short int sin_family
Indica il tipo di protocollo. Nel nostro caso dovrà per forza contenere la costante AF_INET essendo questo un indirizzo Internet.
unsigned short int sin_port
Questo campo dovrà contenere il port associato al socket.
Il port dovrà essere posto in network order, che è un formato numerico indipendente dal processore. Se non interessa il port a cui associare un socket, si può utilizzare la constante 0
struct in_addr sin_addr
Questa struttura conterrà l'indirizzo IP. In realtà la struttura contiene il solo campo unsigned long s_addr, che rappresenta un indirizo Internet a 32 bit, sempre in network order. Se si vuole indicare l'indirizzo di default dell'host su cui siamo, si può utilizzare la costante INADDR_ANY.
unsigned long int htonl(unsigned long int hostlong);
unsigned short int htons(unsigned short int hostshort);
unsigned long int ntohl(unsigned long int netlong);
unsigned short int ntohs(unsigned short int netshort);
Per usare queste funzioni occorre includere l'header file <netinet/in.h>. La loro funzione è quella di trasformare numeri a 16 bit (short int) o a 32 bit (long int) dal formato locale (dell'host - h) a quello della rete (net - n) o viceversa.
int setsockopt(int s, int level, int optname, const void *optval, int optlen);
Un socket appena creato ha delle opzioni di default. Vale a dire, per esempio, che non è abilitato a trasmettere pacchetti di broadcast. La nostra applicazione prevede di inviare pacchetti di broadcast, quindi dovremo modificare questa opzione per il socket di trasmissione. Vediamo i parametri della funzione per ottenere questo risultato:
int s
È ovviamente il socket del quale modificare le opzioni.
int level
Dato che il socket si riferisce al protocollo in cima ad una pila di protocolli, level indica il livello nella pila a cui deve venire inviato il comando. Nel caso del broadcast, si riferisce direttamente al socket, quindi indicheremo SOL_SOCKET.
int optname
Indica quale opzione modificare. L'opzione che controlla l'invio dei pacchetti di broadcast si chiama SO_BROADCAST
const void *optval
Questo è un puntatore al valore delle opzioni da settare. Dato che alcune opzioni potrebbero richiedere valori di tipo diverso come stringhe oppure strutture, il parametro è un puntatore a tipo indefinito (void).
Per la nostra opzione questo puntatore deve puntare ad un int, questo int conterrà un valore zero per disabilitare il broadcast oppure diverso da zero per abilitare il broadcast
int optlen
Dato che, come abbiamo visto, abbiamo un puntatore alle opzioni, che può anche puntare ad una stringa o ad un qualsiasi tipo di dato, per evitare errori è necessario indicare alla funzione la dimensione del parametro passato, cosa indispensabile se il parametro è una stringa. Nel nostro caso dovremo indicare sizeof (int).
Come al solito, la funzione ritorna -1 in caso di errore, lasciando il codice di errore nella variabile di sistema errno. In caso di successo, la funzione ritornerà invece 0.
int sendto(int s, const void *msg, int len, unsigned int flags, const struct sockaddr *to, int tolen);
Questa funzione consente di inviare un pacchetto UDP ad un particolare host il cui indirizzo sia indicato dal parametro to. Non può essere usata con socket di tipo TCP, in quanto essi devono prima essere connessi con l'ascoltatore, quindi non posso indicare il destinatario all'invio del pacchetto.
Vediamo ora i parametri:
int s
È ovviamente il socket del quale modificare le opzioni.
const void *msg
È un puntatore ai dati da trasmettere.
int len
Indica la dimensione dei dati da trasmettere. Bisogna ricordare che in UDP ogni trasmissione deve risolversi in un solo pacchetto di rete, quindi sarà nostra preoccupazione passare alla sendto blocchi di dati che non superino circa 1500 byte. Se il pacchetto è troppo grande, la funzione ci restituirà il valore EMSGSIZE nella globale errno dopo aver ritornato il valore -1.
unsigned int flags
Indica degli eventuali opzioni relative al pacchetto. Non ce ne sono che ci interessino.
const struct sockaddr *to
Questo puntatore punterà ad una struttura sockaddr_in nel quale inseriremo l'indirizzo del destinatario o, se necessario, l'indirizzo di broadcast della rete.
int tolen
Dato che i record di descrizione dell'indirizzo possono avere dimensione diversa, come abbiamo visto più sopra, è necessario indicare la dimensione del record di indirizzo utilizzato.
Come al solito, la funzione ritorna -1 in caso di errore, lasciando il codice di errore nella variabile di sistema errno. In caso di successo, la funzione ritornerà invece il numero di byte spediti.
int recvfrom(int s, void *buf, int len, unsigned int flags, struct sockaddr *from, int *fromlen);
Questa funzione consente di ricevere i dati. Dato che in UDP è possibile ricevere dati da qualunque corrispondente, questa funzione ha un parametro aggiuntivo nel quale può ritornarci l'indirizzo del mittente del pacchetto che stiamo ricevendo. Vediamo ora i parametri:
int s
È ovviamente il socket del quale modificare le opzioni.
void *buf
È un puntatore al buffer nel quale saranno posti i dati ricevuti.
int len
Indica la dimensione del buffer disponibile. Bisogna ricordare che in UDP se il pacchetto ricevuto eccede il buffer, i dati in eccesso vengono perduti.
struct sockaddr *from
Questo puntatore punterà ad una struttura sockaddr_in nella quale verrà restituito l'indirizzo del mittente.
int fromlen
Dato che i record di descrizione dell'indirizzo possono avere dimensione diversa, come abbiamo visto più sopra, è necessario indicare la dimensione del record di indirizzo utilizzato.
Come al solito, la funzione ritorna -1 in caso di errore, lasciando il codice di errore nella variabile di sistema errno. In caso di successo, la funzione ritornerà invece il numero di byte ricevuti. Dato che, come abbiamo visto, se i dati ricevuti eccedono il buffer, i dati in eccesso vengono persi, occorrerà controllare il parametro per garantirsi che tutti i dati siano stati ricevuti.

La funzione select

Nel nostro programma dovremo controllare continuamente due sorgenti di ingresso, vale a dire il socket dal quale riceviamo i messaggi remoti e lo standard input dal quale riceviamo i comandi ed i messaggi del nostro utente.

Per poter controllare continuamente più di un input esistono vari sistemi, alcuni più semplici, altri più complessi.

Un sistema potrebbe essere quello di utilizzare la comunicazione tra processi per convogliare su di un unico processo i due flussi di ingresso. Ad esempio si potrebbe utilizzare il metodo del message posting oppure quello del message passing per inviare ad un processo i suoi input. Questo sistema prevede la creazione di tre processi, nel nostro caso: uno per lo standard input, uno per il socket ed uno che faccia il lavoro vero e proprio. In più occorre creare una mailbox, se si vuole utilizzare il message posting.

In alternativa, il metodo più semplice potrebbe essere quello di configurare lo standard input ed il socket come non bloccanti, quindi interrogarli a turno, con il metodo del polling. Questo metodo, proceduralmente semplice, occupa in mainera inutile le risorse del sistema. La maggior parte del tempo macchina è perso ad interrogare flussi che non hanno dati.

Il metodo più corretto di risolvere il problema è quello di usare la funzione select del sistema operativo. Questa funzione consente di gestire una serie di file o di socket per la lettura, la scrittura o gli errori ed inoltre consente di ritornare il controllo dopo un certo tempo, per gestire delle condizioni di errore di mancanza di ingresso.

Questa funzione è in grado di controllare solamente degli handle come i socket oppure i file unbuffered (quelli creati con la chiamata open).

Se a noi invece interessa controllare lo standard input o lo standard output, dobbiamo ottenerne l'handle con la funzione fileno.

La funzione controlla gli handle indicati in un gruppo di handle, descritto tramite variabili di tipo fd_set. Queste variabili vengono gestite con quattro macro:

FD_ZERO(fd_set *set);
Serve per inizializzare il set set, rendendolo un set vuoto.
FD_SET(int fd, fd_set *set);
Aggiunge al set set il file handle fd.
FD_CLR(int fd, fd_set *set);
Elimina dal set set il file handle fd.
FD_ISSET(int fd, fd_set *set);
Controlla se il file handle fd è contenuto nel set set.

Noi non dobbiamo preoccuparci della struttura delle variabili di tipo fd_set, in quanto utilizzeremo sempre e solo le macro per gestirle.

La funzione select ha il seguente protopipo:

int select(int n, fd_set *readfds, fd_set *writefds, fd_set *exceptfds, struct timeval *timeout);

dove:

int n
è la dimensione dei set usati, sarà quindi sizeof(fd_set)
fd_set *readfds
è il set dei descrittori da controllare per vedere se vi giungono dati (se sono pronti per la lettura). Se questo parametro è NULL, nessun descrittore viene controllato per la lettura.
fd_set *writefds
è il set dei descrittori da controllare per vedere se sono pronti per la scrittura.Se questo parametro è NULL, nessun descrittore viene controllato per la scrittura.
fd_set *exceptfds
è il set dei descrittori da controllare per vedere se vi sono stati errori. Se questo parametro è NULL, nessun descrittore viene controllato per eventuali errori.
struct timeval *timeout
è la struttura che indica dopo quanto tempo la funzione ritornerà, nel caso non avvenga nulla sui descrittori. Questa struttura contiene due campi, int tv_sec per indicare i secondi da attendere e int tv_usec per indicare i microsecondi di attesa. Naturalmente non possiamo attenderci precisioni al microsecondo! Nel caso questo parametro sia NULL, la funzione ritornerà solo per eventi sui descrittori.

La funzione select ritornerà il numero dei descrittori che hanno "fatto qualcosa" tra quelli compresi nei tre set, che potrebbero anche essere zero, se la funzione è ritornata per lo scadere del tempo indicato in timeout. In caso di errore ritornerà -1 e la causa dell'errore sarà indicata nella variabile di sistema errno.

Per usare questa funzione, per prima cosa azzereremo i set che useremo, tramite la macro FD_ZERO, quindi inseriremo, tramite la macro FD_SET gli handle che vorremo controllare nei vari set, eventualmente utilizzando la macro fileno per ottenere gli handle relativi a variabili di tipo FILE*, quindi inizializzeremo l'eventuale timeout.

A questo punto potremo chiamare la funzione select e, se non avrà dato errore, potremo gestire il timeout, se non ci saranno descrittori selezionati, oppure controllare quali descrittori hanno "fatto il loro lavoro" verificando uno ad uno i descrittori precedentemente selezionati con FD_SET tramite la macro FD_ISSET.

Bisogna ricordarsi che la funzione select elimina dai set gli handle che non abbiano fatto niente, quindi l'inizializzazione dei set va ripetuta ad ogni ciclo.

La funzione select richiede l'inclusione degli header file <sys/time.h>, <sys/types.h> e <unistd.h>.

L'indirizzo di broadcast

Il nostro problema prevede l'invio di un pacchetto di broadcast sulla nostra rete, per conoscere l'indirizzo ed il nome di tutti i possibili corrispondenti.

Per fare questo è necessario (come abbiamo spiegato prima) usare la funzione setsockopt per abilitare il socket per la trasmissione dei pacchetti di broadcast.

Una volta abilitato il socket, dobbiamo conoscere l'indirizzo di broadcast della nostra rete, per potere inviare il pacchetto. Questa informazione non è affatto facile da reperire, ed il modo di reperirla dipende in parte dal sistema.

Per prima cosa, vediamo i passi da compiere, quindi vedremo le funzioni specifiche.

  1. Dovremo ottenere il nome del nostro host (della nostra macchina) tramite la funzione gethostname.
  2. Una volta ottenuto il nome, dovremo ottenere il suo IP principale, tramite il database dei nomi, con la funzione gethostbyname.
  3. A questo punto dovremo sapere a che interfaccia (che scheda di rete) corrisponde questo IP. Per fare questo dovremo, sempre tramite la funzione ioctl ed il nostro socket:
    1. Ottenere la lista delle interfacce.
    2. Per ogni interfaccia nella lista, chiedere l'indirizzo e confrontarlo con il nostro indirizzo principale.
    3. Se l'indirzzo di una delle interfacce corrisponde, abbiamo finito.
    In genere troveremo l'interfaccia eth0.
  4. Una volta ottenuto il nome dell'interfaccia (in genere eth0), potremo, sempre tramite una chiamata ad ioctl conoscere l'indirizzo di broadcast di quell'interfaccia.
La funzione gethostname è standard e funziona sempre. Per la funzione gethostbyname, anch'essa standard, è necessario che il nostro host sia elencato nella tabella /etc/hosts oppure sia registrato in un name server.

Il problema stà nella funzione ioctl. Questa funzione è il metodo standard per accedere ai dispositivi, ma i comandi per i dispositivi non sono affatto standard, quindi le ioctl usate in questo codice potrebbero non funzionare su un altro tipo di unix.

Veniamo ora alle singole funzioni che ci serviranno:

int gethostname(char *name, size_t len);
Questa funzione trova il nome dell'host sul quale sta funzionando il nostro programma. Il nome verrà posto nella stringa puntata da char *name.
Il secondo parametro (size_t len) indica la dimensione della variabile per il nome, per evitare di eccedere la dimensione della stringa. Questa funzione ritorna 0 se va tutto bene, -1 se avviene un errore, nel qual caso la variabile globale errno conterrà la causa dell'errore. Questa funzione richiede l'header <unistd.h>.
struct hostent *gethostbyname(const char *name);
Questa funzione interroga il network database per individuare i dati dell'host a partire dal nome. Il network database può essere un name server oppure il file /etc/hosts, a seconda di cosa è indicato nel file /etc/resolv.conf.
Naturalmente il parametro const char *name indicherà il nome dell'host da cercare. Il valore di ritorno sarà un puntatore ad un record di descrizione di host. Il record cui punta il valore di ritorno è un record statico riutilizzato da tutte le chiamate alla funzione, quindi occorrerà copiare al più presto i dati che ci servono, perché questo record potrebbe venir sovrascritto.
Se la funzione dà errore, ritorna il puntatore NULL e pone un codice di errore nella variabile globale extern int h_errno;. Per usare questa funzione occorre includere l'header <netdb.h>.
struct hostent
La struttura hostent descrive un host, indicandone tutti i nomi e gli IP. I suoi campi sono:
char *h_name; /* official name of host */
è il nome dell'host (praticamente quello che abbiamo passato alla funzione gethostbyname).
char **h_aliases; /* alias list */
è il puntatore ad un array di puntatori a stringa che puntano ad una serie di nomi alternativi per l'host. Tra questi nomi, oltre al nome ufficiale, potremo anche trovare nomi abbreviati o nomi estesi.
Ammettiamo che il nome ufficiale del nostro host sia ws1, tra i nomi dell'host, oltre a ws1, troveremo anche il nome competo ws1.itis.mn.it.
int h_addrtype; /* host address type */
indica il tipo di indirizzo. Dato che stiamo cercando indirizzi Internet, questo campo avrà valore AF_INET.
int h_length; /* length of address */
è la dimensione di ogni indirizzo di host. Se stiamo usando normali indirizzi IP, varrà 4, dato che un indirizzo IP è lungo quattro byte.
char **h_addr_list; /* list of addresses from name server */
questo campo punta ad un vettore di puntatori, ognuno dei quali punta ad un diverso indirizzo IP per l'host.
#define h_addr h_addr_list[0] /* address, for backward compatiblity */
questa macro corrisponde ad un puntatore al primo indirizzo IP dell'host, vale a dire l'indirizzo IP ufficiale dell'host.
int ioctl(int d, int request, ...)
La famosa funzione ioctl esegue la funzione indicata da int request sull'handle o sul socket indicato da int d. Gli eventuali altri argomenti sono quelli richiesti dalla particolare funzione invocata. La funzione ritorna 0 se ha funzionato, -1 in caso di errore. Se avviene un errore, nella globale errno ne troveremo la causa. Occorrerà anche includere l'header <sys/ioctl.h>. I comandi ed i parametri dipenderanno dal dispositivo cui si riferisce l'handle d. I valori usabili per int request insieme con un accenno ai parametri aggiuntivi si può trovare nella pagina di manuale ioctl_list.
int ioctl(int d, SIOCGIFCONF, struct ifconf *ifc);
Questa funzione è applicabile solo se int d è un socket.
Il parametro struct ifconf *ifc deve puntare ad una struttura che metteremo a disposizione. Questa struttura ha i seguenti campi:
int ifc_len; /* size of buffer */
è la dimensione in byte del buffer che metteremo a disposizione per le caratteristiche delle interfacce.
char *ifc_buf;
questo campo (in realtà è una macro) punta al buffer nel quale la chiamata inserirà i descrittori delle interfacce. Il buffer dovrà essere un array di caratteri, la cui dimensione sarà indicata nel campo int ifc_len.
struct ifreq *ifc_req;
questo campo (in realtà un'altra macro) è un secondo modo per accedere al buffer, utilizzando il tipo corretto per le strutture tornate.
Noi dovremo creare una struttura ifconf, predisporre un buffer di un migliaio di caratteri, mettere la dimensione del buffer nel campo ifc_len della struttura creata, l'indirizzo del buffer lo porremo in ifc_buf, quindi invocheremo la funzione ioctl, passandole l'indirizzo della nostra struttura come parametro. Al ritorno, se non ci saranno stati errori, il campo ifc_len della nostra struttura conterrà la dimensione dei dati contenuti nel nostro buffer ed il buffer conterrà un descrittore per ogni interfaccia, contenente il nome dell'interfaccia stessa. Per usare questa funzione dovremo includere l'header <net/if.h>.
int ioctl(int d, SIOCGIFADDR, struct ifreq *ifr);
int ioctl(int d, SIOCGIFBRDADDR, struct ifreq *ifr);
Queste funzioni sono applicabili solo se int d è un socket.
La funzione SIOCGIFADDR, dato il nome dell'interfaccia ritorna il suo indirizzo IP.
La funzione SIOCGIFBRDADDR, dato il nome dell'interfaccia ritorna il suo indirizzo di broadcast.
Esse funzionano nello stesso modo, utilizzando la struct ifreq sia per il passaggio del nome che per ritornare il dato. La struttura ha la seguente forma:
struct ifreq 
{
#define IFHWADDRLEN     6
#define IFNAMSIZ        16
        union
        {
                char    ifrn_name[IFNAMSIZ];            /* if name, e.g. "en0" */
        } ifr_ifrn;
        
        union {
                struct  sockaddr ifru_addr;
                struct  sockaddr ifru_dstaddr;
                struct  sockaddr ifru_broadaddr;
                struct  sockaddr ifru_netmask;
                struct  sockaddr ifru_hwaddr;
                short   ifru_flags;
                int     ifru_metric;
                int     ifru_mtu;
                struct  ifmap ifru_map;
                char    ifru_slave[IFNAMSIZ];   /* Just fits the size */
                caddr_t ifru_data;
        } ifr_ifru;
};
In pratica possiede due campi, uno per il nome dell'interfaccia, l'altro per il parametro di ritorno. Per accedere ai campi possiamo anche utilizzare le seguenti macro:
#define ifr_name        ifr_ifrn.ifrn_name      /* interface name       */
#define ifr_hwaddr      ifr_ifru.ifru_hwaddr    /* MAC address          */
#define ifr_addr        ifr_ifru.ifru_addr      /* address              */
#define ifr_dstaddr     ifr_ifru.ifru_dstaddr   /* other end of p-p lnk */
#define ifr_broadaddr   ifr_ifru.ifru_broadaddr /* broadcast address    */
#define ifr_netmask     ifr_ifru.ifru_netmask   /* interface net mask   */
#define ifr_flags       ifr_ifru.ifru_flags     /* flags                */
#define ifr_metric      ifr_ifru.ifru_metric    /* metric               */
#define ifr_mtu         ifr_ifru.ifru_mtu       /* mtu                  */
#define ifr_map         ifr_ifru.ifru_map       /* device map           */
#define ifr_slave       ifr_ifru.ifru_slave     /* slave device         */
#define ifr_data        ifr_ifru.ifru_data      /* for use by interface */
Nell'elenco delle interfacce che viene ritornato da SIOCGIFCONF ogni struct ifreq nel buffer ha già il nome dell'interfaccia. Noi potremo quindi, usando queste struct ifreq, chiamare SIOCGIFADDR e troveremo in ifr_addr l'indirizzo dell'interfaccia, oppure chiamare SIOCGIFBRDADDR e troveremo in ifr_broadaddr l'indirizzo di broadcast dell'interfaccia. Per usare questa funzione dovremo includere l'header <net/if.h>.

I file header necessari

Per prima cosa, vediamo i file da includere. In pratica sono tutti quelli precedentemente indicati, più i file standard <stdio.h> e <stdlib.h>, oltre a <string.h> per le operazioni sulle stringhe.


/* chat.c
 * Programma di chat
 */

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/time.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <netdb.h>
#include <fcntl.h>
#include <unistd.h>
#include <net/if.h>

Le variabili globali, le costanti e i prototipi

Per prima cosa troviamo i prototipi delle funzioni:

rxsock
crea il socket in ricezione, con il numero di prot indicato nella variabile chatport.
txsock
crea il socket per la trasmissione, con port assegnato dal sistema, abilitato al broadcast.
GetPeer
invia il pacchetto di broadcast per conoscere chi c'è in linea, quindi presenta un menù dei corrispondenti, attende la scelta dell'utente e ritorna in addr l'indirizzo del corrispondente.
GetPeerName
interroga e stampa il nome del corrispondente, a partire dal suo indirizzo.
DoChat
gestisce la chat, inviando e ricevendo messaggi e consentendo all'utente di cambiare corrispondente o di chiudere il programma.


int rxsock (void);
int txsock (void);
int GetPeer (int rx,int tx,struct sockaddr_in *addr);
void GetPeerName (int rx,int tx,struct sockaddr_in *addr);
void DoChat (int rx,int tx,struct sockaddr_in *addr);

Le costanti che seguono sono i codici che servono ad identificare i tre tipi di pacchetto utilizzati dal nostro protocollo.

I pacchetti MESSAGE sono i messaggi inviati al corrispondente o ricevuti dal corrispondente. Quando arriva un pacchetto MESSAGE da un qualunque host, questo viene eletto a nuovo corrispondente.

I pacchetti QUERY contengono il solo prefisso e vengono inviati in broadcast per sapere quali corrispondenti esistono sulla rete oppure in unicast per sapere il nome del corrispondente corrente. Ad un pacchetto QUERY si risponde sempre con un pacchetto NAME.

I pacchetti NAME contengono il nome che l'utente ha indicato all'avvio del programma chat.

La variabile MSGLEN indica la massima lunghezza per un messaggio. Verrà anche utilizzata per la lunghezza del nome.


#define	MESSAGE		'#'	/* prefisso di un messaggio (testo) */
#define	QUERY		'Q'	/* Prefisso di una richiesta di identificazione */
#define	NAME		'N'	/* Prefisso della comunicazione del nome della stazione */

#define	MSGLEN		80	/* Lunghezza massima di un messaggio */

Ci sono solo due variabili globali.

Una è il nostro nome, che verrà comunicato ai nostri corrispondenti.

L'altra è il numero di port associato alla chat, che può essere modificato in fase di avvio del programma.


char peername [MSGLEN];	/* Nome della stazione */
int chatport = 2000;	/* Port standard per il servizio */

La funzione main

La funzione main è dichiarata come al solito, con valore di ritorno intero e i due parametri argc, numero di argomenti ed argv, lista degli argomenti.


int main (int argc,char **argv)
{

Le variabili i e j sono variabili d'uso, per il loop dei parametri e per il numero di port.

Le variabili rx e tx sono i due handle dei socket rispettivamente per la ricezione e la trasmissione.

La variabile addr serve per immagazzinare l'indirizzo del corrispondente.


int i,j;
int rx,tx;
struct sockaddr_in addr;

A scopo di debug, il file stdout viene forzato a scrivere ogni riga, invece di riempire il buffer prima di scrivere sul video. Questo consente di visualizzare immediatamente gli eventuali messaggi di errore.


    /* forza la 'printf' a scrivere subito */
    setvbuf (stdout,(char *)0,_IOLBF,1024); 

Il nostro nome locale viene inizializzato vuoto.


    peername [0] = 0;	/* Nome inizialmente vuoto */

Adesso comincia il loop sugli argomenti della linea di comando, passati al programma, scartando argv[0], che è il nome del programma.


    for (i = 1;i < argc;i ++)	/* Loop sugli argomenti */
    {

Prendiamo in considerazione, tra gli argomenti, i parametri, vale a dire quelli che cominciano con un -.


        if (argv [i][0] == '-')	/* L'argomento e' un parametro */
        {

Il carattere che segue il - indica il tipo di parametro.


            switch (argv [i][1])
            {

Il parametro n (minuscolo) serve per indicare il nostro nome tramite la riga di comando. Se vogliamo usarlo dovremo scrivere:

chat -n <nome>

separando il parametro dal nome con uno spazio.


                case 'n' :	// Nome Stazione
                    strcpy (peername,argv [++i]);
                    break;

Il parametro p (sempre minuscolo) consente di indicare un port diverso da 2000 per la nostra chat. Il port deve essere maggiore di 1024, in quanto quelli al di sotto di tale valore sono riservati per il sistema.

Se il port non è maggiore di 1024 viene indicato un errore ed il port non viene cambiato.

Se vogliamo cambiare il port dovremo scrivere:

chat -p <port>

separando il parametro dal port con uno spazio. Ovviamente, se il port è corretto, il suo valore viene posto nella variabile globale chatport.


                case 'p' :	// Port per la chat
                    j = strtol (argv [++i],(char **) 0,0);
                    if (j < 1024)
                        printf ("Errore: il port deve essere maggiore di 1024\n");
                    else
                        chatport = j;
                    break;

Se si indica un parametro diverso dai due citati (gli unici validi), viene stampato un messaggio di errore ed il programma termina.


                default:
                    printf ("Errore: \n"
                        "parametri validi -n <nome> -p <port>\n");
                    exit (0);
                    break;
            }
        }

Se si introduce un argomento diverso da un parametro (che non cominci con -), viene stampato un messaggio di errore (non ci sono argomenti validi oltre i due parametri indicati) ed il programma viene terminato.


        else	/* Argomento non parametro (non -?) */
        {
            printf ("Errore: \n"
                "parametri validi -n <nome> -p <port>\n");
            exit (0);
        }
    }

Se il nome della stazione non è stato indicato tramite l'opportuno parametro, viene richiesto ora.


    if (!peername [0])	/* Se manca il nome della stazione... */
    {  /* Chiede il nome */

Prima di tutto viene stampato il prompt.


        printf ("Introduci il nome della macchina : ");

Quindi viene letto il nome. Si utilizza la funzione fgets al posto della più naturale gets in quanto la prima consente di controllare la lunghezza della stringa introdotta, evitando pericolosissimi superamenti dei limiti delle stringhe.


        fgets (peername,MSGLEN,stdin);

Per finire, la stringa viene terminata al primo blank o carattere di controllo.


        for (i = 0;i < MSGLEN;i ++)
           if (peername [i] < ' ')
              peername [i] = 0;
    }

Se ancora il nome non è stato introdotto, viene stampato un messaggio di errore ed il programma viene terminato, in quanto non posso accettare corrispondenti senza nome.


    if (!peername [0])	/* Il nome e' obbligatorio ... */
    {
        printf ("Nessun nome - termino\n");
        exit (0);
    }

A questo punto creo i socket per la trasmissione e la ricezione, utilizzando le apposite funzioni.


    rx = rxsock ();	/* Apre il socket di ricezione */
    tx = txsock ();	/* Apre il socket per la trasmissione */

Una volta aperti e configurati i socket, stampo il menù per la connessione.


    if (GetPeer (rx,tx,&addr))	/* Menu' degli altri utenti per scelta */
    {

Se la funzione GetPeer ha trovato un corrispondente, entro nella chat.


        DoChat (rx,tx,&addr);	/* Connessione */
    }       

Quando la chat è terminata, chiudo i due socket aperti e ritorno un codice di errore 0, ad indicare nessun errore.


    close (rx);	/* Chiude il socket di ricezione */
    close (tx); /* Chiude il socket di trasmissione */
    return 0;
}

La funzione rxsock

La funzione non ha parametri, in quanto il port per la ricezione è indicato nella variabile globale chatport. Se tutto va bene, ritorna l'handle del socket per la ricezione, se avvengono errori, termina il programma.


int rxsock (void)
{

Le due variabili locali sono il socket rx che verrà ritornato e la struttura sockaddr_in addr che verrà usata per associare un indirizzo locale al socket.


int rx;
struct sockaddr_in addr;

Viene creato un socket nello spazio Internet (AF_INET), con protocollo UDP (SOCK_DGRAM).


        rx = socket (AF_INET,SOCK_DGRAM,0);	// Socket in ricezione

Se non riusciamo a creare il socket, stampiamo l'errore con perror e poi terminiamo con exit, dando codice di errore 1.


        if (rx < 0)
        {
        	perror ("Creazione socket rx");
                exit (1);
        }

Dobbiamo adesso preparare l'indirizzo locale per il socket. Per prima cosa azzeriamo la struttura addr con la funzione memset. Assegniamo al campo sin_family il valore relativo ad Internet, vale a dire AF_INET. Facciamo scegliere al sistema l'indirizzo di dafault del server indicando nel campo sin_addr.s_addr il valore INADDR_ANY, che però deve essere posto in ordine di rete usando la funzione htonl. Per finire indichiamo il port nel campo sin_port usando la variabile globale chatport. Anche il port va nell'ordine di rete, quindi lo convertiremo con la funzione htons, dato che il port è un numero a 16 bit, quindi uno short int.


        memset (&addr,0,sizeof (addr));
        addr.sin_family = AF_INET;
        addr.sin_addr.s_addr = htonl (INADDR_ANY);
        addr.sin_port = htons (chatport);

Adesso che l'indirizzo è pronto, lo associamo al socket usando la funzione bind. Dato che questa funzione può essere usata per qualsiasi tipo di socket, l'indirizzo indicato è di tipo generico, quindi dobbiamo prendere l'indirizzo di memoria del nostro addr e fare un cast al tipo generico struct sockaddr * ed indicare nel successivo parametro la dimensione dell'indirizzo effettivo. Se la funzione bind ritorna un errore, segnalato da un valore di ritorno diverso da 0, stampiamo l'errore con perror e terminiamo il programma con un codice di errore 1.


        if (bind (rx,(struct sockaddr *) &addr,sizeof (addr)))
        {
        	perror ("Bind socket rx");
                exit (1);
        }

Se non c'è stato alcun errore, giungiamo in questo punto e possiamo ritornare al chiamante l'handle del socket creato.


        return rx;
}

La funzione txsock

La funzione non ha parametri, in quanto il port per la trasmissione non ci interessa e lo lasceremo scegliere al sistema. Se tutto va bene, ritorna l'handle del socket per la trasmissione, se avvengono errori, termina il programma. Questa funzione è molto simile alla precedente che crea il socket di ricezione.


int txsock (void)
{

Le tre variabili locali sono il socket tx che verrà ritornato, una variabile temporanea o che serve per indicare il valore dell'opzione nella funzione setsockopt e la struttura sockaddr_in addr che verrà usata per associare un indirizzo locale al socket.


int tx,o;
struct sockaddr_in addr;

Viene creato un socket nello spazio Internet (AF_INET), con protocollo UDP (SOCK_DGRAM).


   	tx = socket (AF_INET,SOCK_DGRAM,0);	// Socket in trasmissione

Se non riusciamo a creare il socket, stampiamo l'errore con perror e poi terminiamo con exit, dando codice di errore 1.


        if (tx < 0)
        {
        	perror ("Creazione socket tx");
                exit (1);
        }

A questo punto dobbiamo abilitare il socket alla trasmissione di pacchetti di broadcast. Per fare ciò, dovremo utilizzare la funzione setsockopt, indicando che setteremo un'opzione del socket (SOL_SOCKET) e non ad un livello inferiore. Indicheremo l'opzione SO_BROADCAST e passeremo l'indirizzo di una variabile (o) che conterrà il valore per l'opzione, 1 nel nostro caso, per abilitare il broadcast. Per finire dovremo indicare la dimensione della variabile o.

Naturalmente, se la funzione ritornerà errore, stamperemo la causa con perror e termineremo il programma, indicando codice di errore 1.


        /* Abilito il socket alla trasmissione broadcast */
        o = 1;	// Parametro a 1 (attivo)
        if (setsockopt (tx,SOL_SOCKET,SO_BROADCAST,&o,sizeof (o)))
        {
        	perror ("Opzioni socket tx");
                exit (1);
        }

Dobbiamo adesso preparare l'indirizzo locale per il socket. Per prima cosa azzeriamo la struttura addr con la funzione memset. Assegniamo al campo sin_family il valore relativo ad Internet, vale a dire AF_INET. Facciamo scegliere al sistema l'indirizzo di dafault del server indicando nel campo sin_addr.s_addr il valore INADDR_ANY, che però deve essere posto in ordine di rete usando la funzione htonl. Per finire indichiamo il port nel campo sin_port usando la costante 0, per far scegliere al sistema il primo port libero.. Anche il port va nell'ordine di rete, quindi lo convertiremo con la funzione htons, dato che il port è un numero a 16 bit, quindi uno short int.


        memset (&addr,0,sizeof (addr));
        addr.sin_family = AF_INET;
        addr.sin_addr.s_addr = htonl (INADDR_ANY);
        addr.sin_port = htons (0);	// Primo socket libero

Adesso che l'indirizzo è pronto, lo associamo al socket usando la funzione bind. Dato che questa funzione può essere usata per qualsiasi tipo di socket, l'indirizzo indicato è di tipo generico, quindi dobbiamo prendere l'indirizzo di memoria del nostro addr e fare un cast al tipo generico struct sockaddr * ed indicare nel successivo parametro la dimensione dell'indirizzo effettivo. Se la funzione bind ritorna un errore, segnalato da un valore di ritorno diverso da 0, stampiamo l'errore con perror e terminiamo il programma con un codice di errore 1.


        if (bind (tx,(struct sockaddr *) &addr,sizeof (addr)))
        {
        	perror ("Bind socket tx");
                exit (1);
        }

Se non c'è stato alcun errore, giungiamo in questo punto e possiamo ritornare al chiamante l'handle del socket creato.


        return tx;
}

La funzioneGetPeer

Questa è la funzione più complessa del sistema. Essa deve richiedere tramite un broadcast quali corrispondenti sono presenti sulla rete, quindi presentarne un menù e ottenere dall'utente una scelta del corrispondente.

Naturalmente, mentre attende le risposte dei corrispondenti deve essere in grado di ricevere anche richieste di nome da parte di altri corrispondenti, alle quali rispondere, oppure messaggi, nel qual caso accetterà il mittente del messaggio come corrispondente e quindi ritornerà.

Per inviare il broadcast dovrà ovviamente individuare l'indirizzo di broadcast della rete.

I suoi parametri sono i due socket rx e tx che utilizzerà per la comunicazione ed il puntatore ad una struttura addr, di tipo sockadr_in, nella quale porrà l'indirizzo del corrispondente.

Se l'utente sceglie un corrispondente oppure arriva un messaggio, la funzione ritorna -1. Se l'utente non vuole più effettuare la scelta (rispondendo q alla richiesta di selezione), ritorna 0.

Se avviene un errore di comunicazione la funzione termina il programma, con un codice di ritorno 1.


int GetPeer (int rx,int tx,struct sockaddr_in *addr)
{

Vediamo le variabili che servono per la nostra routine:

char message [MSGLEN];
La variabile che conterrà il messaggio ricevuto, ma viene anche usata ovunque serve una stringa.
struct sockaddr_in ad [20];
La lista degli indirizzi dei referenti.
int noth = 0;
Il numero dei referenti contati (inizialmente 0) e immagazzinati nel vettore ad.
int len,adlen;
Variabili usate nella ricezione per contenere la lunghezza del pacchetto ricevuto e del relativo indirizzo.
fd_set set;
Il set usato dalla funzione select.
struct hostent *host;
Il record dell'host da cui trarre l'indirizzo principale.
long bcaddr;
La variabile che conterrà l'indirizzo dell'host e poi l'indirizzo di broadcast.
extern int h_errno;
La variabile esterna di sistema che contiene l'errore della funzione gethostbyname.
char buff[1024];
Il buffer che conterrà i dati sulle interfacce.
struct ifreq *ifr = NULL;
Un puntatore a record d'interfaccia, usato per il loop sulle interfacce.
struct ifconf ifc;
La struttura che contiene le caratteristiche di tutte le interfacce, usata da ioctl.
int i;
Il contatore usato per il loop sulle interfacce.


char message [MSGLEN];
struct sockaddr_in ad [20];	/* lista di indirizzi di corrispondenti */
int noth = 0;
int len,adlen;
fd_set set;
struct hostent *host;
long bcaddr;
extern int h_errno;

char buff[1024];
struct ifreq *ifr = NULL;
struct ifconf ifc;
int i;

Adesso cominciamo a costruire il record addr con l'indirizzo di broadcast.

Per prima cosa, come sempre, azzeriamo il record con la funzione memset, quindi indichiamo il tipo di indirizzo come AF_INET e il suo port come indicato nella variabile globale chatport, convertito in ordine di rete con la funzione htns.


    /* Prepara l'indirizzo a cui richiedere i nomi (broadcast) */
    memset (addr,0,sizeof (addr));	// Azzera
    addr -> sin_family = AF_INET;	// Prorocollo Internet
    addr -> sin_port = htons (chatport);// Port (in network order)

A questo punto dobbiamo introdurre l'indirizzo dell'host cui spedire, che nel nostro caso è l'indirizzo di broadcast della rete.

Iniziamo recuperando il nome dell'host su cui stiamo lavorando, con la funzione gethostname, che pone l'indirizzo dell'host nella variabile message, passata come parametro. Una volta ottenuto l'indirizzo, lo stampiamo, per bellezza.


    /* Cerca l'indirizzo di broadcast */
    /* Cerca il nome dell'host locale */
    gethostname (message,MSGLEN);	// Nome host locale
    printf ("Hostname %s\n",message,n);

Ottenuto il nome dell'host, ne richiediamo l'indirizzo al database della rete tramite la funzione gethostbyname. Se non siamo in grado di ricavare i dati dell'host (la funzione gethostbyname ritorna un puntatore NULL, che indica errore), stampiamo un messaggio di errore e terminiamo il programma.

Il puntatore host, se la funzione non ha dato errore, punta ad un record privato della funzione gethostbyname, quindi ne estrarremo al più presto le informazioni.


    /* cerca l'IP dal nome (usa hosts o bind) */
    host = gethostbyname (message);	// Struttura hostent per nome
    if (!host)
    {
      perror ("gethostbyname");
      printf ("h_errno = %d\n",h_errno);
      exit (1);
    }

Poniamo allora in bcaddr l'indirizzo dell'host (facendo un cast a long), tratto dal record puntato da host, tramite la macro h_addr che ritorna il primo degli indirizzi associati all'host, vale a dire il principale.


    bcaddr = *((long*)(host -> h_addr));// Indirizzo principale host

Ora è giunto il momento di interrogare le interfacce.

Prepariamo per prima cosa il record ifc, facendo puntare il suo campo ifc_buf (in realtà una macro) al buffer all'uopo predisposto ed indicandone la lunghezza nel campo ifc_len (un'altra macro).

Una volta preparato il record con i parametri, possiamo invocare la funzione ioctl per eseguire una SIOCGIFCONF sul socket tx.

Se ioctl ritorna errore, stampiamo un messaggio e terminiamo il programma.


    /* Recupero le informazioni su tutte le interfacce lo, eth0, ecc.. */
    ifc.ifc_len = sizeof(buff);
    ifc.ifc_buf = buff;

    if (ioctl(tx, SIOCGIFCONF, &ifc) < 0) {
        perror("ioctl SIOCGIFCONF");
        exit(-1);
    }

Prendiamo ora il puntatore ifr e lo facciamo puntare al primo dei record di descrizione delle interfacce, puntato dal campo ifc.ifc_req (in realtà ifc_req è una macro).


    ifr = ifc.ifc_req;

Adesso iniziamo il loop sulle interfacce. Per prima cosa mettiamo il numero di interfacce trovate in i.

Il campo ifc_len di ifc contiene, al ritorno da ioctl, la dimensione dei dati restituiti nel buffer. Dato che questi dati sono record di tipo struct ifreq, dividendo ifc.ifc_len per la dimensione del record sizeof (struct ifreq), otteniamo il numero di record ritornati.

Il ciclo deve finire quando non ci sono più record, quindi quando i (predecrementato) diventa minore di 0 (continuiamo finchè i è maggiore o uguale a 0).

Ad ogni clclo faremo avanzare il puntatore ifr, per puntare al nuovo record di interfaccia.


    /* Itero su ogni interfaccia e estraggo le informazioni */
    for (i = ifc.ifc_len / sizeof(struct ifreq); --i >= 0; ifr++) 
    {

Per ogni ciclo, per prima cosa individuiamo l'indirizzo dell'interfaccia tramite la funzione SIOCGIFADDR di ioctl, eseguito sul record ifr dell'interfaccia, tramite il socket tx.

Naturalmente, se otteniamo un errore, stampiamo un messaggio e terminiamo il programma.


       /* Recupero l'indirizzo di interfaccia */
       if (ioctl(tx, SIOCGIFADDR, ifr) < 0) {
          perror("ioctl SIOCGIFADDR");
          exit(-1);
       }

Confrontiamo ora l'indirizzo principale dell'host bcaddr con l'indirizzo dell'interfaccia, che troviamo nel campo ifr_addr (in realtà una macro) del record dell'interfaccia puntato da ifr. Naturalmente, il campo ifr_addr è un indirizzo generico, quindi noi dovremo farne un cast a struct sockaddr_in.


       if (*((long*)&((struct sockaddr_in *) 
             &(ifr->ifr_addr))->sin_addr) == bcaddr)
       {     /* Ho trovato l'interfaccia principale */

Se l'indirizzo dell'interfaccia corrisponde con il nostro indirizzo principale, otteniamo, tramite la funzione SIOCGIFBRADDR di ioctl, l'indirizzo di broadcast dell'interfaccia, nello stesso record puntato da ifr e sempre per il socket tx.

Se ioctl dà errore, stampiamo un messaggio e terminiamo il programma.


             /* Recupero l'indirizzo di broadcast */
             if (ioctl(tx, SIOCGIFBRDADDR, ifr) < 0) 
             {
          	perror("ioctl SIOCGIFBRDADDR");
                exit(-1);
             }

Ora stampiamo, sempre per bellezza, l'indirizzo di broadcast. Per farlo usiamo la funzione inet_ntoa che stampa l'indirizzo nella consueta notazione "puntata".

Naturalmente, dato che il tipo del campo ifr_broadarr è del tipo generico struct sockaddr, dovremo fare un poco di lavoro per convertirlo nel nostro tipo struct sockaddr_in, passando attraverso i puntatori.


             printf("  Broadcast: %s\n", inet_ntoa(((struct sockaddr_in *) 
                &(ifr->ifr_broadaddr))->sin_addr));

Una volta stampato l'indirizzo di broadcast, lo poniamo nel nostro addr, sempre utilizzando il giro attraverso i puntatori, per fare la nostra chiamata.


             addr -> sin_addr = ((struct sockaddr_in *) 
                &(ifr->ifr_broadaddr))->sin_addr;

Avendo trovato l'indirizzo giusto, terminiamo il loop.


             break;
       }
    }

Prepariamo ora il messaggio di richiesta (che conterrà il solo codice QUERY ed il terminatore di stringa).


    message [0] = QUERY;	/* Richiede il nome */
    message [1] = 0;		/* Termina il messaggio */

Ora possiamo inviare il messaggio all'indirizzo di broadcast (contenuto in addr). Per la lunghezza del messaggio, per generalità, utilizziamo la funzione strlen.


    sendto (tx,message,strlen (message) + 1,0,
       (struct sockaddr *) addr,sizeof (*addr));

Iniziamo ora un loop infinito che si aspetterà una serie di possibili eventi:

Se l'utente dà una risposta diversa da q o da un numero compreso tra 0 ed il numero dei corrispondenti - 1 la risposta viene scartata.

Se arriva un messaggio dalla rete non riconosciuto, viene scartato.


    /* Attende tutti i nomi e contemporaneamente la scelta
     * da parte dell'utente */
    while (1) 
    {

Prepariamo ora il set per attendere, tramite la funzione select un evento.

Per prima cosa azzereremo il set con la macro FD_ZERO, quindi aggiungeremo, con la macro FD_SET, prima l'handle relativo al file stdin, su cui arrivano i messaggi dell'utente, estratto dal file descriptor stdin tamite la macro fileno, poi l'handle del socket rx su cui giungono i pacchetti dalla rete.


        FD_ZERO (&set);
        FD_SET (fileno (stdin), &set);
        FD_SET (rx, &set);

Possiamo ora chiamare la funzione select, indicando solo il set da controllare per l'ingresso.

I set da controllare per la scrittura o per gli errori sono settati a NULL, come pure il timeout.

Se la select ritorna errore, stampiamo un messaggio e terminiamo il programma.


        if (select (FD_SETSIZE, &set, NULL, NULL, NULL) < 0)
        {
            perror ("Select ");
            exit (1);
        }

All'uscita della funzione select, visto che non abbiamo preso in considerazione il timeout, o è arrivata una stringa su stdin oppure un pacchetto su rx.

Per prima cosa controlliamo, tramite la funzione FD_ISSET, se il file stdin è pronto per la ricezione.


        if (FD_ISSET (fileno (stdin), &set))
        {  // stdin

Dato che dovremo leggere un numero, ci serve una variabile locale n.


        int n;

Recuperiamo la stringa da stdin tramite la funzione fgets. Non usiamo la funzione gets perchè non ci consente di evitare superamenti della dimensione del buffer messo a disposizione da parte della stringa letta.


            fgets (message,MSGLEN,stin);

Ottenuta la stringa, controlliamo se l'utente ha inviato una q (solo minuscola), nel qual caso interrompiamo il ciclo. Vedremo più avanti che se il ciclo termina la funzione ritorna 0, valore che forza la terminazione del programma (se è la prima selezione di corrispondente) oppure il mantenimento del corrispondente precedente.


            if (message [0] == 'q')
               break;  

Se non è stata introdotta una q, convertiamo la stringa in numero. Questo numero sarà il numero del corrispondente scelto.


            n = strtol (message,(char **) 0,0);

Naturalmente, se il numero è minore di 0 oppure maggiore di noth - 1, vale a dire supera l'ultimo corrispondente registrato, stampiamo un messaggio di errore e torniamo ad attendere.


            if (n < 0 || n > noth - 1)
               printf ("Nessun host corrisponde al numero dato (0 .. %d)\n",
                  noth);
            else
            {

Se invece il numero era corretto, copiamo nell'indirizzo puntato dal parametro addr l'indirizzo selezionato. Per questo utilizzeremo la funzione memmove.


            	memmove (addr,ad + n,sizeof (*addr));	// Seleziona utente

Interroghiamo il nome del corrispondente, e lo stampiamo, utilizzando la funzione GetPeerName che descriveremo più avanti.


                GetPeerName (rx,tx,addr);	// Chiede nome utente

Per terminare, ritorneremo -1, per indicare che l'indirizzo è disponibile.


            	return -1;
            }
        }

Passiamo adesso a controllare se è arrivato un pacchetto sul socket rx, sempre tramite la macro FD_ISSET. Ricordo che il fatto che sia arrivata una stringa su stdin non esclude che possa anche essere arrivato una pacchetto su rx.


        if (FD_ISSET (rx, &set))
        {  // Socket in ricezione

Riceviamo allora il pacchetto. Per prima cosa inizializzeremo la variabile adlen con la dimensione del buffer per l'indirizzo messo a disposizione (useremo l'indirizzo che ci è stato passato come parametro). Questa variabile verrà riempita dalla funzione con l'effettiva dimensione dell'indirizzo ritornato.

Chiamiamo quindi la funzione recvfrom per leggere il pacchetto. La funzione ci ritornerà, come valore di ritorno, il numero di byte letti, oppure -1 in caso di errore.


            adlen = sizeof (*addr);
            len = recvfrom (rx,message,MSGLEN,0,
               (struct sockaddr *)addr,&adlen);

Se recvfrom ritorna un valore minore di zero (errore), stampiamo un messaggio di errore e terminiamo il programma.


            if (len < 0)
            {
                perror ("Ricevo Nome");
                exit (1);
            }

Se il messaggio è più lungo di un byte, andiamo a controllarlo. Se è più corto, allora non è un messaggio corretto, quindi non ne teniamo conto.


            if (len > 1)
            {

Il primo carattere del messaggio ne indica il tipo.


                switch (message [0])
                {

Se arriva un pacchetto di tipo MESSAGE, modifichiamo l'indirizzo del mittente, introducendovi il port standard, contenuto nella variabile chatport, per ottenere l'indirizzo corretto e completo del corrispondente.


                    case MESSAGE :
                        addr -> sin_port = htons (chatport);

Ottenuto l'indirizzo del corrispondente, ne interroghiamo il nome del e lo stampiamo, utilizzando la funzione GetPeerName che descriveremo più avanti.


                        GetPeerName (rx,tx,addr);

Quindi stampiamo il testo del messaggio arrivato, tra "<<" e ">>", per riconoscerlo come messaggio ricevuto. Naturalmente, il testo del messaggio sta dopo il codice MESSAGE, quindi parte in message + 1.


                        printf ("<< %s >>\n",message + 1);

Per finire ritorniamo al chiamante, indicando (con il codice -1) che l'indirizzo è stato individuato.


                        return -1;

Nel caso di pacchetto QUERY, vale a dire se un altro corrispondente ci chiede il nome, costruiamo un pacchetto di tipo NAME (mettendo il flag NAME nel primo carattere del messaggio), che contiene il nostro nome, immagazzinato nella variabile globale peername, a partire dal secondo carattere.


                    case QUERY :
                        message [0] = NAME;
                        strcpy (message + 1,peername);

Otteniamo l'indirizzo corretto del richiedente, introducendo nell'indirizzo ricevuto il port standard, contenuto nella variabile chatport, per ottenere l'indirizzo corretto e completo del corrispondente.


                        addr -> sin_port = htons (chatport);

Quindi inviamo al richiedente il messaggio con il nostro nome. Non c'è altro da fare, quindi usciamo dallo switch.


                        sendto (tx,message,strlen (message) + 1,
                            0,(struct sockaddr *) addr,sizeof (*addr));
                        break;

Se arriva un pacchetto di tipo NAME, modifichiamo l'indirizzo del mittente, introducendovi il port standard, contenuto nella variabile chatport, per ottenere l'indirizzo corretto e completo del corrispondente.


                    case NAME :	// Riceve nome
                        addr -> sin_port = htons (chatport);

Quindi stampiamo una riga di menù, che riporta l'indice di questo corrispondente nell'array degli indirizzi di corrispondente, ed il nome del corrispondente stesso.


                        /* Stampa riga di menu' */
                        printf ("%d) %s\n",noth,message + 1);

Per finire, copiamo l'indirizzo ricevuto nell'array degli indirizzi di corrispondente ad, tramite la funzione memcpy ed incrementiamo il conteggio dei nomi di corrispondente ricevuti, quindi terminiamo la gestione del messaggio.


                        /* Memorizza indirizzo */
                        memcpy (ad + noth ++,addr,sizeof (*addr));
                        break;

Se ci arriva un messaggio non riconoscibile, lo trascuriamo.


                    default:
                        break;

A questo punto terminiamo lo switch, l'if relativo alla lunghezza del messaggio, l'if relativo al pacchetto ricevuto ed il ciclo while.


                }
            }
        }
    }

All'esterno del ciclo (dove arriviamo solo se l'utente risponde q al posto che un numero di corrispondente), ritorniamo il codice 0 che indica che nessun corrispondente è stato scelto.


    return 0;
}

La funzione DoChat

Troviamo finalmente la funzione che esegue la comunicazione vera e propria.

Il suo scopo principale sarà quello di attendere o un messaggio introdotto dall'utente o un pacchetto dalla rete.

Per svolgere il suo lavoro, la funzione riceverà come parametri gli handle dei socket in ricezone rx ed in trasmissione tx, oltre all'indirizzo del corrispondente, nel record puntato da addr.

Se arriva una stringa da parte dell'utente, per prima cosa controllerà se essa comincia con "**", nel qual caso viene interpretata come comando; se non è un comando, allora la invia al corrispondente tramite un pacchetto di tipo MESSAGE.

Se invece arriva un pacchetto, ne esamina il tipo. Se è un MESSAGE, lo stampa. Se il MESSAGE arriva da un indirizzo diverso da quello del corrispondente corrente, cambia corrispondente e segnala il cambiamento tramite la funzione GetPeerName.

Se arriva un pacchetto QUERY risponde inviando al chiamante un pacchetto NAME contenente il proprio nome.

Se arriva un pacchetto NAME o qualunque altro tipo di pacchetto non riconosciuto, lo ignora.

La funzione termina se l'utente dà il comando "**q". Se l'utente dà il comando "**n" la funzione richiama la GetPeer per ottenere un nuovo corrispondente.


void DoChat (int rx,int tx,struct sockaddr_in *addr)
{

La variabile message serve per contenere il pacchetto ricevuto oppure il messaggio introdotto dall'utente.

Il set set, di tipo fd_set serve per la funzione select.

La struttura rxad, di tipo sockaddr_in, serve per contenere l'indirizzo del pacchetto in arrivo, che può essere diverso da quello del corrispondente corrente.


char message [MSGLEN];
fd_set set;
struct sockaddr_in rxad;

Passiamo ora al loop infinito della chat.


    /* Loop infinito sugli eventi: tastiera o rete */
    while (1) 
    {

La funzione del ciclo è quella di attendere un evento, quindi prepariamo il set per attendere, tramite la funzione select questo evento.

Per prima cosa azzereremo il set con la macro FD_ZERO, quindi aggiungeremo, con la macro FD_SET, prima l'handle relativo al file stdin, su cui arrivano i messaggi dell'utente, estratto dal file descriptor stdin tramite la macro fileno, poi l'handle del socket rx su cui giungono i pacchetti dalla rete.


        FD_ZERO (&set);
        FD_SET (fileno (stdin), &set);
        FD_SET (rx, &set);

Possiamo ora chiamare la funzione select, indicando solo il set da controllare per l'ingresso.

I set da controllare per la scrittura o per gli errori sono settati a NULL, come pure il timeout.

Se la select ritorna errore, stampiamo un messaggio e terminiamo il programma.


        if (select (FD_SETSIZE, &set, NULL, NULL, NULL) < 0)
        {
            perror ("Select ");
            exit (1);
        }

All'uscita della funzione select, visto che non abbiamo preso in considerazione il timeout, o è arrivata una stringa su stdin oppure un pacchetto su rx.

Per prima cosa controlliamo, tramite la funzione FD_ISSET, se il file stdin è pronto per la ricezione.


        if (FD_ISSET (fileno (stdin), &set))
        {  // stdin

Recuperiamo la stringa da stdin tramite la funzione fgets. Non usiamo la funzione gets perchè non ci consente di evitare superamenti della dimensione del buffer messo a disposizione da parte della stringa letta.

Nell'ipotesi che il messaggio letto debba poi essere spedito, non lo immagazziniamo all'inizio della stringa, ma lasciamo il posto per il tipo del pacchetto, quindi lo poniamo in message + 1.


            fgets (message + 1,MSGLEN,stdin);

Controlliamo adesso se l'utente ha inviato un comando o un messaggio.

Il comando comincia con due asterischi. Dato che message [0] è stato riservato per l'eventuale tipo di messaggio, gli asterischi dovranno essere in message [1] e message [2] rispettivamente.


            /* I messaggi che cominciano con "**" sono comandi
             * e non vanno spediti ma interpretati */
            if (message [1] == '*' && message [2] == '*')
            {

Se message [1] e message [2] sono entrambi asterischi, allora message [3] sarà il comando.


            	switch (message [3])
                {

Il comando "**q" oppure "**Q" richiede la fine del programma, quindi non facciamo altro che return.


                   case 'q':
                   case 'Q' :
                      return;	// Termine programma

Il comando "**n" oppure "**N" richiede la selezione di un nuovo corrispondente, quindi richiamiamo la funzione GetPeer per selezionarlo.


                   case 'n' :
                   case 'N' :	// Cambio interlocutore
                      GetPeer (rx,tx,addr);
                      break;
                }
            }

Se la stringa letta non era un comando, allora era un messaggio.

Poniamo nel primo carattere del messaggio il tipo MESSAGE, quindi inviamo il pacchetto all'indirizzo del corrispondente indicato da addr.


            else
            {   // Spedisco il messaggio all'interlocutore
                message [0] = MESSAGE;
                sendto (tx,message,strlen (message) + 1,0,
                   (struct sockaddr *) addr,sizeof (*addr));
            }
        }

Passiamo adesso a controllare se è arrivato un pacchetto sul socket rx, sempre tramite la macro FD_ISSET. Ricordo che il fatto che sia arrivata una stringa su stdin non esclude che possa anche essere arrivato una pacchetto su rx.


        if (FD_ISSET (rx, &set))
        {  // Socket in ricezione

Per la ricezione ci servono due variabili: una per la dimensione del pacchetto ricevuto ed un'altra per la dimensione dell'indirizzo ritornato.


        int len,adlen;

Riceviamo il pacchetto. Per prima cosa inizializzeremo la variabile adlen con la dimensione del buffer per l'indirizzo messo a disposizione. Questa variabile verrà riempita dalla funzione con l'effettiva dimensione dell'indirizzo ritornato.

Chiamiamo quindi la funzione recvfrom per leggere il pacchetto. La funzione ci ritornerà, come valore di ritorno, il numero di byte letti, oppure -1 in caso di errore.


            adlen = sizeof (*addr);
            len = recvfrom (rx,message,MSGLEN,0,
               (struct sockaddr *)&rxad,&adlen);

Se recvfrom ritorna un valore minore di zero (errore), stampiamo un messaggio di errore e terminiamo il programma.


            if (len < 0)
            {
                perror ("Ricevo Messaggio");
                exit (1);
            }

Se il messaggio è più lungo di un byte, andiamo a controllarlo. Se è più corto, allora non è un messaggio corretto, quindi non ne teniamo conto.


            if (len > 1)
            {

Il primo carattere del messaggio ne indica il tipo.


                switch (message [0])
                {

Se è arrivato un MESSAGE, per prima cosa ne controlliamo il mittente.


                    case MESSAGE :
                        if (rxad.sin_addr.s_addr != addr -> sin_addr.s_addr)
                        {   /* Messaggio da qualcuno diverso 
                             * dall'interlocutore 
                             * corrente -> cambio interlocutore */

Se il mittente del messaggio è diverso dal corrispondente corrente, cambiamo l'indirizzo del corrispondente con quello del mittente del messaggio.


                            addr -> sin_addr.s_addr = rxad.sin_addr.s_addr;

Quindi chiediamo, tramite la funzione GetPeerName, al nuovo corrispondente il suo nome.


                            GetPeerName (rx,tx,addr);
                        }

Per finire stampiamo il messaggio tra "<<" ed ">>" per riconoscerlo come messaggio ricevuto.


                        printf ("<< %s >>\n",message + 1);
                        break;

Nel caso di pacchetto QUERY, vale a dire se un altro corrispondente ci chiede il nome, costruiamo un pacchetto di tipo NAME (mettendo il flag NAME nel primo carattere del messaggio), che contiene il nostro nome, immagazzinato nella variabile globale peername, a partire dal secondo carattere.


                    case QUERY :	// Mi chiedono il nome e lo dico
                        message [0] = NAME;
                        strcpy (message + 1,peername);

Otteniamo l'indirizzo corretto del richiedente, introducendo nell'indirizzo ricevuto il port standard, contenuto nella variabile chatport, per ottenere l'indirizzo corretto e completo del corrispondente.


                        rxad.sin_port = htons (chatport);

Quindi inviamo al richiedente il messaggio con il nostro nome. Non c'è altro da fare, quindi usciamo dallo switch.


                        sendto (tx,message,strlen (message) + 1,
                            0,(struct sockaddr *) &rxad,sizeof (*addr));
                        break;

Se il messaggio è un NAME, lo trascuriamo.


                    case NAME :	// Se arriva un nome, scarto
                        break;

Anche se il messaggio non è riconoscibile, lo trascuriamo.


                    default:
                        break;

E con questo abbiamo finito il lavoro, quindi chiudiamo tutti i blocchi rimasti aperti.


                }
            }
        }
    }
}

La funzione GetPeerName

La funzione GetPeerName richiede all'host il cui indirizzo è indicato nella variabile puntata da addr il relativo nome.

Attende per un poco che arrivi un pacchetto con il nome, se questo non accade, ritorna al chiamante dopo aver stampato il messaggio " === > Connesso con host ignoto < ===".

Naturalmente, al posto del messaggio NAME possono anche arrivare pacchetti MESSAGE oppure QUERY.

In questo caso reagiremo correttamente al messaggio, ma non saremo in grado di individuare il nome dell'host, quindi stamperemo sempre il messaggio di host ignoto.

Oltre all'indirizzo dell'host cui chiedere il nome, indicato nella variabile addr, la funzione richiede i socket per la ricezione rx e trasmissione tx.


void GetPeerName (int rx,int tx,struct sockaddr_in *addr)
{

La variabile message serve per contenere il pacchetto ricevuto.

Il set set, di tipo fd_set serve per la funzione select.

La struttura rxad, di tipo sockaddr_in, serve per contenere l'indirizzo del pacchetto in arrivo, che può essere diverso da quello del corrispondente corrente.

La variabile timeout, di tipo struct timeval serve per indicare il timeout per l'attesa del pacchetto.


char message [MSGLEN];	/* Buffer per la ricezione */
struct sockaddr_in rxad;
fd_set set;
struct timeval timeout;

Prepariamo ora il messaggio, contenente il flag QUERY ed il terminatore di stringa, quindi lo inviamo all'host ce ci interessa, tramite sendto.


    message [0] = QUERY;
    message [1] = 0;
    sendto (tx,message,strlen (message) + 1,0,(struct sockaddr *) addr,sizeof (*addr));

Prepariamo ora la variabile per il timeout, che indicherà un timeout di 0 secondi e 100.000 microsecondi, vale a dire un decimo di secondo


    timeout.tv_sec = 0;
    timeout.tv_usec = 100000;

Prepariamo anche il set set, azzerandolo prima di tutto con la macro FD_ZERO e quindi introducendo il solo handle del socket in ricezione tramite la macro FD_SET.


    FD_ZERO (&set);
    FD_SET (rx, &set);

Possiamo ora chiamare la funzione select, indicando il set da controllare per l'ingresso ed il timeout.

I set da controllare per la scrittura o per gli errori sono settati a NULL.

Se la select ritorna errore, stampiamo un messaggio e terminiamo il programma.


    if (select (FD_SETSIZE, &set, NULL, NULL, &timeout) < 0)
    {
        perror ("Select ");
        exit (1);
    }

Controlliamo se è arrivato un pacchetto sul socket rx, tramite la macro FD_ISSET. Non è detto che sia arrivato un pacchetto, in quanto la funzione select avrebbe potuto ritornare a causa del timeout.


    if (FD_ISSET (rx, &set))
    {  // Socket in ricezione

Per la ricezione ci servono due variabili: una per la dimensione del pacchetto ricevuto ed un'altra per la dimensione dell'indirizzo ritornato.


    int len,adlen;

Riceviamo il pacchetto. Per prima cosa inizializzeremo la variabile adlen con la dimensione del buffer per l'indirizzo messo a disposizione. Questa variabile verrà riempita dalla funzione con l'effettiva dimensione dell'indirizzo ritornato.

Chiamiamo quindi la funzione recvfrom per leggere il pacchetto. La funzione ci ritornerà, come valore di ritorno, il numero di byte letti, oppure -1 in caso di errore.


        adlen = sizeof (*addr);
        len = recvfrom (rx,message,MSGLEN,0,(struct sockaddr *) &rxad,&adlen);

Se recvfrom ritorna un valore minore di zero (errore), stampiamo un messaggio di errore e terminiamo il programma.


        if (len < 0)
        {
            perror ("Ricevo Nome");
            exit (1);
        }

Se il messaggio è più lungo di un byte, andiamo a controllarlo. Se è più corto, allora non è un messaggio corretto, quindi non ne teniamo conto.


        if (len > 1)
        {

Il primo carattere del messaggio ne indica il tipo.


            switch (message [0])
            {

Se ci arriva un MESSAGE, ne accettiamo l'indirizzo senza controlli, ponendolo nella variabile puntata da addr, quindi lo stampiamo al solito modo, quindi interrompiamo lo switch, in modo da stampare il messaggio di host sconosciuto.

È da notare che il messaggio di host sconosciuto viene stampato dopo il messaggio ricevuto e che, nel caso la funzione sia stata invocata dalla DoChat, il messaggio ora ricevuto viene stampato prima di quello che ha causato l'invocazione della GetPeerName, quindi fuori ordine.


                case MESSAGE :	/* Ho ricevuto un messaggio: stampo */
                    addr -> sin_addr.s_addr = rxad.sin_addr.s_addr;
                    printf ("<< %s >>\n",message + 1);
                    break;

Nel caso di pacchetto QUERY, vale a dire se un altro corrispondente ci chiede il nome, costruiamo un pacchetto di tipo NAME (mettendo il flag NAME nel primo carattere del messaggio), che contiene il nostro nome, immagazzinato nella variabile globale peername, a partire dal secondo carattere.


                case QUERY :	/* Ho ricevuto una richiesta: mi qualifico */
                    message [0] = NAME;
                    strcpy (message + 1,peername);

Otteniamo l'indirizzo corretto del richiedente, introducendo nell'indirizzo ricevuto il port standard, contenuto nella variabile chatport, per ottenere l'indirizzo corretto e completo del corrispondente.


                    rxad.sin_port = htons (chatport);

Quindi inviamo al richiedente il messaggio con il nostro nome. Non c'è altro da fare, quindi usciamo dallo switch. Naturalmente stamperemo il messaggio di host sconosciuto.


                    sendto (tx,message,strlen (message) + 1,
                        0,(struct sockaddr *) &rxad,sizeof (*addr));
                    break;

Finalmente, se ci arriverà un messaggio NAME, stamperemo il messaggio con il nome dell'host e ritorneremo, senza stampare il messaggio di host sconosciuto.


                case NAME :	/* Ho ricevuto un nome : stampo */
                    printf (" --- > Connesso con %s < ---\n",message + 1);
                    return;

Se arriva un messaggio non riconoscibile, lo ignoreremo, ma stamperemo sempre il messaggio di host sconosciuto.


                default:
                    break;
            }
        }
    }

Se c'è stato un timeout oppure è arrivato un messaggio diverso da NAME, arriviamo in questo punto e stampiamo il messaggio di host sconosciuto.


    printf (" === > Connesso con host ignoto < ===\n");
}

Il Sorgente Completo

Abbiamo visto, una per una, le righe del nostro programma. Riporto qui il sorgente completo, tutto di seguito.

Se volete provare il programma, potete selezionare il sorgente qui di seguito, "copiarlo" usando il menù edit o modifica del browser, quindi incollarlo in un qualunque editor e salvarlo con il nome chat.c in una sottodirectory della vostra home directory.

In alternativa potete salvare l'intero documento, selezionando file di testo come formato del documento, poi cancellare dal file la relazione e tenere solo questo sorgente.


/* chat.c
 * Programma di chat
 */

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/time.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <netdb.h>
#include <fcntl.h>
#include <unistd.h>
#include <net/if.h>

int rxsock (void);
int txsock (void);
int GetPeer (int rx,int tx,struct sockaddr_in *addr);
void GetPeerName (int rx,int tx,struct sockaddr_in *addr);
void DoChat (int rx,int tx,struct sockaddr_in *addr);

#define	MESSAGE		'#'	/* prefisso di un messaggio (testo) */
#define	QUERY		'Q'	/* Prefisso di una richiesta di identificazione */
#define	NAME		'N'	/* Prefisso della comunicazione del nome della stazione */

#define	MSGLEN		80	/* Lunghezza massima di un messaggio */

char peername [MSGLEN];	/* Nome della stazione */
int chatport = 2000;	/* Port standard per il servizio */

int main (int argc,char **argv)
{
int i,j;
int rx,tx;
struct sockaddr_in addr;

    /* forza la 'printf' a scrivere subito */
    setvbuf (stdout,(char *)0,_IOLBF,1024); 
    
    peername [0] = 0;	/* Nome inizialmente vuoto */

    for (i = 1;i < argc;i ++)	/* Loop sugli argomenti */
    {
        if (argv [i][0] == '-')	/* L'argomento e' un parametro */
        {
            switch (argv [i][1])
            {
                case 'n' :	// Nome Stazione
                    strcpy (peername,argv [++i]);
                    break;
                case 'p' :	// Port per la chat
                    j = strtol (argv [++i],(char **) 0,0);
                    if (j < 1024)
                        printf ("Errore: il port deve essere maggiore di 1024\n");
                    else
                        chatport = j;
                    break;
                default:
                    printf ("Errore: \n"
                        "parametri validi -n <nome> -p <port>\n");
                    exit (0);
                    break;
            }
        }
        else	/* Argomento non parametro (non -?) */
        {
            printf ("Errore: \n"
                "parametri validi -n <nome> -p <port>\n");
            exit (0);
        }
    }
    
    if (!peername [0])	/* Se manca il nome della stazione... */
    {  /* Chiede il nome */
        printf ("Introduci il nome della macchina : ");
        fgets (peername,MSGLEN,stdin);
        for (i = 0;i < MSGLEN;i ++)
           if (peername [i] < ' ')
              peername [i] = 0;
    }
    
    if (!peername [0])	/* Il nome e' obbligatorio ... */
    {
        printf ("Nessun nome - termino\n");
        exit (0);
    }

    rx = rxsock ();	/* Apre il socket di ricezione */
    tx = txsock ();	/* Apre il socket per la trasmissione */

    if (GetPeer (rx,tx,&addr))	/* Menu' degli altri utenti per scelta */
    {
        DoChat (rx,tx,&addr);	/* Connessione */
    }       
        
    close (rx);	/* Chiude il socket di ricezione */
    close (tx); /* Chiude il socket di trasmissione */
    return 0;
}

int rxsock (void)
{
int rx;
struct sockaddr_in addr;

        rx = socket (AF_INET,SOCK_DGRAM,0);	// Socket in ricezione
        if (rx < 0)
        {
        	perror ("Creazione socket rx");
                exit (1);
        }
        
        memset (&addr,0,sizeof (addr));
        addr.sin_family = AF_INET;
        addr.sin_addr.s_addr = htonl (INADDR_ANY);
        addr.sin_port = htons (chatport);
        
        if (bind (rx,(struct sockaddr *) &addr,sizeof (addr)))
        {
        	perror ("Bind socket rx");
                exit (1);
        }
        
        return rx;
}

int txsock (void)
{
int tx,o;
struct sockaddr_in addr;

   	tx = socket (AF_INET,SOCK_DGRAM,0);	// Socket in trasmissione
        if (tx < 0)
        {
        	perror ("Creazione socket tx");
                exit (1);
        }
        
        /* Abilito il socket alla trasmissione broadcast */
        o = 1;	// Parametro a 1 (attivo)
        if (setsockopt (tx,SOL_SOCKET,SO_BROADCAST,&o,sizeof (o)))
        {
        	perror ("Opzioni socket tx");
                exit (1);
        }
        
        memset (&addr,0,sizeof (addr));
        addr.sin_family = AF_INET;
        addr.sin_addr.s_addr = htonl (INADDR_ANY);
        addr.sin_port = htons (0);	// Primo socket libero
        
        if (bind (tx,(struct sockaddr *) &addr,sizeof (addr)))
        {
        	perror ("Bind socket tx");
                exit (1);
        }
        
        return tx;
}

int GetPeer (int rx,int tx,struct sockaddr_in *addr)
{
char message [MSGLEN];
struct sockaddr_in ad [20];	/* lista di indirizzi di corrispondenti */
int noth = 0;
int len,adlen;
fd_set set;
struct hostent *host;
long bcaddr;
char *n;
extern int h_errno;

char buff[1024];
struct ifreq *ifr = NULL;
struct ifconf ifc;
int i;


    /* Prepara l'indirizzo a cui richiedere i nomi (broadcast) */
    memset (addr,0,sizeof (addr));	// Azzera
    addr -> sin_family = AF_INET;	// Prorocollo Internet
    addr -> sin_port = htons (chatport);// Port (in network order)
    
    /* Cerca l'indirizzo di broadcast */
    /* Cerca il nome dell'host locale */
    gethostname (message,MSGLEN);	// Nome host locale
    printf ("Hostname %s\n",message,n);
    /* cerca l'IP dal nome (usa hosts o bind) */
    host = gethostbyname (message);	// Struttura hostent per nome
    if (!host)
    {
      perror ("gethostbyname");
      printf ("h_errno = %d\n",h_errno);
      exit (1);
    }
    bcaddr = *((long*)(host -> h_addr));// Indirizzo principale host

    /* Recupero le informazioni su tutte le interfacce lo, eth0, ecc.. */
    ifc.ifc_len = sizeof(buff);
    ifc.ifc_buf = buff;
    
    if (ioctl(tx, SIOCGIFCONF, &ifc) < 0) {
        perror("ioctl SIOCGIFCONF");
        exit(-1);
    }
    
    ifr = ifc.ifc_req;
    
    /* Itero su ogni interfaccia e estraggo le informazioni */
    for (i = ifc.ifc_len / sizeof(struct ifreq); --i >= 0; ifr++) 
    {
      // printf("\n- Interfaccia: %s\n", ifr->ifr_ifrn.ifrn_name);
    
       /* Recupero l'indirizzo di interfaccia */
       if (ioctl(tx, SIOCGIFADDR, ifr) < 0) {
          perror("ioctl SIOCGIFADDR");
          exit(-1);
       }
    
       if (*((long*)&((struct sockaddr_in *) 
             &(ifr->ifr_addr))->sin_addr) == bcaddr)
       {     /* Ho trovato l'interfaccia principale */
             /* Recupero l'indirizzo di broadcast */
             if (ioctl(tx, SIOCGIFBRDADDR, ifr) < 0) 
             {
          	perror("ioctl SIOCGIFBRDADDR");
                exit(-1);
             }

             printf("  Broadcast: %s\n", inet_ntoa(((struct sockaddr_in *) 
                &(ifr->ifr_broadaddr))->sin_addr));
    
             addr -> sin_addr = (struct sockaddr_in *) 
             	&(ifr->ifr_broadaddr))->sin_addr;
             
             break;
       }
    }

    message [0] = QUERY;	/* Richiede il nome */
    message [1] = 0;		/* Termina il messaggio */
    sendto (tx,message,strlen (message) + 1,0,
       (struct sockaddr *) addr,sizeof (*addr));
    
    /* Attende tutti i nomi e contemporaneamente la scelta
     * da parte dell'utente */
    while (1) 
    {
        FD_ZERO (&set);
        FD_SET (fileno (stdin), &set);
        FD_SET (rx, &set);
        
        if (select (FD_SETSIZE, &set, NULL, NULL, NULL) < 0)
        {
            perror ("Select ");
            exit (1);
        }
            
        if (FD_ISSET (fileno (stdin), &set))
        {  // stdin
        int n;
            gets (message);
            if (message [0] == 'q')
               break;  
            n = strtol (message,(char **) 0,0);
            if (n < 0 || n > noth - 1)
               printf ("Nessun host corrisponde al numero dato (0 .. %d)\n",
                  noth);
            else
            {
            	memmove (addr,ad + n,sizeof (*addr));	// Seleziona utente
                GetPeerName (rx,tx,addr);	// Chiede nome utente
            	return -1;
            }
        }
        
        if (FD_ISSET (rx, &set))
        {  // Socket in ricezione
            adlen = sizeof (*addr);
            len = recvfrom (rx,message,MSGLEN,0,
               (struct sockaddr *)addr,&adlen);
            if (len < 0)
            {
                perror ("Ricevo Nome");
                exit (1);
            }
            if (len > 1)
            {
                switch (message [0])
                {
                    case MESSAGE :
                        addr -> sin_port = htons (chatport);
                        GetPeerName (rx,tx,addr);
                        printf ("<< %s >>\n",message + 1);
                        return -1;
                        
                    case QUERY :
                        message [0] = NAME;
                        strcpy (message + 1,peername);
                        addr -> sin_port = htons (chatport);
                        sendto (tx,message,strlen (message) + 1,
                            0,(struct sockaddr *) addr,sizeof (*addr));
                        break;
                        
                    case NAME :	// Riceve nome
                        addr -> sin_port = htons (chatport);
                        /* Stampa riga di menu' */
                        printf ("%d) %s\n",noth,message + 1);
                        /* Memorizza indirizzo */
                        memcpy (ad + noth ++,addr,sizeof (*addr));
                        break;
                        
                    default:
                        break;
                }
            }
        }
    }
    return 0;
}

void DoChat (int rx,int tx,struct sockaddr_in *addr)
{
char message [MSGLEN];
fd_set set;
struct sockaddr_in rxad;

    /* Loop infinito sugli eventi: tastiera o rete */
    while (1) 
    {
        FD_ZERO (&set);
        FD_SET (fileno (stdin), &set);
        FD_SET (rx, &set);
        
        if (select (FD_SETSIZE, &set, NULL, NULL, NULL) < 0)
        {
            perror ("Select ");
            exit (1);
        }
            
        if (FD_ISSET (fileno (stdin), &set))
        {  // stdin
            gets (message + 1);
            /* I messaggi che cominciano con "**" sono comandi
             * e non vanno spediti ma interpretati */
            if (message [1] == '*' && message [2] == '*')
            {
            	switch (message [3])
                {
                   case 'q':
                   case 'Q' :
                      return;	// Termine programma
                   case 'n' :
                   case 'N' :	// Cambio interlocutore
                      GetPeer (rx,tx,addr);
                      break;
                }
            }
            else
            {   // Spedisco il messaggio all'interlocutore
                message [0] = MESSAGE;
                sendto (tx,message,strlen (message) + 1,0,
                   (struct sockaddr *) addr,sizeof (*addr));
            }
        }
        
        if (FD_ISSET (rx, &set))
        {  // Socket in ricezione
        int len,adlen;
            adlen = sizeof (*addr);
            len = recvfrom (rx,message,MSGLEN,0,
               (struct sockaddr *)&rxad,&adlen);
            if (len < 0)
            {
                perror ("Ricevo Messaggio");
                exit (1);
            }
            
            if (len > 1)
            {
                switch (message [0])
                {
                    case MESSAGE :
                        if (rxad.sin_addr.s_addr != addr -> sin_addr.s_addr)
                        {   /* Messaggio da qualcuno diverso 
                             * dall'interlocutore 
                             * corrente -> cambio interlocutore */
                            addr -> sin_addr.s_addr = rxad.sin_addr.s_addr;
                            GetPeerName (rx,tx,addr);
                        }
                        printf ("<< %s >>\n",message + 1);
                        break;
                        
                    case QUERY :	// Mi chiedono il nome e lo dico
                        message [0] = NAME;
                        strcpy (message + 1,peername);
                        rxad.sin_port = htons (chatport);
                        sendto (tx,message,strlen (message) + 1,
                            0,(struct sockaddr *) &rxad,sizeof (*addr));
                        break;
                        
                    case NAME :	// Se arriva un nome, scarto
                        break;
                        
                    default:
                        break;
                }
            }
        }
    }
}

void GetPeerName (int rx,int tx,struct sockaddr_in *addr)
{
char message [MSGLEN];	/* Buffer per la ricezione */
struct sockaddr_in rxad;
fd_set set;
struct timeval timeout;
    
    message [0] = QUERY;
    message [1] = 0;
    sendto (tx,message,strlen (message) + 1,0,(struct sockaddr *) addr,sizeof (*addr));
    
    timeout.tv_sec = 0;
    timeout.tv_usec = 100000;

    FD_ZERO (&set);
    FD_SET (rx, &set);
    
    if (select (FD_SETSIZE, &set, NULL, NULL, &timeout) < 0)
    {
        perror ("Select ");
        exit (1);
    }
        
    if (FD_ISSET (rx, &set))
    {  // Socket in ricezione
    int len,adlen;
        adlen = sizeof (*addr);
        len = recvfrom (rx,message,MSGLEN,0,(struct sockaddr *) &rxad,&adlen);
        if (len < 0)
        {
            perror ("Ricevo Nome");
            exit (1);
        }
        if (len > 1)
        {
            switch (message [0])
            {
                case MESSAGE :	/* Ho ricevuto un messaggio: stampo */
                    addr -> sin_addr.s_addr = rxad.sin_addr.s_addr;
                    printf ("<< %s >>\n",message + 1);
                    break;

                case QUERY :	/* Ho ricevuto una richiesta: mi qualifico */
                    message [0] = NAME;
                    strcpy (message + 1,peername);
                    rxad.sin_port = htons (chatport);
                    sendto (tx,message,strlen (message) + 1,
                        0,(struct sockaddr *) &rxad,sizeof (*addr));
                    break;
                case NAME :	/* Ho ricevuto un nome : stampo */
                    printf (" --- > Connesso con %s < ---\n",message + 1);
                    return;
                    
                default:
                    break;
            }
        }
    }
    printf (" === > Connesso con host ignoto < ===\n");
}

[Home Page dell'ITIS "Fermi"] [Indice Angolinux] [Precedente] [Successivo]