首页
学习
活动
专区
圈层
工具
发布
社区首页 >问答首页 >锁步RTS游戏的Winsock代码

锁步RTS游戏的Winsock代码
EN

Code Review用户
提问于 2023-05-01 14:40:23
回答 1查看 65关注 0票数 3

摘要

这是我正在创建的锁步RTS游戏的核心网络代码。客户端通过TCP套接字连接到中继服务器,发送到中继服务器的任何数据包都转发给所有其他播放器。

中继服务器不关心数据包内容或游戏状态,它只是传递数据包。客户端发送包含其命令的每个滴答号,只有当客户端接收到所有其他玩家的命令时,才会处理滴答号--这确保了每个人都在运行相同的模拟,因此所有客户端都保持同步。为了缓解延迟,命令将在未来对'n‘号进行调度(当前设置为10次,或~166 or )。

目前,这似乎运行良好(至少在Windows上)。

任何反馈都是受欢迎的,无论是关于代码、架构还是我所做的任何其他决定。我一直在寻求改进!

套接字类

这样做的目的是让它成为一个跨平台的包装器,使用RAII管理连接的原始套接字。

  • 提供了命名工厂方法来初始化客户端/服务器的套接字。
  • send方法发送给定的缓冲区,receive方法阻塞,直到它填充了给定的缓冲区。
  • 如果在初始化过程中发生错误,就会抛出异常(我知道异常在游戏开发中并不流行,但我仍然在使用它们)。
  • 如果在发送/接收数据时发生错误,则套接字将优雅地关闭。
  • 可以移动Socket,但不能复制。

Socket.h

这包含类声明,并且(理想情况下)应该完全与平台无关。

代码语言:javascript
复制
#pragma once

#include <cstdint>
#include <memory>
#include <string>
#include <vector>

// Ideally we would not have any platform-specific stuff here,
// but this seems unavoidable since we need to know the underlying type!
#ifdef _WIN32
#include <winsock2.h>
#else
// I am only targeting Windows for now, so don't worry too much about this.
typedef int SOCKET;
#endif

namespace Rival {

enum class SocketState : std::uint8_t
{
    Open,
    Closed
};

/**
 * Wrapper class for a native socket.
 *
 * Implementation details are platform-specific.
 */
class Socket
{
public:
    /** Creates a listening socket connected to localhost. */
    static Socket createServer(int port);

    /** Creates a socket and attempts to connect it to the given address and port. */
    static Socket createClient(const std::string& address, int port);

    /** Creates a socket that wraps a raw socket handle. The socket is assumed to be open if the handle is valid. */
    static Socket wrap(SOCKET rawSocket);

    ~Socket();

    bool Socket::operator==(const Socket& other) const;
    bool Socket::operator!=(const Socket& other) const;

    // Allow moving but prevent copying and move-assignment
    Socket(const Socket& other) = delete;
    Socket(Socket&& other) noexcept;
    Socket& operator=(const Socket& other) = delete;
    Socket& operator=(Socket&& other) = delete;

    /** Blocking call that waits for a new connection. */
    Socket accept();

    /** Closes the socket; forces any blocking calls to return. */
    void close() noexcept;

    /** Determines if this socket is valid. */
    bool isValid() const;

    /** Determines if this socket is closed. */
    bool isClosed() const;

    /** Sends data on this socket. */
    void send(std::vector<char>& buffer);

    /** Blocking call that waits for data to arrive and adds it to the given buffer. */
    void receive(std::vector<char>& buffer);

private:
    Socket(const std::string& address, int port, bool server);
    Socket(SOCKET rawSocket);

    void init();

private:
    SOCKET sock;
    SocketState state = SocketState::Closed;
};

}  // namespace Rival

Socket.cpp

这包含所有平台共有的任何套接字功能。

代码语言:javascript
复制
#include "pch.h"

#include "net/Socket.h"

