Differences between revisions 10 and 29 (spanning 19 versions)
Revision 10 as of 2009-04-26 20:52:30
Size: 10180
Editor: lukisi
Comment:
Revision 29 as of 2009-05-04 15:51:42
Size: 6181
Editor: lukisi
Comment:
Deletions are marked like this. Additions are marked like this.
Line 3: Line 3:
Quando viene creato un socket usando il wrapper {{{ntk.wrap.sock.Sock}}}, viene registrato nella mappa di {{{asyncore}}} (un modulo standard di python stackless) e viene avviato un microthread nel '''modulo microsock'''. Lo scopo del '''modulo microsock''' è quello di permettere la gestione di operazioni di I/O sui socket in un ambiente multithread cooperativo.
<<BR>>
In un tale ambiente è necessario evitare l'uso di chiamate di sistema bloccanti per l'intero processo, come sono le chiamate di I/O sui socket.

Quando viene creato un socket usando il wrapper {{{ntk.wrap.sock.Sock}}}, viene in effetti creato anche un oggetto {{{asyncore.dispatcher}}} che lo incapsula e questo viene registrato nella mappa di {{{asyncore}}} (un modulo standard di python stackless); inoltre viene avviato un microthread nel modulo microsock.
<<BR>>
Il microthread avviato esegue la funzione {{{ManageSockets}}}. Questa richiama {{{asyncore.poll(0.05)}}} ciclicamente (intervallando con {{{micro_block}}} per passare lo scheduler agli altri) fin quando esiste un socket (nel caso di Netsukuku per sempre) nella mappa di asyncore.
Line 6: Line 12:
<<BR>>
Il microthread avviato esegue la funzione {{{ManageSockets}}}. Questa richiama {{{asyncore.poll(0.05)}}} ciclicamente (intervallando con {{{micro_block}}} per passare lo scheduler agli altri) fin quando esiste un socket (nel caso di netsukuku per sempre) nella mappa di asyncore.
Line 15: Line 19:
Il metodo {{{asyncore.poll}}}, tramite il metodo {{{select.select}}}, scopre quali dei socket registrati nella sua mappa ha dei dati da inviare o da ricevere. Il metodo {{{asyncore.poll}}}, per prima cosa fa uso dei due metodi {{{readable}}} e {{{writable}}} di {{{asyncore.dispatcher}}} su tutti i dispatcher registrati nella sua mappa. Questi metodi possono essere overridati; nella implementazione base restituiscono {{{True}}}. Il loro obiettivo è quello di identificare i socket che hanno ''intenzione'' di inviare ({{{writable}}}) o di leggere ({{{readable}}}) qualcosa.
Line 17: Line 21:
Vediamo quali passi segue la creazione e connessione di un socket, prima di analizzare quello che avviene quando si trasmette o riceve qualcosa. Di seguito, per i socket che sono readable o writable secondo i significati sopra esposti, tramite il metodo {{{select.select}}} scopre quali hanno dei dati da ricevere oppure sono subito in grado di inviare dati. Cioè per quali socket le chiamate {{{recv}}} e {{{send}}} non risulterebbero bloccanti.
Line 19: Line 23:
== Creazione di un socket ==
Quando si crea un socket all'interno del programma (con {{{socket.socket(...)}}}) avvengono queste operazioni:
 1. viene effettivamente creato un vero socket (con {{{stdsocket.socket(...)}}}).
 1. viene creato anche un {{{microsock.stacklesssocket}}}. Questo nel suo costruttore fa queste operazioni:
   a. memorizza il vero socket.
   a. si crea e memorizza una istanza dell'oggetto {{{microsock.dispatcher}}}, che è una derivata di {{{asyncore.dispatcher}}}.
   a. {{{microsock.dispatcher}}} nel suo costruttore fa queste operazioni:
     I. memorizza il vero socket.
     I. la classe ha i membri {{{connectChannel}}}, {{{acceptChannel}}} e {{{recvChannel}}}.
     I. istanzia per {{{recvChannel}}} un Channel con {{{micro_send = True}}}.
     I. prepara una stringa buffer e una lista per i send ({{{sendBuffer}}} e {{{sendToBuffers}}})
     I. prepara una stringa buffer e una lista per i recv ({{{readBufferString}}} e {{{readBufferList}}})
     I. prepara una costante {{{maxreceivebuf}}} = 65536
     I. memorizza il vero socket nella mappa di asyncore (questo lo fa {{{asyncore.dispatcher.__init__}}} => {{{asyncore.dispatcher.set_socket}}} => {{{asyncore.dispatcher.add_channel}}})
     I. la sua rimozione dalla mappa avverrà con {{{microsock.stacklesssocket.__del__}}} => {{{microsock.dispatcher.close}}} => {{{asyncore.dispatcher.close}}} => {{{asyncore.dispatcher.del_channel}}}
 1. viene restituito l'oggetto {{{microsock.stacklesssocket}}}.
