Φτιάχνοντας έναν Game Server σε C++ #1: Η Ιδέα
Έχω πάνω από δεκαετία να γράψω κώδικα σε C++, και με έπιασε μια νοσταλγία. Επίσης, πάντα ήθελα να φτιάξω έναν game server, οπότε σήμερα αποφάσισα να πάρω βαθιά ανάσα και να ικανοποιήσω και τα δύο απωθημένα.
Θέλω ο server να υποστηρίζει όλων των ειδών τα παιχνίδια: ενός παίχτη, δύο παιχτών, πολλών παιχτών, turn-based, πραγματικού χρόνου… τα πάντα όλα! Επίσης θα πρέπει να μπορεί να δημιουργεί πολλαπλά στιγμιότυπα του ίδιου παιχνιδιού (worlds), ώστε ο χρήστης να μπορεί να επιλέξει όχι μόνο ποιο παιχνίδι να παίξει, αλλά και σε ποιο στιγμιότυπο να συνδεθεί. Αφότου ο χρήστης συνδεθεί και διαπιστευτεί (authenticate), θα πρέπει να μπορεί να μπει σε ένα είδος “προθάλαμου”. Εκεί θα έχει την ευκαιρία να δει τα διαθέσιμα ενεργοποιημένα παιχνίδια και να αποφασίσει σε ποιο θα συμμετάσχει. Ας αρχίσουμε λοιπόν ορίζοντας τις διαφορετικές καταστάσεις στις οποίες μπορεί να βρεθεί ένας χρήστης:
Προφανώς θα μπορεί να αποσυνδεθεί οποιαδήποτε στιγμή, ανεξάρτητα από την κατάσταση στην οποία βρίσκεται. Αυτό μπορεί να συμβεί είτε λόγω κάποιου θέματος με το δίκτυο είτε επίτηδες. Στη δεύτερη περίπτωση, μπορεί να αποσυνδεθεί εκούσια ή να τον αποσυνδέσει ο server (πχ λόγω timeout, κατάχρησης πόρων κλπ).
Η κατάσταση της αυθεντικοποίησης υπονοεί κάποιο είδος αποθηκευτικού χώρου, όπου διατηρούνται τα διαπιστευτήρια. Είμαι βέβαιος ότι θα προκύψουν κι άλλες ανάγκες αποθήκευσης, γι αυτό τείνω στο ενδεχόμενο μιας βάση δεδομένων. Πιθανότατα θα χρησιμοποιήσω MySQL, μιας που έχω αρκετή εξοικείωση με αυτή.
Επίσης υπονοεί συνεδρίες (sessions). Το οποίο με οδηγεί στο εύλογο ερώτημα του πρωτοκόλλου που θα χρησιμοποιήσω. Από τη μια, το UDP είναι η φυσική επιλογή για έναν game server, ειδικά για παιχνίδια πραγματικού χρόνου, διότι είναι stateless και επομένως γρήγορο. Από την άλλη όμως, η υλοποίηση συνεδριών είναι πολύ ευκολότερη με TCP. Επομένως, για τις δικές μου ανάγκες νομίζω θα ακολουθήσω μια υβριδική λύση, όπου οι πληροφορίες που αφορούν τη συνεδρία θα ανταλάσσονται με TCP, αλλά τα δεδομένα των παιχνιδιών με UDP.
Προτιμώ αυτές τις συνεδρίες να τρέχουν σαν ξεχωριστά threads. Όταν συμβαίνει μια έα σύνδεση TCP, θα δημιουργείται ένα νέο thread που θα εξυπηρετεί την επικοινωνία με τον συγκεκριμένο client, ώστε ο server να μπορεί να συνεχίσει περιμένοντας την επόμενη σύνδεση. Επιπλέον κάθε συνεδρία θα πρέπει να έχει ένα διακριτό αναγνωριστικό (id). Αυτό το αναγνωριστικό θα παρέχεται στο νέο thread σαν παράμετρος, μαζί με την νέα σύνδεση TCP. Η πρώτη δουλειά του thread θα είναι να στείλει το αναγνωριστικό στον client ως απόκριση στη σύνδεση. Τέλος, το thread θα πρέπει να έχει κάποιον τρόπο να επικοινωνεί με το πατρικό thread, επομένως έχει νόημα μια τρίτη παράμετρος με αυτό το backlink.
Ένα άλλο θέμα που προκύπτει είναι η καταγραφή μηνυμάτων (debug log). Για την ώρα θα αρκεστώ στο παλιό, καλό και αξιόπιστο stdout/stderr, παρόλο που θα το υλοποιήσω πιθανότατα σαν backend αντικείμενο, για να είναι πιο αφαιρετικό. Σίγουρα θα χρειαστώ κι άλλα backends πέρα από την καταγραφή και την αποθήκευση που προανέφερα, αλλά για την ώρα θα αρκεστούμε σε αυτά.
Οπότε, ας οπτικοποιήσουμε το σύστημα με τις παραδοχές που έχω κάνει ως τώρα:
Προφανώς ο server θα πρέπει να διατηρεί μια λίστα με τα δεδομένα της κάθε συνεδρίας. Με κάθε νέα σύνδεση θα δημιουργείται ένα νέο στοιχείο στη λίστα, το οποίο θα διαγράφεται όταν ο χρήστης αποσυνδεθεί. Και εφόσον τα διάφορα threads θα έχουν πρόσβαση σε αυτή τη λίστα, θα πρέπει να προστατεύεται από κάποιου είδους mutex.
Μιλώντας για δεδομένα συνεδριών, μια παράμετρος που σκέφτομαι είναι η σύζευξη της συνεδρίας με την αντίστοιχη διεύθυνση/port του client πάνω στο UDP. Όπως προανέφερα ο client λαμβάνει το αναγνωριστικό συνεδρίας μετά τη σύνδεσή του, οπότε θα είναι απαραίτητο να το στείλει πίσω μέσω UDP. Με αυτή τη σύζευξη (handshake), ο server θα μπορέσει να διακρίνει ποια διεύθυνση αντιστοιχεί στη συνεδρία, και να ενημερώσει αναλόγως το στοιχείο της λίστας.
Αν εξαιρέσουμε αυτό το πακέτο σύζευξης, η επικοινωνία πάνω στο UDP θα πρέπει να γίνεται αποκλειστικά μεταξύ του χρήστη και του παιχνιδιού στο οποίο συμμετέχει. Θέλω να αφήσω αυτό το πρωτόκολλο όσο μπορώ πιο ελεύθερο, ώστε να το καθορίζει κάθε παιχνίδι ξεχωριστά. Η μόνη δουλειά του server θα είναι η δρομολόγηση των πακέτων UDP μεταξύ των παιχτών και των παιχνιδιών στα οποία έχουν συνδεθεί.
Τέλος, θέλω ο server να ξεκινά με ένα σετ παραμέτρων από κάποιο αρχείο ρυθμίσεων. Τέτοιες παράμετροι μπορεί να είναι για παράδειγμα η πόρτα στην οποία θα “ακούει” ή τα στοιχεία σύνδεσης με τον database server (server/χρήστης/συνθηματικό/βάση). Οι απούσες ή προβληματικές παράμετροι θα αντικαθίστανται από άλλες εξ ορισμού, ενώ αυτές που θα δίνονται στη γραμμή εντολής (argv) θα υπερισχύουν του αρχείου ρυθμίσεων. Μια ειδική παράμετρος θα είναι η ίδια η διαδρομή του αρχείου ρυθμίσεων, η οποία προφανώς θα υποσκελίζεται μόνο από κάποιο όρισμα της γραμμή εντολής.
Ένα σωρό άλλα πράγματα περνάνε από το μυαλό μου, αλλά για τώρα θα το κλείσουμε εδώ. Νομίζω ότι έχουμε αρκετά για τον αρχικό κώδικα, τον οποίο θα καλύψω στο επόμενο άρθρο.


