CI / build-test (push) Successful in 1m16s
Browsers can't speak UDP, so WebSocket is the engine's one transport. The server side is a dependency-free RFC 6455 implementation over TcpListener (handshake, frame codec with masking and fragmentation, ping/pong — unit-tested against the RFC example vectors); the client wraps ClientWebSocket, which works on desktop and maps to the browser WebSocket in Blazor WASM. Both sit behind the poll-based INetConnection so simulation systems drain messages from their own thread; client sends are chained fire-and-forget (no blocking — wasm-safe). Replication is server-authoritative: games register unmanaged component types in a ReplicationSchema (same order both sides, up to 32 types), ReplicationServer snapshots entities carrying NetId once per send and ships each connection only the components that changed since its last snapshot — a reliable ordered transport needs no acks for deltas. New connections receive the full state through the same path; despawns are tracked by set difference. ReplicationClient applies snapshots to a local EntityStore and raises EntitySpawned so the game can decorate replicated entities with presentation components. Covered by 16 tests including a real loopback exchange between WebSocketClient and WebSocketServer. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
89 lines
2.6 KiB
C#
89 lines
2.6 KiB
C#
using MrGameEng.Net;
|
|
using Xunit;
|
|
|
|
namespace MrGameEng.Net.Tests;
|
|
|
|
public class WebSocketProtocolTests
|
|
{
|
|
[Fact]
|
|
public void AcceptKey_MatchesRfc6455Example()
|
|
{
|
|
// Пример рукопожатия прямо из RFC 6455, раздел 1.3.
|
|
Assert.Equal(
|
|
"s3pPLMBiTxaQ9kYGzzhZRbK+xOo=",
|
|
WebSocketProtocol.AcceptKey("dGhlIHNhbXBsZSBub25jZQ==")
|
|
);
|
|
}
|
|
|
|
[Theory]
|
|
[InlineData(0)]
|
|
[InlineData(125)]
|
|
[InlineData(126)]
|
|
[InlineData(70_000)]
|
|
public void Frame_Roundtrips_Unmasked(int payloadLength)
|
|
{
|
|
var payload = MakePayload(payloadLength);
|
|
var frame = WebSocketProtocol.EncodeFrame(payload, WebSocketOpcode.Binary);
|
|
|
|
using var stream = new MemoryStream(frame);
|
|
Assert.True(
|
|
WebSocketProtocol.TryReadFrame(stream, out var opcode, out var fin, out var decoded)
|
|
);
|
|
Assert.Equal(WebSocketOpcode.Binary, opcode);
|
|
Assert.True(fin);
|
|
Assert.Equal(payload, decoded);
|
|
}
|
|
|
|
[Fact]
|
|
public void Frame_Roundtrips_Masked()
|
|
{
|
|
var payload = MakePayload(1000);
|
|
var frame = WebSocketProtocol.EncodeFrame(
|
|
payload,
|
|
WebSocketOpcode.Binary,
|
|
maskKey: [0x12, 0x34, 0x56, 0x78]
|
|
);
|
|
|
|
using var stream = new MemoryStream(frame);
|
|
Assert.True(WebSocketProtocol.TryReadFrame(stream, out _, out _, out var decoded));
|
|
Assert.Equal(payload, decoded);
|
|
}
|
|
|
|
[Fact]
|
|
public void TryReadHandshakeKey_ExtractsHeader()
|
|
{
|
|
var request =
|
|
"GET /chat HTTP/1.1\r\n"
|
|
+ "Host: localhost\r\n"
|
|
+ "Upgrade: websocket\r\n"
|
|
+ "Connection: Upgrade\r\n"
|
|
+ "Sec-WebSocket-Key: dGhlIHNhbXBsZSBub25jZQ==\r\n"
|
|
+ "Sec-WebSocket-Version: 13\r\n"
|
|
+ "\r\n";
|
|
using var stream = new MemoryStream(System.Text.Encoding.ASCII.GetBytes(request));
|
|
|
|
Assert.True(WebSocketProtocol.TryReadHandshakeKey(stream, out var key));
|
|
Assert.Equal("dGhlIHNhbXBsZSBub25jZQ==", key);
|
|
}
|
|
|
|
[Fact]
|
|
public void TryReadFrame_TruncatedStream_ReturnsFalse()
|
|
{
|
|
var frame = WebSocketProtocol.EncodeFrame(MakePayload(100), WebSocketOpcode.Binary);
|
|
using var stream = new MemoryStream(frame, 0, frame.Length - 10);
|
|
|
|
Assert.False(WebSocketProtocol.TryReadFrame(stream, out _, out _, out _));
|
|
}
|
|
|
|
private static byte[] MakePayload(int length)
|
|
{
|
|
var payload = new byte[length];
|
|
for (var i = 0; i < length; i++)
|
|
{
|
|
payload[i] = (byte)(i * 31);
|
|
}
|
|
|
|
return payload;
|
|
}
|
|
}
|