Generazione server unix

Da Tesine Linguaggi e Traduttori.

Jump to: navigation, search

Contents

Introduzione

La creazione di server (iterativi o concorrenti) in C prevede principalmente due parti: una del tutto variabile, costituita dalle routine domain specific attraverso le quali il server offre le funzionalità previste, ed una piuttosto ripetitiva, costituita dalla struttura di codice necessaria affinché il server possa accettare una o più connessioni simultanee secondo configurazioni ragionevolmente limitate. L’idea è dunque quella di definire un linguaggio la cui sintassi consenta la predisposizione veloce ed agevole di un server C sollevando il programmatore dalla necessità di conoscere tutti i passi necessari alla definizione della sua struttura.

Paradigma di funzionamento

Lo scenario che stiamo ipotizzando è il seguente: Un programmatore ha realizzato una funzione in C che si appoggia ad una serie di librerie per le quali possiede i file sorgente .h e .c; essendo interessato ad esporre tale funzionalità calando il codice in un’architettura client-server, si scontra con una serie di problematiche legate alla programmazione di rete per sistemi Unix. Ognuna delle scelte da lui operate in termini di design della soluzione (server concorrente o iterativo, basato su TCP o UDP,…) influenza la sintassi del codice necessario al funzionamento desiderato. La variabilità di tale codice di base risulta tuttavia predicibile sotto certe assunzioni preliminari. Giusto per fare un esempio che faccia chiarezza sul senso di tutto questo, mostriamo di seguito una porzione di codice C necessaria a definire una connessione TCP piuttosto che UDP rispettivamente:

TCP:

   //Variabili usate per la gestione del socket
   struct sockaddr_in server_addr, client_addr;
   
   //Creazione del socket tramite la funzione Socket
   listen_fd = Socket(AF_INET, SOCK_STREAM, 0);
   
   //Inizializzia la struttura socket listen_fd
   memset(&server_addr, 0, sizeof(server_addr));
   
   server_addr.sin_family = AF_INET;
   server_addr.sin_port = htons(port);
   server_addr.sin_addr.s_addr = htonl(INADDR_ANY);
   
   //Assegna un identificativo al socket listen_fd
   Bind(listen_fd, (SA*) &server_addr, sizeof(server_addr));

UDP:

   //Variabili usate per la gestione del socket
   struct sockaddr_in server_addr, client_addr;
   
   //Creazione del socket tramite la funzione Socket
   sockfd = Socket(AF_INET, SOCK_DGRAM, 0);
   
   //Inizializzia la struttura socket listen_fd
   memset(&server_addr, 0, sizeof(server_addr));
   
   server_addr.sin_family = AF_INET;
   server_addr.sin_port = htons(port);
   server_addr.sin_addr.s_addr = htonl(INADDR_ANY);
   
   //Assegna un identificativo al socket listen_fd
   Bind(sockfd, (SA*) &server_addr, sizeof(server_addr));


Un linguaggio che punti a definire solo le parti variabili del codice appena esposto potrebbe portare allo stesso risultato semplicemente scrivendo:

TCP:

 Conn_opt(port , TCP);
 Interface(ANY,null);

UDP:

 Conn_opt(port,UDP);
 Interface(ANY,null);

Analizziamo dapprima il funzionamento generale della soluzione proposta col supporto della seguente figura:

immaginepio.jpg

- Il punto di partenza consiste in un file contenente il codice in linguaggio Easy Unix Server (.eus) che viene analizzato dalla coppia di scanner e parser :

  • scanner.jflex è l’analizzatore lessicale per il linguaggio di input prestabilito. Lo scanner, attraverso un esame carattere per carattere dell'ingresso, separa il programma sorgente in parti chiamate token. Il token, che è una stringa di caratteri, è memorizzato in una tabella. I valori delle costanti sono memorizzati in una constant table, mentre i nomi delle variabili in una symbol table. Attraverso jflex viene generata il file Lexer.java che esegue le specifiche definite dal programma in jflex.
  • parser.cup contiene l’implementazione del parser in Cup, effettua l’analisi sintattica del testo basandosi sulla grammatica specificata e infine esegue le azioni semantiche per produrre l’output desiderato. In particolare il parser genera un albero sintattico le cui foglie sono i simboli terminali della grammatica, ovvero i token che lo scanner ha estratto dal linguaggio sorgente e passato al parser. I file .java generati sono parser.java e sym.java.

