Skip to content

Commit d8e1f7c

Browse files
committed
wip: ws async improvements
1 parent 118e6ec commit d8e1f7c

File tree

7 files changed

+107
-57
lines changed

7 files changed

+107
-57
lines changed

extensions/Sisk.SslProxy/Sisk.SslProxy.csproj

+1-1
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,7 @@
2222
<PackageTags>http-server,http,web framework</PackageTags>
2323
<RepositoryType>git</RepositoryType>
2424

25-
<Version>1.4.1-alpha9</Version>
25+
<Version>1.4.1.1-alpha9</Version>
2626
<AssemblyVersion>1.4.1</AssemblyVersion>
2727
<FileVersion>1.4.1</FileVersion>
2828

src/GlobalSuppressions.cs

+1-2
Original file line numberDiff line numberDiff line change
@@ -10,5 +10,4 @@
1010
using System.Diagnostics.CodeAnalysis;
1111

1212
[assembly: SuppressMessage ( "Usage", "CA2225:Operator overloads have named alternates", Scope = "member", Target = "~M:Sisk.Core.Http.HttpStatusInformation.op_Implicit(System.Net.HttpStatusCode)~Sisk.Core.Http.HttpStatusInformation" )]
13-
[assembly: SuppressMessage ( "Usage", "CA2225:Operator overloads have named alternates", Scope = "member", Target = "~M:Sisk.Core.Http.HttpStatusInformation.op_Implicit(System.Int32)~Sisk.Core.Http.HttpStatusInformation" )]
14-
[assembly: SuppressMessage ( "Json", "SYSLIB0020:JsonSerializerOptions.IgnoreNullValues is obsolete" )]
13+
[assembly: SuppressMessage ( "Usage", "CA2225:Operator overloads have named alternates", Scope = "member", Target = "~M:Sisk.Core.Http.HttpStatusInformation.op_Implicit(System.Int32)~Sisk.Core.Http.HttpStatusInformation" )]

src/Http/Streams/HttpRequestEventSource.cs

+8
Original file line numberDiff line numberDiff line change
@@ -233,10 +233,18 @@ internal async ValueTask FlushAsync () {
233233
public void Dispose () {
234234
if (isDisposed)
235235
return;
236+
237+
GC.SuppressFinalize ( this );
238+
236239
Close ();
237240
sendQueue.Clear ();
238241
terminatingMutex.Dispose ();
239242
isDisposed = true;
240243
}
244+
245+
/// <exclude/>
246+
~HttpRequestEventSource () {
247+
Dispose ();
248+
}
241249
}
242250
}

src/Http/Streams/HttpWebSocket.cs

+49-53
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,8 @@
88
// Repository: https://github.com/sisk-http/core
99

1010
using System.Net.WebSockets;
11-
using System.Runtime.CompilerServices;
11+
using System.Text;
12+
using Sisk.Core.Internal;
1213