namespace Rival {

Socket Socket::createServer(int port)
{
    return { "localhost", port, true };
}

Socket Socket::createClient(const std::string& address, int port)
{
    return { address, port, false };
}

Socket Socket::wrap(SOCKET rawSocket)
{
    return { rawSocket };
}

Socket::Socket(SOCKET rawSocket)
    : sock(rawSocket)
{
    init();

    state = (rawSocket == INVALID_SOCKET) ? SocketState::Closed : SocketState::Open;
}

Socket::~Socket()
{
    close();
}

bool Socket::operator==(const Socket& other) const
{
    return sock == other.sock;
}

bool Socket::operator!=(const Socket& other) const
{
    return !(*this == other);
}

bool Socket::isClosed() const
{
    return state == SocketState::Closed;
}

}  // namespace Rival

WindowsSocket.cpp

这是Windows特有的套接字功能。

代码语言:javascript
复制
#include "pch.h"

#ifdef _WIN32

// These comments...
#include "net/Socket.h"
// ... prevent the auto-formatter from moving the include

#include <winsock2.h>

#include <ws2tcpip.h>

#include <iostream>
#include <stdexcept>
#include <utility>  // std::exchange

namespace Rival {

Socket::Socket(const std::string& address, int port, bool server)
{
    // Specify socket properties
    addrinfo hints;
    ZeroMemory(&hints, sizeof(hints));
    hints.ai_family = AF_INET;
    hints.ai_socktype = SOCK_STREAM;
    hints.ai_protocol = IPPROTO_TCP;
    if (server)
    {
        hints.ai_flags = AI_PASSIVE;
    }

    // Resolve the local address and port to be used by the server
    addrinfo* addrInfo = nullptr;
    if (int err = getaddrinfo(address.c_str(), std::to_string(port).c_str(), &hints, &addrInfo))
    {
        throw std::runtime_error("Failed to get net address: " + std::to_string(err));
    }

    // Create the socket
    sock = INVALID_SOCKET;
    sock = socket(addrInfo->ai_family, addrInfo->ai_socktype, addrInfo->ai_protocol);

    // Check for errors
    if (sock == INVALID_SOCKET)
    {
        int err = WSAGetLastError();
        freeaddrinfo(addrInfo);
        throw std::runtime_error("Failed to create socket: " + std::to_string(err));
    }

    // Common socket initialization
    init();

    if (server)
    {
        // Bind the socket
        int bindResult = bind(sock, addrInfo->ai_addr, static_cast<int>(addrInfo->ai_addrlen));
        if (bindResult == SOCKET_ERROR)
        {
            int err = WSAGetLastError();
            freeaddrinfo(addrInfo);
            throw std::runtime_error("Failed to bind socket: " + std::to_string(err));
        }

        // Address info is no longer needed
        freeaddrinfo(addrInfo);

        // Listen (server-only)
        if (listen(sock, SOMAXCONN) == SOCKET_ERROR)
        {
            throw std::runtime_error("Failed to listen on socket: " + std::to_string(WSAGetLastError()));
        }
    }
    else
    {
        // Connect to server
        int connectResult = connect(sock, addrInfo->ai_addr, static_cast<int>(addrInfo->ai_addrlen));
        int err = WSAGetLastError();
        if (connectResult == SOCKET_ERROR)
        {
            sock = INVALID_SOCKET;
        }

        // Address info is no longer needed
        freeaddrinfo(addrInfo);

        if (sock == INVALID_SOCKET)
        {
            throw std::runtime_error("Failed to connect to server: " + std::to_string(err));
        }
    }

    if (isValid())
    {
        state = SocketState::Open;
    }
}

Socket::Socket(Socket&& other) noexcept
    // Reset the source object so its destructor is harmless
    : sock(std::exchange(other.sock, INVALID_SOCKET))
    , state(std::exchange(other.state, SocketState::Closed))
{
}

void Socket::init()
{
    // Disable Nagle algorithm to ensure packets are not held up
    BOOL socketOptionValue = TRUE;
    setsockopt(sock, IPPROTO_TCP, TCP_NODELAY, (char*) (&socketOptionValue), sizeof(BOOL));
}

void Socket::close() noexcept
{
    if (state == SocketState::Closed)
    {
        return;
    }

    state = SocketState::Closed;
    closesocket(sock);
}

Socket Socket::accept()
{
    SOCKET clientSocket = INVALID_SOCKET;

    clientSocket = ::accept(sock, nullptr, nullptr);

    if (clientSocket == INVALID_SOCKET && !isClosed())
    {
        std::cout << "Failed to accept client: " + std::to_string(WSAGetLastError()) << "\n";
    }

    return Socket::wrap(clientSocket);
}

bool Socket::isValid() const
{
    return sock != INVALID_SOCKET;
}

void Socket::send(std::vector<char>& buffer)
{
    std::size_t bytesSent = 0;

    while (bytesSent < buffer.size())
    {
        int bytesRemaining = buffer.size() - bytesSent;
        int result = ::send(sock, buffer.data() + bytesSent, bytesRemaining, 0);

        if (result == SOCKET_ERROR)
        {
            if (isClosed())
            {
                // Socket has been closed, so just abort the send
                break;
            }
            else
            {
                // Socket is still open on our side but may have been closed by the other side
                std::cerr << "Failed to send on socket: " + std::to_string(WSAGetLastError()) << "\n";
                close();
                break;
            }
        }

        bytesSent += result;
    }
}

void Socket::receive(std::vector<char>& buffer)
{
    std::size_t bytesReceived = 0;

    while (bytesReceived < buffer.size())
    {
        int bytesExpected = buffer.size() - bytesReceived;
        int result = ::recv(sock, buffer.data() + bytesReceived, bytesExpected, 0);

        if (result == SOCKET_ERROR)
        {
            if (isClosed())
            {
                // Socket has been closed, so just abort the read
                break;
            }
            else
            {
                // Socket is still open on our side but may have been closed by the other side
                std::cerr << "Failed to read from socket: " + std::to_string(WSAGetLastError()) << "\n";
                close();
                break;
            }
        }

        if (result == 0)
        {
            // Connection has been gracefully closed
            close();
            break;
        }

        bytesReceived += result;
    }
}

}  // namespace Rival

