Il protocollo applicativo TCP/IP fornisce gli strumenti per realizzare un protocollo applicativo (gestisce un byte stream). TCP/IP non definisce il protocollo applicativo. Una volta definito, il protocollo applicativo gode di vita propria indipendente dalle singole implementazioni. Esempio di protocollo applicativo Trasmettere stringhe di caratteri a lunghezza variabile (esempio molto comune). Il mittente ed il destinatario concordano che la fine della stringa è sancita dal carattere \0. Il funzionamento è del tutto analogo al meccanismo di cin e cout del C++ (!!). 1
Un esempio di client/server Interprete comandi distribuito. Viene utilizzato il seguente protocollo: il client si connette al server e gli invia un comando (di un solo carattere): a è il comando senza parametri; b è il comando seguito da alcuni parametri, in formato stringa; il server esegue il comando specificato ed invia come risposta una stringa terminata da *\n. Interprete comandi distribuito Il client riconosce, nella risposta inviata dal server, la stringa *\n. Deve quindi essere in grado di distinguere: *a\n ; *x\n ; x*a\n ; a***********\n. 2
La macchina a stati finiti Consente di descrivere il funzionamento di un sistema individuando: uno stato iniziale (a t=0); gli stati nei quali il sistema può trovarsi; le transizioni di stato (da uno stato corrente ad uno stato successivo); gli ingressi che causano una transizione di stato; uno o piu stati finali. E anche detta fsm (finite state machine). Funzionamento del client Invia comando E-/E Chiudi conn. A-/A B-/B arg Attendi risposta -(!= \n )/- -(!= * )/- - * /- - \n /- Attendi \n - * /- Notazione: xy/z: x = input da terminale y = input dal server z = output verso il server - indica informazione irrilevante 3
Funzionamento del server Dialogo di connessione Attesa conness. \0 /- E/- Notazione: x/z : x = input dal client z = output verso il client - indica uscita irrilevante Esegui A Leggi param. 2!= \0 /-!= \0 /- A/- B/- Attesa comando Leggi param. 1 \0 /- -/ risposta -/ *\n Termina risposta -/ risposta Esegui B Listato di UClient3 (i) #include <stdio.h> #include <sys/types.h> #include <sys/socket.h> #include <netinet/in.h> #define PORT 4000 void addr_initialize(); /* programma UCLIENT3 */ void main(int argc, char* argv[]) int sd; struct sockaddr_in server_addr; int error,fine; char command, combuf[2],result; char param1[]="alfa"; char param2[]="beta"; addr_initialize(&server_addr, PORT, inet_addr(argv[1])); sd=socket(af_inet,sock_stream,0); error=connect(sd,(struct sockaddr*) &server_addr,sizeof(server_addr)); 4
Listato di UClient3 (ii) if (error==0) printf("ho eseguito la connessione\n"); while(1) printf("\ninserire il comando seguito da CR:"); read(0,combuf,2); command=combuf[0]; send(sd,&command,1,0); if (command=='e') break; if (command=='b') send(sd,param1,5,0); send(sd,param2,5,0); } fine=0; Listato di UClient3 (iii) } while(fine!=1) recv(sd,&result,1,0); while (result=='*') recv(sd,&result,1,0); if (result=='\n') fine=1; else printf("*");} printf("%c",result); } } printf("\nchiudo la connessione \n"); close(sd); } else printf("%s","\nerrore di connect\n\n"); 5
Listato di UServer3 (i) /* programma USERVER3 */ #include <stdio.h> #include <sys/types.h> #include <sys/socket.h> #include <netinet/in.h> #define PORT 4000 #define MAXCONN 5 void addr_initialize(); void main(int argc, char * argv[]) int sd,new_sd; struct sockaddr_in server_addr; struct sockaddr_in client_addr; int client_len=sizeof(client_addr); char inbuf; char rispostaa[]="**il Server ha eseguito il comando A**"; char rispostab[]="**il Server ha eseguito il comando B**"; char errmessage[]="**comando errato**"; char terminatore[2]='*','\n'}; Listato di UServer3 (ii) addr_initialize(&server_addr, PORT, INADDR_ANY); sd=socket(af_inet,sock_stream,0); bind(sd,(struct sockaddr*) &server_addr,sizeof(server_addr)); listen(sd,maxconn); while (1) printf("\nmi pongo in attesa di richieste di connessione\n"); new_sd=accept(sd,(struct sockaddr*) &client_addr, &client_len); printf("ho accettato una connessione\n"); do recv(new_sd,&inbuf,1,0); printf("\nho ricevuto il comando %c",inbuf); switch (inbuf) case 'e':break; case 'a':send(new_sd,rispostaa,sizeof(rispostaa),0); send(new_sd,terminatore,2,0);break;} 6
Listato di UServer3 (iii) } case 'b':printf("\ni parametri sono:"); do recv(new_sd,&inbuf,1,0); printf("%c",inbuf); } while (inbuf!='\0'); printf(" "); do recv(new_sd,&inbuf,1,0); printf("%c",inbuf); } while (inbuf!='\0'); send(new_sd,rispostab,sizeof(rispostab),0); send(new_sd,terminatore,2,0);break;} default:send(new_sd,errmessage,sizeof(errmessage),0); send(new_sd,terminatore,2,0);break;} } } while (inbuf!='e'); close(new_sd); printf("\nchiudo la connessione\n"); } close(sd); 7
8
La programmazione di rete in ambiente multiprocesso Il parallelismo nei server 9
Necessità di parallelismo nei server Server sequenziali: accettano una sola connessione alla volta; non sono pertanto particolarmente utili. E solo una comunicazione tra due processi per volta, eventualmente accodando altri processi che vogliano connettersi allo stesso server (listen); sono anche detti monoconnessione. Necessità di parallelismo nei server Server paralleli: superano i limiti dei server sequenziali sfruttando il meccanismo di parallelismo; utilizzeremo il parallelismo di Unix, trascurando quello di Windows; il client non ha modo di distinguere se si è connesso ad un server sequenziale o ad un server parallelo: può solo accorgersi di una connessione rifiutata. 10
Funzionamento di un server parallelo Il server si pone in attesa di richieste di connessioni (accept) su un port srvport; alla ricezione di una richiesta di connessione (connect del client su srvport), il server crea tramite la fork un nuovo processo detto figlio, identico a se stesso (processo padre, impropriamente detto anche server principale). All interno del server parallelo Dopo la creazione del processo figlio, il padre chiude la connessione col client e rimane in attesa di nuove richieste di connect, che utilizzeranno sempre lo stesso srvport; Il figlio utilizza il socket aperto dal padre alla connect. Al termine, il figlio chiude la connessione (close) e termina (exit). 11
Funzionamento di base di un server parallelo while (1) /* il server è in un ciclo perenne */ new_sd = accept(sd,...) pid = fork(); if (pid==0) /*sono nel figlio */ /*esegui le richieste del client utilizzando il socket connesso new_sd; alla fine chiudi new_sd ed esegui exit */ } else /* sono nel padre */ /*chiudi new_sd e riprendi ad eseguire la accept su sd */ } } Realizzazione di un server parallelo Modificare UServer3 così da renderlo un server parallelo (UServer4). n.b. sui calcolatori Unix, il tipo pid_t coincide con il tipo int. Verrà quindi utilizzata la dichiarazione del tipo: int p_id; 12
Listato di UServer4 (i) #include <stdio.h> #include <sys/types.h> #include <sys/socket.h> #include <netinet/in.h> #define PORT 4000 #define MAXCONN 5 /* programma USERVER4 */ void addr_initialize(); void main(int argc, char * argv[]) int sd,new_sd; int pid; char inbuf; struct sockaddr_in server_addr; struct sockaddr_in client_addr; int client_len=sizeof(client_addr); char rispostaa[]="**il Server ha eseguito il comando A**"; char rispostab[]="**il Server ha eseguito il comando B**"; char errmessage[]="**comando errato**"; char terminatore[2]='*','\n'}; Listato di UServer4 (ii) addr_initialize(&server_addr, PORT, INADDR_ANY); sd=socket(af_inet,sock_stream,0); bind(sd,(struct sockaddr*) &server_addr,sizeof(server_addr)); listen(sd,maxconn); while (1) printf("\nmi pongo in attesa di richieste di connessione\n"); new_sd=accept(sd,(struct sockaddr*) &client_addr, &client_len); printf("ho accettato una connessione da: %d\n", ntohs(client_addr.sin_port)); pid=fork(); 13
Listato di UServer4 (iii) if (pid==0) do recv(new_sd,&inbuf,1,0); printf("ho ricevuto il comando %c ",inbuf); printf("da: %d \n", ntohs(client_addr.sin_port)); switch (inbuf) case 'e':break; case 'a':send(new_sd,rispostaa, sizeof(rispostaa),0); send(new_sd,terminatore,2,0);break;} case 'b':printf("i parametri sono: "); dorecv(new_sd,&inbuf,1,0); printf("%c",inbuf); }while (inbuf!='\0'); printf(" "); do recv(new_sd,&inbuf,1,0); printf("%c",inbuf); }while (inbuf!='\0'); printf(" \n"); send(new_sd,rispostab,sizeof(rispostab),0); send(new_sd,terminatore,2,0);break;} Listato di UServer4 (iv) default:send(new_sd,errmessage, sizeof(errmessage),0); send(new_sd,terminatore,2,0);break;} } } while (inbuf!='e'); printf("\nchiudo la connessione con: %d\n\n", ntohs(client_addr.sin_port)); close(new_sd); exit(); } /* chiude if (pid == 0) */ close(new_sd); /* il padre chiude il socket che */ /* viene utilizzato dal figlio */ } /* chiude while (1) */ } /* chiude main */ 14
Esecuzione di UServer4 Consideriamo ora un esempio di esecuzione di UServer4 che accetta connessioni da 3 distinti processi, rispettivamente C1, C2 e C3. C1, C2 e C3 sono esecuzioni del programma UClient3 (o WClient3). UClient3 non ha modo di riconoscere se il server cui si collega è sequenziale o parallelo. Esecuzione di UServer4 I processi figli di UServer4 utilizzano la stessa sessione di terminale del processo padre. I messaggi del padre e dei figli saranno alternati su una unica finestra. 15
Avvio di UServer4 C1 esegue il comando a 16
Avvio di C2 C2 esegue i comandi a e b 17
C1 esegue il comando x C2 esegue il comando y 18
Avvio di C3 C3 esegue il comando a 19
C2 chiude la connessione C1 esegue il comando b 20
C3 esegue il comando 3 C3 chiude la connessione 21
C1 chiude la connessione Invocazione di programmi di servizio Un server presenta un interfaccia attraverso cui differenti client richiedono a quel server l esecuzione di programmi già esistenti. Il server parallelo (fork e generazione di figlio per ogni connessione ricevuta) deve: eseguire il programma, specificato dal client, tramite una exec passandogli il descrittore del socket; attendere il termine del programma (wait). 22
UServer5 Rispetto a Userver4: è sempre multiprocesso, quindi per ogni connessione esegue una fork; ad ogni richiesta dal client, genera un ulteriore processo (tramite una fork) che esegue il comando indicato dal client (tramite la execve); passa al programma invocato (MainA o MainB) il descrittore del socket per comunicare col client. Listato di UServer5 (i) #include <stdio.h> #include <sys/types.h> #include <sys/socket.h> #include <netinet/in.h> #define PORT 4000 #define MAXCONN 5 /* programma USERVER5 */ void addr_initialize(); void main(int argc, char * argv[]) int sd,new_sd; char inbuf; struct sockaddr_in server_addr; struct sockaddr_in client_addr; int client_len=sizeof(client_addr); char errmessage[]="**comando errato**"; /* rispetto a UServer4 mancano */ char terminatore[2]='*','\n'}; /* rispostaa e rispostab */ char sdstring[7]; /* nuovo rispetto a UServer4 */ char *envp[3] = NULL, NULL, NULL}; int pid,my_pid,status; 23
Listato di UServer5 (ii) addr_initialize(&server_addr, PORT, INADDR_ANY); sd=socket(af_inet,sock_stream,0); bind(sd,(struct sockaddr*) &server_addr,sizeof(server_addr)); listen(sd,maxconn); while (1) printf("\nmi pongo in attesa di richieste di connessione\n"); new_sd=accept(sd,(struct sockaddr*) &client_addr, &client_len); printf("ho accettato una connessione da: %d\n", ntohs(client_addr.sin_port)); pid=fork(); if (pid==0) do recv(new_sd,&inbuf,1,0); printf("ho ricevuto il comando %c ",inbuf); printf("da: %d \n", ntohs(client_addr.sin_port)); Listato di UServer5 (iii) switch (inbuf) case 'e':break; case 'a':sprintf(sdstring,"%d",new_sd); argv[1]=sdstring; pid=fork(); if (pid==0) execve("maina", argv, envp);} else my_pid = wait(&status); break;}} case 'b':sprintf(sdstring,"%d",new_sd); argv[1]=sdstring; pid=fork(); if (pid==0) execve("mainb", argv, envp);} else my_pid = wait(&status); break;}} default: send(new_sd,errmessage, sizeof(errmessage),0); send(new_sd,terminatore,2,0);break;} } /* chiude switch */ } while (inbuf!='e'); /* chiude do */ 24
Listato di UServer5 (iv) printf("\nchiudo la connessione con: %d\n\n", ntohs(client_addr.sin_port)); close(new_sd); exit(); } /* chiude if (pid == 0) */ close(new_sd); /* il padre chiude il socket che */ /* viene utilizzato dal figlio */ } /* chiude while (1) */ } /* chiude main */ Listato di MainA /* programma maina */ void main(int argc, char * argv[]) int sd; char terminatore[2] = '*','\n'}; char rispostaa[] = "**MainA ha eseguito il comando A**"; } printf(" MainA\n"); sd = atoi(argv[1]); send(sd, rispostaa, sizeof(rispostaa),0); send(sd, terminatore,2,0); printf(" MainA: ho terminato\n"); 25
Comunicazione UClient3-MainB La comunicazione tra UClient3 e MainB avviene attraverso il descrittore del socket. UClient3 non si accorge di parlare con MainB: è convinto di parlare con UServer5. MainB riceve sulla linea di comando il descrittore del socket per colloquiare con UClient3. Listato di MainB /* programma mainb*/ void main(int argc, char * argv[]) int sd; char inbuf; char rispostab[] = "**MainB ha eseguito il comando B**"; char terminatore[2] = '*','\n'}; } sd = atoi(argv[1]); printf(" MainB: i parametri sono:"); do recv(sd, &inbuf, 1, 0); printf("%c", inbuf); } while (inbuf!= '\0'); prinf(" "); do recv(sd, &inbuf, 1, 0); printf("%c",inbuf); } while (inbuf!= '\0'); printf("\n" ); send(sd, rispostab, sizeof(rispostab),0); send(sd, terminatore, 2, 0); printf(" MainB: ho terminato\n"); 26
Avvio di UServer5 UClient3 richiede il comando e lo invia al server 27
UServer5 riceve il comando b UServer5 lancia il programma MainB 28
UClient3 riceve i messaggi da MainB UClient3 esegue il comando b 29
UClient3 esegue il comando x UClient3 esegue il comando a 30
UClient3 termina Considerazione conclusive sui server Molti server sono attivati quando la macchina viene accesa e devono rimanere sempre attivi (ad es. server Web). In Unix sono detti daemons. Spesso i daemons non devono disporre di una sessione di interprete comandi e di terminale associati (esecuzione in background). Non dispongono di un terminale associato al processo. 31
Considerazioni conclusive sui server I daemons utilizzano file di log, sui quali scrivono eventuali messaggi di errore. Per alcuni servizi vengono utilizzati port riservati (con numero compreso tra 0 e 1023). Per avviare un server che utilizzi un port riservato è necessario avere i privilegi di gestore del sistema. 32