diff --git a/Benchmark.AspNetCore.ServerSentEvents/Benchmarks/ServerSentEventsServiceBenchmarks.cs b/Benchmark.AspNetCore.ServerSentEvents/Benchmarks/ServerSentEventsServiceBenchmarks.cs index 134c1f9..56fb43b 100644 --- a/Benchmark.AspNetCore.ServerSentEvents/Benchmarks/ServerSentEventsServiceBenchmarks.cs +++ b/Benchmark.AspNetCore.ServerSentEvents/Benchmarks/ServerSentEventsServiceBenchmarks.cs @@ -15,13 +15,16 @@ public class ServerSentEventsServiceBenchmarks #region Fields private const int MULTIPLE_CLIENTS_COUNT = 10000; + private const string EVENT_TYPE = "Benchmark"; + private const string EVENT_DATA = "Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur.Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum."; + private readonly ServerSentEventsClient _serverSentEventsClient; private readonly ServerSentEventsService _serverSentEventsService; private readonly ServerSentEvent _event = new ServerSentEvent { Id = Guid.NewGuid().ToString(), - Type = "Benchmark", - Data = new List { "Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur.Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum." } + Type = EVENT_TYPE, + Data = new List { EVENT_DATA } }; #endregion @@ -40,9 +43,27 @@ public ServerSentEventsServiceBenchmarks() #region Benchmarks [Benchmark] - public void SendEventAsync_SingleEvent_SingleClient() + public Task SendEventAsync_SingleData_SingleClient() + { + return _serverSentEventsClient.SendEventAsync(EVENT_DATA); + } + + [Benchmark] + public Task SendEventAsync_SingleEvent_SingleClient() + { + return _serverSentEventsClient.SendEventAsync(_event); + } + + [Benchmark] + public Task ChangeReconnectIntervalAsync_SingleClient() + { + return _serverSentEventsClient.ChangeReconnectIntervalAsync(5000); + } + + [Benchmark] + public Task SendEventAsync_SingleData_MultipleClients() { - _serverSentEventsClient.SendEvent(_event); + return _serverSentEventsService.SendEventAsync(EVENT_DATA); } [Benchmark] diff --git a/Lib.AspNetCore.ServerSentEvents/Internals/RawServerSentEvent.cs b/Lib.AspNetCore.ServerSentEvents/Internals/RawServerSentEvent.cs deleted file mode 100644 index 22d7fa4..0000000 --- a/Lib.AspNetCore.ServerSentEvents/Internals/RawServerSentEvent.cs +++ /dev/null @@ -1,48 +0,0 @@ -using System; -using System.Text; -using System.Collections.Generic; - -namespace Lib.AspNetCore.ServerSentEvents.Internals -{ - internal readonly struct RawServerSentEvent - { - #region Properties - internal byte[] Id { get; } - - internal byte[] Type { get; } - - internal IReadOnlyList Data { get; } - #endregion - - #region Constructor - internal RawServerSentEvent(ServerSentEvent serverSentEvent) - : this() - { - if (!String.IsNullOrWhiteSpace(serverSentEvent.Id)) - { - Id = Encoding.UTF8.GetBytes(serverSentEvent.Id); - } - - if (!String.IsNullOrWhiteSpace(serverSentEvent.Type)) - { - Type = Encoding.UTF8.GetBytes(serverSentEvent.Type); - } - - if (serverSentEvent.Data != null) - { - List data = new List(serverSentEvent.Data.Count); - - foreach (string dataItem in serverSentEvent.Data) - { - if (dataItem != null) - { - data.Add(Encoding.UTF8.GetBytes(dataItem)); - } - } - - Data = data; - } - } - #endregion - } -} diff --git a/Lib.AspNetCore.ServerSentEvents/Internals/ServerSentEventBytes.cs b/Lib.AspNetCore.ServerSentEvents/Internals/ServerSentEventBytes.cs new file mode 100644 index 0000000..35ab86d --- /dev/null +++ b/Lib.AspNetCore.ServerSentEvents/Internals/ServerSentEventBytes.cs @@ -0,0 +1,20 @@ +namespace Lib.AspNetCore.ServerSentEvents.Internals +{ + internal readonly struct ServerSentEventBytes + { + #region Properties + internal byte[] Bytes { get; } + + internal int BytesCount { get; } + #endregion + + #region Constructor + internal ServerSentEventBytes(byte[] bytes, int bytesCount) + : this() + { + Bytes = bytes; + BytesCount = bytesCount; + } + #endregion + } +} diff --git a/Lib.AspNetCore.ServerSentEvents/Internals/ServerSentEventsHelper.cs b/Lib.AspNetCore.ServerSentEvents/Internals/ServerSentEventsHelper.cs index 3ec24be..e5e9ee7 100644 --- a/Lib.AspNetCore.ServerSentEvents/Internals/ServerSentEventsHelper.cs +++ b/Lib.AspNetCore.ServerSentEvents/Internals/ServerSentEventsHelper.cs @@ -1,4 +1,6 @@ -using System.Text; +using System; +using System.Text; +using System.Globalization; using System.Threading.Tasks; using Microsoft.AspNetCore.Http; @@ -7,65 +9,130 @@ namespace Lib.AspNetCore.ServerSentEvents.Internals internal static class ServerSentEventsHelper { #region Fields + private const byte CR = 13; + private const byte LF = 10; + private const int CRLF_LENGTH = 2; + private static byte[] _sseRetryField = Encoding.UTF8.GetBytes(Constants.SSE_RETRY_FIELD); private static byte[] _sseIdField = Encoding.UTF8.GetBytes(Constants.SSE_ID_FIELD); private static byte[] _sseEventField = Encoding.UTF8.GetBytes(Constants.SSE_EVENT_FIELD); private static byte[] _sseDataField = Encoding.UTF8.GetBytes(Constants.SSE_DATA_FIELD); - private static byte[] _endOfLine = new byte[] { 13, 10 }; #endregion - #region HttpResponse Extensions - internal static async Task AcceptSse(this HttpResponse response) + #region Methods + internal static Task AcceptAsync(this HttpResponse response) { response.ContentType = Constants.SSE_CONTENT_TYPE; - await response.Body.FlushAsync(); + return response.Body.FlushAsync(); + } + + internal static Task WriteAsync(this HttpResponse response, ServerSentEventBytes serverSentEvent) + { + return response.Body.WriteAsync(serverSentEvent.Bytes, 0, serverSentEvent.BytesCount); + } + + internal static ServerSentEventBytes GetReconnectIntervalBytes(uint reconnectInterval) + { + string reconnectIntervalStringified = reconnectInterval.ToString(CultureInfo.InvariantCulture); + + byte[] bytes = new byte[GetFieldMaxBytesCount(_sseRetryField, reconnectIntervalStringified) + CRLF_LENGTH]; + int bytesCount = GetFieldBytes(_sseRetryField, reconnectIntervalStringified, bytes, 0); + + bytes[bytesCount++] = CR; + bytes[bytesCount++] = LF; + + return new ServerSentEventBytes(bytes, bytesCount); } - internal static async Task WriteSseRetryAsync(this HttpResponse response, byte[] reconnectInterval) + internal static ServerSentEventBytes GetEventBytes(string text) { - await response.WriteSseEventFieldAsync(_sseRetryField, reconnectInterval); - await response.WriteSseEventBoundaryAsync(); + byte[] bytes = new byte[GetFieldMaxBytesCount(_sseDataField, text) + CRLF_LENGTH]; + int bytesCount = GetFieldBytes(_sseDataField, text, bytes, 0); + + bytes[bytesCount++] = CR; + bytes[bytesCount++] = LF; + + return new ServerSentEventBytes(bytes, bytesCount); } - internal static async Task WriteSseEventAsync(this HttpResponse response, byte[] data) + internal static ServerSentEventBytes GetEventBytes(ServerSentEvent serverSentEvent) { - await response.WriteSseEventFieldAsync(_sseDataField, data); - await response.WriteSseEventBoundaryAsync(); + int bytesCount = 0; + byte[] bytes = new byte[GetEventMaxBytesCount(serverSentEvent)]; + + if (!String.IsNullOrWhiteSpace(serverSentEvent.Id)) + { + bytesCount = GetFieldBytes(_sseIdField, serverSentEvent.Id, bytes, bytesCount); + } + + if (!String.IsNullOrWhiteSpace(serverSentEvent.Type)) + { + bytesCount = GetFieldBytes(_sseEventField, serverSentEvent.Type, bytes, bytesCount); + } + + if (serverSentEvent.Data != null) + { + for (int dataItemIndex = 0; dataItemIndex < serverSentEvent.Data.Count; dataItemIndex++) + { + if (serverSentEvent.Data[dataItemIndex] != null) + { + bytesCount = GetFieldBytes(_sseDataField, serverSentEvent.Data[dataItemIndex], bytes, bytesCount); + } + } + } + + bytes[bytesCount++] = CR; + bytes[bytesCount++] = LF; + + return new ServerSentEventBytes(bytes, bytesCount); } - internal static async Task WriteSseEventAsync(this HttpResponse response, RawServerSentEvent serverSentEvent) + private static int GetEventMaxBytesCount(ServerSentEvent serverSentEvent) { - if (serverSentEvent.Id != null) + int bytesCount = CRLF_LENGTH; + + if (!String.IsNullOrWhiteSpace(serverSentEvent.Id)) { - await response.WriteSseEventFieldAsync(_sseIdField, serverSentEvent.Id); + bytesCount += GetFieldMaxBytesCount(_sseIdField, serverSentEvent.Id); } - if (serverSentEvent.Type != null) + if (!String.IsNullOrWhiteSpace(serverSentEvent.Type)) { - await response.WriteSseEventFieldAsync(_sseEventField, serverSentEvent.Type); + bytesCount += GetFieldMaxBytesCount(_sseEventField, serverSentEvent.Type); } if (serverSentEvent.Data != null) { - for (int i = 0; i < serverSentEvent.Data.Count; i++) + for (int dataItemIndex = 0; dataItemIndex < serverSentEvent.Data.Count; dataItemIndex++) { - await response.WriteSseEventFieldAsync(_sseDataField, serverSentEvent.Data[i]); + if (serverSentEvent.Data[dataItemIndex] != null) + { + bytesCount += GetFieldMaxBytesCount(_sseDataField, serverSentEvent.Data[dataItemIndex]); + } } } - await response.WriteSseEventBoundaryAsync(); + return bytesCount; } - private static async Task WriteSseEventFieldAsync(this HttpResponse response, byte[] field, byte[] data) + private static int GetFieldBytes(byte[] field, string data, byte[] bytes, int bytesCount) { - await response.Body.WriteAsync(field, 0, field.Length); - await response.Body.WriteAsync(data, 0, data.Length); - await response.Body.WriteAsync(_endOfLine, 0, _endOfLine.Length); + for (int fieldIndex = 0; fieldIndex < field.Length; fieldIndex++) + { + bytes[bytesCount++] = field[fieldIndex]; + } + + bytesCount += Encoding.UTF8.GetBytes(data, 0, data.Length, bytes, bytesCount); + + bytes[bytesCount++] = CR; + bytes[bytesCount++] = LF; + + return bytesCount; } - private static Task WriteSseEventBoundaryAsync(this HttpResponse response) + private static int GetFieldMaxBytesCount(byte[] field, string data) { - return response.Body.WriteAsync(_endOfLine, 0, _endOfLine.Length); + return field.Length + Encoding.UTF8.GetMaxByteCount(data.Length) + CRLF_LENGTH; } #endregion } diff --git a/Lib.AspNetCore.ServerSentEvents/ServerSentEventsClient.cs b/Lib.AspNetCore.ServerSentEvents/ServerSentEventsClient.cs index 5da4f37..a986f6f 100644 --- a/Lib.AspNetCore.ServerSentEvents/ServerSentEventsClient.cs +++ b/Lib.AspNetCore.ServerSentEvents/ServerSentEventsClient.cs @@ -1,6 +1,4 @@ using System; -using System.Text; -using System.Globalization; using System.Security.Claims; using System.Threading.Tasks; using Microsoft.AspNetCore.Http; @@ -52,7 +50,7 @@ internal ServerSentEventsClient(Guid id, ClaimsPrincipal user, HttpResponse resp /// The task object representing the asynchronous operation. public Task SendEventAsync(string text) { - return SendEventAsync(Encoding.UTF8.GetBytes(text)); + return SendAsync(ServerSentEventsHelper.GetEventBytes(text)); } /// @@ -62,33 +60,19 @@ public Task SendEventAsync(string text) /// The task object representing the asynchronous operation. public Task SendEventAsync(ServerSentEvent serverSentEvent) { - return SendEventAsync(new RawServerSentEvent(serverSentEvent)); + return SendAsync(ServerSentEventsHelper.GetEventBytes(serverSentEvent)); } - internal Task SendEventAsync(byte[] data) + internal Task SendAsync(ServerSentEventBytes serverSentEvent) { CheckIsConnected(); - return _response.WriteSseEventAsync(data); - } - - internal Task SendEventAsync(RawServerSentEvent serverSentEvent) - { - CheckIsConnected(); - - return _response.WriteSseEventAsync(serverSentEvent); + return _response.WriteAsync(serverSentEvent); } internal Task ChangeReconnectIntervalAsync(uint reconnectInterval) { - return ChangeReconnectIntervalAsync(Encoding.UTF8.GetBytes(reconnectInterval.ToString(CultureInfo.InvariantCulture))); - } - - internal Task ChangeReconnectIntervalAsync(byte[] reconnectInterval) - { - CheckIsConnected(); - - return _response.WriteSseRetryAsync(reconnectInterval); + return SendAsync(ServerSentEventsHelper.GetReconnectIntervalBytes(reconnectInterval)); } private void CheckIsConnected() diff --git a/Lib.AspNetCore.ServerSentEvents/ServerSentEventsMiddleware.cs b/Lib.AspNetCore.ServerSentEvents/ServerSentEventsMiddleware.cs index a6962ee..630e8db 100644 --- a/Lib.AspNetCore.ServerSentEvents/ServerSentEventsMiddleware.cs +++ b/Lib.AspNetCore.ServerSentEvents/ServerSentEventsMiddleware.cs @@ -45,7 +45,7 @@ public async Task Invoke(HttpContext context) HandleContentEncoding(context); - await context.Response.AcceptSse(); + await context.Response.AcceptAsync(); ServerSentEventsClient client = new ServerSentEventsClient(Guid.NewGuid(), context.User, context.Response); diff --git a/Lib.AspNetCore.ServerSentEvents/ServerSentEventsService.cs b/Lib.AspNetCore.ServerSentEvents/ServerSentEventsService.cs index 3207d3b..06f56b1 100644 --- a/Lib.AspNetCore.ServerSentEvents/ServerSentEventsService.cs +++ b/Lib.AspNetCore.ServerSentEvents/ServerSentEventsService.cs @@ -1,7 +1,5 @@ using System; using System.Linq; -using System.Text; -using System.Globalization; using System.Threading.Tasks; using System.Collections.Generic; using System.Collections.Concurrent; @@ -58,9 +56,9 @@ public Task ChangeReconnectIntervalAsync(uint reconnectInterval) { ReconnectInterval = reconnectInterval; - byte[] reconnectIntervalBytes = Encoding.UTF8.GetBytes(reconnectInterval.ToString(CultureInfo.InvariantCulture)); + ServerSentEventBytes reconnectIntervalBytes = ServerSentEventsHelper.GetReconnectIntervalBytes(reconnectInterval); - return ForAllClientsAsync(client => client.ChangeReconnectIntervalAsync(reconnectIntervalBytes)); + return ForAllClientsAsync(client => client.SendAsync(reconnectIntervalBytes)); } /// @@ -70,9 +68,9 @@ public Task ChangeReconnectIntervalAsync(uint reconnectInterval) /// The task object representing the asynchronous operation. public Task SendEventAsync(string text) { - byte[] data = Encoding.UTF8.GetBytes(text); + ServerSentEventBytes serverSentEventBytes = ServerSentEventsHelper.GetEventBytes(text); - return ForAllClientsAsync(client => client.SendEventAsync(data)); + return ForAllClientsAsync(client => client.SendAsync(serverSentEventBytes)); } /// @@ -82,9 +80,9 @@ public Task SendEventAsync(string text) /// The task object representing the asynchronous operation. public Task SendEventAsync(ServerSentEvent serverSentEvent) { - RawServerSentEvent rawServerSentEvent = new RawServerSentEvent(serverSentEvent); + ServerSentEventBytes serverSentEventBytes = ServerSentEventsHelper.GetEventBytes(serverSentEvent); - return ForAllClientsAsync(client => client.SendEventAsync(rawServerSentEvent)); + return ForAllClientsAsync(client => client.SendAsync(serverSentEventBytes)); } ///