-
Notifications
You must be signed in to change notification settings - Fork 4
Choosing a Transport
ConnectionType::TCP or ConnectionType::ZDT, set in the config and matching on
both ends. Nothing above the session changes: same packets, same handlers, same
events.
ClientConfig config{"127.0.0.1", 25000, std::chrono::seconds(10),
ConnectionType::ZDT};Use ZDT unless you have a specific reason not to. It is faster under loss, it does not head-of-line block, and it can send unreliably when that is the right choice. TCP is there for environments that only pass TCP, and for peers you do not control.
| TCP | ZDT | |
|---|---|---|
| Underneath | Kernel TCP | UDP, reliability in znet |
| Delivery | Reliable, ordered, always | Per-message, four combinations |
| Head-of-line blocking | Yes, one stream | Only within a channel, and only if ordered |
| Channels | No | 256 |
| Max message size | 4,039 bytes | ~366 KiB at a 1492 MTU (see below) |
| Congestion control | Kernel, loss-based | znet, delay-based |
| MTU | Not your problem | Probed during the handshake |
| Handshake | Kernel + znet's key exchange | Cookie exchange, then znet's key exchange |
Head-of-line blocking is the difference that matters most. On TCP a lost segment stalls everything behind it, including messages that had nothing to do with it. On ZDT that stall is confined to one channel, and only when the message asked to be ordered.
The second argument to SendPacket is per message. Reliable and ordered are
both on by default.
void Send(PeerSession& session, std::shared_ptr<Packet> packet) {
// default: reliable and ordered, on channel 0
session.SendPacket(packet);
// position updates: the newest one supersedes a lost one, so neither
// retransmitting nor holding the line for it is worth the latency
SendOptions unreliable;
unreliable.Set<ReliableKey>(false);
unreliable.Set<OrderedKey>(false);
session.SendPacket(packet, unreliable);
// reliable but order-independent: nothing is lost, and one gap does not
// hold back everything queued behind it
SendOptions unordered;
unordered.Set<OrderedKey>(false);
session.SendPacket(packet, unordered);
// a separate channel, so chat is not stuck behind a bulk transfer
SendOptions chat;
chat.Set<ChannelKey>(2);
session.SendPacket(packet, chat);
}At C++20 or newer the same thing reads better with designated initializers:
session.SendPacket(packet, SendOptions{{.reliable = false, .ordered = false}});Note the double brace — the inner one builds a SendOptionsInit. This form does
not compile at C++14 or C++17; use Set<Key>() there. GCC and Clang will accept
it below C++20 as an extension, but not under -pedantic, and MSVC will not.
| Reliable | Ordered | Behavior | Use for |
|---|---|---|---|
| yes | yes | Retransmitted, delivered in send order | Chat, commands, state that must not be lost or reordered |
| yes | no | Retransmitted, delivered on arrival | Bulk transfers, independent events; one loss stalls nothing |
| no | yes | Not retransmitted, stale ones dropped | Position and animation, where only the newest matters |
| no | no | Not retransmitted, delivered on arrival | Voice, telemetry, anything self-contained |
"Ordered" without "reliable" means sequenced: a message older than one already delivered is dropped rather than held for its turn, since waiting for something that will never be retransmitted would stall the channel forever.
256 of them, independent sequence spaces, allocated lazily so idle ones cost nothing. Reliable and unreliable traffic on one channel do not interfere.
Channels exist to keep unrelated traffic from blocking each other. A file transfer on channel 1 and chat on channel 2 will not stall one another even when both are reliable and ordered. Splitting traffic that is already unordered buys nothing.
Both transports cap a single message, and both refuse rather than truncate.
TCP frames inside a fixed buffer of ZNET_MAX_BUFFER_SIZE (4096) bytes, so
a message must fit in that minus the frame header: 4,039 bytes. Send()
logs an error and returns false above it. This is why the benchmark tables show
TCP as unsupported at an 8 KiB payload.
ZDT fragments, so the limit is 255 fragments of MTU − 22 bytes each:
| MTU | Per fragment | Largest message |
|---|---|---|
| 1492 | 1470 B | ~366 KiB |
| 1200 | 1178 B | ~293 KiB |
| 576 | 554 B | ~138 KiB |
Above that the message is dropped with an error naming the size and the fragment count it would have needed. Since the MTU is whatever the handshake settled on, a message near the ceiling can succeed on one connection and fail on another. Keep large payloads well under it, or chunk them yourself.
max_reassembly_bytes (16 MiB) is a separate, connection-wide bound on
partially reassembled messages held at once, not a per-message limit.
- No streams. It carries messages. A message is delivered whole or not at all, and there is no byte-stream interface.
- No unencrypted-by-default assumption. Encryption is on unless turned off, same as TCP. See Encryption and Compression.
- No NAT traversal on its own. That is Peer-to-Peer.
Defaults are meant to be usable as-is. The two worth knowing about:
-
max_datagrams_in_flight(512) caps the congestion window. It counts datagrams, not bytes, so a half-full datagram costs as much as a full one. On a long fat link this is what bounds throughput. -
rto_min(100 ms) floors the retransmit timeout. On a LAN, lowering it recovers from loss faster; on the open internet, lowering it causes retransmits of packets that were merely late.
Everything else is in Configuration Reference.
The benchmarks in the README compare both transports against ENet, Valve's GameNetworkingSockets and RakNet, on a clean loopback and over a link with 5% loss and a 50 ms round trip. Read the footnotes: several rows have enough spread that a single measurement from any of these libraries is not a number.