1314
namespace Sisk.Core.Http.Streams {
1415

@@ -17,8 +18,10 @@ namespace Sisk.Core.Http.Streams {
1718
/// </summary>
1819
public sealed class HttpWebSocket : IDisposable {
1920
bool isListening = true;
21+
bool isDisposed;
2022
readonly HttpStreamPingPolicy pingPolicy;
2123

24+
internal SemaphoreSlim sendSemaphore = new SemaphoreSlim ( 1 );
2225
internal WebSocketMessage? lastMessage;
2326
internal CancellationTokenSource asyncListenerToken = null!;
2427
internal ManualResetEvent closeEvent = new ManualResetEvent ( false );
@@ -178,40 +181,22 @@ public HttpWebSocket WithPing ( string probeMessage, TimeSpan interval ) {
178181
return this;
179182
}
180183

181-
/// <summary>
182-
/// Asynchronously sends an message to the remote point.
183-
/// </summary>
184-
/// <param name="message">The target message which will be as an encoded UTF-8 string.</param>
185-
public Task<bool> SendAsync ( object message ) {
186-
return Task.FromResult ( Send ( message ) );
187-
}
188-
189184
/// <summary>
190185
/// Asynchronously sends an text message to the remote point.
191186
/// </summary>
192187
/// <param name="message">The target message which will be as an encoded UTF-8 string.</param>
193-
public Task<bool> SendAsync ( string message ) {
194-
return Task.FromResult ( Send ( message ) );
188+
public ValueTask<bool> SendAsync ( string message ) {
189+
ArgumentNullException.ThrowIfNull ( message );
190+
191+
return SendInternalAsync ( Encoding.UTF8.GetBytes ( message ), WebSocketMessageType.Text );
195192
}
196193

197194
/// <summary>
198195
/// Asynchronously sends an binary message to the remote point.
199196
/// </summary>
200197
/// <param name="buffer">The target message which will be as an encoded UTF-8 string.</param>
201-
public Task<bool> SendAsync ( byte [] buffer ) {
202-
return Task.FromResult ( Send ( buffer ) );
203-
}
204-
205-
/// <summary>
206-
/// Sends an text message to the remote point.
207-
/// </summary>
208-
/// <param name="message">The target message which will be as an encoded UTF-8 string.</param>
209-
public bool Send ( object message ) {
210-
string? t = message.ToString ();
211-
if (t is null)
212-
throw new ArgumentNullException ( nameof ( message ) );
213-
214-
return Send ( t );
198+
public ValueTask<bool> SendAsync ( ReadOnlyMemory<byte> buffer ) {
199+
return SendInternalAsync ( buffer, WebSocketMessageType.Binary );
215200
}
216201

217202
/// <summary>
@@ -220,9 +205,7 @@ public bool Send ( object message ) {
220205
/// <param name="message">The target message which will be as an encoded using the request preferred encoding.</param>
221206
public bool Send ( string message ) {
222207
ArgumentNullException.ThrowIfNull ( message );
223-
224-
byte [] messageBytes = request.RequestEncoding.GetBytes ( message );
225-
return SendInternal ( messageBytes, WebSocketMessageType.Text );
208+
return SendAsync ( message ).GetSyncronizedResult ();
226209
}
227210

228211
/// <summary>
@@ -239,15 +222,15 @@ public bool Send ( string message ) {
239222
/// <param name="length">The number of items in the memory.</param>
240223
public bool Send ( byte [] buffer, int start, int length ) {
241224
ReadOnlyMemory<byte> span = new ReadOnlyMemory<byte> ( buffer, start, length );
242-
return SendInternal ( span, WebSocketMessageType.Binary );
225+
return SendAsync ( span ).GetSyncronizedResult ();
243226
}
244227

245228
/// <summary>
246229
/// Sends an binary message to the remote point.
247230
/// </summary>
248231
/// <param name="buffer">The target byte memory.</param>
249232
public bool Send ( ReadOnlyMemory<byte> buffer ) {
250-
return SendInternal ( buffer, WebSocketMessageType.Binary );
233+
return SendAsync ( buffer ).GetSyncronizedResult ();
251234
}
252235

253236
/// <summary>
@@ -263,7 +246,7 @@ public HttpResponse Close () {
263246
// the resources of this websocket
264247
try {
265248
ctx.WebSocket.CloseOutputAsync ( WebSocketCloseStatus.NormalClosure, null, CancellationToken.None )
266-
.Wait ();
249+
.GetAwaiter ().GetResult ();
267250
}
268251
catch (Exception) {
269252
;
@@ -282,41 +265,43 @@ public HttpResponse Close () {
282265
};
283266
}
284267

285-
[MethodImpl ( MethodImplOptions.Synchronized )]
286-
private bool SendInternal ( ReadOnlyMemory<byte> buffer, WebSocketMessageType msgType ) {
287-
if (_isClosed) { return false; }
268+
private async ValueTask<bool> SendInternalAsync ( ReadOnlyMemory<byte> buffer, WebSocketMessageType msgType ) {
269+
if (_isClosed)
270+
return false;
288271

289272
if (closeTimeout.TotalMilliseconds > 0)
290273
asyncListenerToken?.CancelAfter ( closeTimeout );
291274

275+
await sendSemaphore.WaitAsync ( asyncListenerToken?.Token ?? default );
292276
try {
293-
int totalLength = buffer.Length;
294-
int chunks = (int) Math.Ceiling ( (double) totalLength / BUFFER_LENGTH );
277+
try {
278+
int totalLength = buffer.Length;
279+
int chunks = (int) Math.Ceiling ( (double) totalLength / BUFFER_LENGTH );
295280

296-
for (int i = 0; i < chunks; i++) {
297-
int ca = i * BUFFER_LENGTH;
298-
int cb = Math.Min ( ca + BUFFER_LENGTH, buffer.Length );
281+
for (int i = 0; i < chunks; i++) {
282+
int ca = i * BUFFER_LENGTH;
283+
int cb = Math.Min ( ca + BUFFER_LENGTH, buffer.Length );
299284

300-
ReadOnlyMemory<byte> chunk = buffer [ ca..cb ];
285+
ReadOnlyMemory<byte> chunk = buffer [ ca..cb ];
301286

302-
var sendVt = ctx.WebSocket.SendAsync ( chunk, msgType, i + 1 == chunks, asyncListenerToken?.Token ?? default );
303-
if (!sendVt.IsCompleted) {
304-
sendVt.AsTask ().GetAwaiter ().GetResult ();
287+
await ctx.WebSocket.SendAsync ( chunk, msgType, i + 1 == chunks, asyncListenerToken?.Token ?? default );
288+
length += chunk.Length;
305289
}
306290

307-
length += chunk.Length;
291+
attempt = 0;
308292
}
309-
310-
attempt = 0;
311-
}
312-
catch (Exception) {
313-
attempt++;
314-
if (MaxAttempts >= 0 && attempt >= MaxAttempts) {
315-
Close ();
316-
return false;
293+
catch (Exception) {
294+
attempt++;
295+
if (MaxAttempts >= 0 && attempt >= MaxAttempts) {
296+
Close ();
297+
return false;
298+
}
317299
}
300+
return true;
301+
}
302+
finally {
303+
sendSemaphore.Release ();
318304
}
319-
return true;
320305
}
321306

322307
/// <summary>
@@ -365,11 +350,22 @@ public void WaitForClose () {
365350

366351
/// <inheritdoc/>
367352
public void Dispose () {
353+
if (isDisposed)
354+
return;
355+
356+
GC.SuppressFinalize ( this );
357+
368358
Close ();
369359
pingPolicy.Dispose ();
370360
closeEvent.Dispose ();
371361
waitNextEvent.Dispose ();
372362
receiveThread.Join ();
363+
isDisposed = true;
364+
}
365+
366+
/// <exclude/>
367+
~HttpWebSocket () {
368+
Dispose ();
373369
}
374370
}
375371

src/Internal/AsyncHelpers.cs

+1-1
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,6 @@ public static T GetSyncronizedResult<T> ( this ValueTask<T> task ) {
1414
if (task.IsCompleted) {
1515
return task.Result;
1616
}
17-
return task.GetAwaiter ().GetResult ();
17+
return task.AsTask ().GetAwaiter ().GetResult ();
1818
}
1919
}

tests/Server.cs

+7
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,13 @@ public static void AssemblyInit ( TestContext testContext ) {
2222
Status = HttpStatusInformation.Ok
2323
};
2424
} );
25+
router.MapGet ( "/tests/plaintext/chunked", delegate ( HttpRequest request ) {
26+
return new HttpResponse () {
27+
Content = new StringContent ( "Hello, world!", Encoding.UTF8, "text/plain" ),
28+
Status = HttpStatusInformation.Ok,
29+
SendChunked = true
30+
};
31+
} );
2532
} )
2633
.Build ();
2734

tests/Tests/HttpResponseTests.cs

+40
Original file line numberDiff line numberDiff line change
@@ -17,4 +17,44 @@ public async Task OkPlainText () {
1717
Assert.AreEqual ( response.Content.Headers.ContentType?.CharSet, "utf-8" );
1818
}
1919
}
20+
21+
[TestMethod]
22+
public async Task OkPlainTextChunked () {
23+
using (var client = Server.GetHttpClient ()) {
24+
25+
var request = new HttpRequestMessage ( HttpMethod.Get, "tests/plaintext/chunked" );
26+
var response = await client.SendAsync ( request );
27+
var content = await response.Content.ReadAsStringAsync ();
28+
29+
Assert.IsTrue ( response.IsSuccessStatusCode );
30+
Assert.IsTrue ( response.Headers.TransferEncodingChunked == true );
31+
Assert.AreEqual ( content, "Hello, world!", ignoreCase: true );
32+
Assert.AreEqual ( response.Content.Headers.ContentType?.MediaType, "text/plain" );
33+
Assert.AreEqual ( response.Content.Headers.ContentType?.CharSet, "utf-8" );
34+
}
35+
}
36+
37+
[TestMethod]
38+
public async Task NotFound () {
39+
using (var client = Server.GetHttpClient ()) {
40+
41+
var request = new HttpRequestMessage ( HttpMethod.Get, "tests/not-found" );
42+
var response = await client.SendAsync ( request );
43+
var content = await response.Content.ReadAsStringAsync ();
44+
45+
Assert.IsTrue ( response.StatusCode == System.Net.HttpStatusCode.NotFound );
46+
}
47+
}
48+
49+
[TestMethod]
50+
public async Task MethodNotAllowed () {
51+
using (var client = Server.GetHttpClient ()) {
52+
53+
var request = new HttpRequestMessage ( HttpMethod.Post, "tests/plaintext" );
54+
var response = await client.SendAsync ( request );
55+
var content = await response.Content.ReadAsStringAsync ();
56+
57+
Assert.IsTrue ( response.StatusCode == System.Net.HttpStatusCode.MethodNotAllowed );
58+
}
59+
}
2060
}

0 commit comments

Comments
 (0)