Igor Devescovi Si occupa, come freelance, di analisi e sviluppo (c++, c#) di engine graci, generazione sonora e sistemi di rete per applica- zioni multimediali distribuite. Ha lavorato per 6 anni nel campo della simulazione real-time (Sindel, gruppo Faros s.a.), e ora collabora con IBR sistemi (GE).
Limpaginazione automatica di questa rivista e realizzata al ` 100% con strumenti Open Source usando OpenOffice, Emacs, BHL, LaTeX, Gimp, Inkscape e i linguaggi Lisp, Python e BASH
For copyright information about the contents of Computer Programming, please see the section Copyright at the end of each article if exists, otherwise ask authors. Infomedia contents is 2004 Infomedia and released as Creative Commons 2.5 BY-NC-ND. Turing Club content is 2004 Turing Club released as Creative Commons 2.5 BY-ND. Le informazioni di copyright sul contenuto di Computer Programming sono riportate nella sezione Copyright alla ne di ciascun articolo o vanno richieste direttamente agli autori. Il contenuto Infomedia e 2004 Infome` dia e rilasciato con Licenza Creative Commons 2.5 BYNC-ND. Il contenuto Turing Club e 2004 Turing Club ` e rilasciato con Licenza Creative Commons 2.5 BY-ND. Si applicano tutte le norme di tutela dei marchi e dei segni distintivi. ` E in ogni caso ammessa la riproduzione parziale o totale dei testi e delle immagini per scopo didattico purch e vengano integralmente citati gli autori e la completa identicazione della testata. Manoscritti e foto originali, anche se non pubblicati, non si restituiscono. Contenuto pubblicitario inferiore al 45%. La biograa dellautore riportata nellarticolo e sul sito www.infomedia.it e di norma quella disponibi` le nella stampa dellarticolo o aggiornata a cura dellautore stesso. Per aggiornarla scrivere a info@infomedia.it o farlo in autonomia allindirizzo http://mags.programmers.net/moduli/biograa
PROGRAMMING
e i timer dei computer misurassero in maniera precisa il passare del tempo, non ci sarebbe nessuna ragione per scrivere un articolo come questo. Dove necessario, sarebbe sufficiente sincronizzare gli orologi dei computer ad un determinato momento, e da l in poi non dovremmo pi porci il problema. Tuttavia non cos: i timer dei computer vanno a velocit leggermente differenti tra loro, ed esistono molte applicazioni distribuite dove mantenere sincronizzato il tempo di primaria importanza. In generale, se in unapplicazione di rete uno o pi processi distribuiscono in rete dei dati correlati al momento in cui questi dati sono stati prodotti (timestamp) e uno o pi processi necessitano del timestamp per consumare correttamente tali dati, allora si ha bisogno di un meccanismo di sincronizzazione. La sincronizzazione dei timer in una rete di computer rappresenta un problema di non facile soluzione. Una decina danni fa era terreno di caccia esclusivo per gli sviluppatori di applicazioni distribuite (per esempio simulazioni real-time, applicazioni finanziarie, controllo aereo, eccetera). Oggigiorno lespansione a macchia dolio dellonline gaming lo ha tramutato in un acceso argomento di discussione (tra sviluppatori, sintende) di cui facile trovare tracce nei forum e nei siti che si occupano di programmazione. Tuttavia, non esiste uno standard certo a cui affidarsi per risolvere problemi di sincronizzazione. In questo articolo analizzeremo un metodo per sincronizzare (e mantenere sincronizzati) i timer di pi computer in una rete. Limplementazione delle classi che descriveremo scaricabile dal sito ftp di Infomedia.
Non c un cronometro
o un orologio che possiamo interrogare a piacimento
Andiamo ora ad analizzare i quattro elementi del nostro sistema: timer, rete, server e client.
Igor Devescovi
idevescovi@infomedia.it
Si occupa, come freelance, di analisi e sviluppo (c++, c#) di engine grafici, generazione sonora e sistemi di rete per applicazioni multimediali distribuite. Ha lavorato per 6 anni nel campo della simulazione real-time (Sindel, gruppo Faros s.a.), e ora collabora con IBR sistemi (GE).
43
networking
FIGURA 1
lungo termine, dovuto al fatto che le frequenze usate dalla scheda madre non sono sottomultiple del secondo. Il sistema operativo mantiene a sua volta dei contatori che aggiorna in maniera periodica (con frequenza pi bassa rispetto allhardware). Limprecisione dei contatori del s.o. aggravata dalle latenze che si possono creare nel momento stesso in cui il s.o. legge il tempo dai contatori hardware, oppure, in maniera ancora pi pesante, se il sistema occupato a gestire qualche interrupt hardware. In questultimo caso laggiornamento del contatore software pu addirittura saltare. Il comportamento dei contatori, e di conseguenza del timer del computer, non certo cos erratico da essere percettibile dallutente, ma alla lunga gli errori si accumulano e leffetto proprio quello di un timer che conta il tempo pi velocemente o pi lentamente di come trascorra nella realt. Qui di seguito sono elencate, in ordine daccuratezza di risposta, le funzioni che le API di Windows ci offrono: GetTickCount() e GetSystemTimeAsFileTime(): scarsa precisione (5 55ms a seconda del sistema); timeGetTime(): lenta nellesecuzione ma abbastanza precisa (1ms);
QueryPerformanceCounter(): altissima precisione, ma non disponibile su tutti gli hardware. Escludiamo le prime due funzioni (55ms, per un computer, sono uneternit). Quello di cui abbiamo bisogno una classe che ci permetta di ottenere il valore del timer selezionando, tra i due metodi rimanenti, il migliore disponibile. La QueryPerformanceCounter() fornisce i risultati pi precisi, ma il suo funzionamento, soprattutto su hardware datati (fino al P-III), affetto da qualche problema. La classe cLocalTimer, verifica come prima cosa il funzionamento del performance counter, tramite la cLocalTimer::prvCanUsePerfCounter()
BYTE cLocalTimer::prvCanUsePerfCounter(void) { // Test the performance counter for 3 second long lTickCountStart = GetTickCount();
//Some performance timers are buggy, //check that the timer always increase. BYTE bTest = 1; LARGE_INTEGER liPerfCounter; LARGE_INTEGER liSavePerfCounter; if(QueryPerformanceCounter(&liPerfCounter)) {
44
PROGRAMMING
Se paragoniamo il TCP/IP allimpianto elettrico di una casa, il socket come una presa di corrente. Quando creiamo un socket generiamo un condotto bidirezionale tramite il quale possiamo inviare e ricevere dati dalla rete. In Windows, le funzionalit del TCP/IP sono contenute in un sottoinsieme delle Win32 API chiamato Winsock, e possono essere utilizzate per comunicare in due modi differenti. Nel primo si usano i datagram-oriented sockets. I datagrammi sono paragonabili alle cartoline: possono trasmettere piccole quantit di dati e contengono tutte le informazioni per arrivare dal mittente al destinatario. In questo caso la ricezione dei dati da parte del destinatario non garantita. Nel secondo si usano gli stream-oriented sockets. Le connessioni di questo tipo sono paragonabili invece ad una telefonata. Si stabilisce un circuito virtuale tra le due entit e da quel momento i dati possono percorrerlo sia in un verso che nellaltro, fino a che la connessione non viene chiusa. Questa modalit assicura che i dati trasmessi vengano correttamente ricevuti. Nellimplementazione delle classi di questo progetto, il codice di rete (ridotto allosso) utilizza le connessioni stream-oriented.
// Bind the socket to the local server address SOCKADDR_IN skaddrServer; skaddrServer.sin_family = AF_INET;
RIQUADRO 1
liSavePerfCounter = liPerfCounter; while(((long)GetTickCount() - lTickCountStart) < 3000) { QueryPerformanceCounter(&liPerfCounter); if(liPerfCounter.QuadPart < liSavePerfCounter.QuadPart) return 0; Sleep(10); } } else bTest = 0; //PerformanceCounter is unavailable
Quindi, attendiamo che sul socket arrivi una richiesta di connessione da parte di un client. Mentre la cTimeServer ritorna nella sua modalit di ascolto, per ogni connessione accettata viene instanziato un oggetto cTimeCliHandler che, in un thread dedicato (vedi Riquadro 2), ascolta e soddisfa le richieste di un solo client.
return bTest; }
Se il risultato positivo utilizzeremo la QueryPerformanceCounter() come sorgente di tempo, altrimenti la nostra scelta ricadr sulla timeGetTime().
Il Server
Come accennato in precedenza, il compito del server abbastanza semplice. Consiste sostanzialmente nellattendere richieste di connessione da parte dei client. Per far questo apriamo un nuovo socket TCP (vedi Riquadro 1), gli assegniamo una porta e lo mettiamo in modalit di ascolto
m_skListen = socket(AF_INET, SOCK_STREAM, IPPROTO_TCP); if(m_skListen == INVALID_SOCKET) { //Error }
45
networking
T0cli: il client fa una richiesta di sincronizzazione al server, e salva il valore T0cli; T1srv: il server riceve la richiesta, e spedisce indietro il valore T1srv come tempo di riferimento; T2cli: il client riceve la risposta, calcola D = T1srv T0cli; da questo momento in poi, il timer fornito dal client sar Tout = Tlocal + D. In una situazione ideale, dal momento nel quale il client spedisce la richiesta, al momento in cui questa viene ricevuta e gestita dal server, il tempo trascorso dovrebbe essere ZERO. Se cos fosse, T0cli e T1srv rappresenterebbero i valori dei timer locali del client e del server presi contemporaneamente. Il ritardo introdotto dalla rete invece, per quanto piccolo, fa s che i due valori T0cli e T1srv siano ottenuti in due momenti diversi tra loro e inoltre, cosa pi importante, questo ritardo non misurabile con precisione. Quello che possiamo misurare, per, il tempo che trascorre dallattimo in cui la richiesta partita (T0cli), a quello in cui la risposta arrivata (T2cli). Chiameremo questo lasso di tempo latenza (L = T2cli T0cli). La latenza misura in realt il tempo totale di trasferimento di due messaggi, ovvero la richiesta e la risposta insieme. Quindi possiamo presumere che il tempo di viaggio del singolo messaggio di richiesta sia pari alla met del tempo di latenza e inserire questo nuovo termine nel calcolo di Tout: D = T1srv T0cli L/2 Tout = Tlocal + D In questo modo azzeriamo virtualmente il tempo perso dal messaggio di richiesta avvicinandoci alla situazione ideale. La classe cTimeClient manda le richieste di sincronizzazione al server e gestisce le risposte, calcolando la differenza tra i timer (D) e la latenza (L), secondo il metodo appena descritto. Tutto questo avviene in un thread separato (Riquadro 2). La scelta di un thread separato rende il processo di sincronizzazione trasparente allapplicazione chiamante, che negli intervalli tra due sincronizzazioni otterr, tramite la cTimeClient, un valore di timer (Tout) calcolato sulla base del timer locale e della differenza (D) risultante dallultima sincronizzazione.
La creazione di un thread in C++ utilizzando gli oggetti porta spesso, come risultato, a creare codice che risulta poco elegante al di l della sua effettiva efficienza. Questo perch alla funzione CreateThread(), anche se usata allinterno di un metodo di una classe, dobbiamo passare il puntatore ad una funzione esterna alla classe che diventer il corpo principale del thread. Per aggirare il problema possiamo usare come funzione del thread un membro della classe, dichiarato come nellesempio che segue.
class cTimeCliHandler { public: cTimeCliHandler(SOCKET s, cLocalTimer* t); ~cTimeCliHandler(void); static DWORD __stdcall ThreadProc(void* pv); HANDLE }; m_hThread; /// thread handle
Il prefisso static fa s che il puntatore a funzione esista a prescindere dallistanza della classe, mentre la chiamata __stdcall e il parametro void*, rendono questa funzione compatibile con quanto richiesto dalla CreateThread() come parametro. Nel creare il nuovo thread dobbiamo specificare il puntatore this come parametro della ThreadProc().
cTimeCliHandler::cTimeCliHandler(SOCKET s, cLocalTimer* t) { //Create the thread that will handle each clients sync requests DWORD dwThreadId = 0; m_hThread = CreateThread(NULL, 0, ThreadProc, (void*)this, 0, &dwThreadId); }
La ThreadProc()otterr il puntatore alla classe tramite il parametro di input. Infatti, modificando la chiamata con _ _stdcall il puntatore this allistanza delloggetto non pi implicitamente passato ad ogni chiamata come normalmente accade con le funzioni membro di un qualsiasi oggetto.
DWORD __stdcall cTimeCliHandler::ThreadProc (void* pv) { // Retrieve this pointer cTimeCliHandler* pThis = (cTimeCliHandler*)pv; }
Filtrare le sincronizzazioni
Particolari condizioni di carico di rete o di stress della CPU possono interferire con il processo di sincronizzazione, rallentando o addirittura bloccando la gestione delle richieste o delle risposte. Leffetto di tali condizioni rende il comportamento del timer instabile, facendolo scattare avanti o indietro ad ogni sincronizzazione. Fortunatamente queste condizioni sono misurabili, perch in un modo o nellaltro aumentano inevitabilmente il tempo di latenza. Tutto ci rende possibile filtrare i dati delle sincronizzazioni in modo da evitare instabilit nel valore del timer. La cTimeClient effettua questattivit di selezione tra
RIQUADRO 2
46
PROGRAMMING
sincronizzazioni buone e cattive in due modi distinti. Per prima cosa, per ogni ciclo di attivit il client fa diverse richieste di sincronizzazione in sequenza, attendendo la risposta dopo ogni singola richiesta e selezionando la sincronizzazione migliore tra tutte quelle effettuate (quella con minore latenza). In tal modo vengono automaticamente scartati i ritardi provocati da picchi momentanei del carico di rete o di lavoro della CPU.
pThis->m_dTimerDelta = dSrvTime - dReqTime - .5 * dLatency; pThis->m_dExpLatency = dLatency; } else { //Accept syncronization only if the latency is near the expected value if(dLatency <= pThis->m_dExpLatency) { //Get the best sync out of all the tries if(dLatency < dMinLatency)
if(pThis->m_bFirstGoodSync) { //After the first good syncronization if(bGotGoodSync) { pThis->m_dTimerDelta = dTimerDelta; //Got another good syncronization pThis->m_dExpLatency = dMinLatency * 0.9; //Next time try lower latency } else pThis->m_dExpLatency *= 1.1;//Increase latency for the next try }
E alla fine?
La soluzione descritta si adatta alle applicazioni che richiedono semplicit dutilizzo e velocit di risposta, avvicinandosi molto ai metodi adottati nel campo della simulazione real-time e dei videogiochi con modalit multiplayer. Sebbene non esista ancora una soluzione standard, assai probabile che, come avvenuto in passato per lhardware grafico, le evoluzioni future delle reti prendano in considerazione anche questo problema.
RIFERIMENTI
[1] The Network time protocol http://www.ntp.org [2] Timing pitfalls and solutions http://www.gamdev.net/reference/articles/article2086.asp [3] Johnnies Winsock tutorial http://www.halpc.org/~johnnie2/winsock.html
if(pThis->SkRecv(&dSrvTime, sizeof(double))) { //Measure current latency double dLatency = pThis->m_pLocTimer->Get() - dReqTime; if(!pThis->m_bFirstGoodSync) { //Take the first syncronization as it is
CODICE ALLEGATO
ftp.infomedia.it
Sinctimer
47