#endif

读取和中继数据包

在不受Connection类细节限制的情况下,下面是从套接字接收数据的代码。

关于客户:

  • 接收到的数据被PacketFactory解析为分组。
  • 数据包被排队,直到getReceivedPackets被调用。

在服务器上:

  • 接收到的数据包被包装在RelayedPacket中。
  • 这些将传递给侦听器类以进行转发。
代码语言:javascript
复制
void Connection::receiveThreadLoop()
{
    while (!socket.isClosed())
    {
        // First read the packet size
        if (!readFromSocket(Packet::sizeBytes))
        {
            break;
        }

        // Extract the packet size from the buffer
        std::size_t offset = 0;
        int nextPacketSize = 0;
        BufferUtils::readFromBuffer(recvBuffer, offset, nextPacketSize);
        recvBuffer.clear();

        // Sanity-check the packet size
        if (nextPacketSize > maxBufferSize)
        {
            throw std::runtime_error("Unexpected packet size: " + std::to_string(nextPacketSize));
        }

        // Now read the packet itself
        if (!readFromSocket(nextPacketSize))
        {
            break;
        }

        // If this Connection has no packet factory, then it belongs to the relay server. The relay server doesn't care
        // about the contents of incoming packets, it just wraps them in RelayedPackets.
        std::shared_ptr<const Packet> packet = packetFactory
                ? packetFactory->deserialize(recvBuffer)
                : std::make_shared<RelayedPacket>(recvBuffer, remoteClientId);

        if (packet)
        {
            // Pass packets directly to the listener if present, otherwise queue them until requested
            if (listener)
            {
                listener->onPacketReceived(*this, packet);
            }
            else
            {
                std::scoped_lock lock(receivedPacketsMutex);
                receivedPackets.push_back(packet);
            }
        }
    }
}

bool Connection::readFromSocket(std::size_t numBytes)
{
    recvBuffer.resize(numBytes);
    socket.receive(recvBuffer);

    // The socket may get closed during a call to `receive`
    bool success = !socket.isClosed();
    return success;
}

