You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
The synchronization process currently happens at the protocol layer, and I think it would be better if it happened at the socket layer (if at all), because (I believe) it isn't actually needed for other network transports like matchbox, steam networking, renet, etc.
Why
Moving the sync process to the swappable transport layer would allow users of other transports like matchbox to skip the synchronization process, which would be beneficial in a few ways:
Synchronization causes a variable delay in starting a match, which can't be skipped.
Synchronization is an extra step to figure out in solving built-in-to-ggrs support for hot join (or reconnecting disconnected players) - which is what prompted me to raise this issue.
As far as I can tell, synchronization doesn't do anything that is strictly necessary for natively-multiplexed-and-authenticated transports - more on this below.
Lastly, there is currently no way for synchronization to time out - it just hangs indefinitely if e.g. a remote client doesn't respond. (Obviously this can be fixed separately, but by moving synchronization out of the protocol layer this issue will only affect those who use the default UDP transport.)
The Synchronization Process
Currently GGRS uses the same "synchronization" process as GGPO; to summarize:
Before starting the game simulation, P2P and spectator sessions will send a SyncRequest containing a (randomly generated) number to each (anticipated to be) connected client.
When a client receives a SyncRequest, it sends a SyncReply back with the same number.
When a client receives a SyncReply, it checks whether this reply's number matches that in the last request it sent, and drops the message if not.
If it does match, and this is the 4th or less SyncReply, the process is repeated
Otherwise, the 5th SyncReply's header's "magic number" (a different a random number) is stored as the final remote_magic in the protocol's state; this is used to drop future messages whose headers' "magic" does not match the stored remote_magic.
Purpose of Synchronizing
As far as I can tell based on GGPO and GGRS source code and (a lot of) guessing, here's why synchronizing is in place:
Handshaking: making sure that the other side actually has a ggrs client listening on that UDP port.
Needed for UDP: not strictly needed - if the remote doesn't actually connect, they will instead be disconnected as part of the usual disconnection of non-responsive peers.
Needed by other transports: no - matchbox, renet, iroh, steam networking, quinn all handle authentication of a peer as part of their own transport duties.
Basic stateful-firewall/NAT traversal: by sending an outbound UDP packet to IP:PORT, many stateful firewalls/NAT gateways will automatically allow/forward an incoming response from IP:PORT too - so both clients synchronizing will bypass some NATs.
Needed for UDP: yes, or at least, it's necessary but insufficient for a production game: a robust implementation of NAT traversal would require quite a bit more features - STUN, TURN or similar relaying, etc.
Needed by other transports: no - because other transports do their own handshaking to connect, they have to do their own NAT traversal too (the synchronization's NAT hole punching would be "too late" for them).
Basic authentication: the protocol can drop packets that aren't from any of known peers (i.e. when packets' headers' magics don't match the stored remote magic)
Needed for UDP: yes for a production game - since source IP and port can be spoofed, attackers could send messages with different inputs, which would cause desyncs.
Needed by other transports: no - as far as I'm aware, each of matchbox, renet, iroh and steam networking do their own (more robust) authentication.
So as I reason right now, other transports don't benefit from the synchronization process, and it isn't free, so I think it shouldn't be part of P2P and spectator sessions?
Options
That leaves us with a few approaches:
Remove synchronization entirely: if ggrs is meant to be focused on the tricky logic of rollbacks and ggrs::UdpNonBlockingSocket is just a bare minimum demo implementation of the NonBlockingSocket (i.e. it is expected to be swapped out entirely for any "serious" game), then I think removing synchronization entirely is a reasonable approach.
I've done this in my personal ggrs fork (caspark@f7b8fdd) and it has the expected effect: everything seems to work as before with both UDP and Matchbox sockets (on both good and terrible connections), except for that 1) there isn't a pause at the start to wait for synchronization and 2) a client that fails to join the game will be disconnected (which is a feature in my opinion)
Move the synchronizing logic from the sessions and protocol down to the socket layer (into ggrs::UdpNonBlockingSocket). Doesn't regress any functionality, but will require a bit of refactoring to allow ggrs-users to still observe synchronization progress & completion (UDP socket will need to emit events, so would have to expose a way to get at the socket or expect users to Rc/Arc-wrap the socket).
Add a skip_synchronization(bool) (default false) to the GGRS session builder, which starts a session off in synchronized state - tell using users to turn it on if using a transport that satisfies criteria X/Y/Z. Easiest to implement by far but feels like a hack to me.
So if you're happy with approach 1 then I can PR that pretty quickly - or if you prefer #2 then I could have a shot at that.
Or if you don't want to explore this, you can just close the issue :)
The text was updated successfully, but these errors were encountered:
I like the idea of using ggrs::UdpNonBlockingSocket as "a bare minimum demo implementation of the NonBlockingSocket". I would like to keep the synchronization around, but inside the UdpNonBlockingSocket might be a better space for it.
I have implemented this in a branch in my fork; it works but I haven't written any tests for it yet so I'll hold off PRing until I get around to that. The approach I've taken so far is to add a HandshakingSocket that wraps the existing UDP socket, making synchronization optional for both UDP and other sockets.
What
The synchronization process currently happens at the protocol layer, and I think it would be better if it happened at the socket layer (if at all), because (I believe) it isn't actually needed for other network transports like matchbox, steam networking, renet, etc.
Why
Moving the sync process to the swappable transport layer would allow users of other transports like matchbox to skip the synchronization process, which would be beneficial in a few ways:
The Synchronization Process
Currently GGRS uses the same "synchronization" process as GGPO; to summarize:
SyncRequest
containing a (randomly generated) number to each (anticipated to be) connected client.SyncRequest
, it sends aSyncReply
back with the same number.SyncReply
, it checks whether this reply's number matches that in the last request it sent, and drops the message if not.SyncReply
, the process is repeatedSyncReply
's header's "magic number" (a different a random number) is stored as the finalremote_magic
in the protocol's state; this is used to drop future messages whose headers' "magic" does not match the storedremote_magic
.Purpose of Synchronizing
As far as I can tell based on GGPO and GGRS source code and (a lot of) guessing, here's why synchronizing is in place:
So as I reason right now, other transports don't benefit from the synchronization process, and it isn't free, so I think it shouldn't be part of P2P and spectator sessions?
Options
That leaves us with a few approaches:
ggrs::UdpNonBlockingSocket
is just a bare minimum demo implementation of theNonBlockingSocket
(i.e. it is expected to be swapped out entirely for any "serious" game), then I think removing synchronization entirely is a reasonable approach.ggrs::UdpNonBlockingSocket
). Doesn't regress any functionality, but will require a bit of refactoring to allow ggrs-users to still observe synchronization progress & completion (UDP socket will need to emit events, so would have to expose a way to get at the socket or expect users to Rc/Arc-wrap the socket).skip_synchronization(bool)
(default false) to the GGRS session builder, which starts a session off in synchronized state - tell using users to turn it on if using a transport that satisfies criteria X/Y/Z. Easiest to implement by far but feels like a hack to me.So if you're happy with approach 1 then I can PR that pretty quickly - or if you prefer #2 then I could have a shot at that.
Or if you don't want to explore this, you can just close the issue :)
The text was updated successfully, but these errors were encountered: