Il modulo microsock

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.
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, ...).
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.

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 qualcosa da inviare (writable) o che sono pronti a leggere qualcosa (readable).

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.

Vediamo quali passi segue la creazione e connessione di un socket, prima di analizzare quello che avviene quando si trasmette o riceve qualcosa.

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(...)).

  2. viene creato anche un microsock.stacklesssocket. Questo nel suo costruttore fa queste operazioni:

    1. memorizza il vero socket.
    2. si crea e memorizza una istanza dell'oggetto microsock.dispatcher, che è una derivata di asyncore.dispatcher.

    3. microsock.dispatcher nel suo costruttore fa queste operazioni:

      1. memorizza il vero socket.
      2. la classe ha i membri connectChannel, acceptChannel e recvChannel.

      3. istanzia per recvChannel un Channel con micro_send = True.

      4. prepara una stringa buffer e una lista per i send (sendBuffer e sendToBuffers)

      5. prepara una stringa buffer e una lista per i recv (readBufferString e readBufferList)

      6. prepara una costante maxreceivebuf = 65536

      7. memorizza il vero socket nella mappa di asyncore (questo lo fa asyncore.dispatcher.__init__ => asyncore.dispatcher.set_socket => asyncore.dispatcher.add_channel)

      8. la sua rimozione dalla mappa avverrà con microsock.stacklesssocket.__del__ => microsock.dispatcher.close => asyncore.dispatcher.close => asyncore.dispatcher.del_channel

  3. 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. ???

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.

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.

Quando si richiama il metodo accept di un microsock.dispatcher (overridato rispetto al asyncore.dispatcher) il dispatcher si istanzia un oggetto Channel con micro_send = True e lo memorizza sul membro acceptChannel. Su questo oggetto richiama il metodo recv.
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.
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.
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.
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...!
Il microthread prima bloccato viene ora ri-schedulato e il metodo microsock.dispatcher.accept restituisce un stacklesssocket connesso.

Se vogliamo implementare un client con il protocollo TCP, dobbiamo creare un socket di tipo SOCK_STREAM e poi richiamare il metodo connect.

Quando si richiama il metodo connect di un microsock.dispatcher questo per prima cosa richiama il metodo base, cioè il metodo asyncore.dispatcher.connect.
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.
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.
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 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.
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.
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.
TODO: Perché avviamo un UDPServer per ogni scheda se poi l'argomento dev non viene mai usato nella fase di attesa messaggi? Cioè, non viene fatta l'associazione del socket alla scheda con sk_bindtodevice.
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 con sk_bindtodevice (supportato solo su linux) e poi richiamare il metodo connect dove al posto dell'indirizzo dell'host specifichiamo la stringa parola chiave <broadcast>.
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 modulo RPC).
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.
TODO: Questa è una mia deduzione. La documentazione di Python non è così esplicita. E' corretta?

Trasmissione dati tra due socket

La trasmissione di dati tra due 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 questo memorizza il messaggio da inviare in coda al buffer nel suo membro sendBuffer e fa subito ritorno al chiamante, segnalando di aver trasmesso tutti i bytes richiesti.
La presenza di dati in questo buffer fa sì che il metodo writable restituisca True. Questo a sua volta, come detto sopra, fa sì che asyncore.poll, periodicamente e solo quando la trasmissione non risulterebbe bloccante, richiami il metodo handle_write_event che a sua volta richiama handle_write overridato nella classe microsock.dispatcher.
Il metodo microsock.dispatcher.handle_write richiama il metodo asyncore.dispatcher.send sulla classe base, non più quello overridato. Questo effetua l'invio di un certo numero di bytes senza bloccare il processo e restituisce il numero di bytes trasmessi. Il metodo microsock.dispatcher.handle_write di seguito toglie dal buffer i bytes già inviati e fa ritorno.

Abbiamo detto che il metodo microsock.dispatcher.send fa subito ritorno segnalando la trasmissione di tutti i bytes richiesti. In realtà la trasmissione avviene in seguito.
Se si vuole essere sicuri che la trasmissione sia già avvenuta si chiama il metodo microsock.dispatcher.sendall. Questo metodo fa ritorno solo quando il buffer nel suo membro sendBuffer è completamente svuotato.
Per attendere questo evento effettua un loop in cui intervalla il test alla schedulazione degli altri microthread con micro_block. TODO: Il loop in questione è in effetti un busy-wait. C'è un qualche motivo per cui qui non usiamo il xtime.swait() ?

Appunti

TCP il client invia con sendall, il server legge con varie recv, il server risponde con send, il client legge con varie recv.
UDP il client invia con sendto, il server legge con recvfrom, non ci sono risposte.