std::vector<std::shared_ptr<const Packet>> Connection::getReceivedPackets()
{
    std::vector<std::shared_ptr<const Packet>> packetsToReturn;

    {
        std::scoped_lock lock(receivedPacketsMutex);
        packetsToReturn = receivedPackets;
        receivedPackets.clear();
    }

    return packetsToReturn;
}

BufferUtils

最后,下面是一些实用工具方法,用于向缓冲区中添加数据/从缓冲区提取数据:

代码语言:javascript
复制
#pragma once

#include <cstddef>  // std::size_t
#include <cstring>  // std::memcpy
#include <stdexcept>
#include <vector>

namespace Rival { namespace BufferUtils {

/** Adds a value to the end of the given buffer.
 * This should not be used for anything which manages its own memory (e.g. containers and strings). */
template <typename T>
void addToBuffer(std::vector<char>& buffer, const T& val)
{
    // Later, we may need to ensure a certain endianness for cross-platform compatibility.
    // See: https://stackoverflow.com/questions/544928/reading-integer-size-bytes-from-a-char-array

    size_t requiredBufferSize = buffer.size() + sizeof(val);
    if (requiredBufferSize > buffer.capacity())
    {
        throw std::runtime_error("Trying to overfill buffer");
    }

    char* destPtr = buffer.data() + buffer.size();

    // Since we are writing to the vector's internal memory we need to manually change the size
    buffer.resize(requiredBufferSize);

    std::memcpy(destPtr, &val, sizeof(val));
}

/** Reads a value from the given buffer, at some offset.
 * The value is stored in dest, and the offset is increased by the size of the value. */
template <typename T>
void readFromBuffer(const std::vector<char>& buffer, std::size_t& offset, T& dest)
{
    if (offset + sizeof(dest) > buffer.size())
    {
        throw std::runtime_error("Trying to read past end of buffer");
    }

    std::memcpy(&dest, buffer.data() + offset, sizeof(dest));
    offset += sizeof(dest);
}

}}  // namespace Rival::BufferUtils
EN

回答 1

Code Review用户

回答已采纳

发布于 2023-05-02 17:29:37

快速查看您的套接字类:

使用INVALID_SOCKET表示关闭的套接字

套接字袜子;SocketState状态= SocketState::Closed;

如果使用INVALID_SOCKET指示封闭套接字,则不需要state变量。使用两个变量可能容易出错,因为它们可能不同步。

构造函数/赋值

Socket(const::string& address,int端口,bool server);套接字(SOCKET rawSocket);

我觉得把这些公之于众是没问题的。

套接字(套接字及其他)不例外;套接字&operator=( Socket&其他)=删除;

不知道为什么我们阻止移动任务。它可能是有用的,而且易于实现。特别是如果我们添加一个默认的构造函数,它只是一个封闭的套接字。

isOpen()函数

/**确定此套接字是否有效。*/ bool isValid() const;/**确定该套接字是否已关闭。*/ bool isClosed() const;

如果像上面提到的那样使用INVALID_SOCKET,我们只需要其中之一(我可能称之为isOpen)。

Const-correctness

无效发送(std::vector&缓冲器);

因为我们不想改变buffer,所以我们应该通过const&传递它,而不仅仅是&

重构Socket构造函数

