In breve, puoi trovare una sorta di discussione in Architettura software orientata ai modelli vol.2 - stai chiedendo (e sto descrivendo approssimativamente in basso) il pattern Reactor.
Puoi anche leggere un'implementazione di questo nella libreria ACE di Doug Schmidt, ma come la maggior parte delle librerie del mondo reale, è coperta da verruche per la portabilità ed è comunque un codice piuttosto vecchio stile.
Quindi, hai un semplice ciclo di selezione / sondaggio / evento, e vuoi implementare callback I / O, timer periodici e I / O asincroni.
1: I / O di base
Il modo generico (ovvero riutilizzabile in senso bibliografico) per inviare gli eventi è fornire un'interfaccia astratta che un client possa implementare. Se guardi all'interfaccia select / poll / any, vedrai gli eventi associati a un descrittore di file che ti diranno esattamente cosa ti serve:
class IOHandler {
public:
virtual ~IOHandler();
// lib needs fd for syscall
virtual int getFD() const = 0;
// lib needs to know what flags to set
virtual bool readFlag() const = 0;
virtual bool writeFlag() const = 0;
virtual bool errorFlag() const = 0;
// lib needs to dispatch callbacks
virtual void onReadable() = 0;
virtual void onWriteable() = 0;
virtual void onError() = 0;
};
Ora, se hai un vettore di IOHandler*
o qualsiasi altra cosa, dovresti riuscire a scrivere il ciclo.
Tieni presente che la gestione degli errori, consentendo ai gestori di rimuovere se stessi durante il ciclo ecc. richiederà uno sforzo leggermente maggiore.
2: Timer
Noterai che il tuo syscall di scelta ha un parametro di timeout. Forniremo un'altra interfaccia per i timer:
class Timer {
public:
virtual ~Timer() {}
// some kind of period
virtual unsigned period() const = 0;
// some kind of callback at the end of each period
virtual void fire() = 0;
};
Ora, la tua biblioteca ha bisogno di un'interfaccia per aggiungere & rimuovere quei timer. Una volta che li hai, puoi mantenere un contenitore di Timer*
, ordinato da il prossimo tempo di sparo assoluto (un heap è tradizionale).
Quindi, ogni volta che chiami select / poll / whatever, ottieni la prossima scadenza dalla parte anteriore dell'heap e la usi per il tuo timeout. Se ottieni ETIMEDOUT, invia i timer dalla parte anteriore del tuo heap finché non ne raggiungi uno in futuro.
Qui stiamo parlando solo di timer periodici, quindi ogni volta che ne estrai uno in avanti, devi aggiungere l'ora corrente al suo periodo e reinserirla in coda.
Anche in questo caso, la gestione di una singola o di una frequenza variabile, che consente ai timer di rimuovere se stessi ecc., viene lasciata come esercizio. Se i normali callback I / O potrebbero essere lenti (rispetto al periodo dei timer), è possibile eseguire un ciclo di invio del timer dopo aver gestito anche questi callback.
3: I / O asincrono
Il solito meccanismo POSIX per questo è rendere i tuoi socket non bloccanti (questo in genere non funziona per i file). Dopo averlo fatto, devi anche aggiungere molto più gestione dello stato per gestire i casi in cui
- non è stato possibile completare la scrittura di un buffer, quindi è necessario tenerlo da qualche parte (e assicurarsi di controllare che il file fd sia nuovamente scrivibile)
- eventuali future scritture dovranno ora essere accodate o aggiunte a quel buffer fino a quando il socket non ha raggiunto
- non puoi leggere quanto vuoi, quindi devi tenere un messaggio parziale da qualche parte (fino a quando il file fd diventa leggibile) e puoi riassicurare il messaggio
Probabilmente questo può essere gestito all'interno del framework I / O sopra disegnato, ma non ho intenzione di implementarlo qui.