Φτιάχνοντας έναν Game Server σε C++ #7: Σύνδεση με MySQL
Όταν ξεκίνησα την υλοποίηση της ιδέας μου για έναν game server, το κομμάτι της αποθήκευσης δεδομένων ήταν πολύ χαμηλά στις προτεραιότητές μου. Σήμερα αποφάσισα να πάρω βαθιά ανάσα και να ξεμπερδέψω με αυτό. Σε αυτό το άρθρο θα υλοποιήσουμε μια βάση δεδομένων και θα αναδομήσουμε την κλάση Storage για να συνδέεται με αυτήν.
Σχεδιάζοντας τη Βάση Δεδομένων
Όπως έχουμε ήδη ξεκαθαρίσει, θα πρέπει να υπάρχει ένας πίνακας με users και passwords. Αλλά θέλω επίσης να προσθέσω privileges που θα έχουν οι χρήστες είτε ρητά, είτε έμμεσα ανήκοντας σε groups. Αυτά τα privileges μπορεί να είναι είτε general (πχ κλείσιμο του server, πρόσθεση/αφαίρεση χρηστών κλπ) ή σχετικά με κάποιο συγκεκριμένο object (πχ δημιουργία στιγμιότυπου κάποιου συγκεκριμένου παιχνιδιού). Υπάρχει και μια τρίτη κατηγορία σύνθετων δικαιωμάτων, αλλά αφού αυτά εξαρτώνται από διάφορους παράγοντες, θα πρέπει να υλοποιηθούν στο επίπεδο της εφαρμογής.
Με αυτά κατά νου, ας θέσουμε μερικούς βασικούς κανόνες:
- Έχουμε οντότητες δύο ειδών: χρήστες και ομάδες
- Οι οντότητες μπορούν να έχουν γενικά προνόμια
- Κάποια προνόμια μπορεί να αναφέρονται σε αντικείμενα
- Οι οντότητες μπορεί να έχουν προνόμια-σε-αντικείμενο πάνω σε αντικείμενα
- Ένας ειδικός τύπος αντικειμένου είναι το παιχνίδι
Εξ αρχής, έχουμε τέσσερις σχέσεις IS-A:
- Ένας χρήστης είναι οντότητα
- Μια ομάδα είναι οντότητα
- Ένα προνόμιο-σε-αντικείμενο είναι προνόμιο
- Ένα παιχνίδι είναι αντικείμενο
Πέρα από αυτά, η συσχέτιση μεταξύ οντοτήτων και προνομίων είναι m:n, το ίδιο και αυτή μεταξύ χρηστών και ομάδων. Αυτή μεταξύ οντοτήτων, αντικειμένων και προνομίων-σεαντικείμενα είναι m:n:k. Θεωρώντας όλα τα παραπάνω, ας φτιάξουμε ένα διάγραμμα οντοτήτων-συσχετίσεων:
Υλοποίηση με MySQL
Πρώτα απ’ όλα, πρέπει να δημιουργήσουμε μια βάση δεδομένων και έναν χρήστη με όλα τα προνόμια πάνω της:
create database gamesrv; create user user@localhost identified by 'opensessame'; grant all privileges on gamesrv.* to user@localhost;
Ας ορίσουμε πρώτα τα αντικείμενα και τα προνόμια, αφού είναι τα πιο απλά. Έχουμε μόνο ένα παιχνίδι (rps) και θα δημιουργήσουμε μερικά προνόμια, ένα από τα οποία εφαρμόζεται σε αντικείμενα:
create table object (id varchar(100) not null primary key);
create table game (id varchar(100) not null primary key references object(id));
insert into object (id) values ('rps');
insert into game (id) values ('rps');
create table privilege (id varchar(100) not null primary key);
create table object_privilege (id varchar(100) not null primary key references privilege(id));
insert into privilege (id) values ('shutdown'),('manage_users'),('run');
insert into object_privilege (id) values ('run');
Έπειτα θα κάνουμε το ίδιο για το κομμάτι των χρηστών/ομάδων:
create table entity (name varchar(100) not null primary key);
create table user (
username varchar(100) not null primary key references entity(name),
password varchar(100) not null
);
insert into entity (name) values
('root'),('mario'),('luigi'),('michelangelo'),('sonic'),('robotnik');
insert into user (username,password) values
('root','123qwe'),('mario','Its-aMe'),('luigi','OkeyDokey'),
('michelangelo','cowabunga'),('sonic','u2slo'),('robotnik','eggman');
create table `group` (name varchar(100) not null primary key references entity(name));
insert into entity (name) values ('admins'),('heroes'),('villains');
insert into `group` (name) values ('admins'),('heroes'),('villains');
create table belongs (
user varchar(100) not null references user,
`group` varchar(100) not null references `group`(name),
primary key(user,`group`)
);
insert into belongs (`group`,user) values
('admins','mario'),('admins','michelangelo'),
('villains','robotnik'),
('heroes','mario'),('heroes','luigi'),('heroes','sonic');
Όπως βλέπουμε στο τελευταίο μέρος, ο mario και ο michelangelo είναι admins, ο robotnik είναι ο μόνος vilain και οι mario, luigi και sonic ανήκουν στην μάδα των heroes. Περαιτέρω, ας μοιράσουμε μερικά προνόμια:
create table can (
entity varchar(100) not null references entity,
privilege varchar(100) not null references privilege,
primary key (entity,privilege)
);
insert into can (entity,privilege) select 'root',id from privilege;
insert into can (entity,privilege) values ('admins','manage_users'),('admins','run');
create table can_object (
entity varchar(100) not null references entity,
object varchar(100) not null references object,
privilege varchar(100) not null references object_privilege,
primary key (entity,object,privilege)
);
insert into can_object (entity,object,privilege) values ('villains','rps','run');
Μετά από αυτό, ο root μπορεί να κάνει τα πάντα, οι admins (ο mario και ο michelangelo) μπορούν να διαχειρίζονται τους χρήστες και να ξεκινούν οποιοδήποτε παιχνίδι, και οι vilaind (δηλαδή ο robotnik) μπορούν να ξεκινούν συγκεκριμένα το rps — προφανώς για να τους ανταγωνιστούν οι heroes…
Τέλος, θέλω να έχω ένα βολικό τρόπο να ανακτώ τα προνόμια των χρηστών ανεξάρτητα από την πηγή τους, δηλαδή είτε μέσω ρητής ανάθεσης ή μέσω συμμετοχής σε ομάδα. Για να το πετύχουμε αυτό, θα φτιάξουμε δύο views:
create view ext_can as
select distinct u.username,c.privilege from user u
left join belongs b on (u.username=b.user)
join can c on (c.entity=u.username or c.entity=b.`group`);
create view ext_can_object as
select distinct u.username,c.object,c.privilege from user u
left join belongs b on (u.username=b.user)
join can_object c on (c.entity=u.username or c.entity=b.`group`);
Ας τις δοκιμάσουμε:
mysql> select * from ext_can; +--------------+--------------+ | username | privilege | +--------------+--------------+ | mario | manage_users | | mario | run | | michelangelo | manage_users | | michelangelo | run | | root | manage_users | | root | run | | root | shutdown | +--------------+--------------+ 7 rows in set (0,04 sec) mysql> select * from ext_can_object; +----------+--------+-----------+ | username | object | privilege | +----------+--------+-----------+ | robotnik | rps | run | +----------+--------+-----------+ 1 row in set (0,01 sec)
Αναδόμηση της κλάσης Storage
Από την πλευρά της C++, θα χρησιμοποιήσω το Connector/C++. Θα ξεκινήσουμε την αναδόμηση από τον constructor, και θα εισάγουμε έναν νέο destructor:
class Storage {
private:
Connection *db;
public:
Storage(string,string,string,string);
~Storage();
bool matchPassword(string,string);
};
Storage::Storage(string h,string d,string u,string p) {
db=get_driver_instance()->connect("tcp://"+h,u,p);
db->setSchema(d);
}
Storage::~Storage() { delete db; }
Αρκετά απλό: αφού αποκτήσουμε μια σύνδεση με τον server, επιλέγουμε τη βάση και, όταν το αντικείμενο καταστρέφεται, ελευθερώνουμε τη μνήμη που δεσμεύσαμε με το σχετικό πεδίο. Έπειτα, ας δούμε πως μεταβάλλεται η matchPassword() για να δουλεύει με τη βάση:
bool Storage::matchPassword(string username,string password) {
string q="select password from user where username=?";
PreparedStatement *ps=db->prepareStatement(q);
ps->setString(1,username);
ResultSet *r=ps->executeQuery();
bool match=r->next()&&(r->getString(1)==password);
delete ps;
delete r;
return match;
}
Η ιδέα εδώ είναι ότι ο έλεγχος αποτυγχάνει αν ο χρήστης δεν υπάρχει (το ερώτημα δεν επέστρεψε γραμμές) ή υπάρχει αλλά το password του δεν ταιριάζει με αυτό που δόθηκε.

