Sei sulla pagina 1di 7

CP pensareprogettareprogrammare n.

147 settembre 2004

Sincronizzare i timer in rete


di Igor Devescovi
Descrizione di unarchitettura client-server per la sincronizzazione continua dei timer di due o ` piu PC in una rete.

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

pubblicato su WWW.INFOMEDIA.IT stampa digitale da Lulu Enterprises Inc. stores.lulu.com/infomedia


Infomedia
` Infomedia e limpresa editoriale che da quasi venti anni ha raccolto la voce dei programmatori, dei sistemisti, dei professionisti, degli studenti, dei ricercatori e dei professori dinformatica italiani. Sono pi` di 800 gli autori che hanno realizzato per le teu state Computer Programming, Dev, Login, Visual Basic Journal e Java Journal, molte migliaia di articoli tecnici, presentazioni di prodotti, tecnologie, protocolli, strumenti di lavoro, tecniche di sviluppo e semplici trucchi e stratagemmi. Oltre 6 milioni di copie distribuite, trentamila pagine stampate, fanno di questa impresa la pi` grande ed u inuente realt` delleditoria specializzata nel campo della a programmazione e della sistemistica. In tutti questi anni le riviste Infomedia hanno vissuto della passione di quanti vedono nella programmazione non solo la propria professione ma unattivit` vitale e un vero a divertimento. ` Nel 2009, Infomedia e cambiata radicalmente adottando ` un nuovo modello aziendale ed editoriale e si e organizzata attorno ad una idea di Impresa Sociale di Comunit` , a partecipata da programmatori e sistemisti, separando le attivit` di gestione dellinformazione gestite da un board a comunitario professionale e quelle di produzione gesti` te da una impresa strumentale. Questo assetto e in linea con le migliori esperienze internazionali e rende Infomedia ancora di pi` parte della Comunit` nazionale degli u a sviluppatori di software. ` Infomedia e media-partner di manifestazioni ed eventi in ambito informatico, collabora con molti dei pi` imporu tanti editori informatici italiani come partner editoriale e fornitore di servizi di localizzazione in italiano di testi in lingua inglese.

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

Sincronizzare i timer in rete


Descrizione di unarchitettura client-server per la sincronizzazione continua dei timer di due o pi PC in una rete
di Igor Devescovi

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.

Sincronizziamo gli orologi?


Il nostro obiettivo mantenere i valori dei timer dei computer nella rete il pi possibile vicini tra loro. Come fare? Lidea molto semplice, e la Figura 1 la presenta in modo schematico: stabiliamo che tutti i computer prendano come riferimento il timer di uno solo di essi, sul quale girer il nostro server (la classe cTimeServer) il cui compito sar quello di rispondere alle richieste di sincronizzazione. Sugli altri computer, le applicazioni client (cTimeClient) manderanno periodicamente le richieste di sincronizzazione al server, e tenteranno di mantenersi sincronizzati al tempo di riferimento nel miglior modo possibile, tenendo conto dei ritardi introdotti dalla rete e della precisione del timer locale (cLocalTimer).

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.

Il timer locale: la classe cLocalTimer


Nei periodi che intercorrono tra una sincronizzazione e la successiva, il valore del timer sar calcolato basandosi sul valore del timer locale. Tuttavia ottenere il timer del pc con accuratezza gi di per s una piccola impresa, perch non c un cronometro o un orologio che possiamo interrogare a piacimento, ma semplicemente dei contatori hardware che vengono incrementati ad intervalli di tempo regolari secondo un clock secondario generato dalla scheda madre o (dal pentium in poi) dal clock del processore. Gi a questo livello sintroduce un certo errore a

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

Computer Programming n. 147 - Giugno 2005

43

networking

FIGURA 1

Schema del processo di sincronizzazione

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

Computer Programming n. 147 - Giugno 2005

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;

skaddrServer.sin_addr.s_addr = INADDR_ANY; skaddrServer.sin_port = htons(wPort);

if(SOCKET_ERROR == bind(m_skListen, (LPSOCKADDR)&skaddrServer, sizeof(SOCKADDR))) { //Error }

// Listen to any connection request if(SOCKET_ERROR == listen(m_skListen, 32)) { //Error }

//Accept incoming connection SOCKET skClient = accept(m_skListen, &skaddrClient, &iAddrClientLen);

RIQUADRO 1

Un cenno ai socket del tcp/ip

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.

Anche nei casi migliori,


i componenti che costituiscono le reti introducono sempre un certo ritardo nella trasmissione dei messaggi
Il Client e le latenze di rete
Anche nei casi migliori, i componenti che costituiscono le reti introducono sempre un certo ritardo nella trasmissione dei messaggi. Ritardo che va da meno di un millisecondo a pochi millisecondi nel caso di una LAN, fino ad arrivare da centinaia di millisecondi al secondo via via che la rete cresce (internet, ovviamente, costituisce il caso peggiore). Ma non il ritardo introdotto nella comunicazione che ci deve preoccupare, quanto il fatto che esso non pu essere misurato. Vediamo meglio di cosa si tratta analizzando il funzionamento del sistema di sincronizzazione che stiamo sviluppando, mettendo una serie di eventi in sequenza temporale:

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 }

Computer Programming n. 147 - Giugno 2005

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

Creare un thread da una classe

46

Computer Programming n. 147 - Giugno 2005

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)

Non il ritardo introdotto


nella comunicazione che ci deve preoccupare, quanto il fatto che esso non pu essere misurato
Se questo primo filtraggio non bastasse, per esempio in condizioni di carico continuo e non di singoli picchi di lavoro, dopo la sequenza delle richieste di sincronizzazione, la latenza della migliore tra le sincronizzazioni effettuate viene confrontata con un valore di riferimento, a sua volta calcolato dalla latenza del ciclo precedente. La sincronizzazione va a buon fine solamente se la nuova latenza minore di quella attesa. In caso affermativo la latenza attesa nel ciclo che seguir sar lultima calcolata, diminuita di una certa percentuale. In caso negativo il calcolo del timer continuer con i valori di sincronizzazione invariati, e la latenza attesa nel ciclo seguente sar pari al valore di riferimento aumentato della stessa percentuale. Questo secondo filtro fa s che una volta avuta una buona sincronizzazione, a bassa latenza, diverse sincronizzazioni con latenze pi alte vengano scartate prima che il sistema si adatti.
for(long lTry = 0; && lTry < pThreadData->lTries; lTry++) { //Request tsSyncReq SyncReq; SyncReq.wSize = sizeof(tsSyncReq); } } } }

{ dTimerDelta = dSrvTime - dReqTime - .5 * dLatency; dMinLatency = dLatency; bGotGoodSync++; }

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.

SyncReq.bMsgCode = tsSYNCREQ_CODE; double dReqTime = pThis->m_pLocTimer->Get();

long lSentBytes = send(pThis->m_skClient, (char*)&SyncReq, sizeof(tsSyncReq), 0);

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

//Wait for the answer double dSrvTime = 0;

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

Computer Programming n. 147 - Giugno 2005

47

Potrebbero piacerti anche