Simple Web Game Server  0.1
A C++ library for creating authenticated scalable backends for multiplayer web games.
matchmaking_server.hpp
1 /*
2  * Copyright (c) 2020 Daniel Aven Bross
3  *
4  * Permission is hereby granted, free of charge, to any person obtaining a copy
5  * of this software and associated documentation files (the "Software"), to deal
6  * in the Software without restriction, including without limitation the rights
7  * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
8  * copies of the Software, and to permit persons to whom the Software is
9  * furnished to do so, subject to the following conditions:
10  *
11  * The above copyright notice and this permission notice shall be included in all
12  * copies or substantial portions of the Software.
13  *
14  * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
15  * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
16  * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
17  * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
18  * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
19  * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
20  * SOFTWARE.
21  */
22 
23 #ifndef JWT_GAME_SERVER_MATCHMAKING_SERVER_HPP
24 #define JWT_GAME_SERVER_MATCHMAKING_SERVER_HPP
25 
26 #include "base_server.hpp"
27 
28 #include <unordered_set>
29 #include <chrono>
30 #include <functional>
31 #include <tuple>
32 
33 namespace simple_web_game_server {
34  // Time literals to initialize timestep variables
35  using namespace std::chrono_literals;
36 
37  // datatype implementations
38  using std::unordered_set;
39 
41 
45  template<typename matchmaker, typename jwt_clock, typename json_traits,
46  typename server_config, typename close_reasons = default_close_reasons>
48  // type definitions
49  private:
51  typename matchmaker::player_traits,
52  jwt_clock,
53  json_traits,
54  server_config,
55  close_reasons
56  >;
57 
58  using combined_id = typename jwt_base_server::combined_id;
59  using player_id = typename jwt_base_server::player_id;
60  using session_id = typename jwt_base_server::session_id;
61  using id_hash = typename jwt_base_server::id_hash;
62 
63  using json = typename jwt_base_server::json;
64  using clock = typename jwt_base_server::clock;
65 
66  using game = std::tuple<vector<session_id>, session_id, json>;
67  using message = pair<session_id, std::string>;
68  using session_data = typename matchmaker::session_data;
69 
70  using ssl_context_ptr = typename jwt_base_server::ssl_context_ptr;
71 
73  struct connection_update {
74  connection_update(const combined_id& i) : id(i),
75  disconnection(true) {}
76  connection_update(const combined_id& i, json&& d) : id(i),
77  data(std::move(d)), disconnection(false) {}
78 
79  combined_id id;
80  json data;
81  bool disconnection;
82  };
83 
84  // main class body
85  public:
87 
92  const jwt::verifier<jwt_clock, json_traits>& v,
93  function<std::string(const combined_id&, const json&)> f,
94  std::chrono::milliseconds t
95  ) : m_jwt_server{v, f, t}
96  {
97  m_jwt_server.set_open_handler(
98  bind(
99  &matchmaking_server::player_connect,
100  this,
101  simple_web_game_server::_1,
102  simple_web_game_server::_2
103  )
104  );
105  m_jwt_server.set_close_handler(
106  bind(
107  &matchmaking_server::player_disconnect,
108  this,
109  simple_web_game_server::_1
110  )
111  );
112  m_jwt_server.set_message_handler(
113  bind(
114  &matchmaking_server::process_message,
115  this,
116  simple_web_game_server::_1,
117  simple_web_game_server::_2
118  )
119  );
120  }
121 
124  const jwt::verifier<jwt_clock, json_traits>& v,
125  function<std::string(const combined_id&, const json&)> f
126  ) : matchmaking_server{v, f, 3600s} {}
127 
129  void set_tls_init_handler(function<ssl_context_ptr(connection_hdl)> f) {
130  m_jwt_server.set_tls_init_handler(f);
131  }
132 
134  void run(uint16_t port, bool unlock_address = false) {
135  m_jwt_server.run(port, unlock_address);
136  }
137 
140  m_jwt_server.process_messages();
141  }
142 
144  void reset() {
145  stop();
146  m_jwt_server.reset();
147  }
148 
150  void stop() {
151  m_jwt_server.stop();
152  {
153  lock_guard<mutex> guard(m_match_lock);
154  m_session_data.clear();
155  m_session_players.clear();
156  m_connection_updates.second.clear();
157  }
158  {
159  lock_guard<mutex> guard(m_connection_update_list_lock);
160  m_connection_updates.first.clear();
161  }
162  m_match_condition.notify_one();
163  }
164 
166  std::size_t get_player_count() {
167  return m_jwt_server.get_player_count();
168  }
169 
170  bool is_running() {
171  return m_jwt_server.is_running();
172  }
173 
175 
181  void match_players(std::chrono::milliseconds timestep) {
182  auto time_start = clock::now();
183  pair<vector<session_id>, vector<session_id> > finished_sessions;
184 
185  while(m_jwt_server.is_running()) {
186  unique_lock<mutex> match_lock(m_match_lock);
187 
188  if(!m_matchmaker.can_match(m_session_data)) {
189  match_lock.unlock();
190  unique_lock<mutex> conn_lock(m_connection_update_list_lock);
191  while(m_connection_updates.first.empty()) {
192  m_match_condition.wait(conn_lock);
193  if(!m_jwt_server.is_running()) {
194  return;
195  }
196  }
197  match_lock.lock();
198  }
199 
200  auto delta_time =
201  std::chrono::duration_cast<std::chrono::milliseconds>(
202  clock::now() - time_start
203  );
204  const long dt_count = delta_time.count();
205 
206  if(delta_time < timestep) {
207  match_lock.unlock();
208  std::this_thread::sleep_for(std::min(1us, timestep-delta_time+1us));
209  } else {
210  time_start = clock::now();
211 
212  process_connection_updates(finished_sessions.second);
213 
214  // we remove data here to catch any possible players submitting
215  // connections in the last timestep when the session ends
216  for(const session_id& sid : finished_sessions.first) {
217  spdlog::trace("erasing data for session {}", sid);
218  m_session_data.erase(sid);
219  m_session_players.erase(sid);
220  }
221  finished_sessions.first.clear();
222  std::swap(finished_sessions.first, finished_sessions.second);
223 
224  vector<game> games;
225  {
226  vector<pair<session_id, std::string> > messages;
227  m_matchmaker.match(games, messages, m_session_data, dt_count);
228 
229  for(message& msg : messages) {
230  auto session_players_it = m_session_players.find(msg.first);
231  if(session_players_it != m_session_players.end()) {
232  for(player_id pid : session_players_it->second) {
233  m_jwt_server.send_message(
234  { pid, msg.first }, std::move(msg.second)
235  );
236  }
237  }
238  }
239  }
240 
241  for(game& g : games) {
242  vector<session_id> sessions;
243  session_id game_sid;
244  json game_data;
245  std::tie(sessions, game_sid, game_data) = std::move(g);
246 
247  spdlog::trace("matched game: {}", game_data.dump());
248 
249  for(session_id& sid : sessions) {
250  m_jwt_server.complete_session(
251  sid, game_sid, game_data
252  );
253  finished_sessions.first.push_back(sid);
254  }
255  }
256  }
257  }
258  }
259 
260  private:
261  void process_connection_updates(vector<session_id>& finished_sessions) {
262  {
263  unique_lock<mutex> conn_lock(m_connection_update_list_lock);
264  std::swap(m_connection_updates.first, m_connection_updates.second);
265  }
266 
267  for(connection_update& update : m_connection_updates.second) {
268  auto it = m_session_data.find(update.id.session);
269  if(update.disconnection) {
270  if(it != m_session_data.end()) {
271  spdlog::trace(
272  "processing disconnection for session {}", update.id.session
273  );
274  m_jwt_server.complete_session(
275  update.id.session,
276  update.id.session,
277  m_matchmaker.get_cancel_data()
278  );
279  m_session_data.erase(it);
280  m_session_players.erase(update.id.session);
281  finished_sessions.push_back(update.id.session);
282  }
283  } else {
284  spdlog::trace(
285  "processing connection for session {}", update.id.session
286  );
287  if(it == m_session_data.end()) {
288  session_data data{update.data};
289 
290  if(data.is_valid()) {
291  m_session_data.emplace(
292  update.id.session, std::move(data)
293  );
294  m_session_players.emplace(
295  update.id.session, set<player_id>{ update.id.player }
296  );
297  } else {
298  m_jwt_server.complete_session(
299  update.id.session,
300  update.id.session,
301  m_matchmaker.get_cancel_data()
302  );
303  finished_sessions.push_back(update.id.session);
304  }
305  } else {
306  m_session_players.at(update.id.session).insert(update.id.player);
307  }
308  }
309  }
310  m_connection_updates.second.clear();
311  }
312 
313  // proper procedure for client to cancel matchmaking is to send a message
314  // and wait for the server to close the connection; by acquiring the match
315  // lock and marking the user as unavailable we avoid the situation where a
316  // player disconnects after the match function is called, but before a game
317  // token is successfully sent to them
318  void process_message(const combined_id& id, std::string&& data) {
319  {
320  lock_guard<mutex> guard(m_connection_update_list_lock);
321  m_connection_updates.first.emplace_back(id);
322  }
323  m_match_condition.notify_one();
324  }
325 
326  void player_connect(const combined_id& id, json&& data) {
327  {
328  lock_guard<mutex> guard(m_connection_update_list_lock);
329  m_connection_updates.first.emplace_back(
330  id, std::move(data)
331  );
332  }
333  m_match_condition.notify_one();
334  }
335 
336  void player_disconnect(const combined_id& id) {
337  {
338  lock_guard<mutex> guard(m_connection_update_list_lock);
339  m_connection_updates.first.emplace_back(id);
340  }
341  m_match_condition.notify_one();
342  }
343 
344  // member variables
345  matchmaker m_matchmaker;
346 
347  unordered_map<session_id, session_data, id_hash> m_session_data;
348  unordered_map<session_id, set<player_id>, id_hash> m_session_players;
349  mutex m_match_lock;
350 
351  pair<
352  vector<connection_update>,
353  vector<connection_update>
354  > m_connection_updates;
355  mutex m_connection_update_list_lock;
356 
357  condition_variable m_match_condition;
358 
359  jwt_base_server m_jwt_server;
360  };
361 }
362 
363 #endif // JWT_GAME_SERVER_MATCHMAKING_SERVER_HPP
A WebSocket server that performs authentication and manages sessions.
Definition: base_server.hpp:107
std::chrono::high_resolution_clock clock
The type of clock for server time-step management.
Definition: base_server.hpp:131
typename combined_id::player_id player_id
The type of the player component of a client id.
Definition: base_server.hpp:122
typename player_traits::id combined_id
The type of a client id.
Definition: base_server.hpp:120
typename combined_id::session_id session_id
The type of the session component of a client id.
Definition: base_server.hpp:124
websocketpp::lib::shared_ptr< websocketpp::lib::asio::ssl::context > ssl_context_ptr
The type of a pointer to an asio ssl context.
Definition: base_server.hpp:135
typename json_traits::json json
The type of a json object.
Definition: base_server.hpp:129
typename combined_id::hash id_hash
The type of the hash struct for all id types.
Definition: base_server.hpp:126
A matchmaking server built on the base_server class.
Definition: matchmaking_server.hpp:47
void set_tls_init_handler(function< ssl_context_ptr(connection_hdl)> f)
Sets the tls_init_handler for the underlying base_server.
Definition: matchmaking_server.hpp:129
void stop()
Stops the server and clears all data and connections.
Definition: matchmaking_server.hpp:150
void match_players(std::chrono::milliseconds timestep)
Loop to match players.
Definition: matchmaking_server.hpp:181
matchmaking_server(const jwt::verifier< jwt_clock, json_traits > &v, function< std::string(const combined_id &, const json &)> f, std::chrono::milliseconds t)
The constructor for the matchmaking_server class.
Definition: matchmaking_server.hpp:91
void run(uint16_t port, bool unlock_address=false)
Runs the underlying base_server.
Definition: matchmaking_server.hpp:134
matchmaking_server(const jwt::verifier< jwt_clock, json_traits > &v, function< std::string(const combined_id &, const json &)> f)
Constructs the underlying base_server with a default time-step.
Definition: matchmaking_server.hpp:123
std::size_t get_player_count()
Returns the number of verified clients connected.
Definition: matchmaking_server.hpp:166
void reset()
Stops, clears, and resets the server so it may be run again.
Definition: matchmaking_server.hpp:144
void process_messages()
Runs the process_messages loop on the underlying base_server.
Definition: matchmaking_server.hpp:139
Definition: base_server.hpp:52