mirror of
https://github.com/esiur/esiur-dotnet.git
synced 2026-07-30 17:30:40 +00:00
605 lines
21 KiB
C#
605 lines
21 KiB
C#
using Esiur.Net;
|
|
using Esiur.Net.Sockets;
|
|
using System.Collections.Concurrent;
|
|
using System.Net;
|
|
using System.Net.WebSockets;
|
|
|
|
namespace Esiur.Tests.Unit;
|
|
|
|
public class FrameworkWebSocketTests
|
|
{
|
|
[Fact]
|
|
public async Task AcceptedSocket_ReceivesBinaryDataAndCompletesOnceOnPeerClose()
|
|
{
|
|
var platformSocket = new TestWebSocket();
|
|
platformSocket.QueueBinary(new byte[] { 1, 2, 3 });
|
|
platformSocket.QueueClose();
|
|
|
|
var local = new IPEndPoint(IPAddress.Loopback, 443);
|
|
var remote = new IPEndPoint(IPAddress.Parse("192.0.2.10"), 55123);
|
|
var socket = new FrameworkWebSocket(platformSocket, local, remote);
|
|
var receiver = new RecordingReceiver();
|
|
socket.Receiver = receiver;
|
|
|
|
Assert.True(socket.Begin());
|
|
await socket.Completion.WaitAsync(TimeSpan.FromSeconds(2));
|
|
await socket.CloseNotification.WaitAsync(TimeSpan.FromSeconds(2));
|
|
|
|
Assert.Equal(local, socket.LocalEndPoint);
|
|
Assert.Equal(remote, socket.RemoteEndPoint);
|
|
Assert.Equal(new byte[] { 1, 2, 3 }, Assert.Single(receiver.Messages));
|
|
Assert.Equal(1, receiver.CloseCount);
|
|
Assert.Equal(1, platformSocket.CloseOutputCount);
|
|
|
|
socket.Close();
|
|
socket.Destroy();
|
|
Assert.Equal(1, receiver.CloseCount);
|
|
}
|
|
|
|
[Theory]
|
|
[InlineData(null)]
|
|
[InlineData("")]
|
|
[InlineData("ep")]
|
|
[InlineData("not-ep")]
|
|
public void AcceptedSocket_RequiresExactEpSubprotocol(string? subProtocol)
|
|
{
|
|
var platformSocket = new TestWebSocket(subProtocol);
|
|
|
|
var exception = Assert.Throws<ArgumentException>(() =>
|
|
new FrameworkWebSocket(
|
|
platformSocket,
|
|
new IPEndPoint(IPAddress.Loopback, 443),
|
|
new IPEndPoint(IPAddress.Parse("192.0.2.10"), 55123)));
|
|
|
|
Assert.Contains("'EP' subprotocol", exception.Message, StringComparison.Ordinal);
|
|
}
|
|
|
|
[Fact]
|
|
public async Task OwnerCancellation_EndsReceiveLoopWithoutFaultOrDuplicateClose()
|
|
{
|
|
var platformSocket = new TestWebSocket();
|
|
using var cancellation = new CancellationTokenSource();
|
|
var socket = new FrameworkWebSocket(
|
|
platformSocket,
|
|
new IPEndPoint(IPAddress.Loopback, 443),
|
|
new IPEndPoint(IPAddress.Loopback, 50000),
|
|
cancellation.Token);
|
|
var receiver = new RecordingReceiver();
|
|
socket.Receiver = receiver;
|
|
|
|
Assert.True(socket.Begin());
|
|
cancellation.Cancel();
|
|
await socket.Completion.WaitAsync(TimeSpan.FromSeconds(2));
|
|
await socket.CloseNotification.WaitAsync(TimeSpan.FromSeconds(2));
|
|
|
|
socket.Close();
|
|
socket.Destroy();
|
|
socket.Destroy();
|
|
|
|
Assert.Equal(1, receiver.CloseCount);
|
|
Assert.Equal(SocketState.Closed, socket.State);
|
|
}
|
|
|
|
[Fact]
|
|
public async Task LocalClose_WaitsForPeerAcknowledgementBeforeCompleting()
|
|
{
|
|
var platformSocket = new TestWebSocket();
|
|
var socket = new FrameworkWebSocket(
|
|
platformSocket,
|
|
new IPEndPoint(IPAddress.Loopback, 443),
|
|
new IPEndPoint(IPAddress.Loopback, 50000));
|
|
var receiver = new RecordingReceiver();
|
|
socket.Receiver = receiver;
|
|
|
|
Assert.True(socket.Begin());
|
|
var completion = socket.CloseAsync();
|
|
|
|
await Task.Delay(25);
|
|
Assert.False(completion.IsCompleted);
|
|
Assert.Equal(1, platformSocket.CloseOutputCount);
|
|
|
|
platformSocket.QueueClose();
|
|
await completion.WaitAsync(TimeSpan.FromSeconds(2));
|
|
await socket.CloseNotification.WaitAsync(TimeSpan.FromSeconds(2));
|
|
|
|
Assert.Equal(1, receiver.CloseCount);
|
|
Assert.Equal(SocketState.Closed, socket.State);
|
|
}
|
|
|
|
[Fact]
|
|
public async Task SimultaneousPeerClose_AcknowledgesAfterAnInFlightSend()
|
|
{
|
|
var platformSocket = new TestWebSocket();
|
|
platformSocket.BlockSends();
|
|
var socket = new FrameworkWebSocket(
|
|
platformSocket,
|
|
new IPEndPoint(IPAddress.Loopback, 443),
|
|
new IPEndPoint(IPAddress.Loopback, 50000));
|
|
socket.Receiver = new RecordingReceiver();
|
|
|
|
Assert.True(socket.Begin());
|
|
var send = socket.SendAsync(new byte[] { 1 }, 0, 1);
|
|
await platformSocket.SendStarted.WaitAsync(TimeSpan.FromSeconds(2));
|
|
|
|
var completion = socket.CloseAsync();
|
|
platformSocket.QueueClose();
|
|
platformSocket.ReleaseSends();
|
|
|
|
await completion.WaitAsync(TimeSpan.FromSeconds(2));
|
|
Assert.True(await send);
|
|
Assert.Equal(1, platformSocket.CloseOutputCount);
|
|
}
|
|
|
|
[Fact]
|
|
public async Task SendQueue_OverflowAbortsOnceAndSettlesEveryReply()
|
|
{
|
|
var platformSocket = new TestWebSocket();
|
|
platformSocket.BlockSends();
|
|
var socket = new FrameworkWebSocket(
|
|
platformSocket,
|
|
new IPEndPoint(IPAddress.Loopback, 443),
|
|
new IPEndPoint(IPAddress.Loopback, 50000))
|
|
{
|
|
MaximumPendingSendBytes = 4,
|
|
};
|
|
var receiver = new RecordingReceiver();
|
|
socket.Receiver = receiver;
|
|
|
|
var first = socket.SendAsync(new byte[] { 1, 2 }, 0, 2);
|
|
await platformSocket.SendStarted.WaitAsync(TimeSpan.FromSeconds(2));
|
|
|
|
var second = socket.SendAsync(new byte[] { 3, 4 }, 0, 2);
|
|
var overflow = socket.SendAsync(new byte[] { 5 }, 0, 1);
|
|
|
|
Assert.True(overflow.Failed);
|
|
Assert.Contains("send queue exceeded", overflow.Exception?.Message);
|
|
Assert.True(second.Failed);
|
|
Assert.Contains("send queue exceeded", second.Exception?.Message);
|
|
Assert.False(await first);
|
|
await Assert.ThrowsAsync<InvalidOperationException>(() => socket.Completion);
|
|
await socket.CloseNotification.WaitAsync(TimeSpan.FromSeconds(2));
|
|
|
|
Assert.Equal(SocketState.Closed, socket.State);
|
|
Assert.Equal(0, socket.PendingSendBytes);
|
|
Assert.Equal(1, platformSocket.AbortCount);
|
|
Assert.Equal(1, receiver.CloseCount);
|
|
Assert.Empty(platformSocket.SentMessages);
|
|
|
|
var later = socket.SendAsync(new byte[] { 6 }, 0, 1);
|
|
Assert.True(later.Ready);
|
|
Assert.False(await later);
|
|
|
|
socket.Destroy();
|
|
Assert.Equal(1, platformSocket.AbortCount);
|
|
Assert.Equal(1, receiver.CloseCount);
|
|
}
|
|
|
|
[Fact]
|
|
public async Task SynchronousSend_OverflowAbortsBeforeThrowing()
|
|
{
|
|
var platformSocket = new TestWebSocket();
|
|
var socket = new FrameworkWebSocket(
|
|
platformSocket,
|
|
new IPEndPoint(IPAddress.Loopback, 443),
|
|
new IPEndPoint(IPAddress.Loopback, 50000))
|
|
{
|
|
MaximumPendingSendBytes = 1,
|
|
};
|
|
var receiver = new RecordingReceiver();
|
|
socket.Receiver = receiver;
|
|
socket.Hold();
|
|
|
|
socket.Send(new byte[] { 1 }, 0, 1);
|
|
var error = Assert.Throws<InvalidOperationException>(() =>
|
|
socket.Send(new byte[] { 2 }, 0, 1));
|
|
|
|
Assert.Contains("send queue exceeded", error.Message);
|
|
await socket.CloseNotification.WaitAsync(TimeSpan.FromSeconds(2));
|
|
Assert.Equal(SocketState.Closed, socket.State);
|
|
Assert.Equal(0, socket.PendingSendBytes);
|
|
Assert.Equal(1, platformSocket.AbortCount);
|
|
Assert.Equal(1, receiver.CloseCount);
|
|
await Assert.ThrowsAsync<InvalidOperationException>(() => socket.Completion);
|
|
}
|
|
|
|
[Fact]
|
|
public async Task SharedSendBudget_BoundsAggregateQueuedBytesAcrossSockets()
|
|
{
|
|
var budget = new TestPendingSendBudget(3);
|
|
var firstPlatform = new TestWebSocket();
|
|
firstPlatform.BlockSends();
|
|
var firstSocket = CreateAcceptedSocket(firstPlatform, budget);
|
|
var secondSocket = CreateAcceptedSocket(new TestWebSocket(), budget);
|
|
|
|
var firstSend = firstSocket.SendAsync(new byte[] { 1, 2 }, 0, 2);
|
|
await firstPlatform.SendStarted.WaitAsync(TimeSpan.FromSeconds(2));
|
|
|
|
var overflow = secondSocket.SendAsync(new byte[] { 3, 4 }, 0, 2);
|
|
|
|
Assert.True(overflow.Failed);
|
|
Assert.Contains("host-wide", overflow.Exception?.Message);
|
|
Assert.Equal(SocketState.Closed, secondSocket.State);
|
|
Assert.Equal(2, budget.ReservedBytes);
|
|
|
|
firstSocket.Destroy();
|
|
Assert.False(await firstSend);
|
|
await firstSocket.Completion.WaitAsync(TimeSpan.FromSeconds(2));
|
|
|
|
Assert.Equal(0, budget.ReservedBytes);
|
|
await Assert.ThrowsAsync<InvalidOperationException>(() => secondSocket.Completion);
|
|
}
|
|
|
|
[Fact]
|
|
public async Task Completion_DoesNotWaitForAnInProgressSendReplyCallback()
|
|
{
|
|
var platformSocket = new TestWebSocket();
|
|
platformSocket.BlockSends();
|
|
var socket = new FrameworkWebSocket(
|
|
platformSocket,
|
|
new IPEndPoint(IPAddress.Loopback, 443),
|
|
new IPEndPoint(IPAddress.Loopback, 50000));
|
|
using var callbackStarted = new ManualResetEventSlim();
|
|
using var releaseCallback = new ManualResetEventSlim();
|
|
|
|
try
|
|
{
|
|
var send = socket.SendAsync(new byte[] { 1 }, 0, 1);
|
|
_ = send.Then(_ =>
|
|
{
|
|
callbackStarted.Set();
|
|
releaseCallback.Wait();
|
|
});
|
|
await platformSocket.SendStarted.WaitAsync(TimeSpan.FromSeconds(2));
|
|
platformSocket.ReleaseSends();
|
|
Assert.True(callbackStarted.Wait(TimeSpan.FromSeconds(2)));
|
|
|
|
socket.Destroy();
|
|
await socket.Completion.WaitAsync(TimeSpan.FromSeconds(2));
|
|
|
|
Assert.False(releaseCallback.IsSet);
|
|
Assert.True(send.Ready);
|
|
Assert.True(await send);
|
|
}
|
|
finally
|
|
{
|
|
releaseCallback.Set();
|
|
socket.Destroy();
|
|
}
|
|
}
|
|
|
|
[Fact]
|
|
public async Task Destroy_DoesNotWaitForABlockedQueuedSendReplyCallback()
|
|
{
|
|
var platformSocket = new TestWebSocket();
|
|
var socket = new FrameworkWebSocket(
|
|
platformSocket,
|
|
new IPEndPoint(IPAddress.Loopback, 443),
|
|
new IPEndPoint(IPAddress.Loopback, 50000));
|
|
using var callbackStarted = new ManualResetEventSlim();
|
|
using var releaseCallback = new ManualResetEventSlim();
|
|
|
|
try
|
|
{
|
|
socket.Hold();
|
|
var send = socket.SendAsync(new byte[] { 1 }, 0, 1);
|
|
_ = send.Then(_ =>
|
|
{
|
|
callbackStarted.Set();
|
|
releaseCallback.Wait();
|
|
});
|
|
|
|
socket.Destroy();
|
|
await socket.Completion.WaitAsync(TimeSpan.FromSeconds(2));
|
|
|
|
Assert.True(send.Ready);
|
|
Assert.False(await send);
|
|
Assert.True(callbackStarted.Wait(TimeSpan.FromSeconds(2)));
|
|
Assert.False(releaseCallback.IsSet);
|
|
}
|
|
finally
|
|
{
|
|
releaseCallback.Set();
|
|
socket.Destroy();
|
|
}
|
|
}
|
|
|
|
[Fact]
|
|
public async Task TextMessage_FaultsCompletionAndNotifiesCloseOnce()
|
|
{
|
|
var platformSocket = new TestWebSocket();
|
|
platformSocket.QueueText("not EP");
|
|
var socket = new FrameworkWebSocket(
|
|
platformSocket,
|
|
new IPEndPoint(IPAddress.Loopback, 443),
|
|
new IPEndPoint(IPAddress.Loopback, 50000));
|
|
var receiver = new RecordingReceiver();
|
|
socket.Receiver = receiver;
|
|
|
|
Assert.True(socket.Begin());
|
|
await Assert.ThrowsAsync<InvalidDataException>(async () =>
|
|
await socket.Completion.WaitAsync(TimeSpan.FromSeconds(2)));
|
|
await socket.CloseNotification.WaitAsync(TimeSpan.FromSeconds(2));
|
|
|
|
socket.Close();
|
|
socket.Destroy();
|
|
Assert.Equal(1, receiver.CloseCount);
|
|
}
|
|
|
|
[Fact]
|
|
public async Task Completion_DoesNotWaitForABlockedReceiverCloseCallback()
|
|
{
|
|
var platformSocket = new TestWebSocket();
|
|
var socket = new FrameworkWebSocket(
|
|
platformSocket,
|
|
new IPEndPoint(IPAddress.Loopback, 443),
|
|
new IPEndPoint(IPAddress.Loopback, 50000));
|
|
using var callbackStarted = new ManualResetEventSlim();
|
|
using var releaseCallback = new ManualResetEventSlim();
|
|
socket.Receiver = new BlockingCloseReceiver(callbackStarted, releaseCallback);
|
|
|
|
try
|
|
{
|
|
socket.Destroy();
|
|
await socket.Completion.WaitAsync(TimeSpan.FromSeconds(2));
|
|
|
|
Assert.True(callbackStarted.Wait(TimeSpan.FromSeconds(2)));
|
|
Assert.False(socket.CloseNotification.IsCompleted);
|
|
}
|
|
finally
|
|
{
|
|
releaseCallback.Set();
|
|
}
|
|
|
|
await socket.CloseNotification.WaitAsync(TimeSpan.FromSeconds(2));
|
|
}
|
|
|
|
[Fact]
|
|
public async Task ThrowingSendCallback_DoesNotCorruptQueueOrCloseTransport()
|
|
{
|
|
var platformSocket = new TestWebSocket();
|
|
var socket = new FrameworkWebSocket(
|
|
platformSocket,
|
|
new IPEndPoint(IPAddress.Loopback, 443),
|
|
new IPEndPoint(IPAddress.Loopback, 50000));
|
|
|
|
socket.Hold();
|
|
var reply = socket.SendAsync(new byte[] { 42 }, 0, 1);
|
|
_ = reply.Then(_ => throw new InvalidOperationException("consumer callback failed"));
|
|
socket.Unhold();
|
|
|
|
Assert.True(await reply);
|
|
Assert.Equal(SocketState.Established, socket.State);
|
|
Assert.Equal(0, socket.PendingSendBytes);
|
|
Assert.Equal(new byte[] { 42 }, Assert.Single(platformSocket.SentMessages));
|
|
|
|
socket.Destroy();
|
|
await socket.Completion.WaitAsync(TimeSpan.FromSeconds(2));
|
|
}
|
|
|
|
[Fact]
|
|
public void OutboundUri_RequiresWebSocketScheme()
|
|
{
|
|
Assert.Throws<ArgumentException>(() =>
|
|
new FrameworkWebSocket(new Uri("https://example.test/esiur")));
|
|
|
|
_ = new FrameworkWebSocket(new Uri("wss://example.test/esiur?tenant=one"));
|
|
Assert.Equal("EP", FrameworkWebSocket.SubProtocol);
|
|
}
|
|
|
|
private sealed class RecordingReceiver : INetworkReceiver<ISocket>
|
|
{
|
|
public List<byte[]> Messages { get; } = new List<byte[]>();
|
|
public int CloseCount;
|
|
|
|
public void NetworkClose(ISocket sender) => Interlocked.Increment(ref CloseCount);
|
|
|
|
public void NetworkConnect(ISocket sender)
|
|
{
|
|
}
|
|
|
|
public void NetworkReceive(ISocket sender, NetworkBuffer buffer)
|
|
{
|
|
var message = buffer.Read();
|
|
if (message != null)
|
|
Messages.Add(message);
|
|
}
|
|
}
|
|
|
|
private sealed class BlockingCloseReceiver : INetworkReceiver<ISocket>
|
|
{
|
|
private readonly ManualResetEventSlim callbackStarted;
|
|
private readonly ManualResetEventSlim releaseCallback;
|
|
|
|
public BlockingCloseReceiver(
|
|
ManualResetEventSlim callbackStarted,
|
|
ManualResetEventSlim releaseCallback)
|
|
{
|
|
this.callbackStarted = callbackStarted;
|
|
this.releaseCallback = releaseCallback;
|
|
}
|
|
|
|
public void NetworkClose(ISocket sender)
|
|
{
|
|
callbackStarted.Set();
|
|
releaseCallback.Wait();
|
|
}
|
|
|
|
public void NetworkConnect(ISocket sender)
|
|
{
|
|
}
|
|
|
|
public void NetworkReceive(ISocket sender, NetworkBuffer buffer)
|
|
{
|
|
}
|
|
}
|
|
|
|
private static FrameworkWebSocket CreateAcceptedSocket(
|
|
TestWebSocket platformSocket,
|
|
IPendingSendBudget budget)
|
|
=> new(
|
|
platformSocket,
|
|
new IPEndPoint(IPAddress.Loopback, 443),
|
|
new IPEndPoint(IPAddress.Loopback, 50000))
|
|
{
|
|
PendingSendBudget = budget,
|
|
};
|
|
|
|
private sealed class TestPendingSendBudget : IPendingSendBudget
|
|
{
|
|
private readonly long limit;
|
|
private long reservedBytes;
|
|
|
|
public TestPendingSendBudget(long limit) => this.limit = limit;
|
|
|
|
public long ReservedBytes => Interlocked.Read(ref reservedBytes);
|
|
|
|
public bool TryReserve(int byteCount)
|
|
{
|
|
while (true)
|
|
{
|
|
var current = ReservedBytes;
|
|
if (byteCount > limit - current)
|
|
return false;
|
|
if (Interlocked.CompareExchange(
|
|
ref reservedBytes,
|
|
current + byteCount,
|
|
current) == current)
|
|
return true;
|
|
}
|
|
}
|
|
|
|
public void Release(int byteCount) =>
|
|
Interlocked.Add(ref reservedBytes, -byteCount);
|
|
}
|
|
|
|
private sealed class TestWebSocket : WebSocket
|
|
{
|
|
private readonly ConcurrentQueue<ReceiveFrame> receiveFrames = new();
|
|
private readonly SemaphoreSlim receiveSignal = new(0);
|
|
private WebSocketState state = WebSocketState.Open;
|
|
private WebSocketCloseStatus? closeStatus;
|
|
private string? closeStatusDescription;
|
|
private readonly string? subProtocol;
|
|
private TaskCompletionSource? sendStarted;
|
|
private TaskCompletionSource? sendRelease;
|
|
|
|
public TestWebSocket(string? subProtocol = FrameworkWebSocket.SubProtocol)
|
|
=> this.subProtocol = subProtocol;
|
|
|
|
public List<byte[]> SentMessages { get; } = new List<byte[]>();
|
|
public int AbortCount;
|
|
public int CloseOutputCount;
|
|
public Task SendStarted => sendStarted?.Task ?? Task.CompletedTask;
|
|
|
|
public void BlockSends()
|
|
{
|
|
sendStarted = new TaskCompletionSource(
|
|
TaskCreationOptions.RunContinuationsAsynchronously);
|
|
sendRelease = new TaskCompletionSource(
|
|
TaskCreationOptions.RunContinuationsAsynchronously);
|
|
}
|
|
|
|
public void ReleaseSends() => sendRelease?.TrySetResult();
|
|
|
|
public override WebSocketCloseStatus? CloseStatus => closeStatus;
|
|
public override string? CloseStatusDescription => closeStatusDescription;
|
|
public override WebSocketState State => state;
|
|
public override string? SubProtocol => subProtocol;
|
|
|
|
public void QueueBinary(byte[] data) =>
|
|
Queue(new ReceiveFrame(data, WebSocketMessageType.Binary, true));
|
|
|
|
public void QueueText(string data) =>
|
|
Queue(new ReceiveFrame(
|
|
System.Text.Encoding.UTF8.GetBytes(data),
|
|
WebSocketMessageType.Text,
|
|
true));
|
|
|
|
public void QueueClose() =>
|
|
Queue(new ReceiveFrame(Array.Empty<byte>(), WebSocketMessageType.Close, true));
|
|
|
|
public override void Abort()
|
|
{
|
|
Interlocked.Increment(ref AbortCount);
|
|
state = WebSocketState.Aborted;
|
|
}
|
|
|
|
public override Task CloseAsync(
|
|
WebSocketCloseStatus closeStatus,
|
|
string? statusDescription,
|
|
CancellationToken cancellationToken)
|
|
{
|
|
this.closeStatus = closeStatus;
|
|
closeStatusDescription = statusDescription;
|
|
state = WebSocketState.Closed;
|
|
return Task.CompletedTask;
|
|
}
|
|
|
|
public override Task CloseOutputAsync(
|
|
WebSocketCloseStatus closeStatus,
|
|
string? statusDescription,
|
|
CancellationToken cancellationToken)
|
|
{
|
|
Interlocked.Increment(ref CloseOutputCount);
|
|
this.closeStatus = closeStatus;
|
|
closeStatusDescription = statusDescription;
|
|
state = WebSocketState.Closed;
|
|
return Task.CompletedTask;
|
|
}
|
|
|
|
public override void Dispose() => state = WebSocketState.Closed;
|
|
|
|
public override async Task<WebSocketReceiveResult> ReceiveAsync(
|
|
ArraySegment<byte> buffer,
|
|
CancellationToken cancellationToken)
|
|
{
|
|
await receiveSignal.WaitAsync(cancellationToken);
|
|
if (!receiveFrames.TryDequeue(out var frame))
|
|
throw new InvalidOperationException("The receive signal had no frame.");
|
|
|
|
if (frame.MessageType == WebSocketMessageType.Close)
|
|
{
|
|
state = WebSocketState.CloseReceived;
|
|
closeStatus = WebSocketCloseStatus.NormalClosure;
|
|
}
|
|
|
|
Buffer.BlockCopy(frame.Data, 0, buffer.Array!, buffer.Offset, frame.Data.Length);
|
|
return new WebSocketReceiveResult(
|
|
frame.Data.Length,
|
|
frame.MessageType,
|
|
frame.EndOfMessage,
|
|
frame.MessageType == WebSocketMessageType.Close
|
|
? WebSocketCloseStatus.NormalClosure
|
|
: null,
|
|
null);
|
|
}
|
|
|
|
public override async Task SendAsync(
|
|
ArraySegment<byte> buffer,
|
|
WebSocketMessageType messageType,
|
|
bool endOfMessage,
|
|
CancellationToken cancellationToken)
|
|
{
|
|
cancellationToken.ThrowIfCancellationRequested();
|
|
sendStarted?.TrySetResult();
|
|
if (sendRelease is not null)
|
|
await sendRelease.Task.WaitAsync(cancellationToken);
|
|
|
|
var copy = new byte[buffer.Count];
|
|
Buffer.BlockCopy(buffer.Array!, buffer.Offset, copy, 0, buffer.Count);
|
|
SentMessages.Add(copy);
|
|
}
|
|
|
|
private void Queue(ReceiveFrame frame)
|
|
{
|
|
receiveFrames.Enqueue(frame);
|
|
receiveSignal.Release();
|
|
}
|
|
|
|
private readonly record struct ReceiveFrame(
|
|
byte[] Data,
|
|
WebSocketMessageType MessageType,
|
|
bool EndOfMessage);
|
|
}
|
|
}
|