我设计了这些类,以用于多人游戏,其中一台服务器的客户端数量可能非常多。这种实现是好的,还是我明显忽略了某些东西或有待改进?我希望这部分具有很高的性能,这样我就可以专注于其他事情,而又不会经常回到基础知识上。

以下是代码清单:

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类(#include "locked_queue.h")是std::queue的简单包装,使用boost::mutex锁定了访问功能。供参考:

#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();
    }
};


在MIT许可证的条件下可以随意使用此代码。

更新:该代码已根据审查进行了更新有关建议,请访问GitHub。

评论

您是否真的需要使用Boost线程API?您不能用std代替它来减少外部库的数量吗?

std :: thread的副作用是,它无法在C ++ / CLI模式下编译(至少从Visual Studio 2013开始)。我个人喜欢VS内置托管测试:)

哎呀,那真不幸:/

没有其他问题吗?我开始认为我是C ++天才:-P让我们添加一些赏金...

我对网络和/或并发编程了解不多。也许其他人可以添加一些东西:)

#1 楼

异常安全

您的locked_queue并非异常安全。特别是:

    queue.pop();
    return value;


如果_T的复制(或移动)构造函数抛出异常,则可能已经从队列中弹出了该项目,则构造函数在返回时抛出异常值,那么该值将丢失并且无法恢复。这就是为什么标准库将检索值与从集合中删除值分开的原因-您首先复制,然后(只有在成功的情况下)才将其从集合中删除。

如果您确定永远不会将其与复制/移动ctor可以抛出的类型一起使用,但是您所做的应该没问题。不幸的是,您将其与std::string一起使用,该bool具有可抛出的复制ctor。

YAGNI

您有一些可能被称为“违反YAGNI的行为”。例如:

    void NetworkServer::SendToAll(std::string message, bool guaranteed)
    {
        for (auto client: clients)
            send(message, client.right);
    };


在这里,您要向函数传递bool,显然是为了表明它是否应尝试保证传递,但是该函数完全忽略了该值。至少在正常使用中,这样的保证基本上是在创建初始连接时指定的(TCP保证传递,而UDP没有)。

布尔参数

以非显而易见的方式使用布尔作为参数。通常我通常建议不要使用SendToAll("whatever", true)参数。有一些例外,但是在这种情况下,SendToAll("whatever", false)bool的区别以及该参数的含义并不明显(尽管如上所述,在这种情况下,它绝对没有任何意义)。

假设您修复了代码,使参数有意义,那么将enum替换为enum class可能会更好,这样可以在代码中直接看到其意图,例如:

enum { ATTEMPT, GUARANTEE };

SendToAll("whatever", ATTEMPT);


...或者给定C ++ 11,您可能想改用SendToAllExcept

enum class delivery { ATTEMPT, GUARANTEE };

SendToAll("whatever", delivery::ATTEMPT);


相同的注释也适用于sentBytes。 />
成员初始化列表

首选成员初始化列表进行初始化。一个明显的例子是:

receivedBytes = sentBytes = receivedMessages = sentMessages = 0;


(在ctor的内部)。在这种情况下,最好使用类似以下的方法:

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(0),
    sentBytes(0),
    receivedMessages(0),
    sentMessages(0)
{
    udp::resolver resolver(io_service);
    udp::resolver::query query(udp::v4(), host, server_port);
    server_endpoint = *resolver.resolve(query);
    Send("");
}


或者,您可能至少要考虑一个专门用于计数器的小型类型:

class counter {
    size_t count;
public:
    counter &operator=(size_t val) { count = val; return *this; }
    counter(size_t count=0) : count(count) {}
    operator size_t() { return count; }
    count &operator++() { ++count; return *this; }
    count operator++(int) { counter ret(count); ++count; return ret; }
    bool operator==(counter const &other) { return count == other.count; }
    bool operator!=(counter const &other) { return count != other.count; }
};


只需将receivedBytescounter等定义为unsigned __int64类的对象,就不可能创建未初始化类型的对象。 >
尽可能使用可移植性