套接字::套接字(const::string& address,int端口,bool server) { //指定套接字属性addrinfo提示;ZeroMemory(及提示,大小(提示));hints.ai_family = AF_INET;hints.ai_socktype = SOCK_STREAM;hints.ai_protocol = IPPROTO_TCP;if (服务器){ hints.ai_flags = AI_PASSIVE;} //解析服务器addrinfo* addrInfo =nullptr使用的本地地址和端口;if (int err = getaddrinfo(address.c_str(),std::to_string( port ).c_str(),&addrInfo)) {抛出std::runtime_error(“未能获得净地址:”+ std::to_string(err));} //创建套接字sock = INVALID_SOCKET;sock = socket (addrInfo->ai_族,addrInfo->ai_socktype,addrInfo->ai_protocol);//检查错误如果(sock == INVALID_SOCKET) { int err = WSAGetLastError();freeaddrinfo(addrInfo);引发std::runtime_error(“未能创建套接字:”+ std::to_string(err));} //公共套接字初始化init();如果(服务器){ //绑定套接字int bindResult = bind(sock,addrInfo->ai_addr,static_cast(addrInfo->ai_addrlen));if (bindResult == SOCKET_ERROR) { int err = WSAGetLastError();freeaddrinfo(addrInfo);抛出std::runtime_error(“绑定套接字失败:”+ std::to_string(err));} //地址信息不再需要freeaddrinfo(addrInfo);// Listen (仅服务器)如果( listen (sock,SOMAXCONN) == SOCKET_ERROR) {抛出std::runtime_error(“未能侦听套接字:”+std::to_string(WSAGetLastError();}{ // Connect到服务器int connectResult = connect( sock,addrInfo->ai_addr,static_cast(addrInfo->ai_addrlen));int err = WSAGetLastError();if (connectResult == SOCKET_ERROR) {sock= INVALID_SOCKET;} // Address信息不再需要自由地址信息(AddrInfo);如果(sock == INVALID_SOCKET) {抛出std::runtime_error(“未能连接到服务器:”+ std::to_string(err));} if (isValid()) { state = SocketState::Open;}

这是一个很长的函数,需要分成单独的、可重用的部分。如果我们能够减少前两部分(地址查找和打开套接字)的负担,我们就可以避免使用布尔server标志,这是一种反模式,只需要编写两个单独的函数。

C++很难处理错误(要么返回错误代码,要么抛出错误),这会使代码的缠绕时间更长。考虑使用std::system_error&参数作为输出参数。

这里另一件可以帮助的事情是用自定义删除器将getaddrinfo中的地址信息指针包装在unique_ptr中。这消除了手动调用freeaddrinfo的负担。

所以..。你可能会得到这样的结果:

代码语言:javascript
复制
Socket::Socket():
    m_socket(INVALID_SOCKET) { }
    
Socket::Socket(SOCKET handle):
    m_socket(handle) { }

Socket::Socket(int domain, int type, int protocol, std::system_error& ec):
    m_socket(::socket(domain, type, protocol))
{
    if (!is_open())
        ec = std::system_error(std::error_code(::WSAGetLastError(), std::system_category()));
}

Socket::~Socket()
{
    auto ec = std::system_error();
    (void)close(ec);
}

void Socket::is_open() const
{
    return m_socket != INVALID_SOCKET;
}

bool Socket::close(std::system_error& ec)
{
    if (!is_open())
        return;
    
    auto const result = ::closesocket(m_socket);
    
    if (result != 0)
    {
        ec = std::system_error(std::error_code(::WSAGetLastError(), std::system_category()));
        return false;
    }
    
    return true;
}

bool Socket::bind(sockaddr const* address, socklen_t address_length, std::system_error& ec)
{
    auto const result = ::bind(m_socket, address, address_length);
    
    if (result != 0)
    {
        ec = std::system_error(std::error_code(::WSAGetLastError(), std::system_category())); // TODO: abstract into a get_last_error()!
        return false;
    }
    
    return true;
}

// listen, connect, etc. left as an exercise for the reader.

...

using addrinfo_ptr = std::unique_ptr<::addrinfo, void(*)(::addrinfo*)>;

addrinfo_ptr lookup_address(char const* node, char const* service, int domain, int type, int protocol, int flags, std::system_error& ec)
{
    assert(node || service);
    
    auto hints = ::addrinfo();
    std::memset(&hints, 0, sizeof(::addrinfo));
    hints.ai_family = domain;
    hints.ai_socktype = type;
    hints.ai_protocol = protocol;
    hints.ai_flags = flags;
    
    auto out = (::addrinfo*)nullptr;
    auto const result = ::getaddrinfo(node, service, &hints, &out);
    
    if (result != 0)
    {
        ec = std::system_error(std::error_code(result, std::system_category())); // note: not WSAGetLastError
        return { };
    }
    
    assert(out);
    
    return addrinfo_ptr(out, &::freeaddrinfo);
}

addrinfo_ptr get_local_address_info(int domain, int type, int protocol, std::uint16_t port, std::system_error& ec)
{
    auto const port_str = std::to_string(port);
    int flags = AI_PASSIVE | AI_NUMERICSERV;
    return lookup_address(nullptr, port_str.data(), domain, type, protocol, flags, ec);
}

addrinfo_ptr get_address_info(int domain, int type, int protocol, std::string const& node, std::uint16_t port, std::system_error& ec)
{
    auto const port_str = std::to_string(port);
    int flags = AI_NUMERICSERV;
    return lookup_address(node.data(), port_str.data(), domain, type, protocol, flags, ec);
}

...

Socket create_server(std::uint16_t port)
{
    auto const domain = AF_INET;
    auto const type = SOCK_STREAM;
    auto const protocol = IPPROTO_TCP;
    
    auto ec = std::system_error();
    auto const addrinfoptr = get_local_address_info(domain, type, protocol, port, ec);
    
    if (!addrinfoptr)
        throw std::runtime_error(std::string("Address lookup failed: ") + ec.what());
    
    auto socket = Socket(domain, type, protocol, ec);
    
    if (!socket.is_open())
        throw std::runtime_error(std::string("Open socket failed: ") + ec.what());
    
    if (!socket.bind(addrinfoptr->ai_addr, addrinfoptr->ai_addrlen, ec))
        throw std::runtime_error(std::string("Bind socket failed: ") + ec.what());
    
    if (!socket.listen(SOMAXCONN, ec))
        throw std::runtime_error(std::string("Listen failed: ") + ec.what());
    
    return socket;
}

Socket create_client(std::string const& host, std::uint16_t port)
{
    auto const domain = AF_INET;
    auto const type = SOCK_STREAM;
    auto const protocol = IPPROTO_TCP;
    
    auto ec = std::system_error();
    auto const addrinfoptr = get_address_info(domain, type, protocol, host, port, ec);
    
    if (!addrinfoptr)
        throw std::runtime_error(std::string("Address lookup failed: ") + ec.what());
    
    auto socket = Socket(domain, type, protocol, ec);
    
    if (!socket.is_open())
        throw std::runtime_error(std::string("Open socket failed: ") + ec.what());
    
    if (!socket.connect(addrinfoptr->ai_addr, addrinfoptr->ai_addrlen, ec))
        throw std::runtime_error(std::string("Connect failed: ") + ec.what());
    
    return socket;
}

(注:未编译/测试)

您还可以使用其他模式来处理返回值/错误处理。这包括抛出错误(以及更累赘的捕获),或者使用类似于std::期望值的方法。

如果您想处理非阻塞套接字,事情会变得更复杂一些。这仅仅意味着在我们假设的Socket::connect()中检查几个“错误”值(例如,WSAEWOULDBLOCK),并将它们视为成功。

使用std::size_t

代码语言:javascript
复制
    int bytesRemaining = buffer.size() - bytesSent;
    ...
    int bytesExpected = buffer.size() - bytesReceived;

这些人应该是std::size_t

一个固定大小的缓冲区?

空套接字::接收(std::vector& bytesReceived = 0;bytesReceived< buffer.size()) { int bytesExpected = buffer.size() - bytesReceived;int结果= ::recv(sock,buffer.data() + bytesReceived,bytesExpected,0);.bytesReceived +=结果;}

循环直到接收到固定大小缓冲区中的所有字节是非常有限和容易出错的。我认为最好在Socket类之外强制执行。很有可能我们将来会收到一包不知道大小的包裹。

(发送也是如此)。

票数 1
EN
页面原文内容由Code Review提供。腾讯云小微IT领域专用引擎提供翻译支持
原文链接:

https://codereview.stackexchange.com/questions/284749

复制
相关文章

相似问题

领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档