Lo scanner interagisce direttamente con il parser che chiama lo scanner quando è necessario il prossimo token nell'analisi sintattica.

- Il codice in linguaggio EUS fornisce le indicazioni dove reperire il codice relativo alla funzione che vogliamo portare nel contesto client-server e quello relativo alle eventuali funzioni di gestione dei segnali (signal handlers), oltre naturalmente ai parametri del tipo di server desiderato (iterativo/concorrente, UDP/TCP, etc).

- Viene prodotto in uscita il file server.c ed il Makefile necessario in fase di compilazione del server.

I passi appena accennati verranno esaminati più nel dettaglio una volta resa chiara la sintassi del linguaggio EUS.

Sintassi del linguaggio EasyUnixServer

Server {
           General {
		Working_directory(path);  
		SetConcurrent (num_child);  		
                   }

	Connection {	
		Conn_opt(port , protocol); 
		Interface(mode , param);
		Listen_q (qDim);		
		Sock_opt(opt_name1 , value); 	
		Sock_opt(opt_name2 , value); 
		Sock_opt(opt_nameN , value);
	           }

	Service	{
		Main_task(file_name, task_name);
		Libraries(lib1_name , lib2_name  , libN_name);
		Target_lib(main_lib);
	        }

	Signals {
		Sig_custom_file(file_name); 
		Sig_custom(sig_name1 , handler);
		Sig_custom(sig_name2 , handler);
		Sig_custom(sig_nameN , handler);
		Sig_ignore(sig_name1 , sig_name2 ,  sig_nameN); 
	        }

}


Un programma EasyUnixServer è composto un unico blocco (nodo radice). Un blocco è definito come un simbolo identificativo della grammatica (Server) seguito da una “{“, terminata da una “}” e formato al suo interno da una lista_sezioni o da una singola sezione. Una sezione è definita come un simbolo identificativo della grammatica (General, Connection, Signals, Service) seguito da “{“, terminata da una “}” e formata al suo interno da una lista_funzioni o da una singola funzione. Una funzione è definita come:

  • un simbolo identificativo della grammatica seguito da “(“, terminata da una “)” più un “;” e formata al suo interno da un altro simbolo identificativo della grammatica (caso Working_dir con Path),
  • un simbolo identificativo della grammatica seguito da “(“, terminata da una “)” più un “;” e formata al suo interno da un simbolo identificativo (Intero), una “,” e da un protocollo (caso Conn_opt),
  • un simbolo identificativo della grammatica seguito da “(“, terminata da una “)” più un “;” e formata al suo interno un modo, una “,” e da un da un simbolo identificativo (Ip), (caso Interface),
  • un simbolo identificativo della grammatica seguito da “(“, terminata da una “)” più un “;” e formata al suo interno da una lista_atomo o da un singolo atomo (caso Sig_custom_file, Sig_custom, Sig_ignore, Main_Task, Libraries),
  • un simbolo identificativo della grammatica seguito da “(“, terminata da una “)” più un “;” e formata al suo interno da una lista_atomo o da un singolo atomo, una “,” e da un simbolo identificativo (Intero)(caso Sock_opt).

Protocollo è un TCP o un UDP, modo un SINGLE o un ANY. Una lista_atomo è definita come una lista non vuota di atomi separati dal carattere “,”.


Programmare in linguaggio EasyUnixServer

Il nostro linguaggio EasyUnixServer è composto, come accennato sopra, da un macroblocco chiamato Server, costituito, a sua volta, da quattro principali sezioni: General, Connection, Service, Signals. La sezione Signals è comunque opzionale e la sua utilità verrà esplicitata in fondo al presente paragrafo.


Sezione General

Tale sezione è stata pensata per ospitare le funzioni utili ad impostare la Working Directory e ad indicare la necessità di creare un server concorrente scegliendo il numero massimo di processi “figli” utilizzabili.

General {
	Working_dir(path);  
	SetConcurrent (num_child);  		
        }

