├── .gitignore ├── LICENSE ├── README.md ├── include └── enetpp │ ├── client.h │ ├── client_connect_params.h │ ├── client_queued_packet.h │ ├── client_statistics.h │ ├── global_state.h │ ├── server.h │ ├── server_event.h │ ├── server_listen_params.h │ ├── server_queued_packet.h │ ├── set_current_thread_name.h │ └── trace_handler.h └── sample ├── CMakeLists.txt └── main.cpp /.gitignore: -------------------------------------------------------------------------------- 1 | #OS junk files 2 | [Tt]humbs.db 3 | *.DS_Store 4 | 5 | #Visual Studio files 6 | *.[Oo]bj 7 | *.user 8 | *.aps 9 | *.pch 10 | *.vspscc 11 | *.vssscc 12 | *_i.c 13 | *_p.c 14 | *.ncb 15 | *.suo 16 | *.tlb 17 | *.tlh 18 | *.bak 19 | *.[Cc]ache 20 | *.ilk 21 | *.log 22 | *.sbr 23 | *.sdf 24 | *.opensdf 25 | *.mdf 26 | *.ldf 27 | obj/ 28 | [Bb]in 29 | [Dd]ebug*/ 30 | [Rr]elease*/ 31 | 32 | #Tooling 33 | _ReSharper*/ 34 | *.resharper 35 | [Tt]est[Rr]esult* 36 | Packages*/ 37 | #Project files 38 | #[Bb]uild/ 39 | readme.html -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2015 Jamie Seward 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | enetpp 2 | --- 3 | A header only C++11 networking library that internally uses [ENet](http://enet.bespin.org/) 4 | 5 | This is an opinionated wrapper. Instead of raw ENet functionality, client and server objects are provided. Both of which spawn a worker thread to make sure networking communication is never interupted by the main thread. 6 | 7 | Getting Started 8 | --- 9 | 1. clone this repository 10 | 2. clone enet's repository (https://github.com/lsalzman/enet) 11 | 3. build enet as a library and link it to your project 12 | 4. add additional include directories to your project : enet/include and enetpp/include 13 | 14 | Global state 15 | --- 16 | Unfortunately enet requires some global state to be initialized / deinitialized. On windows this is winsock. 17 | 18 | ```c++ 19 | enetpp::global_state::get().initialize(); 20 | enetpp::global_state::get().deinitialize(); 21 | ``` 22 | 23 | Using enetpp::client 24 | --- 25 | The enetpp::client object is nice and simple. 26 | 27 | ```c++ 28 | #include "enetpp/client.h" 29 | 30 | enetpp::client client; 31 | client.connect(enetpp::client_connect_params() 32 | .set_channel_count(1) 33 | .set_server_host_name_and_port("localhost", 801)); 34 | 35 | while (game_is_running) { 36 | //send stuff 37 | enet_uint8 data_to_send; 38 | client.send_packet(0, &data_to_send, 1, ENET_PACKET_FLAG_RELIABLE); 39 | 40 | //consume events raised by worker thread 41 | auto on_connected = [&](){}; 42 | auto on_disconnected = [&](){}; 43 | auto on_data_received = [&](const enet_uint8* data, size_t data_size){}; 44 | client.consume_events(on_connected, on_disconnected, on_data_received); 45 | } 46 | 47 | client.disconnect(); 48 | ``` 49 | 50 | Using enetpp::server 51 | --- 52 | The enetpp::server object is a bit more complicated than the client. It is templatized on a type that represents and stores information for each connected client. It's only requirement is the get_uid() member function. See sample code. This is where games should store game specific information per client. (ex. lobby state) 53 | 54 | ```c++ 55 | #include "enetpp/server.h" 56 | 57 | struct server_client { 58 | unsigned int _uid; 59 | unsigned int get_uid() const { return _uid; } //MUST return globally unique value here 60 | }; 61 | 62 | unsigned int next_uid = 0; 63 | auto init_client_func = [&](server_client& client, const char* ip) { 64 | client._uid = next_uid; 65 | next_uid++; 66 | }; 67 | 68 | enetpp::server server; 69 | server.start_listening(enetpp::server_listen_params() 70 | .set_max_client_count(20) 71 | .set_channel_count(1) 72 | .set_listen_port(801) 73 | .set_initialize_client_function(init_client_func)); 74 | 75 | while (game_is_running) { 76 | //send stuff to specific client where uid=123 77 | enet_uint8 data_to_send; 78 | server.send_packet_to(123, 0, &data_to_send, 1, ENET_PACKET_FLAG_RELIABLE); 79 | 80 | //send stuff to all clients (with optional predicate filter) 81 | server.send_packet_to_all_if(0, &data_to_send, 1, ENET_PACKET_FLAG_RELIABLE, [](const server_client&){}); 82 | 83 | //consume events raised by worker thread 84 | auto on_client_connected = [&](server_client& client) {}; 85 | auto on_client_disconnected = [&](unsigned int client_uid) {}; 86 | auto on_client_data_received = [&](server_client& client, const enet_uint8* data, size_t data_size) {}; 87 | server.consume_events(on_connected, on_disconnected, on_data_received); 88 | 89 | //get access to all connected clients 90 | for (auto c : server.get_connected_clients()) { 91 | //do something? 92 | } 93 | } 94 | 95 | server.stop_listening(); 96 | ``` 97 | 98 | Extra 99 | --- 100 | Scan through client.h and server.h for more advanced features. 101 | - trace handler 102 | - more params (ex. timeout) 103 | 104 | Todo 105 | --- 106 | - expose metrics and counters (ex. average roundtrip time) 107 | 108 | Sample app 109 | --- 110 | There is a sample app included that will spawn a server and client and send messages from client->server->other clients randomly. Use CMake to build. Requires the variable ENET_ROOT_PATH to be set. 111 | 112 | License 113 | --- 114 | MIT 115 | -------------------------------------------------------------------------------- /include/enetpp/client.h: -------------------------------------------------------------------------------- 1 | #ifndef ENETPP_CLIENT_H_ 2 | #define ENETPP_CLIENT_H_ 3 | 4 | #include 5 | #include 6 | #include 7 | #include 8 | #include 9 | #include "global_state.h" 10 | #include "client_connect_params.h" 11 | #include "client_queued_packet.h" 12 | #include "client_statistics.h" 13 | #include "set_current_thread_name.h" 14 | #include "trace_handler.h" 15 | 16 | namespace enetpp { 17 | 18 | class client { 19 | private: 20 | trace_handler _trace_handler; 21 | 22 | std::queue _packet_queue; 23 | std::mutex _packet_queue_mutex; 24 | 25 | std::queue _event_queue; 26 | std::queue _event_queue_copy; //member variable instead of stack variable to prevent mallocs? 27 | std::mutex _event_queue_mutex; 28 | 29 | bool _should_exit_thread; 30 | std::unique_ptr _thread; 31 | 32 | client_statistics _statistics; 33 | 34 | public: 35 | client::client() 36 | : _should_exit_thread(false) { 37 | } 38 | 39 | client::~client() { 40 | //responsibility of owners to make sure disconnect is always called. not calling disconnect() in destructor due to 41 | //trace_handler side effects. 42 | assert(_thread == nullptr); 43 | assert(_packet_queue.empty()); 44 | assert(_event_queue.empty()); 45 | assert(_event_queue_copy.empty()); 46 | } 47 | 48 | void set_trace_handler(trace_handler handler) { 49 | assert(!is_connecting_or_connected()); //must be set before any threads started to be safe 50 | _trace_handler = handler; 51 | } 52 | 53 | bool client::is_connecting_or_connected() const { 54 | return _thread != nullptr; 55 | } 56 | 57 | void client::connect(const client_connect_params& params) { 58 | assert(global_state::get().is_initialized()); 59 | assert(!is_connecting_or_connected()); 60 | assert(params._channel_count > 0); 61 | assert(params._server_port != 0); 62 | assert(!params._server_host_name.empty()); 63 | 64 | trace("connecting to '" + params._server_host_name + ":" + std::to_string(params._server_port) + "'"); 65 | 66 | _should_exit_thread = false; 67 | _thread = std::make_unique(&client::run_in_thread, this, params); 68 | } 69 | 70 | void client::disconnect() { 71 | if (_thread != nullptr) { 72 | _should_exit_thread = true; 73 | _thread->join(); 74 | _thread.release(); 75 | } 76 | 77 | destroy_all_queued_packets(); 78 | destroy_all_queued_events(); 79 | } 80 | 81 | void client::send_packet(enet_uint8 channel_id, const enet_uint8* data, size_t data_size, enet_uint32 flags) { 82 | assert(is_connecting_or_connected()); 83 | if (_thread != nullptr) { 84 | std::lock_guard lock(_packet_queue_mutex); 85 | auto packet = enet_packet_create(data, data_size, flags); 86 | _packet_queue.emplace(channel_id, packet); 87 | } 88 | } 89 | 90 | void client::consume_events( 91 | std::function on_connected, 92 | std::function on_disconnected, 93 | std::function on_data_received) { 94 | 95 | if (!_event_queue.empty()) { 96 | 97 | //!IMPORTANT! neet to copy the events for consumption to prevent deadlocks! 98 | //ex. 99 | //- event = JoinGameFailed packet received 100 | //- causes event_handler to call client::disconnect 101 | //- client::disconnect deadlocks as the thread needs a critical section on events to exit 102 | { 103 | std::lock_guard lock(_event_queue_mutex); 104 | assert(_event_queue_copy.empty()); 105 | _event_queue_copy = _event_queue; 106 | _event_queue = {}; 107 | } 108 | 109 | bool is_disconnected = false; 110 | 111 | while (!_event_queue_copy.empty()) { 112 | auto& e = _event_queue_copy.front(); 113 | switch (e.type) { 114 | case ENET_EVENT_TYPE_CONNECT: { 115 | on_connected(); 116 | break; 117 | } 118 | 119 | case ENET_EVENT_TYPE_DISCONNECT: { 120 | on_disconnected(); 121 | is_disconnected = true; 122 | break; 123 | } 124 | 125 | case ENET_EVENT_TYPE_RECEIVE: { 126 | on_data_received(e.packet->data, e.packet->dataLength); 127 | enet_packet_destroy(e.packet); 128 | break; 129 | } 130 | 131 | case ENET_EVENT_TYPE_NONE: 132 | default: 133 | assert(false); 134 | break; 135 | } 136 | _event_queue_copy.pop(); 137 | } 138 | 139 | if (is_disconnected) { 140 | //cleanup everything internally, make sure the thread is cleaned up. 141 | disconnect(); 142 | } 143 | } 144 | } 145 | 146 | const client_statistics& get_statistics() const { 147 | return _statistics; 148 | } 149 | 150 | private: 151 | void client::destroy_all_queued_packets() { 152 | std::lock_guard lock(_packet_queue_mutex); 153 | while (!_packet_queue.empty()) { 154 | enet_packet_destroy(_packet_queue.front()._packet); 155 | _packet_queue.pop(); 156 | } 157 | } 158 | 159 | void client::destroy_all_queued_events() { 160 | std::lock_guard lock(_event_queue_mutex); 161 | while (!_event_queue.empty()) { 162 | destroy_unhandled_event_data(_event_queue.front()); 163 | _event_queue.pop(); 164 | } 165 | } 166 | 167 | void client::destroy_unhandled_event_data(ENetEvent& e) { 168 | if (e.type == ENET_EVENT_TYPE_RECEIVE) { 169 | enet_packet_destroy(e.packet); 170 | } 171 | } 172 | 173 | void client::run_in_thread(const client_connect_params& params) { 174 | set_current_thread_name("enetpp::client"); 175 | 176 | ENetHost* host = enet_host_create(nullptr, 1, params._channel_count, params._incoming_bandwidth, params._outgoing_bandwidth); 177 | if (host == nullptr) { 178 | trace("enet_host_create failed"); 179 | return; 180 | } 181 | 182 | auto address = params.make_server_address(); 183 | ENetPeer* peer = enet_host_connect(host, &address, params._channel_count, 0); 184 | if (peer == nullptr) { 185 | trace("enet_host_connect failed"); 186 | enet_host_destroy(host); 187 | return; 188 | } 189 | 190 | enet_uint32 enet_timeout = static_cast(params._timeout.count()); 191 | enet_peer_timeout(peer, 0, enet_timeout, enet_timeout); 192 | 193 | bool is_disconnecting = false; 194 | enet_uint32 disconnect_start_time = 0; 195 | 196 | while (peer != nullptr) { 197 | 198 | _statistics._round_trip_time_in_ms = peer->roundTripTime; 199 | _statistics._round_trip_time_variance_in_ms = peer->roundTripTimeVariance; 200 | 201 | if (_should_exit_thread) { 202 | if (!is_disconnecting) { 203 | enet_peer_disconnect(peer, 0); 204 | is_disconnecting = true; 205 | disconnect_start_time = enet_time_get(); 206 | } 207 | else { 208 | if ((enet_time_get() - disconnect_start_time) > 1000) { 209 | trace("enet_peer_disconnect took too long"); 210 | enet_peer_reset(peer); 211 | peer = nullptr; 212 | break; 213 | } 214 | } 215 | } 216 | 217 | if (!is_disconnecting) { 218 | send_queued_packets_in_thread(peer); 219 | } 220 | 221 | //flush / capture enet events 222 | //http://lists.cubik.org/pipermail/enet-discuss/2013-September/002240.html 223 | enet_host_service(host, 0, 0); 224 | { 225 | ENetEvent e; 226 | while (enet_host_check_events(host, &e) > 0) { 227 | std::lock_guard lock(_event_queue_mutex); 228 | _event_queue.push(e); 229 | if (e.type == ENET_EVENT_TYPE_DISCONNECT) { 230 | trace("ENET_EVENT_TYPE_DISCONNECT received"); 231 | peer = nullptr; 232 | break; 233 | } 234 | } 235 | } 236 | 237 | std::this_thread::sleep_for(std::chrono::milliseconds(1)); 238 | } 239 | 240 | enet_host_destroy(host); 241 | } 242 | 243 | void client::send_queued_packets_in_thread(ENetPeer* peer) { 244 | if (!_packet_queue.empty()) { 245 | std::lock_guard lock(_packet_queue_mutex); 246 | while (!_packet_queue.empty()) { 247 | auto qp = _packet_queue.front(); 248 | _packet_queue.pop(); 249 | 250 | if (enet_peer_send(peer, qp._channel_id, qp._packet) != 0) { 251 | trace("enet_peer_send failed"); 252 | } 253 | 254 | if (qp._packet->referenceCount == 0) { 255 | enet_packet_destroy(qp._packet); 256 | } 257 | } 258 | } 259 | } 260 | 261 | void client::trace(const std::string& s) { 262 | if (_trace_handler != nullptr) { 263 | _trace_handler(s); 264 | } 265 | } 266 | }; 267 | 268 | } 269 | 270 | #endif -------------------------------------------------------------------------------- /include/enetpp/client_connect_params.h: -------------------------------------------------------------------------------- 1 | #ifndef ENETPP_CLIENT_CONNECT_PARAMS_H_ 2 | #define ENETPP_CLIENT_CONNECT_PARAMS_H_ 3 | 4 | #include "enet/enet.h" 5 | #include 6 | 7 | namespace enetpp { 8 | 9 | class client_connect_params { 10 | public: 11 | size_t _channel_count; 12 | enet_uint32 _incoming_bandwidth; 13 | enet_uint32 _outgoing_bandwidth; 14 | std::string _server_host_name; 15 | enet_uint16 _server_port; 16 | std::chrono::milliseconds _timeout; 17 | 18 | public: 19 | client_connect_params() 20 | : _channel_count(0) 21 | , _incoming_bandwidth(0) 22 | , _outgoing_bandwidth(0) 23 | , _server_host_name() 24 | , _server_port(0) 25 | , _timeout(0) { 26 | } 27 | 28 | client_connect_params& set_channel_count(size_t channel_count) { 29 | _channel_count = channel_count; 30 | return *this; 31 | } 32 | 33 | client_connect_params& set_incoming_bandwidth(enet_uint32 bandwidth) { 34 | _incoming_bandwidth = bandwidth; 35 | return *this; 36 | } 37 | 38 | client_connect_params& set_outgoing_bandwidth(enet_uint32 bandwidth) { 39 | _outgoing_bandwidth = bandwidth; 40 | return *this; 41 | } 42 | 43 | client_connect_params& set_server_host_name_and_port(const char* host_name, enet_uint16 port) { 44 | _server_host_name = host_name; 45 | _server_port = port; 46 | return *this; 47 | } 48 | 49 | client_connect_params& set_timeout(std::chrono::milliseconds timeout) { 50 | _timeout = timeout; 51 | return *this; 52 | } 53 | 54 | ENetAddress make_server_address() const { 55 | ENetAddress address; 56 | enet_address_set_host(&address, _server_host_name.c_str()); 57 | address.port = _server_port; 58 | return address; 59 | } 60 | 61 | }; 62 | 63 | } 64 | 65 | #endif -------------------------------------------------------------------------------- /include/enetpp/client_queued_packet.h: -------------------------------------------------------------------------------- 1 | #ifndef ENETPP_CLIENT_QUEUED_PACKET_H_ 2 | #define ENETPP_CLIENT_QUEUED_PACKET_H_ 3 | 4 | #include "enet/enet.h" 5 | 6 | namespace enetpp { 7 | 8 | class client_queued_packet { 9 | public: 10 | enet_uint8 _channel_id; 11 | ENetPacket* _packet; 12 | 13 | public: 14 | client_queued_packet() 15 | : _channel_id(0) 16 | , _packet(nullptr) { 17 | } 18 | 19 | client_queued_packet(enet_uint8 channel_id, ENetPacket* packet) 20 | : _channel_id(channel_id) 21 | , _packet(packet) { 22 | } 23 | }; 24 | 25 | } 26 | 27 | #endif -------------------------------------------------------------------------------- /include/enetpp/client_statistics.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include 4 | 5 | namespace enetpp { 6 | 7 | class client_statistics { 8 | public: 9 | std::atomic _round_trip_time_in_ms; 10 | std::atomic _round_trip_time_variance_in_ms; 11 | 12 | public: 13 | client_statistics() 14 | : _round_trip_time_in_ms(0) 15 | , _round_trip_time_variance_in_ms(0) { 16 | } 17 | }; 18 | 19 | } -------------------------------------------------------------------------------- /include/enetpp/global_state.h: -------------------------------------------------------------------------------- 1 | #ifndef ENETPP_GLOBAL_STATE_H_ 2 | #define ENETPP_GLOBAL_STATE_H_ 3 | 4 | #include 5 | #include "enet/enet.h" 6 | 7 | namespace enetpp { 8 | 9 | //ugh 10 | class global_state { 11 | private: 12 | bool _is_initialized; 13 | 14 | public: 15 | static global_state& get() { 16 | static global_state g; 17 | return g; 18 | } 19 | 20 | public: 21 | bool is_initialized() const { 22 | return _is_initialized; 23 | } 24 | 25 | void initialize() { 26 | assert(!_is_initialized); 27 | enet_initialize(); 28 | _is_initialized = true; 29 | } 30 | 31 | void deinitialize() { 32 | enet_deinitialize(); 33 | _is_initialized = false; 34 | } 35 | 36 | private: 37 | global_state() 38 | : _is_initialized(false) { 39 | } 40 | }; 41 | 42 | } 43 | 44 | #endif 45 | -------------------------------------------------------------------------------- /include/enetpp/server.h: -------------------------------------------------------------------------------- 1 | #ifndef ENETPP_SERVER_H_ 2 | #define ENETPP_SERVER_H_ 3 | 4 | #include 5 | #include 6 | #include 7 | #include 8 | #include 9 | #include 10 | #include 11 | #include "server_listen_params.h" 12 | #include "server_queued_packet.h" 13 | #include "server_event.h" 14 | #include "global_state.h" 15 | #include "set_current_thread_name.h" 16 | #include "trace_handler.h" 17 | 18 | namespace enetpp { 19 | 20 | template 21 | class server { 22 | public: 23 | using event_type = server_event; 24 | using listen_params_type = server_listen_params; 25 | using client_ptr_vector = std::vector; 26 | 27 | private: 28 | trace_handler _trace_handler; 29 | 30 | bool _should_exit_thread; 31 | std::unique_ptr _thread; 32 | 33 | //mapping of uid to peer so that sending packets to specific peers is safe. 34 | std::unordered_map _thread_peer_map; 35 | 36 | client_ptr_vector _connected_clients; 37 | 38 | std::queue _packet_queue; 39 | std::mutex _packet_queue_mutex; 40 | 41 | std::queue _event_queue; 42 | std::queue _event_queue_copy; //member variable instead of stack variable to prevent mallocs? 43 | std::mutex _event_queue_mutex; 44 | 45 | public: 46 | server() 47 | : _should_exit_thread(false) { 48 | } 49 | 50 | ~server() { 51 | //responsibility of owners to make sure stop_listening is always called. not calling stop_listening() in destructor due to 52 | //trace_handler side effects. 53 | assert(_thread == nullptr); 54 | assert(_packet_queue.empty()); 55 | assert(_event_queue.empty()); 56 | assert(_event_queue_copy.empty()); 57 | assert(_connected_clients.empty()); 58 | } 59 | 60 | void set_trace_handler(trace_handler handler) { 61 | assert(!is_listening()); //must be set before any threads started to be safe 62 | _trace_handler = handler; 63 | } 64 | 65 | bool is_listening() const { 66 | return (_thread != nullptr); 67 | } 68 | 69 | void start_listening(const listen_params_type& params) { 70 | assert(global_state::get().is_initialized()); 71 | assert(!is_listening()); 72 | assert(params._max_client_count > 0); 73 | assert(params._channel_count > 0); 74 | assert(params._listen_port != 0); 75 | assert(params._initialize_client_function != nullptr); 76 | 77 | trace("listening on port " + std::to_string(params._listen_port)); 78 | 79 | _should_exit_thread = false; 80 | _thread = std::make_unique(&server::run_in_thread, this, params); 81 | } 82 | 83 | void stop_listening() { 84 | if (_thread != nullptr) { 85 | _should_exit_thread = true; 86 | _thread->join(); 87 | _thread.release(); 88 | } 89 | 90 | destroy_all_queued_packets(); 91 | destroy_all_queued_events(); 92 | delete_all_connected_clients(); 93 | } 94 | 95 | void send_packet_to(unsigned int client_id, enet_uint8 channel_id, const enet_uint8* data, size_t data_size, enet_uint32 flags) { 96 | assert(is_listening()); 97 | if (_thread != nullptr) { 98 | std::lock_guard lock(_packet_queue_mutex); 99 | auto packet = enet_packet_create(data, data_size, flags); 100 | _packet_queue.emplace(channel_id, packet, client_id); 101 | } 102 | } 103 | 104 | void send_packet_to_all_if(enet_uint8 channel_id, const enet_uint8* data, size_t data_size, enet_uint32 flags, std::function predicate) { 105 | assert(is_listening()); 106 | if (_thread != nullptr) { 107 | std::lock_guard lock(_packet_queue_mutex); 108 | auto packet = enet_packet_create(data, data_size, flags); 109 | for (auto c : _connected_clients) { 110 | if (predicate(*c)) { 111 | _packet_queue.emplace(channel_id, packet, c->get_id()); 112 | } 113 | } 114 | } 115 | } 116 | 117 | void consume_events( 118 | std::function on_client_connected, 119 | std::function on_client_disconnected, 120 | std::function on_client_data_received) { 121 | 122 | if (!_event_queue.empty()) { 123 | 124 | { 125 | std::lock_guard lock(_event_queue_mutex); 126 | assert(_event_queue_copy.empty()); 127 | _event_queue_copy = _event_queue; 128 | _event_queue = {}; 129 | } 130 | 131 | while (!_event_queue_copy.empty()) { 132 | auto& e = _event_queue_copy.front(); 133 | switch (e._event_type) { 134 | case ENET_EVENT_TYPE_CONNECT: { 135 | _connected_clients.push_back(e._client); 136 | on_client_connected(*e._client); 137 | break; 138 | } 139 | 140 | case ENET_EVENT_TYPE_DISCONNECT: { 141 | auto iter = std::find(_connected_clients.begin(), _connected_clients.end(), e._client); 142 | assert(iter != _connected_clients.end()); 143 | _connected_clients.erase(iter); 144 | unsigned int client_id = e._client->get_id(); 145 | delete e._client; 146 | on_client_disconnected(client_id); 147 | break; 148 | } 149 | 150 | case ENET_EVENT_TYPE_RECEIVE: { 151 | on_client_data_received(*e._client, e._packet->data, e._packet->dataLength); 152 | enet_packet_destroy(e._packet); 153 | break; 154 | } 155 | 156 | case ENET_EVENT_TYPE_NONE: 157 | default: 158 | assert(false); 159 | break; 160 | } 161 | _event_queue_copy.pop(); 162 | } 163 | } 164 | } 165 | 166 | const client_ptr_vector& get_connected_clients() const { 167 | return _connected_clients; 168 | } 169 | 170 | private: 171 | void run_in_thread(const listen_params_type& params) { 172 | set_current_thread_name("enetpp::server"); 173 | 174 | auto address = params.make_listen_address(); 175 | ENetHost* host = enet_host_create( 176 | &address, 177 | params._max_client_count, 178 | params._channel_count, 179 | params._incoming_bandwidth, 180 | params._outgoing_bandwidth); 181 | if (host == nullptr) { 182 | trace("enet_host_create failed"); 183 | } 184 | 185 | while (host != nullptr) { 186 | 187 | if (_should_exit_thread) { 188 | disconnect_all_peers_in_thread(); 189 | enet_host_destroy(host); 190 | host = nullptr; 191 | } 192 | 193 | if (host != nullptr) { 194 | send_queued_packets_in_thread(); 195 | capture_events_in_thread(params, host); 196 | } 197 | 198 | std::this_thread::sleep_for(std::chrono::milliseconds(1)); 199 | } 200 | } 201 | 202 | void disconnect_all_peers_in_thread() { 203 | for (auto iter : _thread_peer_map) { 204 | enet_peer_disconnect_now(iter.second, 0); 205 | iter.second->data = nullptr; 206 | } 207 | _thread_peer_map.clear(); 208 | } 209 | 210 | void send_queued_packets_in_thread() { 211 | if (!_packet_queue.empty()) { 212 | std::lock_guard lock(_packet_queue_mutex); 213 | while (!_packet_queue.empty()) { 214 | auto qp = _packet_queue.front(); 215 | _packet_queue.pop(); 216 | 217 | auto pi = _thread_peer_map.find(qp._client_id); 218 | if (pi != _thread_peer_map.end()) { 219 | 220 | //enet_peer_send fails if state not connected. was getting random asserts on peers disconnecting and going into ENET_PEER_STATE_ZOMBIE. 221 | if (pi->second->state == ENET_PEER_STATE_CONNECTED) { 222 | 223 | if (enet_peer_send(pi->second, qp._channel_id, qp._packet) != 0) { 224 | trace("enet_peer_send failed"); 225 | } 226 | 227 | if (qp._packet->referenceCount == 0) { 228 | enet_packet_destroy(qp._packet); 229 | } 230 | } 231 | } 232 | } 233 | } 234 | } 235 | 236 | void capture_events_in_thread(const listen_params_type& params, ENetHost* host) { 237 | //http://lists.cubik.org/pipermail/enet-discuss/2013-September/002240.html 238 | enet_host_service(host, 0, 0); 239 | 240 | ENetEvent e; 241 | while (enet_host_check_events(host, &e) > 0) { 242 | switch (e.type) { 243 | case ENET_EVENT_TYPE_CONNECT: { 244 | handle_connect_event_in_thread(params, e); 245 | break; 246 | } 247 | 248 | case ENET_EVENT_TYPE_RECEIVE: { 249 | handle_receive_event_in_thread(e); 250 | break; 251 | } 252 | 253 | case ENET_EVENT_TYPE_DISCONNECT: { 254 | handle_disconnect_event_in_thread(e); 255 | break; 256 | } 257 | 258 | case ENET_EVENT_TYPE_NONE: 259 | default: 260 | assert(false); 261 | break; 262 | } 263 | } 264 | } 265 | 266 | void handle_connect_event_in_thread(const listen_params_type& params, const ENetEvent& e) { 267 | enet_uint32 enet_timeout = static_cast(params._peer_timeout.count()); 268 | enet_peer_timeout(e.peer, 0, enet_timeout, enet_timeout); 269 | 270 | char peer_ip[256]; 271 | enet_address_get_host_ip(&e.peer->address, peer_ip, 256); 272 | 273 | //!IMPORTANT! PeerData and it's UID must be created immediately in this worker thread. Otherwise 274 | //there is a chance the first few packets are received on the worker thread when the peer is not 275 | //initialized with data causing them to be discarded. 276 | 277 | auto client = new ClientT(); 278 | params._initialize_client_function(*client, peer_ip); 279 | 280 | assert(e.peer->data == nullptr); 281 | e.peer->data = client; 282 | 283 | _thread_peer_map[client->get_id()] = e.peer; 284 | 285 | { 286 | std::lock_guard lock(_event_queue_mutex); 287 | _event_queue.emplace(ENET_EVENT_TYPE_CONNECT, 0, nullptr, client); 288 | } 289 | } 290 | 291 | void handle_disconnect_event_in_thread(const ENetEvent& e) { 292 | auto client = reinterpret_cast(e.peer->data); 293 | if (client != nullptr) { 294 | auto iter = _thread_peer_map.find(client->get_id()); 295 | assert(iter != _thread_peer_map.end()); 296 | assert(iter->second == e.peer); 297 | e.peer->data = nullptr; 298 | _thread_peer_map.erase(iter); 299 | 300 | std::lock_guard lock(_event_queue_mutex); 301 | _event_queue.emplace(ENET_EVENT_TYPE_DISCONNECT, 0, nullptr, client); 302 | } 303 | } 304 | 305 | void handle_receive_event_in_thread(const ENetEvent& e) { 306 | auto client = reinterpret_cast(e.peer->data); 307 | if (client != nullptr) { 308 | std::lock_guard lock(_event_queue_mutex); 309 | _event_queue.emplace(ENET_EVENT_TYPE_RECEIVE, e.channelID, e.packet, client); 310 | } 311 | } 312 | 313 | void destroy_all_queued_packets() { 314 | std::lock_guard lock(_packet_queue_mutex); 315 | while (!_packet_queue.empty()) { 316 | enet_packet_destroy(_packet_queue.front()._packet); 317 | _packet_queue.pop(); 318 | } 319 | } 320 | 321 | void destroy_all_queued_events() { 322 | std::lock_guard lock(_event_queue_mutex); 323 | while (!_event_queue.empty()) { 324 | destroy_unhandled_event_data(_event_queue.front()); 325 | _event_queue.pop(); 326 | } 327 | } 328 | 329 | void delete_all_connected_clients() { 330 | for (auto c : _connected_clients) { 331 | delete c; 332 | } 333 | _connected_clients.clear(); 334 | } 335 | 336 | void destroy_unhandled_event_data(event_type& e) { 337 | if (e._event_type == ENET_EVENT_TYPE_CONNECT) { 338 | delete e._client; 339 | } 340 | else if (e._event_type == ENET_EVENT_TYPE_RECEIVE) { 341 | enet_packet_destroy(e._packet); 342 | } 343 | } 344 | 345 | void trace(const std::string& s) { 346 | if (_trace_handler != nullptr) { 347 | _trace_handler(s); 348 | } 349 | } 350 | 351 | }; 352 | 353 | } 354 | 355 | #endif -------------------------------------------------------------------------------- /include/enetpp/server_event.h: -------------------------------------------------------------------------------- 1 | #ifndef ENETPP_SERVER_EVENT_H_ 2 | #define ENETPP_SERVER_EVENT_H_ 3 | 4 | #include "enet/enet.h" 5 | 6 | namespace enetpp { 7 | 8 | //server can't use ENetEvent as ENetPeer is not thread safe. Instead track data that is safe. 9 | template 10 | class server_event { 11 | public: 12 | ENetEventType _event_type; 13 | enet_uint8 _channel_id; 14 | ENetPacket* _packet; 15 | ClientT* _client; 16 | 17 | public: 18 | server_event() 19 | : _event_type(ENET_EVENT_TYPE_NONE) 20 | , _channel_id(0) 21 | , _packet(nullptr) 22 | , _client(nullptr) { 23 | } 24 | 25 | server_event(ENetEventType event_type, enet_uint8 channel_id, ENetPacket* packet, ClientT* client) 26 | : _event_type(event_type) 27 | , _channel_id(channel_id) 28 | , _packet(packet) 29 | , _client(client) { 30 | } 31 | }; 32 | 33 | } 34 | 35 | #endif 36 | -------------------------------------------------------------------------------- /include/enetpp/server_listen_params.h: -------------------------------------------------------------------------------- 1 | #ifndef ENETPP_SERVER_LISTEN_PARAMS_H_ 2 | #define ENETPP_SERVER_LISTEN_PARAMS_H_ 3 | 4 | #include "enet/enet.h" 5 | #include 6 | #include 7 | 8 | namespace enetpp { 9 | 10 | template 11 | class server_listen_params { 12 | public: 13 | using initialize_client_function = std::function; 14 | 15 | public: 16 | size_t _max_client_count; 17 | size_t _channel_count; 18 | enet_uint32 _incoming_bandwidth; 19 | enet_uint32 _outgoing_bandwidth; 20 | enet_uint16 _listen_port; 21 | std::chrono::milliseconds _peer_timeout; 22 | initialize_client_function _initialize_client_function; 23 | 24 | public: 25 | server_listen_params() 26 | : _max_client_count(0) 27 | , _channel_count(0) 28 | , _incoming_bandwidth(0) 29 | , _outgoing_bandwidth(0) 30 | , _peer_timeout(0) { 31 | } 32 | 33 | server_listen_params& set_listen_port(enet_uint16 port) { 34 | _listen_port = port; 35 | return *this; 36 | } 37 | 38 | server_listen_params& set_max_client_count(size_t count) { 39 | _max_client_count = count; 40 | return *this; 41 | } 42 | 43 | server_listen_params& set_channel_count(size_t channel_count) { 44 | _channel_count = channel_count; 45 | return *this; 46 | } 47 | 48 | server_listen_params& set_incoming_bandwidth(enet_uint32 bandwidth) { 49 | _incoming_bandwidth = bandwidth; 50 | return *this; 51 | } 52 | 53 | server_listen_params& set_outgoing_bandwidth(enet_uint32 bandwidth) { 54 | _outgoing_bandwidth = bandwidth; 55 | return *this; 56 | } 57 | 58 | server_listen_params& set_peer_timeout(std::chrono::milliseconds timeout) { 59 | _peer_timeout = timeout; 60 | return *this; 61 | } 62 | 63 | server_listen_params& set_initialize_client_function(initialize_client_function f) { 64 | _initialize_client_function = f; 65 | return *this; 66 | } 67 | 68 | ENetAddress make_listen_address() const { 69 | ENetAddress address; 70 | address.host = ENET_HOST_ANY; 71 | address.port = _listen_port; 72 | return address; 73 | } 74 | }; 75 | 76 | } 77 | 78 | #endif 79 | -------------------------------------------------------------------------------- /include/enetpp/server_queued_packet.h: -------------------------------------------------------------------------------- 1 | #ifndef ENETPP_SERVER_QUEUED_PACKET_H_ 2 | #define ENETPP_SERVER_QUEUED_PACKET_H_ 3 | 4 | #include "enet/enet.h" 5 | 6 | namespace enetpp { 7 | 8 | class server_queued_packet { 9 | public: 10 | enet_uint8 _channel_id; 11 | ENetPacket* _packet; 12 | unsigned int _client_id; 13 | 14 | public: 15 | server_queued_packet() 16 | : _channel_id(0) 17 | , _packet(nullptr) 18 | , _client_id(0) { 19 | } 20 | 21 | server_queued_packet(enet_uint8 channel_id, ENetPacket* packet, unsigned int client_id) 22 | : _channel_id(channel_id) 23 | , _packet(packet) 24 | , _client_id(client_id) { 25 | } 26 | }; 27 | 28 | } 29 | 30 | #endif -------------------------------------------------------------------------------- /include/enetpp/set_current_thread_name.h: -------------------------------------------------------------------------------- 1 | #ifndef ENETPP_SET_CURRENT_THREAD_NAME_H_ 2 | #define ENETPP_SET_CURRENT_THREAD_NAME_H_ 3 | 4 | #include "enet/enet.h" 5 | 6 | namespace enetpp { 7 | 8 | inline void set_current_thread_name(const char* name) { 9 | 10 | #ifdef _WIN32 11 | 12 | //https://msdn.microsoft.com/en-us/library/xcb2z8hs.aspx 13 | const DWORD MS_VC_EXCEPTION = 0x406D1388; 14 | 15 | #pragma pack(push,8) 16 | typedef struct tagTHREADNAME_INFO { 17 | DWORD dwType; // Must be 0x1000. 18 | LPCSTR szName; // Pointer to name (in user addr space). 19 | DWORD dwThreadID; // Thread ID (-1=caller thread). 20 | DWORD dwFlags; // Reserved for future use, must be zero. 21 | } THREADNAME_INFO; 22 | #pragma pack(pop) 23 | 24 | THREADNAME_INFO info; 25 | info.dwType = 0x1000; 26 | info.szName = name; 27 | info.dwThreadID = ::GetCurrentThreadId(); 28 | info.dwFlags = 0; 29 | 30 | __try { 31 | RaiseException(MS_VC_EXCEPTION, 0, sizeof(info) / sizeof(ULONG_PTR), (ULONG_PTR*)&info); 32 | } 33 | __except (EXCEPTION_EXECUTE_HANDLER) { 34 | } 35 | 36 | #else 37 | 38 | //todo - unknown platform 39 | 40 | #endif 41 | 42 | } 43 | 44 | } 45 | 46 | #endif 47 | -------------------------------------------------------------------------------- /include/enetpp/trace_handler.h: -------------------------------------------------------------------------------- 1 | #ifndef ENETPP_TRACE_HANDLER_H_ 2 | #define ENETPP_TRACE_HANDLER_H_ 3 | 4 | #include 5 | #include 6 | 7 | namespace enetpp { 8 | 9 | //!IMPORTANT! handler must be thread safe as trace messages come from worker threads as well. 10 | using trace_handler = std::function; 11 | 12 | } 13 | 14 | #endif 15 | -------------------------------------------------------------------------------- /sample/CMakeLists.txt: -------------------------------------------------------------------------------- 1 | cmake_minimum_required(VERSION 2.8) 2 | 3 | project("enetpp sample app") 4 | 5 | add_subdirectory("${ENET_ROOT_PATH}" "${CMAKE_CURRENT_BINARY_DIR}/enet") 6 | 7 | include_directories("${PROJECT_SOURCE_DIR}/../include") 8 | include_directories("${ENET_ROOT_PATH}/include") 9 | 10 | add_executable(enetpp main.cpp) 11 | 12 | target_link_libraries(enetpp enet) 13 | 14 | if(WIN32) 15 | target_link_libraries(enetpp winmm wsock32 Ws2_32) 16 | endif(WIN32) 17 | -------------------------------------------------------------------------------- /sample/main.cpp: -------------------------------------------------------------------------------- 1 | #include 2 | #include 3 | #include 4 | #include 5 | #include "enetpp/client.h" 6 | #include "enetpp/server.h" 7 | 8 | static const int CLIENT_COUNT = 10; 9 | static const int PORT = 123; 10 | 11 | static bool s_exit = false; 12 | static std::mutex s_cout_mutex; 13 | 14 | class server_client { 15 | public: 16 | unsigned int _uid; 17 | 18 | public: 19 | server_client() 20 | : _uid(0) { 21 | } 22 | 23 | unsigned int get_uid() const { 24 | return _uid; 25 | } 26 | }; 27 | 28 | void run_server() { 29 | auto trace_handler = [&](const std::string& msg) { 30 | std::lock_guard lock(s_cout_mutex); 31 | std::cout << "server: " << msg << std::endl; 32 | }; 33 | 34 | enetpp::server server; 35 | server.set_trace_handler(trace_handler); 36 | 37 | unsigned int next_uid = 0; 38 | auto init_client_func = [&](server_client& client, const char* ip) { 39 | client._uid = next_uid; 40 | next_uid++; 41 | }; 42 | 43 | server.start_listening(enetpp::server_listen_params() 44 | .set_max_client_count(CLIENT_COUNT) 45 | .set_channel_count(1) 46 | .set_listen_port(PORT) 47 | .set_initialize_client_function(init_client_func)); 48 | 49 | while (server.is_listening()) { 50 | 51 | auto on_client_connected = [&](server_client& client) { trace_handler("on_client_connected"); }; 52 | auto on_client_disconnected = [&](unsigned int client_uid) { trace_handler("on_client_disconnected"); }; 53 | auto on_client_data_received = [&](server_client& client, const enet_uint8* data, size_t data_size) { 54 | trace_handler("received packet from client : '" + std::string(reinterpret_cast(data), data_size) + "'"); 55 | trace_handler("forwarding packet to all other clients..."); 56 | server.send_packet_to_all_if(0, data, data_size, ENET_PACKET_FLAG_RELIABLE, [&](const server_client& destination) { 57 | return destination.get_uid() != client.get_uid(); 58 | }); 59 | }; 60 | 61 | server.consume_events( 62 | on_client_connected, 63 | on_client_disconnected, 64 | on_client_data_received); 65 | 66 | if (s_exit) { 67 | server.stop_listening(); 68 | } 69 | 70 | std::this_thread::sleep_for(std::chrono::milliseconds(1)); 71 | } 72 | } 73 | 74 | void run_client(int client_index) { 75 | auto trace_handler = [&](const std::string& msg) { 76 | std::lock_guard lock(s_cout_mutex); 77 | std::cout << "client(" << client_index << "): " << msg << std::endl; 78 | }; 79 | 80 | enetpp::client client; 81 | client.set_trace_handler(trace_handler); 82 | client.connect(enetpp::client_connect_params() 83 | .set_channel_count(1) 84 | .set_server_host_name_and_port("localhost", PORT)); 85 | 86 | std::mt19937 rand; 87 | rand.seed(static_cast(client_index)); 88 | std::uniform_int_distribution<> rand_distribution(5000, 20000); 89 | 90 | auto last_send_time = std::chrono::system_clock::now(); 91 | unsigned int next_send_time_delta = rand_distribution(rand); 92 | 93 | while (client.is_connecting_or_connected()) { 94 | 95 | if (std::chrono::system_clock::now() - last_send_time > std::chrono::milliseconds(next_send_time_delta)) { 96 | last_send_time = std::chrono::system_clock::now(); 97 | next_send_time_delta = rand_distribution(rand); 98 | trace_handler("sending packet to server"); 99 | std::string packet = "hello from client:" + std::to_string(client_index); 100 | assert(sizeof(char) == sizeof(enet_uint8)); 101 | client.send_packet(0, reinterpret_cast(packet.data()), packet.length(), ENET_PACKET_FLAG_RELIABLE); 102 | } 103 | 104 | auto on_connected = [&](){ trace_handler("on_connected"); }; 105 | auto on_disconnected = [&]() { trace_handler("on_disconnected"); }; 106 | auto on_data_received = [&](const enet_uint8* data, size_t data_size) { 107 | trace_handler("received packet from server : '" + std::string(reinterpret_cast(data), data_size) + "'"); 108 | }; 109 | 110 | client.consume_events( 111 | on_connected, 112 | on_disconnected, 113 | on_data_received); 114 | 115 | std::this_thread::sleep_for(std::chrono::milliseconds(1)); 116 | } 117 | } 118 | 119 | int main(int argc, char** argv) { 120 | 121 | enetpp::global_state::get().initialize(); 122 | 123 | auto server_thread = std::make_unique(&run_server); 124 | 125 | std::vector> client_threads; 126 | for (int i = 0; i < CLIENT_COUNT; ++i) { 127 | client_threads.push_back(std::make_unique(&run_client, i)); 128 | } 129 | 130 | std::cout << "press any key to exit..." << std::endl; 131 | _getch(); 132 | s_exit = true; 133 | server_thread->join(); 134 | for (auto& ct : client_threads) { 135 | ct->join(); 136 | } 137 | 138 | enetpp::global_state::get().deinitialize(); 139 | 140 | return 0; 141 | } 142 | 143 | --------------------------------------------------------------------------------