Simple Web Game Server  0.1
A C++ library for creating authenticated scalable backends for multiplayer web games.
game_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_GAME_SERVER_HPP
24 #define JWT_GAME_SERVER_GAME_SERVER_HPP
25 
26 #include "base_server.hpp"
27 
28 #include <chrono>
29 #include <algorithm>
30 #include <execution>
31 
32 namespace simple_web_game_server {
33  // time literals to initialize time-step variables
34  using namespace std::chrono_literals;
35 
37 
41  template<typename game_instance, typename jwt_clock, typename json_traits,
42  typename server_config, typename close_reasons = default_close_reasons>
43  class game_server {
44  // type definitions
45  private:
47  typename game_instance::player_traits,
48  jwt_clock,
49  json_traits,
50  server_config,
51  close_reasons
52  >;
53 
54  using combined_id = typename jwt_base_server::combined_id;
55  using player_id = typename jwt_base_server::player_id;
56  using session_id = typename jwt_base_server::session_id;
57  using id_hash = typename jwt_base_server::id_hash;
58 
59  using message = pair<player_id, std::string>;
60 
61  using json = typename jwt_base_server::json;
62  using clock = typename jwt_base_server::clock;
63 
64  using ssl_context_ptr = typename jwt_base_server::ssl_context_ptr;
65 
66  // The data associated to a connecting or disconnecting client.
67  struct connection_update {
68  connection_update(const combined_id& i) : id(i), disconnection(true) {}
69  connection_update(const combined_id& i, json&& d) : id(i),
70  data(std::move(d)), disconnection(false) {}
71 
72  combined_id id;
73  json data;
74  bool disconnection;
75  };
76 
77  // main class body
78  public:
80 
84  const jwt::verifier<jwt_clock, json_traits>& v,
85  function<std::string(const combined_id&, const json&)> f,
86  std::chrono::milliseconds t
87  ) : m_game_count(0), m_jwt_server(v, f, t)
88  {
89  m_jwt_server.set_open_handler(
90  bind(
91  &game_server::player_connect,
92  this,
93  simple_web_game_server::_1,
94  simple_web_game_server::_2
95  )
96  );
97  m_jwt_server.set_close_handler(
98  bind(
99  &game_server::player_disconnect,
100  this,
101  simple_web_game_server::_1
102  )
103  );
104  m_jwt_server.set_message_handler(
105  bind(
106  &game_server::process_message,
107  this,
108  simple_web_game_server::_1,
109  simple_web_game_server::_2
110  )
111  );
112  }
113 
116  const jwt::verifier<jwt_clock, json_traits>& v,
117  function<std::string(const combined_id&, const json&)> f
118  ) : game_server(v, f, 3600s) {}
119 
120 
122  void set_tls_init_handler(function<ssl_context_ptr(connection_hdl)> f) {
123  m_jwt_server.set_tls_init_handler(f);
124  }
125 
127  void run(uint16_t port, bool unlock_address = false) {
128  m_jwt_server.run(port, unlock_address);
129  }
130 
133  m_jwt_server.process_messages();
134  }
135 
137  void reset() {
138  stop();
139  m_jwt_server.reset();
140  }
141 
143  void stop() {
144  m_jwt_server.stop();
145  m_game_condition.notify_one();
146  {
147  lock_guard<mutex> guard(m_game_list_lock);
148  m_games.clear();
149  m_out_messages.clear();
150  m_connection_updates.second.clear();
151  m_in_messages.second.clear();
152  }
153  {
154  lock_guard<mutex> guard(m_in_message_list_lock);
155  m_in_messages.first.clear();
156  }
157  {
158  lock_guard<mutex> guard(m_connection_update_list_lock);
159  m_connection_updates.first.clear();
160  }
161  {
162  lock_guard<mutex> guard(m_game_count_lock);
163  m_game_count = 0;
164  }
165  }
166 
168  std::size_t get_player_count() {
169  return m_jwt_server.get_player_count();
170  }
171 
172  bool is_running() {
173  return m_jwt_server.is_running();
174  }
175 
177 
184  void update_games(std::chrono::milliseconds timestep) {
185  auto time_start = clock::now();
186  vector<session_id> finished_games;
187 
188  while(m_jwt_server.is_running()) {
189  const auto delta_time =
190  std::chrono::duration_cast<std::chrono::milliseconds>(
191  clock::now() - time_start
192  );
193 
194  unique_lock<mutex> game_lock(m_game_list_lock);
195  if(m_games.empty()) {
196  game_lock.unlock();
197  unique_lock<mutex> conn_lock(m_connection_update_list_lock);
198  while(m_connection_updates.first.empty()) {
199  m_game_condition.wait(conn_lock);
200  if(!m_jwt_server.is_running()) {
201  return;
202  }
203  time_start = clock::now();
204  }
205  game_lock.lock();
206  }
207 
208  if(delta_time < timestep) {
209  game_lock.unlock();
210  std::this_thread::sleep_for(std::min(1ms, timestep-delta_time));
211  } else {
212  time_start = clock::now();
213  process_connection_updates();
214 
215  // we remove game data here to catch any possible players submitting
216  // new connections in the last time-step when the game session ends
217  {
218  lock_guard<mutex> gc_guard(m_game_count_lock);
219  for(session_id sid : finished_games) {
220  spdlog::trace("erasing game session {}", sid);
221  m_out_messages.erase(sid);
222  m_games.erase(sid);
223  --m_game_count;
224  }
225  }
226  finished_games.clear();
227 
228  process_game_updates(delta_time.count());
229 
230  for(auto it = m_out_messages.begin(); it != m_out_messages.end();
231  ++it)
232  {
233  for(message& msg : it->second) {
234  m_jwt_server.send_message(
235  { msg.first, it->first },
236  std::move(msg.second)
237  );
238  }
239  it->second.clear();
240  }
241 
242  for(auto it = m_games.begin(); it != m_games.end(); ++it) {
243  if(it->second.is_done()) {
244  spdlog::debug("game session {} ended", it->first);
245  m_jwt_server.complete_session(
246  it->first,
247  it->first,
248  it->second.get_state()
249  );
250  finished_games.push_back(it->first);
251  }
252  }
253  }
254  }
255  }
256 
258  std::size_t get_game_count() {
259  lock_guard<mutex> guard(m_game_count_lock);
260  return m_game_count;
261  }
262 
263  private:
264  void process_connection_updates() {
265  {
266  lock_guard<mutex> conn_guard(m_connection_update_list_lock);
267  std::swap(m_connection_updates.first, m_connection_updates.second);
268  }
269 
270  for(connection_update& update : m_connection_updates.second) {
271  auto games_it = m_games.find(update.id.session);
272  auto out_messages_it = m_out_messages.find(update.id.session);
273 
274  if(update.disconnection) {
275  if(games_it != m_games.end()) {
276  games_it->second.disconnect(
277  out_messages_it->second,
278  update.id.player
279  );
280  }
281  } else {
282  if(games_it == m_games.end()) {
283  game_instance game{update.data};
284 
285  if(!game.is_valid()) {
286  spdlog::error("connection provided invalid game data");
287  m_jwt_server.complete_session(
288  update.id.session, update.id.session, game.get_state()
289  );
290  continue;
291  }
292 
293  spdlog::debug("creating game session {}", update.id.session);
294  games_it = m_games.emplace(
295  update.id.session, std::move(game)
296  ).first;
297  out_messages_it = m_out_messages.emplace(
298  update.id.session, vector<message>{}
299  ).first;
300  {
301  lock_guard<mutex> gc_guard(m_game_count_lock);
302  ++m_game_count;
303  }
304  }
305 
306  games_it->second.connect(out_messages_it->second, update.id.player);
307  }
308  }
309 
310  m_connection_updates.second.clear();
311  }
312 
313  void process_game_updates(long delta_time) {
314  {
315  lock_guard<mutex> msg_guard(m_in_message_list_lock);
316  std::swap(m_in_messages.first, m_in_messages.second);
317  }
318 
319  // game updates are completely independent, so exec in parallel
320  std::for_each(
321  std::execution::par,
322  m_games.begin(),
323  m_games.end(),
324  [&](auto& key_val_pair){
325  auto in_msg_it = m_in_messages.second.find(key_val_pair.first);
326  if(in_msg_it != m_in_messages.second.end()) {
327  key_val_pair.second.update(
328  m_out_messages.at(key_val_pair.first),
329  in_msg_it->second,
330  delta_time
331  );
332  } else {
333  key_val_pair.second.update(
334  m_out_messages.at(key_val_pair.first),
335  vector<message>{},
336  delta_time
337  );
338  }
339  }
340  );
341 
342  m_in_messages.second.clear();
343  }
344 
345  void process_message(const combined_id& id, std::string&& data) {
346  lock_guard<mutex> msg_guard(m_in_message_list_lock);
347  m_in_messages.first[id.session].emplace_back(
348  id.player, std::move(data)
349  );
350  }
351 
352  void player_connect(const combined_id& id, json&& data) {
353  {
354  lock_guard<mutex> guard(m_connection_update_list_lock);
355  m_connection_updates.first.emplace_back(
356  id, std::move(data)
357  );
358  }
359  m_game_condition.notify_one();
360  }
361 
362  void player_disconnect(const combined_id& id) {
363  lock_guard<mutex> guard(m_connection_update_list_lock);
364  m_connection_updates.first.emplace_back(id);
365  }
366 
367  // member variables
368  unordered_map<
369  session_id,
370  game_instance,
371  id_hash
372  > m_games;
373  mutex m_game_list_lock;
374 
375  std::size_t m_game_count;
376  mutex m_game_count_lock;
377 
378  pair<
379  unordered_map<
380  session_id,
381  vector<message>,
382  id_hash
383  >,
384  unordered_map<
385  session_id,
386  vector<message>,
387  id_hash
388  >
389  > m_in_messages;
390  mutex m_in_message_list_lock;
391 
392  pair<
393  vector<connection_update>,
394  vector<connection_update>
395  > m_connection_updates;
396  mutex m_connection_update_list_lock;
397 
398  condition_variable m_game_condition;
399 
400  unordered_map<session_id, vector<message>, id_hash> m_out_messages;
401 
402  jwt_base_server m_jwt_server;
403  };
404 }
405 
406 #endif // JWT_GAME_SERVER_GAME_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 game server built on the base_server class.
Definition: game_server.hpp:43
void update_games(std::chrono::milliseconds timestep)
Loop to run games.
Definition: game_server.hpp:184
game_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: game_server.hpp:115
std::size_t get_player_count()
Returns the number of verified clients connected.
Definition: game_server.hpp:168
std::size_t get_game_count()
Returns the number of running game sessions.
Definition: game_server.hpp:258
void process_messages()
Runs the process_messages loop on the underlying base_server.
Definition: game_server.hpp:132
void stop()
Stops the server and clears all data and connections.
Definition: game_server.hpp:143
void set_tls_init_handler(function< ssl_context_ptr(connection_hdl)> f)
Sets the tls_init_handler for the underlying base_server.
Definition: game_server.hpp:122
void run(uint16_t port, bool unlock_address=false)
Runs the underlying base_server.
Definition: game_server.hpp:127
void reset()
Stops, clears, and resets the server so it may be run again.
Definition: game_server.hpp:137
game_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 game_server class.
Definition: game_server.hpp:83
Definition: base_server.hpp:52