La sezione General è costituita dalle seguenti funzioni:

  • Working_dir(path): tale funzione vuole come argomento il path (unix) della working directory all’interno della quale verrà collocato il server creato ed il Makefile.
  • Set_concurrent(num_child): tale funzione serve ad impostare il server in modalità “concorrente”. Vuole come argomento il massimo numero di figli (processi) raggiungibile. Nello specifico nel nostro serve ci sarà la funzione fork che si occupa, appunto, della gestione dei processi. L’uso della fork avviene secondo due modalità principali: la prima è quella in cui all’interno di un programma si creano processi figli cui viene affidata l’esecuzione di una certa sezione di codice, mentre il processo padre ne esegue un’altra. La seconda è quella in cui il processo vuole eseguire un altro programma(caso shell). La modalità che a noi interessa è la prima. Infatti il nostro è il caso tipico di un programma server (modello client-server) in cui il padre riceve ed accetta le richieste da parte dei programmi client, per ciascuna delle quali pone in esecuzione un figlio che è incaricato di fornire il servizio. La chiamata a fork può fallire se è stato raggiungo il numero massimo di processi figlio permessi (l’argomento della nostra funzione).


Sezione Connection

Connection {	
	      Conn_opt(port , protocol); 
	      Interface(mode , param);
	      Listen_q(qDim);
	      Sock_opt(opt_nameN , value);
	   }

La sezione Connection è costituita dalle seguenti funzioni:

  • Conn_opt(port , protocol): tale funzione prende come argomenti il numero di porta del socket (i numeri da 0 a 1024 sono riservati per le porte “note”) e il nome del protocollo che serve a specificare se il nostro server deve essere basato su TCP o UDP. In un ambiente multitasking in un dato momento più processi devono poter usare sia UDP che TCP, e ci devono poter essere più connessioni in contemporanea. Per poter tenere distinte le diverse connessioni entrambi i protocolli usano, appunto, i numeri di porta, che fanno parte della struttura degli indirizzi del socket.
  • Interface(mode , param): tale funzione vuole due argomenti. Il primo “mode” si riferisce all’indirizzo di un’interfaccia che, insieme al numero di porta, costituisce l’indirizzo del socket in una rete IP. “Mode” può assumere due valori, ANY o SINGLE. Nel primo caso il server si porrà in ascolto su tutte le interfacce, quindi “param” può assumere qualsiasi valore (indirizzo generico 0.0.0.0); nel secondo caso è necessario specificare un’interfaccia valida (nel caso se ne voglia verificare il funzionamento su una singola macchina sarà necessario specificare l’interfaccia 127.0.0.1).
  • Listen_q(qDim): tale funzione serve ad impostare la dimensione della coda di listening prevista dalla funzione C Listen in ambito TCP. L’argomento passato è dunque un intero.
  • Sock_opt(opt_nameN , value): tale funzione serve a impostare le “socket options”. Il primo argomento è quindi il nome dell’opzione (tra quelle definite per il protocollo corrente) su cui operare. Il secondo argomento indica invece il valore che deve assumere la variabile associata all’opzione. Tale variabile può essere un flag il cui valore può essere 0 o diverso da 0. Il primo caso indica che l’opzione deve essere disabilitata, abilitata nel secondo caso. In altri casi “value” è un intero che non assume il ruolo di flag ma di valore, come per le opzioni SO_RCVBUF SO_SNDBUF dove “value” assume il significato della dimensione del buffer rispettivamente di ricezione e di invio.

Le opzioni supportate dal nostro progetto sono quelle appartenenti alla famiglia SOL_SOCKET. Di seguito riportiamo una tabella riassuntiva che dia un’indicazione su quali siano le opzioni disponibili per TCP ed UDP della famiglia citata.

Socket options per TCP e UDP

Socket Option per TCP:

       
SO_DEBUG                                                                      
SO_ERROR 
SO_KEEPALIVE                                                               
SO_RCVBUF SO_SNDBUF                                                                       
SO_RCVLOWAT AND SO_SNDLOWAT 
SO_REUSEADDR  AND SO_REUSEPORT
SO_USELOOPBACK

Socket options per UDP:

        
SO_BROADCAST
SO_RCVBUF SO_SNDBUF
SO_RCVLOWAT AND SO_SNDLOWAT
SO_REUSEADDR  AND SO_REUSEPORT
SO_USELOOPBACK

Sezione Service