In seguito tutte le chiamate {{{__getattr__}}} al finto socket (cioè all'oggetto stacklesssocket ritornato) vengono inoltrate al dispatcher (esempi di queste chiamate sono {{{s.listen}}}, {{{s.bind}}}, ...) mentre le chiamate {{{__setattr__}}} vengono memorizzate localmente. ???
Nel modulo microsock viene definita la classe {{{microsock.dispatcher}}} che è una classe derivata di {{{asyncore.dispatcher}}}. Grazie a questa classe possiamo definire come deve agire il programma quando un microthread richiama i classici metodi di I/O su un presunto socket. Per una analisi di dettaglio rimandiamo alle pagine sulla [[../ModuloMicrosockCreazione|creazione di un socket]], [[../ModuloMicrosockConnessione|connessione di due socket]] e [[../ModuloMicrosockTrasmissione|trasmissione dati tra due socket]].
Line 37: Line 25:
== Connessione di una coppia di socket ==
=== con protocollo TCP ===
Se vogliamo implementare un server con il protocollo TCP, dobbiamo creare un socket di tipo {{{SOCK_STREAM}}}, associargli indirizzo e porta, richiamare il metodo listen e poi accept.
== Il microthread che esegue ManageSockets ==
Abbiamo detto che fin dal primo momento in cui esiste un socket viene avviato un microthread che esegue la funzione {{{ManageSockets}}} presente in questo modulo.
Line 41: Line 28:
Quando si richiama il metodo '''{{{listen}}}''' di un {{{microsock.dispatcher}}} (ereditato dal {{{asyncore.dispatcher}}}) il dispatcher si memorizza lo stato {{{accepting = True}}} e poi esegue il vero metodo listen sul socket. Il metodo listen non è bloccante. La vita di questo microthread è un '''aspetto critico''' di tutto il programma Netsukuku. Se per un qualsiasi motivo questo microthread dovesse bloccarsi in attesa di un evento che tarda a verificarsi (o addirittura non si verificherà mai) l'intera struttura di comunicazione di Netsukuku si blocca.
Line 43: Line 30:
Quando si richiama il metodo '''{{{accept}}}''' di un {{{microsock.dispatcher}}} (overridato rispetto al {{{asyncore.dispatcher}}}) il dispatcher si istanzia un oggetto [[../ClasseChannel|Channel]] con {{{micro_send = True}}} e lo memorizza sul membro {{{acceptChannel}}}. Su questo oggetto richiama il metodo {{{recv}}}. Per questo motivo va usata estrema cautela in tutti i punti del codice (in particolare in questo modulo) in cui questo microthread passa e che fanno uso di chiamate bloccanti.

Le chiamate bloccanti usate in questo microthread sono di due tipi.
 * Chiamate che bloccano l'intero programma. Sono tutte quelle che riguardano l'I/O sul socket reale. Ci sono le chiamate {{{accept}}} per i server, le chiamate {{{connect}}} per i client, le chiamate {{{send}}}, {{{sendto}}} e le chiamate {{{recv}}}, {{{recvfrom}}}.
 * Chiamate che bloccano questo microthread ma lasciano spazio agli altri microthread. Sono le chiamate {{{send}}} e {{{receive}}} dei {{{stackless.channel}}} o della nostra [[../ClasseChannel|classe Channel]] che li incapsula.

Per quanto riguarda le chiamate di I/O sul socket, ci si affida alla funzione {{{select.select}}} la quale ci dice con certezza (e senza bloccarsi indefinitamente) se una certa chiamata risulterebbe bloccante. Quindi se usata correttamente ci può guidare ad un uso non bloccante delle altre chiamate.
Line 45: Line 38:
Questo metodo a sua volta richiama il metodo {{{receive}}} di uno {{{stackless.channel}}}. Questo metodo blocca la tasklet, cioè il microthread attivo, e schedula gli altri microthread. Quello bloccato non sarà più ri-schedulato fino a che non ci saranno messaggi su quel canale.
<<BR>>
Quando il socket reale riceve una richiesta di connessione, anche se non è stato chiamato in realtà il suo metodo {{{accept}}}, la funzione {{{asyncore_poll}}} (periodicamente richiamata in un microthread apposito, come detto sopra) se ne accorge come se il socket fosse in attesa di lettura. Viene quindi richiamato il metodo {{{handle_accept}}}, da overridare.
<<BR>>
Il metodo {{{handle_accept}}} (overridato rispetto al {{{asyncore.dispatcher}}}) verifica che sul {{{channel}}} contenuto sul membro {{{acceptChannel}}} ci sia qualcuno effettivamente in ascolto, guardando l'attributo {{{balance}}}. Poi richiama il metodo {{{asyncore.dispatcher.accept}}} della classe base (non più quello overridato detto prima); questo richiama {{{accept}}} del socket, ma stavolta siamo sicuri che non si bloccherà. Restituirà un socket connesso e l'indirizzo del socket remoto.
<<BR>>
Il metodo {{{handle_accept}}} poi incapsula il socket ricevuto in un oggetto {{{stacklesssocket}}} e lo invia al {{{channel}}} contenuto sul membro {{{acceptChannel}}}.
Siccome il Channel è con {{{micro_send = True}}}, questo invio viene effettuato in un nuovo microthread. '''''TODO''': Questo è davvero necessario? Ci siamo prima accertati che il balance fosse negativo...!''
<<BR>>
Il microthread prima bloccato viene ora ri-schedulato e il metodo {{{microsock.dispatcher.accept}}} restituisce un {{{stacklesssocket}}} connesso.
La funzione {{{select.select}}} è chiamata nella funzione {{{asyncore.poll}}}; noi ce ne avvaliamo facendo l'override nella classe {{{microsock.dispatcher}}} dei metodi di {{{asyncore.dispatcher}}} che vengono chiamati dalla funzione {{{asyncore.poll}}} stessa. Vedi le pagine citate sopra.
Line 56: Line 40:
Se vogliamo implementare un client con il protocollo TCP, dobbiamo creare un socket di tipo {{{SOCK_STREAM}}} e poi richiamare il metodo connect. Per quanto riguarda la trasmissione di messaggi sui channel, bisogna:
Line 58: Line 42:
Quando si richiama il metodo '''{{{connect}}}''' di un {{{microsock.dispatcher}}} questo per prima cosa richiama il metodo base, cioè il metodo {{{asyncore.dispatcher.connect}}}.
<<BR>>
Il metodo {{{asyncore.dispatcher.connect}}} utilizza il metodo {{{socket.connect_ex}}} fornito dalla libreria di Python. Questo avvia un tentativo di connessione ma la chiamata non si blocca. Invece restituisce un numero (errno) con il quale indica se il tentativo è andato a buon fine o se è in corso o se c'è stato un altro errore. <<BR>>
Se la connessione si completa, il metodo {{{asyncore.dispatcher.connect}}} richiama {{{handle_connect_event}}}, questo valorizza a True il suo membro {{{connected}}} e poi richiama il metodo {{{handle_connect}}}, da overridare. Il metodo {{{microsock.dispatcher.connect}}} in questo caso non ha bisogno di fare altro.
<<BR>>
Se, invece, la connessione non si completa, il metodo {{{asyncore.dispatcher.connect}}} ritorna. Il metodo {{{microsock.dispatcher.connect}}} se ne accorge dal fatto che il membro {{{connected}}} è ancora a False. Quindi si prepara nel suo membro {{{connectChannel}}} un Channel con {{{prefer_sender = True}}} (vedi la [[../ClasseChannel|classe Channel]]) e si mette in ascolto su quel canale. In questo modo il microthread si blocca in attesa della connessione, senza bloccare gli altri microthread.
<<BR>>
Quando il sistema completa la connessione sul socket reale, la funzione {{{asyncore_poll}}} (periodicamente richiamata in un microthread apposito, come detto sopra) se ne accorge come se il socket fosse in attesa di lettura. Viene quindi richiamato il metodo {{{handle_connect}}}.
<<BR>>
Il metodo {{{handle_connect}}} (overridato rispetto al {{{asyncore.dispatcher}}}) invia un messaggio al Channel memorizzato nel membro {{{connectChannel}}} così da ri-schedulare il microthread prima bloccato.

=== con protocollo UDP ===
Se vogliamo implementare un server con il protocollo UDP, dobbiamo creare un socket di tipo {{{SOCK_DGRAM}}}, associargli indirizzo e porta.
<<BR>>
'''''TODO''': Perché avviamo un UDPServer per ogni scheda se poi l'argomento {{{dev}}} non viene mai usato nella fase di attesa messaggi?''
<<BR>>
Dopo di questo non ci sono altre operazioni da fare, quindi nessuna bloccante, prima di passare alla trasmissione/ricezione dati.

Se vogliamo implementare un client con il protocollo UDP, dobbiamo creare un socket di tipo {{{SOCK_DGRAM}}}, associare il socket ad una interfaccia di rete (supportato solo su linux) e poi richiamare il metodo {{{connect}}} dove al posto dell'indirizzo dell'host specifichiamo la stringa parola chiave {{{<broadcast>}}}.
<<BR>>
In questo modo otteniamo un socket pronto a comunicare in broadcast su tutta la LAN acceduta tramite quella interfaccia di rete. Per questo il programma Netsukuku si crea un socket UDP per ogni scheda di rete da gestire e li connette ed usa tutti per inviare messaggi ai suoi vicini (vedi la classe {{{BcastClient}}} nel [[../ModuloRPC|modulo RPC]]).
<<BR>>
Abbiamo già visto come il metodo {{{connect}}} di {{{microsock.dispatcher}}} finisce con il richiamare per prima cosa il metodo {{{socket.connect_ex}}} fornito dalla libreria di Python. Questo, quando si usa come indirizzo la parola chiave {{{<broadcast>}}} connette il socket e fa subito ritorno.
<<BR>>
'''''TODO''': Questa è una mia deduzione. La documentazione di Python non è così esplicita. E' corretta?''

== Trasmissione dati tra due socket ==
La trasmissione di dati tra 2 socket connessi con il protocollo TCP avviene in Netsukuku con l'uso dei metodi send, sendall e recv.

Quando si richiama il metodo {{{send}}} di un {{{microsock.dispatcher}}}

TCP il client invia con sendall, il server legge con varie recv, il server risponde con send, il client legge con varie recv.
<<BR>>
UDP il client invia con sendto, il server legge con recvfrom, non ci sono risposte.
 * Per le chiamate {{{receive}}} accertarsi che qualcuno stia inviando qualcosa; usare quindi l'attributo {{{balance}}}. In realtà al momento non ne vengono fatte di chiamate {{{receive}}} all'interno del microthread in questione.
 * Per le chiamate {{{send}}} si può agire in due modi:
   * Di norma non è essenziale sapere il momento esatto in cui sono state portate a compimento; si possono eseguire quindi in un separato microthread. Per questo motivo la [[../ClasseChannel|classe Channel]] può venire istruita (usando il costruttore con {{{micro_send=True}}}) di eseguire tutte le chiamate {{{send}}} sul channel incapsulato in un nuovo microthread. '''''TODO''': Domanda: molteplici chiamate a send in microthread distinti verranno poi rilette sicuramente nell'ordine in cui sono state inviate?''
   * Oppure vanno fatte solo se si è sicuri che una altra tasklet sia in attesa. In questo caso va preferito il costruttore della [[../ClasseChannel|classe Channel]] con {{{prefer_sender=True}}}, perché così la chiamata {{{send}}} trasmette i dati, sblocca la tasklet ricevente, ma mantiene la schedulazione al microthread corrente.

Il modulo microsock

Lo scopo del modulo microsock è quello di permettere la gestione di operazioni di I/O sui socket in un ambiente multithread cooperativo.
In un tale ambiente è necessario evitare l'uso di chiamate di sistema bloccanti per l'intero processo, come sono le chiamate di I/O sui socket.

Quando viene creato un socket usando il wrapper ntk.wrap.sock.Sock, viene in effetti creato anche un oggetto asyncore.dispatcher che lo incapsula e questo viene registrato nella mappa di asyncore (un modulo standard di python stackless); inoltre viene avviato un microthread nel modulo microsock.
Il microthread avviato esegue la funzione ManageSockets. Questa richiama asyncore.poll(0.05) ciclicamente (intervallando con micro_block per passare lo scheduler agli altri) fin quando esiste un socket (nel caso di Netsukuku per sempre) nella mappa di asyncore.
La mappa di asyncore associa il fd (file descriptor, un numero intero) ad una istanza di asyncore.dispatcher che incapsula il vero oggetto sul quale vanno fatte le operazioni di I/O (che può essere un file, o un socket, ...).

Nota: La funzione asyncore.poll è bloccante. Il parametro passato è il numero di secondi dopo i quali cui si vuole cha la chiamata faccia ritorno.
Al suo interno la funzione richiama time.sleep oppure select.select, a seconda se ci sono o meno socket da controllare. In entrambi i casi queste funzioni usano chiamate di sistema che non impegnano la CPU, ma nemmeno permettono agli altri microthread di venire schedulati.
Per questo dobbiamo richiamarla con piccoli timeout intervallando con chiamate a micro_block. Vedi anche la trattazione dell'argomento nella pagina del modulo xtime.

Il metodo asyncore.poll, per prima cosa fa uso dei due metodi readable e writable di asyncore.dispatcher su tutti i dispatcher registrati nella sua mappa. Questi metodi possono essere overridati; nella implementazione base restituiscono True. Il loro obiettivo è quello di identificare i socket che hanno intenzione di inviare (writable) o di leggere (readable) qualcosa.

Di seguito, per i socket che sono readable o writable secondo i significati sopra esposti, tramite il metodo select.select scopre quali hanno dei dati da ricevere oppure sono subito in grado di inviare dati. Cioè per quali socket le chiamate recv e send non risulterebbero bloccanti.

Nel modulo microsock viene definita la classe microsock.dispatcher che è una classe derivata di asyncore.dispatcher. Grazie a questa classe possiamo definire come deve agire il programma quando un microthread richiama i classici metodi di I/O su un presunto socket. Per una analisi di dettaglio rimandiamo alle pagine sulla creazione di un socket, connessione di due socket e trasmissione dati tra due socket.

Il microthread che esegue ManageSockets

Abbiamo detto che fin dal primo momento in cui esiste un socket viene avviato un microthread che esegue la funzione ManageSockets presente in questo modulo.

La vita di questo microthread è un aspetto critico di tutto il programma Netsukuku. Se per un qualsiasi motivo questo microthread dovesse bloccarsi in attesa di un evento che tarda a verificarsi (o addirittura non si verificherà mai) l'intera struttura di comunicazione di Netsukuku si blocca.

Per questo motivo va usata estrema cautela in tutti i punti del codice (in particolare in questo modulo) in cui questo microthread passa e che fanno uso di chiamate bloccanti.

Le chiamate bloccanti usate in questo microthread sono di due tipi.

  • Chiamate che bloccano l'intero programma. Sono tutte quelle che riguardano l'I/O sul socket reale. Ci sono le chiamate accept per i server, le chiamate connect per i client, le chiamate send, sendto e le chiamate recv, recvfrom.

  • Chiamate che bloccano questo microthread ma lasciano spazio agli altri microthread. Sono le chiamate send e receive dei stackless.channel o della nostra classe Channel che li incapsula.

Per quanto riguarda le chiamate di I/O sul socket, ci si affida alla funzione select.select la quale ci dice con certezza (e senza bloccarsi indefinitamente) se una certa chiamata risulterebbe bloccante. Quindi se usata correttamente ci può guidare ad un uso non bloccante delle altre chiamate.
La funzione select.select è chiamata nella funzione asyncore.poll; noi ce ne avvaliamo facendo l'override nella classe microsock.dispatcher dei metodi di asyncore.dispatcher che vengono chiamati dalla funzione asyncore.poll stessa. Vedi le pagine citate sopra.

Per quanto riguarda la trasmissione di messaggi sui channel, bisogna:

  • Per le chiamate receive accertarsi che qualcuno stia inviando qualcosa; usare quindi l'attributo balance. In realtà al momento non ne vengono fatte di chiamate receive all'interno del microthread in questione.

  • Per le chiamate send si può agire in due modi:

    • Di norma non è essenziale sapere il momento esatto in cui sono state portate a compimento; si possono eseguire quindi in un separato microthread. Per questo motivo la classe Channel può venire istruita (usando il costruttore con micro_send=True) di eseguire tutte le chiamate send sul channel incapsulato in un nuovo microthread. TODO: Domanda: molteplici chiamate a send in microthread distinti verranno poi rilette sicuramente nell'ordine in cui sono state inviate?

    • Oppure vanno fatte solo se si è sicuri che una altra tasklet sia in attesa. In questo caso va preferito il costruttore della classe Channel con prefer_sender=True, perché così la chiamata send trasmette i dati, sblocca la tasklet ricevente, ma mantiene la schedulazione al microthread corrente.

Netsukuku/ita/ModuloMicrosock (last edited 2009-05-04 15:51:42 by lukisi)