I've designed those classes for use in a multiplayer game with possibly very high number of clients for one server. Is this implementation good, or is there something obvious I overlooked or something to improve? I expect this part to have very high performance, so that I can just focus on other things and not come back to the basics too often.
Here are code listings:
NetworkServer.h
#pragma once
#include "Constants.h"
#include "locked_queue.h"
#include <boost/array.hpp>
#include <boost/asio.hpp>
#include <boost/bimap.hpp>
#include <boost/thread.hpp>
#include <string>
#include <array>
using boost::asio::ip::udp;
typedef boost::bimap<__int64, udp::endpoint> ClientList;
typedef ClientList::value_type Client;
typedef std::pair<std::string, __int64> ClientMessage;
class NetworkServer {
public:
NetworkServer(unsigned short local_port);
~NetworkServer();
bool HasMessages();
ClientMessage PopMessage();
void SendToClient(std::string message, unsigned __int64 clientID, bool guaranteed = false);
void SendToAllExcept(std::string message, unsigned __int64 clientID, bool guaranteed = false);
void SendToAll(std::string message, bool guaranteed = false);
inline unsigned __int64 GetStatReceivedMessages() {return receivedMessages;};
inline unsigned __int64 GetStatReceivedBytes() {return receivedBytes;};
inline unsigned __int64 GetStatSentMessages() {return sentMessages;};
inline unsigned __int64 GetStatSentBytes() {return sentBytes;};
private:
// Network send/receive stuff
boost::asio::io_service io_service;
udp::socket socket;
udp::endpoint server_endpoint;
udp::endpoint remote_endpoint;
std::array<char, NetworkBufferSize> recv_buffer;
boost::thread service_thread;
void start_receive();
void handle_receive(const boost::system::error_code& error, std::size_t bytes_transferred);
void handle_send(std::string /*message*/, const boost::system::error_code& /*error*/, std::size_t /*bytes_transferred*/) {}
void run_service();
unsigned __int64 get_client_id(udp::endpoint endpoint);
void send(std::string pmessage, udp::endpoint target_endpoint);
// Incoming messages queue
locked_queue<ClientMessage> incomingMessages;
// Clients of the server
ClientList clients;
unsigned __int64 nextClientID;
NetworkServer(NetworkServer&); // block default copy constructor
// Statistics
unsigned __int64 receivedMessages;
unsigned __int64 receivedBytes;
unsigned __int64 sentMessages;
unsigned __int64 sentBytes;
};
NetworkServer.cpp
#define _WIN32_WINNT 0x0501
#include <boost/bind.hpp>
#include "NetworkServer.h"
#include "Logging.h"
NetworkServer::NetworkServer(unsigned short local_port) :
socket(io_service, udp::endpoint(udp::v4(), local_port)),
nextClientID(0L),
service_thread(std::bind(&NetworkServer::run_service, this))
{
LogMessage("Starting server on port", local_port);
};
NetworkServer::~NetworkServer()
{
io_service.stop();
service_thread.join();
}
void NetworkServer::start_receive()
{
socket.async_receive_from(boost::asio::buffer(recv_buffer), remote_endpoint,
boost::bind(&NetworkServer::handle_receive, this, boost::asio::placeholders::error, boost::asio::placeholders::bytes_transferred));
}
void NetworkServer::handle_receive(const boost::system::error_code& error, std::size_t bytes_transferred)
{
if (!error)
{
try {
auto message = ClientMessage(std::string(recv_buffer.data(), recv_buffer.data() + bytes_transferred), get_client_id(remote_endpoint));
if (!message.first.empty())
incomingMessages.push(message);
receivedBytes += bytes_transferred;
receivedMessages++;
}
catch (std::exception ex) {
LogMessage("handle_receive: Error parsing incoming message:",ex.what());
}
catch (...) {
LogMessage("handle_receive: Unknown error while parsing incoming message");
}
}
else
{
LogMessage("handle_receive: error: ", error.message());
}
start_receive();
}
void NetworkServer::send(std::string message, udp::endpoint target_endpoint)
{
socket.send_to(boost::asio::buffer(message), target_endpoint);
sentBytes += message.size();
sentMessages++;
}
void NetworkServer::run_service()
{
start_receive();
while (!io_service.stopped()){
try {
io_service.run();
} catch( const std::exception& e ) {
LogMessage("Server network exception: ",e.what());
}
catch(...) {
LogMessage("Unknown exception in server network thread");
}
}
LogMessage("Server network thread stopped");
};
unsigned __int64 NetworkServer::get_client_id(udp::endpoint endpoint)
{
auto cit = clients.right.find(endpoint);
if (cit != clients.right.end())
return (*cit).second;
nextClientID++;
clients.insert(Client(nextClientID, endpoint));
return nextClientID;
};
void NetworkServer::SendToClient(std::string message, unsigned __int64 clientID, bool guaranteed)
{
try {
send(message, clients.left.at(clientID));
}
catch (std::out_of_range) {
LogMessage("Unknown client ID");
}
};
void NetworkServer::SendToAllExcept(std::string message, unsigned __int64 clientID, bool guaranteed)
{
for (auto client: clients)
if (client.left != clientID)
send(message, client.right);
};
void NetworkServer::SendToAll(std::string message, bool guaranteed)
{
for (auto client: clients)
send(message, client.right);
};
ClientMessage NetworkServer::PopMessage() {
return incomingMessages.pop();
}
bool NetworkServer::HasMessages()
{
return !incomingMessages.empty();
};
NetworkClient.h
#pragma once
#include "locked_queue.h"
#include <boost/array.hpp>
#include <boost/asio.hpp>
#include <boost/thread.hpp>
#include <memory>
#include <array>
#include "Constants.h"
using boost::asio::ip::udp;
class NetworkClient {
public:
NetworkClient(std::string host, std::string server_port, unsigned short local_port = 0);
~NetworkClient();
void Send(std::string message);
inline bool HasMessages() {return !incomingMessages.empty();};
inline std::string PopMessage() { if (incomingMessages.empty()) throw std::logic_error("No messages to pop"); return incomingMessages.pop(); };
inline unsigned __int32 GetStatReceivedMessages(){return receivedMessages;};
inline unsigned __int64 GetStatReceivedBytes(){return receivedBytes;};
inline unsigned __int32 GetStatSentMessages(){return sentMessages;};
inline unsigned __int64 GetStatSentBytes(){return sentBytes;};
private:
// Network send/receive stuff
boost::asio::io_service io_service;
udp::socket socket;
udp::endpoint server_endpoint;
udp::endpoint remote_endpoint;
std::array<char, NetworkBufferSize> recv_buffer;
boost::thread service_thread;
// Queues for messages
locked_queue<std::string> incomingMessages;
void start_receive();
void handle_receive(const boost::system::error_code& error, std::size_t bytes_transferred);
void run_service();
NetworkClient(NetworkClient&); // block default copy constructor
// Statistics
unsigned __int32 receivedMessages;
unsigned __int64 receivedBytes;
unsigned __int32 sentMessages;
unsigned __int64 sentBytes;
};
NetworkClient.cpp
#define _WIN32_WINNT 0x0501
#include <boost/bind.hpp>
#include <boost/thread.hpp>
#include "NetworkClient.h"
#include "Logging.h"
NetworkClient::NetworkClient(std::string host, std::string server_port, unsigned short local_port) :
socket(io_service, udp::endpoint(udp::v4(), local_port)),
service_thread(std::bind(&NetworkClient::run_service, this))
{
receivedBytes = sentBytes = receivedMessages = sentMessages = 0;
udp::resolver resolver(io_service);
udp::resolver::query query(udp::v4(), host, server_port);
server_endpoint = *resolver.resolve(query);
Send("");
}
NetworkClient::~NetworkClient()
{
io_service.stop();
service_thread.join();
}
void NetworkClient::start_receive()
{
socket.async_receive_from(boost::asio::buffer(recv_buffer), remote_endpoint,
boost::bind(&NetworkClient::handle_receive, this, boost::asio::placeholders::error, boost::asio::placeholders::bytes_transferred)
);
}
void NetworkClient::handle_receive(const boost::system::error_code& error, std::size_t bytes_transferred)
{
if (!error)
{
std::string message(recv_buffer.data(), recv_buffer.data() + bytes_transferred);
incomingMessages.push(message);
receivedBytes += bytes_transferred;
receivedMessages++;
}
else
{
LogMessage("NetworkClient::handle_receive:",error);
}
start_receive();
}
void NetworkClient::Send(std::string message)
{
socket.send_to(boost::asio::buffer(message), server_endpoint);
sentBytes += message.size();
sentMessages++;
}
void NetworkClient::run_service()
{
LogMessage("Client network thread started\n");
start_receive();
LogMessage("Client started receiving\n");
while (!io_service.stopped()) {
try {
io_service.run();
}
catch (const std::exception& e) {
LogMessage("Client network exception: ", e.what());
}
catch (...) {
LogMessage("Unknown exception in client network thread");
}
}
LogMessage("Client network thread stopped");
}
locked_queue
class (#include "locked_queue.h"
) is a simple wrapper to a std::queue
, locking access functions with a boost::mutex
. For the reference:
#pragma once
#include <boost/thread/mutex.hpp>
#include <queue>
#include <list>
template<typename _T> class locked_queue
{
private:
boost::mutex mutex;
std::queue<_T> queue;
public:
void push(_T value)
{
boost::mutex::scoped_lock lock(mutex);
queue.push(value);
};
_T pop()
{
boost::mutex::scoped_lock lock(mutex);
_T value;
std::swap(value,queue.front());
queue.pop();
return value;
};
bool empty() {
boost::mutex::scoped_lock lock(mutex);
return queue.empty();
}
};
Feel free to use this code under the conditions of MIT license.
Update: The code updated according to review recommendations is available on GitHub.
std
one to reduce the number of external libaries? \$\endgroup\$std::thread
has an unfortunate side effect in that it doesn't compile in C++/CLI mode (at least as of Visual Studio 2013). And I just like VS built-in managed tests, personally :) \$\endgroup\$