I spent some time thinking about different ways of ack:ing packets and have come up with three possible solutions, which all have their respective pros and cons. I'm looking for some feedback and recommendations on picking an ack:ing strategy.
First, lets lay out some ground rules. Each packet is prefixed by a 16 bit sequence number which wraps around at 65535 to 0. Each peer has a local "window" which represents all the packets currently in transit from itself to a remote which have not been acked or nacked. Each peer also has a local append-only list of received sequence numbers from their remote. Packets that are "late" are dropped by the networking layer.
Option A:
The first option is the most obvious one, when you receive a packet you look at its sequence number and you send a response back to the remote peer which says "I received this sequence number". Lost packets are inferred by the receiver by looking at the distance between the previously received sequence number and the current received sequence number. NACK's are sent with the same type of messages "I did not receive this sequence number".
Pros:
- Allows arbitrarily large send windows
- In the optimal case (zero or very little packet loss) it uses the least bandwidth
Cons:
- If the ACK message gets lost it needs to be resent, and the side that sent the original packet which ACK got lost needs to be be able to tell the remote that an "an ACK/NACK got lost for this sequence number".
- During heavy packet loss it can take a long time to get an ACK/NACK for a packet.
Option B:
The second option is similar to Option A, but instead of sending each ACK/NACK individually the receiving end keeps a history of all received/lost sequence numbers up to the size of the send window and in the header of each packet that goes out from every peer to a remote the entire receive history of that peer is written by first writing the "last received" sequence number with 16 full bits and then one bit that is either 0/1 for each of the packets in the received window depending on if they were lost/received.
Pros:
- Easy to implement
- No need to send messages saying "I have not gotten an ACK for this sequence number" as the entire receive history of the peer is in the packet header for every packet sent.
- Very robust, no risk for "loss" spirals which causes several messages to to back and forth just to get an ACK/NACK for a packet, etc.
Cons:
- Restricts the size of the packet in-transit window as it will use 1 bit for each packet in the window plus 16 bits for the latest sequence number, so for a 128 packet window it would be 128 + 16 = 144 bits in the header of each packet. Could still work for fast paced games which are not built to handle a lot of packet loss (say FPS:es, etc.)
Option C:
The third option is a combination of A and B, basically in each packet header you write the same type of structure as in option B with a sequence number and then a bitmask of N bits to represent lost/received packets. But instead of sending the entire received-history up to the size of the send window like done in option B you send only the most recent part of the send history (say the last 16 or something).
Pros:
- Allows arbitrarily large send windows
- Has some redundancy on ACK/NACKs compared to option A, makes it a bit more durable
Cons:
- More complicated to implement then B as it still needs to be able to handle the "I have not received an ACK/NACK for this packet" in the case of heavy packet loss.
Conclusion:
I am basically looking for input on what is a good ACK/NACK:ing strategy, what you guys have used in the past, what you know works, etc.