这是我正在创建的锁步RTS游戏的核心网络代码。客户端通过TCP套接字连接到中继服务器,发送到中继服务器的任何数据包都转发给所有其他播放器。
中继服务器不关心数据包内容或游戏状态,它只是传递数据包。客户端发送包含其命令的每个滴答号,只有当客户端接收到所有其他玩家的命令时,才会处理滴答号--这确保了每个人都在运行相同的模拟,因此所有客户端都保持同步。为了缓解延迟,命令将在未来对'n‘号进行调度(当前设置为10次,或~166 or )。
目前,这似乎运行良好(至少在Windows上)。
这样做的目的是让它成为一个跨平台的包装器,使用RAII管理连接的原始套接字。
send方法发送给定的缓冲区,receive方法阻塞,直到它填充了给定的缓冲区。Socket,但不能复制。这包含类声明,并且(理想情况下)应该完全与平台无关。
#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这包含所有平台共有的任何套接字功能。
#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这是Windows特有的套接字功能。
#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中。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;
}最后,下面是一些实用工具方法,用于向缓冲区中添加数据/从缓冲区提取数据:
#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发布于 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)。
无效发送(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的负担。
所以..。你可能会得到这样的结果:
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 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类之外强制执行。很有可能我们将来会收到一包不知道大小的包裹。
(发送也是如此)。
https://codereview.stackexchange.com/questions/284749
复制相似问题