Questa sezione è dedicata alle funzioni utili al reperimento del codice che implementa le funzionalità che il programmatore vuole esporre con l’architettura client-server e le librerie di supporto necessarie a tali funzionalità.

Service	{
	Main_task(file_name, task_name);
	Libraries(lib1_name , lib2_name  , libN_name);
	Target_lib(lib_name);
               }

La sezione Service è costituita dalle seguenti funzioni:

  • Main_task(file_name, task_name): tale funzione serve ad indicare il nome della routine principale del server (secondo argomento) scritta su un file il cui nome è dato dal primo argomento passato.
  • Libraries(lib1_name , lib2_name , libN_name): tale funzione prende come argomenti I nomi delle librerie di supporto al nostro server.
  • Target_lib(lib_name): questa funzione serve a definire il nome della libreria principale che verrà create a valle della compilazione da effettuare mediante Makefile.


Sezione Signals

Questa sezione non è obbligatoria in quanto concerne la gestione personalizzata (di fatto non obbligatoria) di alcuni segnali noti in ambiente Unix. I segnali vengono generati dal kernel per un processo all’occorrenza dell’evento che causa il segnale. Si dice che il segnale è viene consegnato al processo quando viene eseguita l’azione per esso prevista, mentre per tutto il tempo che passa fra la generazione del segnale e la sua consegna esso è detto pendente.

Signals {
	Sig_custom_file(file_name); 
	Sig_custom(sig_nameN , handler);
	Sig_ignore(sig_name1 , sig_name2 ,  sig_nameN); 
	}

La sezione Signals è costituita dalle seguenti funzioni:

  • Sig_custom_file(file_name): tale funzione vuole come argomento il nome del file dove si trova il codice degli handlers di eventuali segnali da gestire in modo customizzato.
  • Sig_custom(sig_nameN , handler): tale funzione ha due argomenti. Il primo è il nome del segnale, il secondo rappresenta il “signals handler” al quale registrare il segnale in questione.
  • Sig_ignore(sig_name1 , sig_name2 , sig_nameN): tale funzione ha come argomenti i nomi dei segnali che devono essere ignorati durante l’esecuzione del server.


La classe CodeGenerator

Nella stessa cartella dello scanner e del parser troviamo la classe Java CodeGenerator. Essa si occupa di memorizzare temporaneamente tutti i parametri forniti dall’utente all’interno del file di input in codice EUS. Inoltre espone il metodo generate() il quale si occupa di trasformare l’insieme di parametri memorizzati in un server in C compilabile e funzionante.

I metodi esposti sono:

  • una serie per settare le variabili necessarie alla parametrizzazione del server:

immagine4c.jpg


  • i metodi per la creazione del MakeFile:
 public void createMakeFile()
  • i metodi per la generazione del server sulla base dei parametri memorizzati:
   public void generate()


Vincoli ed assunzioni

Il corretto funzionamento della soluzione proposta è subordinato al rispetto di alcuni vincoli ed assunzioni preliminary:

Vincoli generali

  • Il server creato sarà in grado di rispondere su una rete IP;
  • La routine principale non può avere parametri d’ingresso; l’unico parametro consiste nel file descriptor della socket nel caso TCP o i riferimenti al sender del DATAGRAM nel caso UDP.
  • Le socket options disponibili sono quelle della famiglia SOL_SOCKET.
  • Non è supportata la generazione di server concorrenti basati su UDP, soluzione assai rara e poco raccomandata in letteratura.

Vincoli sul linguaggio

Alcune direttive fornite attraverso il codice EUS sono incompatibili fra loro:

  • La scelta di un server di tipo concorrente (direttiva SetConcurrent(num_child)) esclude il protocollo UDP;
  • Nell’ambito di un server basato su UDP la direttiva Listenq(qDim) non ha alcun valore.


Download e funzionamento

Per far funzionare il progetto proposto e verificarne dunque il funzionamento sono necessari alcuni semplici passi:

Cygwin, disponibile all’indirizzo http://www.cygwin.com/

Jflex, disponibile all’indirizzo http://jflex.de/

Cup, disponibile all’indirizzo http://www.cs.princeton.edu/~appel/modern/java/CUP/

