Play over WebRTC to the server
The server answers each player's WebRTC offer (signaled over the socket) with a connection of its own: frames go on a lossy channel, everything else on a reliable one, and the socket carries them until both open or where UDP is blocked. The client's Net now holds one link to the server instead of one per player. WebRTC takes UDP 47810-47899 by default, set on the command line. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
655332778c
commit
dadfff189d
5 files changed
+228
-195
No files matched your search
+76
-140
@@ -17,9 +17,7 @@ constexpr Uint64 PING_INTERVAL = 1000;
|
||||
constexpr Uint64 HEARTBEAT_INTERVAL = 20000;
|
||||
constexpr float NS_PER_MS = 1e6f;
|
||||
|
||||
// A direct connection to one other member: the connection and, once they're there, its two data channels: a reliable,
|
||||
// ordered one for everything that mustn't go missing, and one for inputs, which are sent again in every packet until
|
||||
// they're acknowledged, so it neither waits on nor resends a lost one.
|
||||
// The WebRTC connection to the server and, once they're there, its two data channels (see Net).
|
||||
struct PeerLink
|
||||
{
|
||||
std::shared_ptr<rtc::PeerConnection> connection;
|
||||
@@ -35,7 +33,7 @@ struct PeerLink
|
||||
struct Net::Impl
|
||||
{
|
||||
std::shared_ptr<rtc::WebSocket> socket;
|
||||
std::map<int, PeerLink> links;
|
||||
PeerLink link;
|
||||
std::string pending;
|
||||
simdjson::dom::parser parser;
|
||||
Uint64 nextPing = 0;
|
||||
@@ -44,7 +42,6 @@ struct Net::Impl
|
||||
struct Delayed
|
||||
{
|
||||
Uint64 due;
|
||||
int to;
|
||||
bool isReliable;
|
||||
std::string bytes;
|
||||
bool isText = false;
|
||||
@@ -101,10 +98,9 @@ Net::~Net()
|
||||
// Closes every connection, waiting for nothing; anything still queued is dropped.
|
||||
void Net::shutdown()
|
||||
{
|
||||
for (auto& [id, link] : impl->links)
|
||||
if (link.connection)
|
||||
link.connection->close();
|
||||
impl->links.clear();
|
||||
if (impl->link.connection)
|
||||
impl->link.connection->close();
|
||||
impl->link = {};
|
||||
if (impl->socket)
|
||||
impl->socket->close();
|
||||
impl->socket.reset();
|
||||
@@ -124,12 +120,12 @@ void Net::lobby_join(Game& game, const char* lobby)
|
||||
json_quote(game.settings.name) + ",\"version\":" + std::to_string(PROTOCOL_VERSION) + "}");
|
||||
}
|
||||
|
||||
// Leaves the lobby, closing every direct connection but keeping the one to the server.
|
||||
// Leaves the lobby, closing the WebRTC connection but keeping the socket to the server.
|
||||
void Net::lobby_leave()
|
||||
{
|
||||
for (auto& [id, link] : impl->links)
|
||||
link.connection->close();
|
||||
impl->links.clear();
|
||||
if (impl->link.connection)
|
||||
impl->link.connection->close();
|
||||
impl->link = {};
|
||||
if (impl->socket && impl->socket->isOpen() && state == NET_LOBBY)
|
||||
impl->socket->send(std::string("{\"type\":\"leave\"}"));
|
||||
members.clear();
|
||||
@@ -174,7 +170,13 @@ void Net::request_send(Game& game, const std::string& request)
|
||||
});
|
||||
impl->socket->onMessage([this, shared](rtc::message_variant data) {
|
||||
if (std::holds_alternative<std::string>(data))
|
||||
{
|
||||
shared->post([this, text = std::get<std::string>(data)]() { message_handle(text); });
|
||||
return;
|
||||
}
|
||||
rtc::binary& bytes = std::get<rtc::binary>(data);
|
||||
std::string packet(reinterpret_cast<const char*>(bytes.data()), bytes.size());
|
||||
shared->post([this, packet]() { packets.push_back(packet); });
|
||||
});
|
||||
impl->socket->onClosed([this, shared]() {
|
||||
shared->post([this]() {
|
||||
@@ -230,14 +232,10 @@ void Net::message_handle(const std::string& text)
|
||||
NetMember member;
|
||||
member.id = entry["id"].get(number) ? -1 : (int)number;
|
||||
member.name = entry["name"].get(field) ? "" : std::string(field);
|
||||
member.isConnected = member.id == selfId;
|
||||
members.push_back(member);
|
||||
}
|
||||
state = NET_LOBBY;
|
||||
// The newcomer reaches out to everyone already here.
|
||||
for (NetMember& member : members)
|
||||
if (member.id != selfId)
|
||||
peer_open(member.id, true);
|
||||
link_open();
|
||||
return;
|
||||
}
|
||||
if (type == "member_join")
|
||||
@@ -266,137 +264,85 @@ void Net::message_handle(const std::string& text)
|
||||
if (!message["kicked"].get(isKicked) && isKicked)
|
||||
kicks[id] = text_clean(message["reason"].get(field) ? "" : field, KICK_REASON_MAX);
|
||||
hostId = message["host"].get(number) ? hostId : (int)number;
|
||||
peer_close(id);
|
||||
std::erase_if(members, [&](NetMember& member) { return member.id == id; });
|
||||
return;
|
||||
}
|
||||
if (type == "signal")
|
||||
{
|
||||
simdjson::dom::element payload;
|
||||
if (!message["from"].get(number) && !message["payload"].get(payload))
|
||||
signal_handle((int)number, simdjson::minify(payload));
|
||||
if (!message["payload"].get(payload))
|
||||
signal_handle(simdjson::minify(payload));
|
||||
}
|
||||
}
|
||||
|
||||
// Starts a direct connection to another member. The one offering makes the data channel, which starts the
|
||||
// negotiation; the other gets it once the offer is answered. Either way, descriptions and candidates go through the
|
||||
// server.
|
||||
void Net::peer_open(int id, bool isOfferer)
|
||||
// Offers the server a WebRTC connection with both data channels, which starts the negotiation. Descriptions and
|
||||
// candidates go back and forth over the socket. Pongs on the reliable channel time the round trip.
|
||||
void Net::link_open()
|
||||
{
|
||||
rtc::Configuration config;
|
||||
config.iceServers.emplace_back(STUN_SERVER);
|
||||
PeerLink& link = impl->links[id];
|
||||
PeerLink& link = impl->link;
|
||||
link.connection = std::make_shared<rtc::PeerConnection>(config);
|
||||
Impl* shared = impl.get();
|
||||
auto signal_send = [this](int to, const std::string& payload) {
|
||||
auto signal_send = [this](const std::string& payload) {
|
||||
if (impl->socket && impl->socket->isOpen())
|
||||
impl->socket->send("{\"type\":\"signal\",\"to\":" + std::to_string(to) + ",\"payload\":" + payload + "}");
|
||||
impl->socket->send("{\"type\":\"signal\",\"payload\":" + payload + "}");
|
||||
};
|
||||
link.connection->onLocalDescription([shared, signal_send, id](rtc::Description description) {
|
||||
link.connection->onLocalDescription([shared, signal_send](rtc::Description description) {
|
||||
std::string payload = "{\"sdp\":" + json_quote(std::string(description)) +
|
||||
",\"kind\":" + json_quote(description.typeString()) + "}";
|
||||
shared->post([signal_send, id, payload]() { signal_send(id, payload); });
|
||||
shared->post([signal_send, payload]() { signal_send(payload); });
|
||||
});
|
||||
link.connection->onLocalCandidate([shared, signal_send, id](rtc::Candidate candidate) {
|
||||
link.connection->onLocalCandidate([shared, signal_send](rtc::Candidate candidate) {
|
||||
std::string payload =
|
||||
"{\"candidate\":" + json_quote(candidate.candidate()) + ",\"mid\":" + json_quote(candidate.mid()) + "}";
|
||||
shared->post([signal_send, id, payload]() { signal_send(id, payload); });
|
||||
shared->post([signal_send, payload]() { signal_send(payload); });
|
||||
});
|
||||
|
||||
// A member is connected once both its channels are open. The reliable one also answers pings and times pongs.
|
||||
// Binary messages on either are queued for the session.
|
||||
auto channel_watch = [this, shared, id](std::shared_ptr<rtc::DataChannel> channel) {
|
||||
auto watched = impl->links.find(id);
|
||||
if (watched == impl->links.end())
|
||||
{
|
||||
channel->close();
|
||||
return;
|
||||
}
|
||||
bool isReliable = channel->label() != INPUTS_LABEL;
|
||||
(isReliable ? watched->second.channel : watched->second.inputs) = channel;
|
||||
channel->onOpen([this, shared, id]() {
|
||||
shared->post([this, id]() {
|
||||
auto link = impl->links.find(id);
|
||||
NetMember* member = member_get(id);
|
||||
if (member && link != impl->links.end())
|
||||
member->isConnected = link->second.is_open();
|
||||
});
|
||||
});
|
||||
channel->onClosed([this, shared, id]() {
|
||||
shared->post([this, id]() {
|
||||
if (NetMember* member = member_get(id))
|
||||
member->isConnected = false;
|
||||
});
|
||||
});
|
||||
channel->onMessage([this, shared, id, isReliable](rtc::message_variant data) {
|
||||
rtc::DataChannelInit lossy;
|
||||
lossy.reliability.unordered = true;
|
||||
lossy.reliability.maxRetransmits = 0;
|
||||
link.channel = link.connection->createDataChannel(CHANNEL_LABEL);
|
||||
link.inputs = link.connection->createDataChannel(INPUTS_LABEL, lossy);
|
||||
for (auto& channel : {link.channel, link.inputs})
|
||||
channel->onMessage([this, shared](rtc::message_variant data) {
|
||||
if (std::holds_alternative<rtc::binary>(data))
|
||||
{
|
||||
rtc::binary& bytes = std::get<rtc::binary>(data);
|
||||
std::string packet(reinterpret_cast<const char*>(bytes.data()), bytes.size());
|
||||
shared->post([this, id, isReliable, packet]() {
|
||||
packets.push_back({.from = id, .isReliable = isReliable, .bytes = packet});
|
||||
});
|
||||
shared->post([this, packet]() { packets.push_back(packet); });
|
||||
return;
|
||||
}
|
||||
shared->post([this, id, text = std::get<std::string>(data)]() {
|
||||
auto link = impl->links.find(id);
|
||||
if (link == impl->links.end() || !link->second.channel || !link->second.channel->isOpen())
|
||||
return;
|
||||
if (text.starts_with("ping:"))
|
||||
text_send(id, "pong:" + text.substr(5));
|
||||
else if (text.starts_with("pong:"))
|
||||
if (NetMember* member = member_get(id))
|
||||
member->ping = (SDL_GetTicksNS() - SDL_strtoull(text.c_str() + 5, nullptr, 10)) / NS_PER_MS;
|
||||
std::string& text = std::get<std::string>(data);
|
||||
if (!text.starts_with("pong:"))
|
||||
return;
|
||||
float ping = (SDL_GetTicksNS() - SDL_strtoull(text.c_str() + 5, nullptr, 10)) / NS_PER_MS;
|
||||
shared->post([this, ping]() {
|
||||
if (NetMember* member = member_get(selfId))
|
||||
member->ping = ping;
|
||||
});
|
||||
});
|
||||
};
|
||||
if (isOfferer)
|
||||
{
|
||||
rtc::DataChannelInit unreliable;
|
||||
unreliable.reliability.unordered = true;
|
||||
unreliable.reliability.maxRetransmits = 0;
|
||||
channel_watch(link.connection->createDataChannel(CHANNEL_LABEL));
|
||||
channel_watch(link.connection->createDataChannel(INPUTS_LABEL, unreliable));
|
||||
}
|
||||
else
|
||||
link.connection->onDataChannel([shared, channel_watch](std::shared_ptr<rtc::DataChannel> channel) {
|
||||
shared->post([channel_watch, channel]() { channel_watch(channel); });
|
||||
});
|
||||
}
|
||||
|
||||
void Net::peer_close(int id)
|
||||
{
|
||||
auto link = impl->links.find(id);
|
||||
if (link == impl->links.end())
|
||||
return;
|
||||
link->second.connection->close();
|
||||
impl->links.erase(link);
|
||||
}
|
||||
|
||||
// A description or candidate from another member. An offer from someone not yet connected starts the connection on
|
||||
// this side.
|
||||
void Net::signal_handle(int from, const std::string& text)
|
||||
// A description or candidate from the server for this screen's connection.
|
||||
void Net::signal_handle(const std::string& text)
|
||||
{
|
||||
simdjson::dom::element payload;
|
||||
std::string_view sdp;
|
||||
std::string_view kind;
|
||||
std::string_view candidate;
|
||||
std::string_view mid;
|
||||
if (impl->parser.parse(text).get(payload) || !member_get(from))
|
||||
PeerLink& link = impl->link;
|
||||
if (impl->parser.parse(text).get(payload) || !link.connection)
|
||||
return;
|
||||
if (!payload["sdp"].get(sdp) && !payload["kind"].get(kind))
|
||||
{
|
||||
if (!impl->links.contains(from))
|
||||
peer_open(from, false);
|
||||
impl->links[from].connection->setRemoteDescription(rtc::Description(std::string(sdp), std::string(kind)));
|
||||
return;
|
||||
}
|
||||
if (!payload["candidate"].get(candidate) && !payload["mid"].get(mid) && impl->links.contains(from))
|
||||
impl->links[from].connection->addRemoteCandidate(rtc::Candidate(std::string(candidate), std::string(mid)));
|
||||
link.connection->setRemoteDescription(rtc::Description(std::string(sdp), std::string(kind)));
|
||||
else if (!payload["candidate"].get(candidate) && !payload["mid"].get(mid))
|
||||
link.connection->addRemoteCandidate(rtc::Candidate(std::string(candidate), std::string(mid)));
|
||||
}
|
||||
|
||||
// Sends a binary message to another member, if they're connected.
|
||||
void Net::send(int to, const std::string& bytes, bool isReliable)
|
||||
// Sends the server a game message, held back or dropped first by the network simulator if it's on.
|
||||
void Net::send(const std::string& bytes, bool isReliable)
|
||||
{
|
||||
if (impl->lag > 0 || impl->loss > 0)
|
||||
{
|
||||
@@ -405,36 +351,31 @@ void Net::send(int to, const std::string& bytes, bool isReliable)
|
||||
return;
|
||||
if (impl->lag > 0)
|
||||
{
|
||||
impl->delayed.push_back({SDL_GetTicks() + impl->lag, to, isReliable, bytes});
|
||||
impl->delayed.push_back({SDL_GetTicks() + impl->lag, isReliable, bytes});
|
||||
return;
|
||||
}
|
||||
}
|
||||
send_now(to, bytes, isReliable);
|
||||
send_now(bytes, isReliable);
|
||||
}
|
||||
|
||||
void Net::send_now(int to, const std::string& bytes, bool isReliable)
|
||||
// Over the data channels once they're open, or the socket until then.
|
||||
void Net::send_now(const std::string& bytes, bool isReliable)
|
||||
{
|
||||
auto link = impl->links.find(to);
|
||||
if (link == impl->links.end() || !link->second.is_open())
|
||||
return;
|
||||
auto& channel = isReliable ? link->second.channel : link->second.inputs;
|
||||
channel->send(reinterpret_cast<const rtc::byte*>(bytes.data()), bytes.size());
|
||||
auto data = reinterpret_cast<const rtc::byte*>(bytes.data());
|
||||
if (is_linked())
|
||||
(isReliable ? impl->link.channel : impl->link.inputs)->send(data, bytes.size());
|
||||
else if (impl->socket && impl->socket->isOpen() && state == NET_LOBBY)
|
||||
impl->socket->send(rtc::binary(data, data + bytes.size()));
|
||||
}
|
||||
|
||||
// A text message (pings and pongs) on the reliable channel, held back by the network simulator like everything else.
|
||||
void Net::text_send(int to, const std::string& text)
|
||||
// Times the round trip to the server over the reliable channel (the network simulator holds it back too).
|
||||
void Net::ping_send()
|
||||
{
|
||||
std::string text = "ping:" + std::to_string(SDL_GetTicksNS());
|
||||
if (impl->lag > 0)
|
||||
impl->delayed.push_back({SDL_GetTicks() + impl->lag, to, true, text, true});
|
||||
else
|
||||
text_send_now(to, text);
|
||||
}
|
||||
|
||||
void Net::text_send_now(int to, const std::string& text)
|
||||
{
|
||||
auto link = impl->links.find(to);
|
||||
if (link != impl->links.end() && link->second.channel && link->second.channel->isOpen())
|
||||
link->second.channel->send(text);
|
||||
impl->delayed.push_back({SDL_GetTicks() + impl->lag, true, text, true});
|
||||
else if (is_linked())
|
||||
impl->link.channel->send(text);
|
||||
}
|
||||
|
||||
// Asks the server to put a member out of the lobby (it only does so for the host).
|
||||
@@ -445,16 +386,13 @@ void Net::kick_send(int member, const std::string& reason)
|
||||
"}");
|
||||
}
|
||||
|
||||
// Whether a direct connection to every other member is open.
|
||||
bool Net::is_connected_all()
|
||||
// Whether the WebRTC connection to the server is up, with both its channels open.
|
||||
bool Net::is_linked()
|
||||
{
|
||||
for (NetMember& member : members)
|
||||
if (!member.isConnected)
|
||||
return false;
|
||||
return state == NET_LOBBY;
|
||||
return impl->link.is_open();
|
||||
}
|
||||
|
||||
// Once a frame: handles what the network reported (on desktop), pings every connected member, and on desktop keeps the
|
||||
// Once a frame: handles what the network reported (on desktop), pings the server over the link, and on desktop keeps the
|
||||
// server connection alive.
|
||||
void Net::update()
|
||||
{
|
||||
@@ -471,17 +409,15 @@ void Net::update()
|
||||
{
|
||||
Impl::Delayed due = impl->delayed.front();
|
||||
impl->delayed.erase(impl->delayed.begin());
|
||||
if (due.isText)
|
||||
text_send_now(due.to, due.bytes);
|
||||
else
|
||||
send_now(due.to, due.bytes, due.isReliable);
|
||||
if (due.isText && is_linked())
|
||||
impl->link.channel->send(due.bytes);
|
||||
else if (!due.isText)
|
||||
send_now(due.bytes, due.isReliable);
|
||||
}
|
||||
if (state == NET_LOBBY && now >= impl->nextPing)
|
||||
{
|
||||
impl->nextPing = now + PING_INTERVAL;
|
||||
for (auto& [id, link] : impl->links)
|
||||
if (link.channel && link.channel->isOpen())
|
||||
text_send(id, "ping:" + std::to_string(SDL_GetTicksNS()));
|
||||
ping_send();
|
||||
}
|
||||
#ifndef SDL_PLATFORM_EMSCRIPTEN
|
||||
if (now >= impl->nextHeartbeat)
|
||||
|
||||
Reference in new issue
Block a user