1- using System . Diagnostics . CodeAnalysis ;
1+ using System . Diagnostics ;
2+ using System . Diagnostics . CodeAnalysis ;
23
34namespace Devlooped . Net ;
45
@@ -17,29 +18,41 @@ static partial class WebSocketChannel
1718 /// purposes.
1819 /// </summary>
1920 /// <param name="webSocket">The <see cref="WebSocket"/> to create the channel over.</param>
21+ /// <param name="displayName">Optional friendly name to identify this channel while debugging or troubleshooting.</param>
2022 /// <returns>A channel to read/write the given <paramref name="webSocket"/>.</returns>
21- public static Channel < ReadOnlyMemory < byte > > Create ( WebSocket webSocket )
22- => new DefaultWebSocketChannel ( webSocket ) ;
23+ public static Channel < ReadOnlyMemory < byte > > Create ( WebSocket webSocket , string ? displayName = default )
24+ => new DefaultWebSocketChannel ( webSocket , displayName ) ;
2325
2426 class DefaultWebSocketChannel : Channel < ReadOnlyMemory < byte > >
2527 {
26- static readonly Exception defaultDoneWritting = new Exception ( nameof ( defaultDoneWritting ) ) ;
28+ #if DEBUG
29+ static readonly TimeSpan closeTimeout = TimeSpan . FromMilliseconds (
30+ Debugger . IsAttached ? int . MaxValue : 250 ) ;
31+ #else
32+ static readonly TimeSpan closeTimeout = TimeSpan . FromMilliseconds ( 250 ) ;
33+ #endif
34+ static readonly Exception defaultDoneWritting = new ( nameof ( defaultDoneWritting ) ) ;
2735 static readonly Exception socketClosed = new WebSocketException ( WebSocketError . ConnectionClosedPrematurely , "WebSocket was closed by the remote party." ) ;
2836
29- static readonly TimeSpan closeTimeout = TimeSpan . FromMilliseconds ( 250 ) ;
37+ readonly CancellationTokenSource completionCancellation = new ( ) ;
3038 readonly TaskCompletionSource < bool > completion = new ( ) ;
3139 readonly object syncObj = new ( ) ;
3240 Exception ? done ;
3341
3442 WebSocket webSocket ;
3543
36- public DefaultWebSocketChannel ( WebSocket webSocket )
44+ public DefaultWebSocketChannel ( WebSocket webSocket , string ? displayName = default )
3745 {
3846 this . webSocket = webSocket ;
47+ DisplayName = displayName ;
3948 Reader = new WebSocketChannelReader ( this ) ;
4049 Writer = new WebSocketChannelWriter ( this ) ;
4150 }
4251
52+ public string ? DisplayName { get ; }
53+
54+ public override string ToString ( ) => DisplayName ?? base . ToString ( ) ;
55+
4356 void Complete ( )
4457 {
4558 if ( done is OperationCanceledException oce )
@@ -66,6 +79,8 @@ void Complete()
6679 ;
6780 }
6881 }
82+
83+ completionCancellation . Cancel ( ) ;
6984 }
7085
7186 async ValueTask Close ( string ? description = default )
@@ -76,14 +91,25 @@ async ValueTask Close(string? description = default)
7691 webSocket . CloseOutputAsync ( description != null ? WebSocketCloseStatus . InternalServerError : WebSocketCloseStatus . NormalClosure , description , default ) ;
7792
7893 // Don't wait indefinitely for the close to be acknowledged
79- await Task . WhenAny ( closeTask , Task . Delay ( closeTimeout ) ) ;
94+ await Task . WhenAny ( closeTask , Task . Delay ( closeTimeout ) ) . ConfigureAwait ( false ) ;
8095 }
8196
8297 class WebSocketChannelReader : ChannelReader < ReadOnlyMemory < byte > >
8398 {
99+ #if DEBUG
100+ static readonly TimeSpan tryReadTimeout = TimeSpan . FromMilliseconds (
101+ Debugger . IsAttached ? int . MaxValue : 250 ) ;
102+ #else
84103 static readonly TimeSpan tryReadTimeout = TimeSpan . FromMilliseconds ( 250 ) ;
104+ #endif
105+
85106 readonly DefaultWebSocketChannel channel ;
86- readonly SemaphoreSlim semaphore = new SemaphoreSlim ( 1 , 1 ) ;
107+ readonly object syncObj = new ( ) ;
108+
109+ readonly SemaphoreSlim semaphore = new ( 1 , 1 ) ;
110+
111+ IMemoryOwner < byte > ? memoryOwner ;
112+ ValueTask < ReadOnlyMemory < byte > > ? readingTask = default ;
87113
88114 public WebSocketChannelReader ( DefaultWebSocketChannel channel ) => this . channel = channel ;
89115
@@ -113,15 +139,53 @@ public override bool TryRead([MaybeNullWhen(false)] out ReadOnlyMemory<byte> ite
113139 if ( channel . webSocket . State != WebSocketState . Open )
114140 return false ;
115141
142+ // We keep a singleton ongoing reading task at a time (single reader),
143+ // since that's how the underlying websocket has to be used (no concurrent
144+ // Receive calls should be performed).
145+ if ( readingTask == null )
146+ {
147+ lock ( syncObj )
148+ {
149+ if ( readingTask == null )
150+ readingTask = ReadCoreAsync ( channel . completionCancellation . Token ) ;
151+ }
152+ }
153+
154+ // Don't lock the call for more than a small timeout time. This allows
155+ // this method, which is not async and cannot be cancelled to signal that
156+ // it couldn't read within an acceptable timeout. This is important considering
157+ // the ReadAllAsync extension method on ChannelReader<T>, which is implemented
158+ // as follows:
159+ //while (await WaitToReadAsync())
160+ // while (TryRead(out T? item))
161+ // yield return item;
162+ // NOTE: our WaitToReadAsync will continue to return true as long as the
163+ // websocket is open, so the underlying reading task can complete.
116164 var cts = new CancellationTokenSource ( tryReadTimeout ) ;
117- var result = ReadCoreAsync ( cts . Token ) ;
118- while ( ! result . IsCompleted )
165+ while ( readingTask != null && readingTask ? . IsCompleted != true && ! cts . IsCancellationRequested )
119166 ;
120167
121- if ( result . IsCompletedSuccessfully )
122- item = result . Result ;
168+ if ( readingTask == null )
169+ return false ;
123170
124- return result . IsCompletedSuccessfully && channel . webSocket . State == WebSocketState . Open ;
171+ lock ( syncObj )
172+ {
173+ if ( readingTask == null )
174+ return false ;
175+
176+ if ( readingTask . Value . IsCompletedSuccessfully == true &&
177+ readingTask . Value . Result . IsEmpty == false )
178+ {
179+ item = readingTask . Value . Result ;
180+ readingTask = null ;
181+ return true ;
182+ }
183+
184+ if ( readingTask . Value . IsCompleted == true )
185+ readingTask = null ;
186+
187+ return false ;
188+ }
125189 }
126190
127191 public override ValueTask < bool > WaitToReadAsync ( CancellationToken cancellationToken = default )
@@ -141,44 +205,32 @@ public override ValueTask<bool> WaitToReadAsync(CancellationToken cancellationTo
141205
142206 async ValueTask < ReadOnlyMemory < byte > > ReadCoreAsync ( CancellationToken cancellation )
143207 {
144- await semaphore . WaitAsync ( cancellation ) ;
208+ await semaphore . WaitAsync ( cancellation ) . ConfigureAwait ( false ) ;
145209 try
146210 {
147- using var owner = MemoryPool < byte > . Shared . Rent ( 512 ) ;
148- var received = await channel . webSocket . ReceiveAsync ( owner . Memory , cancellation ) . ConfigureAwait ( false ) ;
211+ memoryOwner ? . Dispose ( ) ;
212+ memoryOwner = MemoryPool < byte > . Shared . Rent ( 512 ) ;
213+ var received = await channel . webSocket . ReceiveAsync ( memoryOwner . Memory , cancellation ) . ConfigureAwait ( false ) ;
149214 var count = received . Count ;
150215 while ( ! cancellation . IsCancellationRequested && ! received . EndOfMessage && received . MessageType != WebSocketMessageType . Close )
151216 {
152217 if ( received . Count == 0 )
153218 break ;
154219
155- received = await channel . webSocket . ReceiveAsync ( owner . Memory , cancellation ) . ConfigureAwait ( false ) ;
220+ received = await channel . webSocket . ReceiveAsync ( memoryOwner . Memory . Slice ( count ) , cancellation ) . ConfigureAwait ( false ) ;
156221 count += received . Count ;
157222 }
158223
159224 cancellation . ThrowIfCancellationRequested ( ) ;
160225
161226 // We didn't get a complete message, we can't flush partial message.
162227 if ( received . MessageType == WebSocketMessageType . Close )
163- {
164- // Server requested closure.
165- lock ( channel . syncObj )
166- {
167- if ( channel . done == null )
168- {
169- channel . done = socketClosed ;
170- channel . Complete ( ) ;
171- }
172- }
173228 throw socketClosed ;
174- }
175229
176- // Only return from the whole buffer, the slice of bytes that we actually received.
177- return owner . Memory . Slice ( 0 , count ) ;
230+ return memoryOwner . Memory . Slice ( 0 , count ) ;
178231 }
179232 // Don't re-throw the expected socketClosed exception we throw when Close received.
180- catch ( Exception ex ) when ( ex != socketClosed &&
181- ( ex is WebSocketException || ex is InvalidOperationException ) )
233+ catch ( Exception ex ) when ( ex is WebSocketException || ex is InvalidOperationException )
182234 {
183235 // We consider premature closure just as an explicit closure.
184236 if ( ex is WebSocketException wex && wex . WebSocketErrorCode == WebSocketError . ConnectionClosedPrematurely )
@@ -203,7 +255,7 @@ async ValueTask<ReadOnlyMemory<byte>> ReadCoreAsync(CancellationToken cancellati
203255 class WebSocketChannelWriter : ChannelWriter < ReadOnlyMemory < byte > >
204256 {
205257 readonly DefaultWebSocketChannel channel ;
206- readonly SemaphoreSlim semaphore = new SemaphoreSlim ( 1 , 1 ) ;
258+ readonly SemaphoreSlim semaphore = new ( 1 , 1 ) ;
207259
208260 public WebSocketChannelWriter ( DefaultWebSocketChannel channel ) => this . channel = channel ;
209261
@@ -258,10 +310,10 @@ public override ValueTask<bool> WaitToWriteAsync(CancellationToken cancellationT
258310
259311 async ValueTask WriteAsyncCore ( ReadOnlyMemory < byte > item , CancellationToken cancellationToken = default )
260312 {
261- await semaphore . WaitAsync ( cancellationToken ) ;
313+ await semaphore . WaitAsync ( cancellationToken ) . ConfigureAwait ( false ) ;
262314 try
263315 {
264- await channel . webSocket . SendAsync ( item , WebSocketMessageType . Binary , true , cancellationToken ) ;
316+ await channel . webSocket . SendAsync ( item , WebSocketMessageType . Binary , true , cancellationToken ) . ConfigureAwait ( false ) ;
265317 }
266318 finally
267319 {
0 commit comments