Java, disponibile all’indirizzo http://www.java.com/it/download/index.jsp

  • Provvedere all’installazione degli stessi secondo le indicazioni fornite sui siti di download.
  • Scrivere una semplice funzione in C e salvare il file in formato .c (es. mainRoutine.c). Si suggerisce una funzione che si metta in attesa di un input e, verificata la correttezza dello stesso, risponda con un semplice carattere sulla socket di connessione. Per verificare il funzionamento dell’inclusione di librerie esterne, si suggerisce di inserire nella routine principale la chiamata ad una libreria esterna per la quale si dispone dei file .c e .h.
  • In una cartella, che chiamerò source directory, posizionare i file parser.cup, scanner.jflex, CodeGenerator.java, mainRoutine.c e opzionalmente il file contenente gli handlers dei segnali.
  • In un’altra cartella, quella che nel corso della documentazione è stata chiamata working directory, posizionare le librerie esterne (sia il file .c che il .h)

Test e verifiche

L’operatività delle varie funzioni previste dal progetto in esame può essere verificata tramite i file allegati alla presente documentazione. Nello specifico, è possibile fare riferimento alle due cartelle sourceDirectory e workingDirectory.

Contenuto della cartella sourceDirectory:

CodeGenerator.java : sorgente della classe CodeGenerator; parser.cup: parser; scanner.jflex: scanner; main_routine_TCP.c : sorgente di una main routine testabile in ambito TCP tramite telnet; main_routine_UDP.c : routine di una main routine compatibile con il protocollo UDP che implementa la funzionalità ECHO;

concurrent_TCP_input.eus : codice EUS utile alla creazione di un server

  • basato su TCP;
  • concorrente;
  • con un limite di processi figlio pari a due;
  • reattivo su tutte le interfacce alla porta 12345;
  • coda di listening di dimensione 20;
  • che implementa l’opzione SO_REUSEADDR sulla socket;
  • che sfrutta le librerie di supporto sockwrap ed errlib (per le quali sono disponibili i file .c e .h nella workingDirectory);
  • in grado di gestire in modo customizzato i segnali SIGCHLD e SIGINT tramite gli handler forniti all’interno del file signal_management.c;
  • che ignora il segnale SIGPIPE;

iterative_TCP_input.eus : codice EUS utile alla creazione di un server

  • basato su TCP;
  • reattivo sull’interfaccia 127.0.0.1 alla porta 12345;
  • che sfrutta le librerie di supporto sockwrap ed errlib (per le quali sono disponibili i file .c e .h nella workingDirectory);

UDP_input.eus codice EUS utile alla creazione di un server:

  • basato su UDP;
  • reattivo sull’interfaccia 127.0.0.1 alla porta 12345;
  • che implementa l’opzione SO_BROADCAST sulla socket;
  • che sfrutta le librerie di supporto sockwrap ed errlib (per le quali sono disponibili i file .c e .h nella workingDirectory);

Nella cartella workingDirectory troviamo invece le librerie errlib e sockwrap di supporto alla routine principale.


Il server più complesso è di certo quello concorrente basato su TCP. Di seguito proponiamo dunque una serie di passi per verificare la correttezza della sua generazione.

  • Aprire il file concurrent_TCP_input.eus con un normale text editor e modificare il path, passato come argomento della funzione Working_dir alli’nterno della sezione General, coerentemente con la root impostata per cygwin sulla macchina;
  • Posizionarsi con il prompt di comandi sulla sourceDirectory e creare scanner e parser tramite i comandi:

jflex scanner.jflex java java_cup/Main parser.cup

  • Compilare tutti i file java all’interno della sourceDirectory;
  • Lanciare l’analisi del file di input tramite il comando:

java parser concurrent_TCP_input.eus

  • Se la sintassi del file d’ingresso è riconosciuta come corretta la workingDirectory dovrebbe contenere i file server.c e Makefile. Aprire dunque una shell Cygwin e portarsi sulla workingDirectory. Compilare il server con il comando:

make server

  • Lanciare dunque il server con il comando:

run server

  • A questo punto aprire una nuova shell di Cygwin e procedere ad una connessione con il server tramite il comando:

telnet 127.0.0.1 12345

  • Digitare il carattere ‘X’ e premere invio. Il server dovrebbe rispondere con il codice ASCII 1.
Personal tools