Simple Web Game Server  0.1
A C++ library for creating authenticated scalable backends for multiplayer web games.
base_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_BASE_SERVER_HPP
24 #define JWT_GAME_SERVER_BASE_SERVER_HPP
25 
26 #include <websocketpp/server.hpp>
27 #include <websocketpp/common/asio_ssl.hpp>
28 #include <websocketpp/common/asio.hpp>
29 
30 #include <jwt-cpp/jwt.h>
31 
32 #include <spdlog/spdlog.h>
33 
34 #include <vector>
35 #include <queue>
36 #include <set>
37 #include <map>
38 #include <unordered_map>
39 
40 #include <utility>
41 
42 #include <functional>
43 
44 #include <atomic>
45 #include <mutex>
46 #include <condition_variable>
47 
54  using websocketpp::connection_hdl;
55 
56  // datatype implementations
57  using std::vector;
58  using std::pair;
59  using std::set;
60  using std::map;
61  using std::unordered_map;
62  using std::queue;
63 
64  // functional types
65  using std::function;
66  using std::bind;
67  using std::placeholders::_1;
68  using std::placeholders::_2;
69 
70  // threading type implementations
71  using std::atomic;
72  using std::mutex;
73  using std::lock_guard;
74  using std::unique_lock;
75  using std::condition_variable;
76 
79  static inline std::string invalid_jwt() {
80  return "INVALID_TOKEN";
81  };
82  static inline std::string duplicate_connection() {
83  return "DUPLICATE_CONNECTION";
84  };
85  static inline std::string server_shutdown() {
86  return "SERVER_SHUTDOWN";
87  };
88  static inline std::string session_complete() {
89  return "SESSION_COMPLETE";
90  };
91  };
92 
94 
105  template<typename player_traits, typename jwt_clock, typename json_traits,
106  typename server_config, typename close_reasons>
107  class base_server {
108  // type definitions
109  public:
111  class server_error : public std::runtime_error {
112  public:
113  using super = std::runtime_error;
114  explicit server_error(const std::string& what_arg) noexcept :
115  super(what_arg) {}
116  explicit server_error(const char* what_arg) noexcept : super(what_arg) {}
117  };
118 
120  using combined_id = typename player_traits::id;
122  using player_id = typename combined_id::player_id;
124  using session_id = typename combined_id::session_id;
126  using id_hash = typename combined_id::hash;
127 
129  using json = typename json_traits::json;
131  using clock = std::chrono::high_resolution_clock;
132 
135  websocketpp::lib::shared_ptr<websocketpp::lib::asio::ssl::context>;
136 
137  private:
138  using time_point = std::chrono::time_point<clock>;
139 
140  // The types of actions available for our action processing queue.
141  enum action_type {
142  SUBSCRIBE,
143  UNSUBSCRIBE,
144  IN_MESSAGE,
145  OUT_MESSAGE,
146  CLOSE_CONNECTION
147  };
148 
150  using ws_server = websocketpp::server<server_config>;
151  using message_ptr = typename ws_server::message_ptr;
152 
155  struct action {
156  action(action_type t, connection_hdl h) : type(t), hdl(h) {}
157  action(action_type t, connection_hdl h, std::string&& m)
158  : type(t), hdl(h), msg(std::move(m)) {}
159  action(action_type t, connection_hdl h, const std::string& m)
160  : type(t), hdl(h), msg(m) {}
161 
162  action_type type;
163  connection_hdl hdl;
164  std::string msg;
165  };
166 
168  struct session_data {
169  session_data(const session_id& s, const json& d) : session(s), data(d) {}
170  session_id session;
171  json data;
172  };
173 
181  template<typename map_type>
182  class buffered_map {
183  public:
184  using key_type = typename map_type::key_type;
185  using mapped_type = typename map_type::mapped_type;
186  using value_type = typename map_type::value_type;
187 
188  void insert(value_type&& p) {
189  m_map1.insert(std::move(p));
190  }
191 
192  const mapped_type& at(const key_type& key) const {
193  auto it = m_map1.find(key);
194  if(it != m_map1.end()) {
195  return it->second;
196  } else {
197  it = m_map2.find(key);
198  if(it != m_map2.end()) {
199  return it->second;
200  } else {
201  throw std::out_of_range{"key not in buffered_map"};
202  }
203  }
204  }
205 
206  bool contains(const key_type& key) const {
207  bool result = false;
208  if(m_map1.count(key) > 0) {
209  result = true;
210  } else if(m_map2.count(key) > 0) {
211  result = true;
212  }
213  return result;
214  }
215 
216  void clear() {
217  m_map2.clear();
218  std::swap(m_map1, m_map2);
219  }
220 
221  map_type m_map1;
222  map_type m_map2;
223  };
224 
225  // main class body
226  public:
228 
237  const jwt::verifier<jwt_clock, json_traits>& v,
238  function<std::string(const combined_id&, const json&)> f,
239  std::chrono::milliseconds t
240  ) : m_is_running(false), m_jwt_verifier(v), m_get_result_str(f),
241  m_session_release_time(t), m_player_count(0),
242  m_handle_open([](const combined_id&, json&&){}),
243  m_handle_close([](const combined_id&){}),
244  m_handle_message([](const combined_id&, std::string&&){})
245  {
246  m_server.init_asio();
247 
248  m_server.set_open_handler(bind(&base_server::on_open, this,
249  simple_web_game_server::_1));
250  m_server.set_close_handler(bind(&base_server::on_close, this,
251  simple_web_game_server::_1));
252  m_server.set_message_handler(
253  bind(&base_server::on_message, this, simple_web_game_server::_1,
254  simple_web_game_server::_2)
255  );
256  }
257 
259  void set_tls_init_handler(function<ssl_context_ptr(connection_hdl)> f) {
260  if(!m_is_running) {
261  m_server.set_tls_init_handler(f);
262  } else {
263  throw server_error{"set_tls_init_handler called on running server"};
264  }
265  }
266 
268  void set_open_handler(function<void(const combined_id&,json&&)> f) {
269  if(!m_is_running) {
270  m_handle_open = f;
271  } else {
272  throw server_error{"set_open_handler called on running server"};
273  }
274  }
275 
277  void set_close_handler(function<void(const combined_id&)> f) {
278  if(!m_is_running) {
279  m_handle_close = f;
280  } else {
281  throw server_error{"set_close_handler called on running server"};
282  }
283  }
284 
286  void set_message_handler(function<void(const combined_id&,std::string&&)> f) {
287  if(!m_is_running) {
288  m_handle_message = f;
289  } else {
290  throw server_error{"set_message_handler called on running server"};
291  }
292  }
293 
295 
299  void run(uint16_t port, bool unlock_address) {
300  if(!m_is_running) {
301  spdlog::info("server is listening on port {}", port);
302  m_is_running = true;
303 
304  m_server.set_reuse_addr(unlock_address);
305  m_server.listen(port);
306  m_server.start_accept();
307  }
308 
309  m_server.run();
310  }
311 
313  bool is_running() {
314  return m_is_running;
315  }
316 
318  void reset() {
319  if(m_is_running) {
320  stop();
321  }
322  m_server.reset();
323  }
324 
326  void stop() {
327  if(m_is_running) {
328  m_is_running = false;
329  m_server.stop_listening();
330  {
331  lock_guard<mutex> action_guard(m_action_lock);
332  lock_guard<mutex> session_guard(m_session_lock);
333  lock_guard<mutex> conn_guard(m_connection_lock);
334  lock_guard<mutex> pc_guard(m_player_count_lock);
335 
336  // collect all unresolved connection actions
337  while(!m_actions.empty()) {
338  action a = m_actions.front();
339  m_actions.pop();
340  if(a.type == SUBSCRIBE || a.type == CLOSE_CONNECTION) {
341  m_new_connections.insert(a.hdl);
342  }
343  }
344 
345  // collect all open player connections
346  for(auto& con_pair : m_connection_ids) {
347  m_new_connections.insert(con_pair.first);
348  }
349 
350  // close all remaining open connections
351  for(connection_hdl hdl : m_new_connections) {
352  close_hdl(hdl, close_reasons::server_shutdown());
353  }
354 
355  m_player_count = 0;
356  m_connection_ids.clear();
357  m_id_connections.clear();
358  m_new_connections.clear();
359  m_locked_sessions.clear();
360  m_locked_sessions.clear();
361  m_session_players.clear();
362  }
363  m_action_cond.notify_all();
364  } else {
365  throw server_error("stop called on stopped server");
366  }
367  }
368 
370 
375  while(m_is_running) {
376  unique_lock<mutex> action_lock(m_action_lock);
377 
378  while(m_actions.empty()) {
379  m_action_cond.wait(action_lock);
380  if(!m_is_running) {
381  return;
382  }
383  }
384 
385  action a = m_actions.front();
386  m_actions.pop();
387 
388  action_lock.unlock();
389 
390  if (a.type == SUBSCRIBE) {
391  spdlog::trace("processing SUBSCRIBE action");
392  unique_lock<mutex> conn_lock(m_connection_lock);
393  m_new_connections.insert(a.hdl);
394  } else if (a.type == UNSUBSCRIBE) {
395  spdlog::trace("processing UNSUBSCRIBE action");
396  unique_lock<mutex> conn_lock(m_connection_lock);
397 
398  auto it = m_connection_ids.find(a.hdl);
399  if(it == m_connection_ids.end()) {
400  m_new_connections.erase(a.hdl);
401  spdlog::trace(
402  "client hdl {} disconnected without opening session",
403  a.hdl.lock().get()
404  );
405  } else {
406  // connection provided a player id
407  combined_id id = it->second;
408  conn_lock.unlock();
409  player_disconnect(a.hdl, id);
410  }
411  } else if (a.type == IN_MESSAGE) {
412  spdlog::trace("processing IN_MESSAGE action");
413  unique_lock<mutex> conn_lock(m_connection_lock);
414 
415  auto it = m_connection_ids.find(a.hdl);
416  if(it == m_connection_ids.end()) {
417  spdlog::trace(
418  "recieved message from client hdl {} w/no id: {}",
419  a.hdl.lock().get(),
420  a.msg
421  );
422  conn_lock.unlock();
423  open_session(a.hdl, a.msg);
424  } else {
425  combined_id id = it->second;
426  conn_lock.unlock();
427 
428  spdlog::trace(
429  "player {} with session {} sent: {}",
430  id.player,
431  id.session,
432  a.msg
433  );
434 
435  m_handle_message(id, std::move(a.msg));
436  }
437  } else if(a.type == OUT_MESSAGE) {
438  spdlog::trace("processing OUT_MESSAGE action");
439  spdlog::trace(
440  "sending message to client hdl {}: {}",
441  a.hdl.lock().get(),
442  a.msg
443  );
444  send_to_hdl(a.hdl, a.msg);
445  } else if(a.type == CLOSE_CONNECTION) {
446  spdlog::trace("processing CLOSE_CONNECTION action");
447  spdlog::trace(
448  "closing client hdl {} with final message: {}",
449  a.hdl.lock().get(),
450  a.msg
451  );
452 
453  send_to_hdl(a.hdl, a.msg);
454  close_hdl(a.hdl, close_reasons::session_complete());
455  } else {
456  // undefined.
457  }
458  }
459  }
460 
462  std::size_t get_player_count() {
463  lock_guard<mutex> guard(m_player_count_lock);
464  return m_player_count;
465  }
466 
468 
472  void send_message(const combined_id& id, std::string&& msg) {
473  connection_hdl hdl;
474  if(get_connection_hdl_from_id(hdl, id)) {
475  {
476  lock_guard<mutex> guard(m_action_lock);
477  spdlog::trace("out_message: {}", msg);
478  m_actions.push(
479  action{OUT_MESSAGE, hdl, std::move(msg)}
480  );
481  }
482  m_action_cond.notify_one();
483  } else {
484  spdlog::trace(
485  "ignored message sent to player {} with session {}: connection closed",
486  id.player, id.session
487  );
488  }
489  }
490 
492 
502  const session_id& sid,
503  const session_id& result_sid,
504  const json& result_data
505  )
506  {
507  unique_lock<mutex> lock(m_session_lock);
508  update_session_locks();
509  if(!m_locked_sessions.contains(sid)) {
510  spdlog::trace("completing session {}", sid);
511  session_data result{result_sid, result_data};
512 
513  auto it = m_session_players.find(sid);
514  if(it != m_session_players.end()) {
515  if(it->second.size() > 0) {
516  for(const player_id& pid : it->second) {
517  combined_id id{ pid, sid };
518 
519  connection_hdl hdl;
520  if(get_connection_hdl_from_id(hdl, id)) {
521  {
522  lock_guard<mutex> guard(m_action_lock);
523  spdlog::trace("closing session {} player {}", sid, pid);
524  m_actions.push(action(
525  CLOSE_CONNECTION,
526  hdl,
527  m_get_result_str(
528  { id.player, result.session },
529  result.data
530  )
531  ));
532  }
533  m_action_cond.notify_one();
534  } else {
535  spdlog::trace(
536  "can't close player {} session {}: connection already closed",
537  id.player,
538  id.session
539  );
540  }
541  }
542  }
543  }
544 
545  m_locked_sessions.insert(
546  std::make_pair(sid, std::move(result))
547  );
548  }
549  }
550 
551  private:
552  void player_disconnect(connection_hdl hdl, const combined_id& id) {
553  {
554  lock_guard<mutex> connection_guard(m_connection_lock);
555  m_connection_ids.erase(hdl);
556  m_id_connections.erase(id);
557  }
558  {
559  lock_guard<mutex> session_guard(m_session_lock);
560  auto it = m_session_players.find(id.session);
561  if(it != m_session_players.end()) {
562  it->second.erase(id.player);
563  if(it->second.empty()) {
564  m_session_players.erase(it);
565  }
566  }
567  }
568  {
569  lock_guard<mutex> pc_guard(m_player_count_lock);
570  --m_player_count;
571  }
572 
573  spdlog::debug("player {} with session {} disconnected",
574  id.player, id.session);
575  m_handle_close(id);
576  }
577 
578  bool get_connection_hdl_from_id(
579  connection_hdl& hdl,
580  const combined_id& id
581  )
582  {
583  lock_guard<mutex> guard(m_connection_lock);
584  auto it = m_id_connections.find(id);
585  if(it != m_id_connections.end()) {
586  hdl = it->second;
587  return true;
588  }
589 
590  return false;
591  }
592 
593  void send_to_hdl(connection_hdl hdl, const std::string& msg) {
594  try {
595  m_server.send(hdl, msg, websocketpp::frame::opcode::text);
596  } catch (std::exception& e) {
597  spdlog::debug(
598  "error sending message \"{}\": {}",
599  msg,
600  e.what()
601  );
602  }
603  }
604 
605  void close_hdl(connection_hdl hdl, const std::string& reason) {
606  try {
607  m_server.close(
608  hdl,
609  websocketpp::close::status::normal,
610  reason
611  );
612  } catch (std::exception& e) {
613  spdlog::debug("error closing connection: {}",
614  e.what());
615  }
616  }
617 
618  void on_open(connection_hdl hdl) {
619  {
620  lock_guard<mutex> guard(m_action_lock);
621  if(m_is_running) {
622  m_actions.push(action(SUBSCRIBE,hdl));
623  } else {
624  close_hdl(hdl, close_reasons::server_shutdown());
625  }
626  }
627  m_action_cond.notify_one();
628  }
629 
630  void on_close(connection_hdl hdl) {
631  {
632  lock_guard<mutex> guard(m_action_lock);
633  m_actions.push(action(UNSUBSCRIBE, hdl));
634  }
635  m_action_cond.notify_one();
636  }
637 
638  void on_message(connection_hdl hdl, message_ptr msg) {
639  {
640  lock_guard<mutex> guard(m_action_lock);
641  m_actions.push(action(IN_MESSAGE, hdl, std::move(msg->get_raw_payload())));
642  }
643  m_action_cond.notify_one();
644  }
645 
646  // assumes that m_session_lock is acquired
647  void update_session_locks() {
648  auto delta_time = std::chrono::duration_cast<std::chrono::milliseconds>(
649  clock::now() - m_last_session_update_time
650  );
651 
652  // free up sessions if enough time has passed
653  if(delta_time > m_session_release_time) {
654  if(delta_time > 2 * m_session_release_time) {
655  m_locked_sessions.clear();
656  }
657  m_locked_sessions.clear();
658  m_last_session_update_time = clock::now();
659  }
660  }
661 
662  void setup_connection_id(connection_hdl hdl, const combined_id& id) {
663  lock_guard<mutex> connection_guard(m_connection_lock);
664 
665  // immediately close duplicate connections to avoid complications
666  auto id_connections_it = m_id_connections.find(id);
667  if(id_connections_it != m_id_connections.end()) {
668  spdlog::debug(
669  "closing duplicate connection for player {} session {}",
670  id.player,
671  id.session
672  );
673 
674  close_hdl(id_connections_it->second, close_reasons::duplicate_connection());
675 
676  m_connection_ids.erase(id_connections_it->second);
677  m_id_connections.erase(id_connections_it);
678  } else {
679  lock_guard<mutex> pc_guard(m_player_count_lock);
680  ++m_player_count;
681  }
682 
683  m_connection_ids.emplace(hdl, id);
684  m_id_connections.emplace(id, hdl);
685  m_new_connections.erase(hdl);
686  }
687 
688  void open_session(connection_hdl hdl, const std::string& login_token) {
689  combined_id id;
690  json login_json;
691  bool completed = false;
692  try {
693  jwt::decoded_jwt<json_traits> decoded_token =
694  jwt::decode<json_traits>(login_token);
695  m_jwt_verifier.verify(decoded_token);
696  auto claim_map = decoded_token.get_payload_claims();
697  player_id pid = player_traits::parse_player_id(
698  claim_map.at("pid").to_json()
699  );
700  session_id sid = player_traits::parse_session_id(
701  claim_map.at("sid").to_json()
702  );
703  login_json = claim_map.at("data").to_json();
704  id = combined_id{pid, sid};
705  completed = true;
706  } catch(std::out_of_range& e) {
707  spdlog::debug(
708  "connection provided jwt without id and/or data claims: {}",
709  e.what()
710  );
711  } catch(jwt::error::token_verification_exception& e) {
712  spdlog::debug(
713  "connection provided jwt with invalid signature: {}",
714  e.what()
715  );
716  } catch(std::invalid_argument& e) {
717  spdlog::debug(
718  "connection provided invalid jwt token string: {}",
719  e.what()
720  );
721  } catch(std::exception& e) {
722  spdlog::debug("connection provided jwt with invalid claims: {}", e.what());
723  }
724 
725  if(completed) {
726  lock_guard<mutex> session_guard(m_session_lock);
727  update_session_locks();
728 
729  if(!m_locked_sessions.contains(id.session)) {
730  setup_connection_id(hdl, id);
731  m_session_players[id.session].insert(id.player);
732  spdlog::debug(
733  "player {} connected with session {}: {}",
734  id.player,
735  id.session,
736  login_json.dump()
737  );
738  m_handle_open(id, std::move(login_json));
739  } else {
740  send_to_hdl(
741  hdl,
742  m_get_result_str(
743  { id.player, m_locked_sessions.at(id.session).session },
744  m_locked_sessions.at(id.session).data
745  )
746  );
747  close_hdl(hdl, close_reasons::session_complete());
748  }
749  } else {
750  close_hdl(hdl, close_reasons::invalid_jwt());
751  }
752  }
753 
754  // member variables
755  ws_server m_server;
756  atomic<bool> m_is_running;
757 
758  jwt::verifier<jwt_clock, json_traits> m_jwt_verifier;
759  function<std::string(const combined_id&, const json&)> m_get_result_str;
760 
761  set<connection_hdl, std::owner_less<connection_hdl> > m_new_connections;
762  map<
763  connection_hdl,
764  combined_id,
765  std::owner_less<connection_hdl>
766  > m_connection_ids;
767  unordered_map<combined_id, connection_hdl, id_hash> m_id_connections;
768 
769  // m_connection_lock guards the members m_new_connections,
770  // m_id_connections, and m_connection_ids
771  mutex m_connection_lock;
772 
773  time_point m_last_session_update_time;
774  std::chrono::milliseconds m_session_release_time;
775 
776  buffered_map<
777  unordered_map<session_id, session_data, id_hash>
778  > m_locked_sessions;
779  unordered_map<session_id, set<player_id>, id_hash> m_session_players;
780 
781  // m_session_lock guards the members m_locked_sessions, and
782  // m_session_players
783  mutex m_session_lock;
784 
785  queue<action> m_actions;
786  mutex m_action_lock; // m_action_lock guards the member m_actions
787  condition_variable m_action_cond;
788 
789  std::size_t m_player_count;
790  mutex m_player_count_lock; // m_player_count_lock guards the member m_player_count
791 
792  // functions to handle client actions
793  function<void(const combined_id&, json&&)> m_handle_open;
794  function<void(const combined_id&)> m_handle_close;
795  function<void(const combined_id&, std::string&&)> m_handle_message;
796  };
797 }
798 
799 #endif // JWT_GAME_SERVER_BASE_SERVER_HPP
The class representing errors with the base_server.
Definition: base_server.hpp:111
A WebSocket server that performs authentication and manages sessions.
Definition: base_server.hpp:107
void run(uint16_t port, bool unlock_address)
Runs the underlying websocketpp server m_server.
Definition: base_server.hpp:299
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
void stop()
Stops the server, closes all connections, and clears all actions.
Definition: base_server.hpp:326
void set_message_handler(function< void(const combined_id &, std::string &&)> f)
Sets the given function to be called when a client sends a message.
Definition: base_server.hpp:286
bool is_running()
Returns whether the underlying WebSocket++ server is running.
Definition: base_server.hpp:313
base_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 base_server class.
Definition: base_server.hpp:236
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
std::size_t get_player_count()
Returns the number of verified clients connected.
Definition: base_server.hpp:462
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
void send_message(const combined_id &id, std::string &&msg)
Asynchronously sends a message to the given client.
Definition: base_server.hpp:472
typename json_traits::json json
The type of a json object.
Definition: base_server.hpp:129
void set_close_handler(function< void(const combined_id &)> f)
Sets the given function to be called when a client disconnects.
Definition: base_server.hpp:277
void process_messages()
Worker loop that processes server actions.
Definition: base_server.hpp:374
void set_open_handler(function< void(const combined_id &, json &&)> f)
Sets the given function to be called when a client sends a valid JWT.
Definition: base_server.hpp:268
void complete_session(const session_id &sid, const session_id &result_sid, const json &result_data)
Asynchronously closes the given session and sends out result tokens.
Definition: base_server.hpp:501
typename combined_id::hash id_hash
The type of the hash struct for all id types.
Definition: base_server.hpp:126
void set_tls_init_handler(function< ssl_context_ptr(connection_hdl)> f)
Sets a the given function f as the tls_init_handler for m_server.
Definition: base_server.hpp:259
void reset()
Resets the server so it may be started again.
Definition: base_server.hpp:318
Definition: base_server.hpp:52
A struct defining default close message strings.
Definition: base_server.hpp:78