举一个明显的例子,您在许多地方都使用了__int64unsigned long long特定于VC ++。由于没有其他理由,我宁愿使用long long,它在VC ++上也能​​很好地工作,但也可以与其他任何符合标准的实现(C ++ 11)一起工作。与此相对的是,在C ++ 11之前,某些编译器(最著名的是VC ++)不支持ulonglong。如果这是一个问题,我将使用中间的typedef:

#ifdef _MSC_VER
typedef unsigned __int64 ulonglong;
#else
typedef unsigned long long ulonglong;
#endif


...然后其余代码将使用typedef。如果您需要比上述更多的特定于编译器的代码,您仍然可以通过为该特定编译器添加一个const来在一处更改它,(希望)无需对其余代码进行任何更改。

常量正确性

例如,您具有四个功能:

inline unsigned __int32 GetStatReceivedMessages(){return receivedMessages;};
inline unsigned __int64 GetStatReceivedBytes(){return receivedBytes;};
inline unsigned __int32 GetStatSentMessages(){return sentMessages;};
inline unsigned __int64 GetStatSentBytes(){return sentBytes;};


由于这些功能不应修改对象的状态,因此它们应该是stats成员函数:

inline unsigned __int32 GetStatReceivedMessages() const {return receivedMessages;};
inline unsigned __int64 GetStatReceivedBytes() const {return receivedBytes;};
inline unsigned __int32 GetStatSentMessages() const {return sentMessages;};
inline unsigned __int64 GetStatSentBytes() const {return sentBytes;};


考虑更多数据分组

对于与上述相同的四个功能,我更喜欢创建一个operator<<类。父级将返回该类的对象,然后您可以在该类中查询您关心的特定细节:

class stats {
    inline unsigned GetReceivedMessages() const { return receivedMessages; }
    // ...
};

class NetworkClient { 
    stats s;
public:
    stats Stats() const { return s; }
};

// ... and in client code, something like:
NetworkClient c;

std::cout << c.Stats().GetReceivedMessages();


根据情况,您可能还希望重载该类型的q4312079q,以便更轻松地打印统计信息:

 std::cout << c.Stats();


避免不必要的语法

尽管您可以在成员函数的主体后插入分号,但这是不必要的。 IMO,最好将其省略,因此(例如):

inline unsigned __int32 GetStatReceivedMessages() const {return receivedMessages;};


...成为:

inline unsigned __int32 GetStatReceivedMessages() const { return receivedMessages; }


虽然我(比很多人)对此不太坚持,但我确实认为在这种情况下,额外的空格也有助于提高可读性。

评论


\ $ \ begingroup \ $
Bool参数是后来的功能(保证交付)的存根,但我的错在这里-应该表明了这一点。所有有效点,谢谢。
\ $ \ endgroup \ $
– DarkWanderer
2014年5月28日下午5:33

\ $ \ begingroup \ $
@DarkWanderer:我可能没有像我应该强调的那样强调它,但是我主要关心的是从bool变为枚举,并将其作为参数传递给正确的函数-确实需要最初是在建立连接时传递的,而不是在发送消息时传递的(除非您要通过重新发送UDP数据包以确保传递来进行重新发明TCP之类的操作,对此我会认真考虑)。
\ $ \ endgroup \ $
–杰里·科芬(Jerry Coffin)
2014年5月28日下午6:58

\ $ \ begingroup \ $
NP,我只是想证明为什么它排在第一位。至于TCP与UDP之间的关系-有一篇文章证明即使为保证消息也选择UDP是合理的,我倾向于同意那里提到的观点-由于丢失一个数据包而停止整个保证消息队列是没有好处的。这对于实时游戏而言将是致命的(这里就是这种情况)。无论如何,数据的排序在这里并不是什么大问题。
\ $ \ endgroup \ $
– DarkWanderer
2014年5月28日7:30



\ $ \ begingroup \ $
并不是说我认为UDP是灵丹妙药,我只是认为在我的特定情况下它更合适。
\ $ \ endgroup \ $
– DarkWanderer
2014年5月28日在9:11



\ $ \ begingroup \ $
@DarkWanderer:可能是。请注意,我并不是说这不一定是一个坏主意,只是我必须认真考虑并确保该工作对应用程序合理的事情。
\ $ \ endgroup \ $
–杰里·科芬(Jerry Coffin)
2014年5月28日上午9:19