A frame at or before the seeded watermark is a duplicate of an earlier generation and is acknowledged, never delivered again (serial compare within the receive window). A Connect that precedes the current generation's within the window is a stale copy of an earlier attempt: it is answered like a repeat (Accept owed or Ack), changes no state and does not refresh liveness. A Connect unanswered for 3 s is replaced by a fresh Connect with a newly drawn sequence and the same payload. In server mode a Connect from another endpoint is always a new generation. The initial sequence is unset by default and each session draws its own; the public random helper is removed. Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
565 lines
21 KiB
Rust
565 lines
21 KiB
Rust
use std::collections::VecDeque;
|
|
use std::net::UdpSocket;
|
|
use std::time::Duration;
|
|
|
|
use anyhow::{Result, anyhow};
|
|
use cultnet_rs::*;
|
|
|
|
const CONNECTION_ID: u32 = 0x4355_4c54;
|
|
|
|
fn socket() -> Result<UdpSocket> {
|
|
let socket = UdpSocket::bind("127.0.0.1:0")?;
|
|
socket.set_read_timeout(Some(Duration::from_millis(20)))?;
|
|
Ok(socket)
|
|
}
|
|
|
|
fn client(remote: std::net::SocketAddr) -> Result<CultNetRudpSocketTransportConnection> {
|
|
let mut options =
|
|
CultNetRudpSocketTransportOptions::client("provider", socket()?, remote, CONNECTION_ID);
|
|
options.max_fragment_bytes = Some(2_048);
|
|
CultNetRudpSocketTransportConnection::new(options)
|
|
}
|
|
|
|
fn connect(
|
|
hub: &mut CultNetRudpServerHub,
|
|
client: &mut CultNetRudpSocketTransportConnection,
|
|
evidence: &[u8],
|
|
) -> Result<CultNetRudpServerSessionContext> {
|
|
client.connect(evidence.to_vec())?;
|
|
let mut connected = None;
|
|
for _ in 0..20 {
|
|
if let Some(CultNetRudpServerEvent::Connected { session }) = hub.receive_event_once()? {
|
|
connected = Some(session);
|
|
break;
|
|
}
|
|
}
|
|
let session = connected.ok_or_else(|| anyhow!("hub did not receive Connect"))?;
|
|
let _ = client.receive_once()?;
|
|
assert!(client.connected());
|
|
Ok(session)
|
|
}
|
|
|
|
fn receive_frame(hub: &mut CultNetRudpServerHub) -> Result<CultNetRudpServerEvent> {
|
|
for _ in 0..20 {
|
|
if let Some(event @ CultNetRudpServerEvent::Frame { .. }) = hub.receive_event_once()? {
|
|
return Ok(event);
|
|
}
|
|
}
|
|
Err(anyhow!("hub did not receive a frame"))
|
|
}
|
|
|
|
fn receive_message(client: &mut CultNetRudpSocketTransportConnection) -> Result<CultNetMessage> {
|
|
for _ in 0..20 {
|
|
if let Some(message) = client.receive_schema_message_once()? {
|
|
return Ok(message);
|
|
}
|
|
}
|
|
Err(anyhow!("client did not receive a schema message"))
|
|
}
|
|
|
|
#[test]
|
|
fn hub_keeps_independent_peer_sessions_and_exposes_connect_evidence() -> Result<()> {
|
|
let server_socket = socket()?;
|
|
let server_addr = server_socket.local_addr()?;
|
|
let mut hub = CultNetRudpServerHub::new(CultNetRudpServerHubOptions::new(
|
|
"odin",
|
|
server_socket,
|
|
CONNECTION_ID,
|
|
))?;
|
|
let mut first = client(server_addr)?;
|
|
let mut second = client(server_addr)?;
|
|
|
|
let first_session = connect(&mut hub, &mut first, b"session-token-one")?;
|
|
let second_session = connect(&mut hub, &mut second, b"session-token-two")?;
|
|
assert_ne!(first_session.remote_addr, second_session.remote_addr);
|
|
assert_eq!(first_session.connect_payload, b"session-token-one");
|
|
assert_eq!(second_session.connect_payload, b"session-token-two");
|
|
assert_eq!(hub.sessions().len(), 2);
|
|
|
|
let first_request = create_provider_session_request(
|
|
"register-first",
|
|
CULTMESH_PROVIDER_REGISTER_OPERATION,
|
|
CULTMESH_PROVIDER_REGISTRATION_SCHEMA,
|
|
&CultMeshProviderRegistrationWire {
|
|
provider_id: "first".to_string(),
|
|
service_instance_id: "first-1".to_string(),
|
|
endpoint_id: "first-public".to_string(),
|
|
verse_id: "public".to_string(),
|
|
requested_lease_duration_ms: 30_000,
|
|
authority_lease_id: None,
|
|
},
|
|
Some("first-1".to_string()),
|
|
Some("odin".to_string()),
|
|
)?;
|
|
let second_request = create_provider_session_request(
|
|
"register-second",
|
|
CULTMESH_PROVIDER_REGISTER_OPERATION,
|
|
CULTMESH_PROVIDER_REGISTRATION_SCHEMA,
|
|
&CultMeshProviderRegistrationWire {
|
|
provider_id: "second".to_string(),
|
|
service_instance_id: "second-1".to_string(),
|
|
endpoint_id: "second-public".to_string(),
|
|
verse_id: "public".to_string(),
|
|
requested_lease_duration_ms: 30_000,
|
|
authority_lease_id: None,
|
|
},
|
|
Some("second-1".to_string()),
|
|
Some("odin".to_string()),
|
|
)?;
|
|
first.send_schema_message(&first_request)?;
|
|
second.send_schema_message(&second_request)?;
|
|
|
|
for _ in 0..2 {
|
|
let CultNetRudpServerEvent::Frame { session, frame } = receive_frame(&mut hub)? else {
|
|
unreachable!();
|
|
};
|
|
assert_eq!(frame.channel_id, "schema");
|
|
let request = decode_cultnet_message_from_slice(
|
|
&frame.payload,
|
|
CultNetWireContract::CultNetSchemaV0,
|
|
)?;
|
|
let CultNetMessage::OperationRequest { message_id, .. } = &request else {
|
|
return Err(anyhow!("expected operation request"));
|
|
};
|
|
let provider_id = if message_id == "register-first" {
|
|
"first"
|
|
} else {
|
|
"second"
|
|
};
|
|
let lease = CultMeshProviderLeaseWire {
|
|
provider_id: provider_id.to_string(),
|
|
service_instance_id: format!("{provider_id}-1"),
|
|
endpoint_id: format!("{provider_id}-public"),
|
|
verse_id: "public".to_string(),
|
|
lease_id: format!("lease-{provider_id}"),
|
|
valid_from_utc: "2026-07-14T12:00:00Z".to_string(),
|
|
expires_at_utc: "2026-07-14T12:00:30Z".to_string(),
|
|
};
|
|
let response = create_provider_session_response(
|
|
&request,
|
|
CultMeshProviderOperationStatus::Ok,
|
|
CULTMESH_PROVIDER_LEASE_SCHEMA,
|
|
&lease,
|
|
Some("odin".to_string()),
|
|
)?;
|
|
hub.send_schema_message(&session, &response)?;
|
|
}
|
|
|
|
let first_response = receive_message(&mut first)?;
|
|
let second_response = receive_message(&mut second)?;
|
|
let (_, first_lease) = decode_provider_session_response::<CultMeshProviderLeaseWire>(
|
|
&first_response,
|
|
CULTMESH_PROVIDER_REGISTER_OPERATION,
|
|
CULTMESH_PROVIDER_LEASE_SCHEMA,
|
|
)?;
|
|
let (_, second_lease) = decode_provider_session_response::<CultMeshProviderLeaseWire>(
|
|
&second_response,
|
|
CULTMESH_PROVIDER_REGISTER_OPERATION,
|
|
CULTMESH_PROVIDER_LEASE_SCHEMA,
|
|
)?;
|
|
assert_eq!(first_lease.provider_id, "first");
|
|
assert_eq!(second_lease.provider_id, "second");
|
|
assert_eq!(hub.session(first_session.remote_addr), Some(&first_session));
|
|
assert_eq!(
|
|
hub.session(second_session.remote_addr),
|
|
Some(&second_session)
|
|
);
|
|
Ok(())
|
|
}
|
|
|
|
#[test]
|
|
fn hub_fences_replaced_generations() -> Result<()> {
|
|
let server_socket = socket()?;
|
|
let server_addr = server_socket.local_addr()?;
|
|
let mut hub = CultNetRudpServerHub::new(CultNetRudpServerHubOptions::new(
|
|
"odin",
|
|
server_socket,
|
|
CONNECTION_ID,
|
|
))?;
|
|
let mut provider = client(server_addr)?;
|
|
let original_evidence =
|
|
encode_provider_connect_evidence(&CultMeshProviderConnectEvidenceWire {
|
|
client_session_id: "provider-client-1".to_string(),
|
|
session_token: Some("shared-session-token".to_string()),
|
|
})?;
|
|
let replacement_evidence =
|
|
encode_provider_connect_evidence(&CultMeshProviderConnectEvidenceWire {
|
|
client_session_id: "provider-client-2".to_string(),
|
|
session_token: Some("shared-session-token".to_string()),
|
|
})?;
|
|
let original = connect(&mut hub, &mut provider, &original_evidence)?;
|
|
|
|
provider.connect(replacement_evidence.clone())?;
|
|
let mut disconnected = None;
|
|
for _ in 0..20 {
|
|
if let Some(event @ CultNetRudpServerEvent::Disconnected { .. }) =
|
|
hub.receive_event_once()?
|
|
{
|
|
disconnected = Some(event);
|
|
break;
|
|
}
|
|
}
|
|
let disconnected =
|
|
disconnected.ok_or_else(|| anyhow!("replacement did not disconnect old generation"))?;
|
|
let CultNetRudpServerEvent::Disconnected { session, .. } = disconnected else {
|
|
return Err(anyhow!("expected old generation Disconnected event"));
|
|
};
|
|
assert_eq!(session, original);
|
|
let connected = hub
|
|
.receive_event_once()?
|
|
.ok_or_else(|| anyhow!("replacement did not connect new generation"))?;
|
|
let CultNetRudpServerEvent::Connected {
|
|
session: replacement,
|
|
} = connected
|
|
else {
|
|
return Err(anyhow!("expected replacement Connected event"));
|
|
};
|
|
assert_ne!(replacement.session_generation, original.session_generation);
|
|
let original_decoded = decode_provider_connect_evidence(&original.connect_payload)?;
|
|
let replacement_decoded = decode_provider_connect_evidence(&replacement.connect_payload)?;
|
|
assert_eq!(
|
|
original_decoded.session_token,
|
|
replacement_decoded.session_token
|
|
);
|
|
assert_ne!(
|
|
original_decoded.client_session_id,
|
|
replacement_decoded.client_session_id
|
|
);
|
|
assert!(
|
|
hub.send(&original, "schema", vec![0x80])
|
|
.unwrap_err()
|
|
.to_string()
|
|
.contains("no longer active")
|
|
);
|
|
assert_eq!(hub.session(replacement.remote_addr), Some(&replacement));
|
|
Ok(())
|
|
}
|
|
|
|
#[test]
|
|
fn identical_connect_retransmit_reuses_pending_accept_after_loss() -> Result<()> {
|
|
let server_socket = socket()?;
|
|
let server_addr = server_socket.local_addr()?;
|
|
let mut hub = CultNetRudpServerHub::new(CultNetRudpServerHubOptions::new(
|
|
"odin",
|
|
server_socket,
|
|
CONNECTION_ID,
|
|
))?;
|
|
let client_socket = socket()?;
|
|
let mut client_session = CultNetRudpSession::new(CultNetRudpSessionOptions {
|
|
connection_id: CONNECTION_ID,
|
|
initial_sequence: Some(1),
|
|
resend_delay_ms: 250,
|
|
max_pending_reliable_packets: None,
|
|
});
|
|
let evidence = encode_provider_connect_evidence(&CultMeshProviderConnectEvidenceWire {
|
|
client_session_id: "provider-client-lost-accept".to_string(),
|
|
session_token: Some("shared-session-token".to_string()),
|
|
})?;
|
|
let connect_packet = client_session.create_connect(0, evidence)?;
|
|
let connect_wire = encode_rudp_packet(&connect_packet)?;
|
|
|
|
client_socket.send_to(&connect_wire, server_addr)?;
|
|
let CultNetRudpServerEvent::Connected { session } = hub
|
|
.receive_event_once()?
|
|
.ok_or_else(|| anyhow!("hub did not accept first Connect"))?
|
|
else {
|
|
return Err(anyhow!("hub did not emit Connected"));
|
|
};
|
|
let mut buffer = vec![0_u8; 65_535];
|
|
let (first_len, _) = client_socket.recv_from(&mut buffer)?;
|
|
let lost_accept = decode_rudp_packet(&buffer[..first_len])?;
|
|
assert_eq!(lost_accept.packet_type, CultNetRudpPacketType::Accept);
|
|
|
|
client_socket.send_to(&connect_wire, server_addr)?;
|
|
assert!(hub.receive_event_once()?.is_none());
|
|
let (resent_len, _) = client_socket.recv_from(&mut buffer)?;
|
|
let resent_accept = decode_rudp_packet(&buffer[..resent_len])?;
|
|
assert_eq!(resent_accept, lost_accept);
|
|
assert_eq!(hub.session(session.remote_addr), Some(&session));
|
|
|
|
client_session.receive(&resent_accept, 1)?;
|
|
assert!(client_session.connected());
|
|
Ok(())
|
|
}
|
|
|
|
#[test]
|
|
fn exact_reliable_acks_clear_more_than_one_ack_window_of_fragments() -> Result<()> {
|
|
let mut sender = CultNetRudpSession::new(CultNetRudpSessionOptions {
|
|
connection_id: CONNECTION_ID,
|
|
initial_sequence: Some(1),
|
|
resend_delay_ms: 250,
|
|
max_pending_reliable_packets: None,
|
|
});
|
|
let mut receiver = CultNetRudpSession::new(CultNetRudpSessionOptions {
|
|
connection_id: CONNECTION_ID,
|
|
initial_sequence: Some(1),
|
|
resend_delay_ms: 250,
|
|
max_pending_reliable_packets: None,
|
|
});
|
|
let connect_packet = sender.create_connect(0, Vec::new())?;
|
|
let accept = receiver.accept_connect(&connect_packet, 0, Vec::new())?;
|
|
sender.receive(&accept, 0)?;
|
|
receiver.receive(&sender.create_ack_for(accept.sequence), 0)?;
|
|
|
|
let packets = sender.send_many(
|
|
"schema",
|
|
vec![0x5a; 40],
|
|
CultNetRudpSendOptions {
|
|
reliable: true,
|
|
ordered: true,
|
|
sequenced: false,
|
|
now_ms: 1,
|
|
reliable_expire_after_ms: None,
|
|
},
|
|
Some(1),
|
|
)?;
|
|
// Forty one-byte fragments cannot leave inside one reliable send window. The
|
|
// window's worth goes out now and the rest queue until acknowledgements free
|
|
// space, which is the crossing this test exists to prove.
|
|
assert_eq!(packets.len(), CULTNET_RUDP_RELIABLE_SEND_WINDOW_PACKETS);
|
|
assert_eq!(
|
|
sender.queued_reliable_packet_count(),
|
|
40 - CULTNET_RUDP_RELIABLE_SEND_WINDOW_PACKETS
|
|
);
|
|
|
|
let mut wire = VecDeque::from(packets);
|
|
let mut delivered = Vec::new();
|
|
while !wire.is_empty() {
|
|
let admitted = wire.len();
|
|
for _ in 0..admitted {
|
|
let packet = wire.pop_front().expect("admitted packet remains queued");
|
|
let received = receiver.receive(&packet, 2)?;
|
|
delivered.extend(received.delivered);
|
|
}
|
|
let acknowledged = sender.receive(&receiver.create_ack(), 3)?;
|
|
wire.extend(acknowledged.ready_to_send);
|
|
}
|
|
|
|
assert_eq!(delivered.len(), 1);
|
|
assert_eq!(delivered[0].payload, vec![0x5a; 40]);
|
|
assert!(sender.pending_reliable_sequences().is_empty());
|
|
assert_eq!(sender.outstanding_reliable_packet_count(), 0);
|
|
Ok(())
|
|
}
|
|
|
|
#[test]
|
|
fn session_and_hub_reject_configured_memory_bounds() -> Result<()> {
|
|
let mut receiver = CultNetRudpSession::new(CultNetRudpSessionOptions {
|
|
connection_id: CONNECTION_ID,
|
|
initial_sequence: Some(1),
|
|
resend_delay_ms: 250,
|
|
max_pending_reliable_packets: None,
|
|
});
|
|
receiver.set_max_payload_bytes(Some(4));
|
|
receiver.set_max_pending_fragment_sets(1)?;
|
|
let mut sender = CultNetRudpSession::new(CultNetRudpSessionOptions {
|
|
connection_id: CONNECTION_ID,
|
|
initial_sequence: Some(1),
|
|
resend_delay_ms: 250,
|
|
max_pending_reliable_packets: None,
|
|
});
|
|
let connect_packet = sender.create_connect(0, Vec::new())?;
|
|
let accept = receiver.accept_connect(&connect_packet, 0, Vec::new())?;
|
|
sender.receive(&accept, 0)?;
|
|
|
|
let oversized = sender.send_many(
|
|
"schema",
|
|
vec![1, 2, 3, 4, 5],
|
|
CultNetRudpSendOptions {
|
|
reliable: true,
|
|
ordered: true,
|
|
sequenced: false,
|
|
now_ms: 1,
|
|
reliable_expire_after_ms: None,
|
|
},
|
|
Some(2),
|
|
)?;
|
|
receiver.receive(&oversized[0], 1)?;
|
|
receiver.receive(&oversized[1], 1)?;
|
|
assert!(receiver.receive(&oversized[2], 1).is_err());
|
|
|
|
let fragment_options = CultNetRudpSendOptions {
|
|
reliable: true,
|
|
ordered: true,
|
|
sequenced: false,
|
|
now_ms: 2,
|
|
reliable_expire_after_ms: None,
|
|
};
|
|
let first_set = sender.send_many(
|
|
"schema",
|
|
vec![1, 2, 3, 4],
|
|
fragment_options.clone(),
|
|
Some(2),
|
|
)?;
|
|
let second_set = sender.send_many("schema", vec![5, 6, 7, 8], fragment_options, Some(2))?;
|
|
receiver.receive(&first_set[0], 2)?;
|
|
// The fragment-set bound evicts the stalest incomplete set rather than
|
|
// refusing the packet: a receiver that dies at its bound under loss is a
|
|
// fuse, not a limit (measured 2026-09-10, see tests/cultnet.rs).
|
|
assert_eq!(receiver.fragment_sets_evicted(), 0);
|
|
receiver.receive(&second_set[0], 2)?;
|
|
assert_eq!(receiver.fragment_sets_evicted(), 1, "the first set was evicted for the second");
|
|
|
|
let server_socket = socket()?;
|
|
let server_addr = server_socket.local_addr()?;
|
|
let mut options = CultNetRudpServerHubOptions::new("odin", server_socket, CONNECTION_ID);
|
|
options.max_peers = 1;
|
|
let mut hub = CultNetRudpServerHub::new(options)?;
|
|
let mut first = client(server_addr)?;
|
|
let mut second = client(server_addr)?;
|
|
connect(&mut hub, &mut first, b"first")?;
|
|
second.connect(b"second".to_vec())?;
|
|
// The peer limit refuses the second Connect without ending the hub loop:
|
|
// a full table is not a fault of the process that holds it.
|
|
std::thread::sleep(Duration::from_millis(20));
|
|
while hub.receive_event_once()?.is_some() {}
|
|
assert_eq!(hub.sessions().len(), 1);
|
|
assert_eq!(hub.stats().packets_dropped, 1);
|
|
Ok(())
|
|
}
|
|
|
|
#[test]
|
|
fn replay_history_is_bounded_and_idle_hub_sessions_expire() -> Result<()> {
|
|
let mut sender = CultNetRudpSession::new(CultNetRudpSessionOptions {
|
|
connection_id: CONNECTION_ID,
|
|
initial_sequence: Some(1),
|
|
resend_delay_ms: 250,
|
|
max_pending_reliable_packets: None,
|
|
});
|
|
let mut receiver = CultNetRudpSession::new(CultNetRudpSessionOptions {
|
|
connection_id: CONNECTION_ID,
|
|
initial_sequence: Some(1),
|
|
resend_delay_ms: 250,
|
|
max_pending_reliable_packets: None,
|
|
});
|
|
let connect_packet = sender.create_connect(0, Vec::new())?;
|
|
let accept = receiver.accept_connect(&connect_packet, 0, Vec::new())?;
|
|
sender.receive(&accept, 0)?;
|
|
let mut oldest = None;
|
|
for sequence in 0..4_100 {
|
|
let packet = sender.send(
|
|
"realtime",
|
|
vec![(sequence % 251) as u8],
|
|
CultNetRudpSendOptions {
|
|
reliable: false,
|
|
ordered: false,
|
|
sequenced: false,
|
|
now_ms: sequence,
|
|
reliable_expire_after_ms: None,
|
|
},
|
|
)?;
|
|
if oldest.is_none() {
|
|
oldest = Some(packet.clone());
|
|
}
|
|
assert_eq!(receiver.receive(&packet, sequence)?.delivered.len(), 1);
|
|
}
|
|
// Separating the sequence domains moved duplicate suppression onto the
|
|
// reliable domain, so a replayed lossy datagram is delivered again rather
|
|
// than silently dropped. Lossy traffic is remembered nowhere, which is what
|
|
// bounds its history.
|
|
assert_eq!(
|
|
receiver
|
|
.receive(&oldest.expect("oldest packet"), 4_101)?
|
|
.delivered
|
|
.len(),
|
|
1
|
|
);
|
|
|
|
// The bounded replay window still guards the reliable domain, and nothing
|
|
// else asserts it once the lossy path stopped being remembered.
|
|
let mut oldest_reliable = None;
|
|
for step in 0..4_100u64 {
|
|
let packet = sender.send(
|
|
"schema",
|
|
vec![(step % 251) as u8],
|
|
CultNetRudpSendOptions {
|
|
reliable: true,
|
|
ordered: false,
|
|
sequenced: false,
|
|
now_ms: step,
|
|
reliable_expire_after_ms: None,
|
|
},
|
|
)?;
|
|
if oldest_reliable.is_none() {
|
|
oldest_reliable = Some(packet.clone());
|
|
}
|
|
assert_eq!(receiver.receive(&packet, step)?.delivered.len(), 1);
|
|
sender.receive(&receiver.create_ack_for(packet.sequence), step)?;
|
|
}
|
|
assert!(
|
|
receiver
|
|
.receive(&oldest_reliable.expect("oldest reliable packet"), 4_101)?
|
|
.delivered
|
|
.is_empty()
|
|
);
|
|
|
|
let server_socket = socket()?;
|
|
let server_addr = server_socket.local_addr()?;
|
|
let mut hub = CultNetRudpServerHub::new(CultNetRudpServerHubOptions::new(
|
|
"odin",
|
|
server_socket,
|
|
CONNECTION_ID,
|
|
))?;
|
|
let mut provider = client(server_addr)?;
|
|
let context = connect(&mut hub, &mut provider, b"expiring-session")?;
|
|
std::thread::sleep(Duration::from_millis(2));
|
|
assert_eq!(hub.remove_timed_out_sessions(0), vec![context]);
|
|
assert!(hub.sessions().is_empty());
|
|
Ok(())
|
|
}
|
|
|
|
/// A packet that carries no event — an acknowledgement, a ping — must not
|
|
/// make the reader report "nothing waiting" while frames sit behind it.
|
|
/// A caller that polls once per send fell behind its peer's acknowledgements
|
|
/// this way and a reliable channel resent everything.
|
|
#[test]
|
|
fn a_reader_does_not_stop_at_a_packet_that_carries_no_event() -> Result<()> {
|
|
let mut hub = CultNetRudpServerHub::new(CultNetRudpServerHubOptions::new(
|
|
"hub",
|
|
socket()?,
|
|
CONNECTION_ID,
|
|
))?;
|
|
let mut peer = client(hub.local_addr()?)?;
|
|
let session = connect(&mut hub, &mut peer, b"evidence")?;
|
|
|
|
// Hub side: a ping (reply, no event) ahead of a reliable frame.
|
|
peer.ping(b"keepalive".to_vec())?;
|
|
peer.send("schema", b"after the ping".to_vec())?;
|
|
std::thread::sleep(Duration::from_millis(20));
|
|
match hub.receive_event_once()? {
|
|
Some(CultNetRudpServerEvent::Frame { frame, .. }) => {
|
|
assert_eq!(frame.payload, b"after the ping");
|
|
}
|
|
other => panic!("the first read must reach the frame behind the ping, got {other:?}"),
|
|
}
|
|
|
|
// Client side: the hub's acknowledgement of that frame is queued ahead of
|
|
// two unreliable frames; the client must reach both without a "nothing"
|
|
// in between, and must not acknowledge unreliable frames back.
|
|
let mut unreliable_hub_options =
|
|
CultNetRudpServerHubOptions::new("hub-unreliable", socket()?, CONNECTION_ID);
|
|
unreliable_hub_options.media_delivery = Some(CultNetTransportDelivery::Unreliable);
|
|
let mut media_hub = CultNetRudpServerHub::new(unreliable_hub_options)?;
|
|
let mut media_client = client(media_hub.local_addr()?)?;
|
|
let media_session = connect(&mut media_hub, &mut media_client, b"media")?;
|
|
media_client.send("schema", b"reliable, will be acked".to_vec())?;
|
|
std::thread::sleep(Duration::from_millis(20));
|
|
while media_hub.receive_event_once()?.is_some() {}
|
|
media_hub.send(&media_session, "media", b"one".to_vec())?;
|
|
media_hub.send(&media_session, "media", b"two".to_vec())?;
|
|
std::thread::sleep(Duration::from_millis(20));
|
|
let before = media_hub.stats().bytes_received;
|
|
let first = media_client.receive_once()?.expect("the first read reaches the frame behind the ack");
|
|
let second = media_client.receive_once()?.expect("the second frame follows");
|
|
assert_eq!((first.payload, second.payload), (b"one".to_vec(), b"two".to_vec()));
|
|
assert!(media_client.receive_once()?.is_none());
|
|
std::thread::sleep(Duration::from_millis(20));
|
|
while media_hub.receive_event_once()?.is_some() {}
|
|
assert_eq!(
|
|
media_hub.stats().bytes_received,
|
|
before,
|
|
"unreliable frames are not acknowledged; the hub read nothing back"
|
|
);
|
|
let _ = session;
|
|
Ok(())
|
|
}
|