From 59682fde2a924ae80ae73000fe43ac4b54513b19 Mon Sep 17 00:00:00 2001 From: Sergei Zhuravlev Date: Fri, 11 Sep 2026 11:17:35 +0400 Subject: [PATCH 1/2] Expose WebSocket handshake state and send buffer for external hosts --- source/NetCoreServer/WebSocket.cs | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/source/NetCoreServer/WebSocket.cs b/source/NetCoreServer/WebSocket.cs index 17eea102..e2939efe 100644 --- a/source/NetCoreServer/WebSocket.cs +++ b/source/NetCoreServer/WebSocket.cs @@ -581,7 +581,7 @@ public void ClearWsBuffers() /// /// Handshaked flag /// - internal bool WsHandshaked; + public bool WsHandshaked; /// /// Received frame flag /// @@ -623,11 +623,11 @@ public void ClearWsBuffers() /// /// Send buffer lock /// - internal readonly object WsSendLock = new object(); + public readonly object WsSendLock = new object(); /// /// Send buffer /// - internal readonly Buffer WsSendBuffer = new Buffer(); + public readonly Buffer WsSendBuffer = new Buffer(); /// /// Send mask /// From bde5d6510ee36fe3767784bf378b7ab5520aa4b8 Mon Sep 17 00:00:00 2001 From: Sergei Zhuravlev Date: Sat, 12 Sep 2026 13:10:45 +0400 Subject: [PATCH 2/2] Optimize WebSocket framing, masking, accept validation, and buffer management - Use 32-bit chunked XOR in MaskBuffer for fast masking/unmasking - Direct memory copy for unmasked server-to-client frames - Batch append bytes during frame header parsing instead of byte-by-byte loops - Fast base64 validation for Sec-WebSocket-Accept handshake header - Optimized Buffer.Reserve with minimum allocation floor to prevent realloc churn - Add null/empty checks for span and array appends in Buffer - Safe BodyBytes copy in HttpRequest and HttpResponse - Bump project version to 10.0.1.0 --- source/NetCoreServer/Buffer.cs | 22 ++- source/NetCoreServer/HttpRequest.cs | 12 +- source/NetCoreServer/HttpResponse.cs | 12 +- source/NetCoreServer/NetCoreServer.csproj | 2 +- source/NetCoreServer/WebSocket.cs | 160 +++++++++++++++------- 5 files changed, 151 insertions(+), 57 deletions(-) diff --git a/source/NetCoreServer/Buffer.cs b/source/NetCoreServer/Buffer.cs index 6beaf66a..12ee756d 100644 --- a/source/NetCoreServer/Buffer.cs +++ b/source/NetCoreServer/Buffer.cs @@ -1,4 +1,4 @@ -using System; +using System; using System.Diagnostics; using System.Text; @@ -123,8 +123,10 @@ public void Reserve(long capacity) if (capacity > Capacity) { - byte[] data = new byte[Math.Max(capacity, 2 * Capacity)]; - Array.Copy(_data, 0, data, 0, _size); + long newCapacity = Math.Max(capacity, Math.Max(256, 2 * Capacity)); + byte[] data = new byte[newCapacity]; + if (_size > 0) + Array.Copy(_data, 0, data, 0, _size); _data = data; } } @@ -173,6 +175,8 @@ public long Append(byte value) /// Count of append bytes public long Append(byte[] buffer) { + if (buffer == null || buffer.Length == 0) + return 0; Reserve(_size + buffer.Length); Array.Copy(buffer, 0, _data, _size, buffer.Length); _size += buffer.Length; @@ -188,8 +192,10 @@ public long Append(byte[] buffer) /// Count of append bytes public long Append(byte[] buffer, long offset, long size) { + if (buffer == null || size <= 0) + return 0; Reserve(_size + size); - Array.Copy(buffer, offset, _data, _size, size); + Array.Copy(buffer, (int)offset, _data, (int)_size, (int)size); _size += size; return size; } @@ -201,6 +207,8 @@ public long Append(byte[] buffer, long offset, long size) /// Count of append bytes public long Append(ReadOnlySpan buffer) { + if (buffer.IsEmpty) + return 0; Reserve(_size + buffer.Length); buffer.CopyTo(new Span(_data, (int)_size, buffer.Length)); _size += buffer.Length; @@ -212,7 +220,7 @@ public long Append(ReadOnlySpan buffer) /// /// Buffer to append /// Count of append bytes - public long Append(Buffer buffer) => Append(buffer.AsSpan()); + public long Append(Buffer buffer) => buffer != null ? Append(buffer.AsSpan()) : 0; /// /// Append the given text in UTF-8 encoding @@ -221,6 +229,8 @@ public long Append(ReadOnlySpan buffer) /// Count of append bytes public long Append(string text) { + if (string.IsNullOrEmpty(text)) + return 0; int length = Encoding.UTF8.GetMaxByteCount(text.Length); Reserve(_size + length); long result = Encoding.UTF8.GetBytes(text, 0, text.Length, _data, (int)_size); @@ -235,6 +245,8 @@ public long Append(string text) /// Count of append bytes public long Append(ReadOnlySpan text) { + if (text.IsEmpty) + return 0; int length = Encoding.UTF8.GetMaxByteCount(text.Length); Reserve(_size + length); long result = Encoding.UTF8.GetBytes(text, new Span(_data, (int)_size, length)); diff --git a/source/NetCoreServer/HttpRequest.cs b/source/NetCoreServer/HttpRequest.cs index 8006308c..207b69bd 100644 --- a/source/NetCoreServer/HttpRequest.cs +++ b/source/NetCoreServer/HttpRequest.cs @@ -1,4 +1,4 @@ -using System; +using System; using System.Collections.Generic; using System.Diagnostics; using System.Text; @@ -87,7 +87,15 @@ public HttpRequest(string method, string url, string protocol = "HTTP/1.1") /// /// Get the HTTP request body as byte array /// - public byte[] BodyBytes { get { return _cache.Data[_bodyIndex..(_bodyIndex + _bodySize)]; } } + public byte[] BodyBytes + { + get + { + var result = new byte[_bodySize]; + Array.Copy(_cache.Data, _bodyIndex, result, 0, _bodySize); + return result; + } + } /// /// Get the HTTP request body as byte span /// diff --git a/source/NetCoreServer/HttpResponse.cs b/source/NetCoreServer/HttpResponse.cs index 56355f0c..d3270c87 100644 --- a/source/NetCoreServer/HttpResponse.cs +++ b/source/NetCoreServer/HttpResponse.cs @@ -1,4 +1,4 @@ -using System; +using System; using System.Collections.Generic; using System.Diagnostics; using System.Text; @@ -156,7 +156,15 @@ public HttpResponse(int status, string statusPhrase, string protocol) /// /// Get the HTTP request body as byte array /// - public byte[] BodyBytes { get { return _cache.Data[_bodyIndex..(_bodyIndex + _bodySize)]; } } + public byte[] BodyBytes + { + get + { + var result = new byte[_bodySize]; + Array.Copy(_cache.Data, _bodyIndex, result, 0, _bodySize); + return result; + } + } /// /// Get the HTTP request body as read-only byte span /// diff --git a/source/NetCoreServer/NetCoreServer.csproj b/source/NetCoreServer/NetCoreServer.csproj index fa73e8b7..36e2eb6c 100644 --- a/source/NetCoreServer/NetCoreServer.csproj +++ b/source/NetCoreServer/NetCoreServer.csproj @@ -2,7 +2,7 @@ net10.0 - 10.0.0.0 + 10.0.1.0 Ivan Shynkarenka Copyright (c) 2019-2026 Ivan Shynkarenka https://github.com/chronoxor/NetCoreServer diff --git a/source/NetCoreServer/WebSocket.cs b/source/NetCoreServer/WebSocket.cs index e2939efe..6445d22d 100644 --- a/source/NetCoreServer/WebSocket.cs +++ b/source/NetCoreServer/WebSocket.cs @@ -1,4 +1,4 @@ -using System; +using System; using System.Text; using System.Security.Cryptography; using System.Collections.Generic; @@ -45,6 +45,8 @@ public class WebSocket : IWebSocket /// public const byte WS_PONG = 0x0A; + private const string WsGuid = "258EAFA5-E914-47DA-95CA-C5AB0DC85B11"; + /// /// Perform WebSocket client upgrade /// @@ -93,18 +95,16 @@ public bool PerformClientUpgrade(HttpResponse response, Guid id) else if (string.Compare(key, "Sec-WebSocket-Accept", StringComparison.OrdinalIgnoreCase) == 0) { // Calculate the original WebSocket hash - string wskey = Convert.ToBase64String(WsNonce) + "258EAFA5-E914-47DA-95CA-C5AB0DC85B11"; - string wshash; + string wskey = Convert.ToBase64String(WsNonce) + WsGuid; + string expectedAccept; using (SHA1 sha1 = SHA1.Create()) { - wshash = Encoding.UTF8.GetString(sha1.ComputeHash(Encoding.UTF8.GetBytes(wskey))); + byte[] wshash = sha1.ComputeHash(Encoding.UTF8.GetBytes(wskey)); + expectedAccept = Convert.ToBase64String(wshash); } - // Get the received WebSocket hash - wskey = Encoding.UTF8.GetString(Convert.FromBase64String(value)); - // Compare original and received hashes - if (string.Compare(wskey, wshash, StringComparison.InvariantCulture) != 0) + if (string.Compare(value.Trim(), expectedAccept, StringComparison.Ordinal) != 0) { error = true; _wsHandler.OnWsError("Invalid WebSocket handshaked response: 'Sec-WebSocket-Accept' value validation failed"); @@ -189,15 +189,13 @@ public bool PerformServerUpgrade(HttpRequest request, HttpResponse response) } // Calculate the original WebSocket hash - string wskey = value + "258EAFA5-E914-47DA-95CA-C5AB0DC85B11"; - byte[] wshash; + string wskey = value.Trim() + WsGuid; using (SHA1 sha1 = SHA1.Create()) { - wshash = sha1.ComputeHash(Encoding.UTF8.GetBytes(wskey)); + byte[] wshash = sha1.ComputeHash(Encoding.UTF8.GetBytes(wskey)); + accept = Convert.ToBase64String(wshash); } - accept = Convert.ToBase64String(wshash); - wsKey = true; } else if (string.Compare(key, "Sec-WebSocket-Version", StringComparison.OrdinalIgnoreCase) == 0) @@ -245,12 +243,45 @@ public bool PerformServerUpgrade(HttpRequest request, HttpResponse response) // WebSocket successfully handshaked! WsHandshaked = true; - Array.Fill(WsSendMask, (byte)0); + Array.Clear(WsSendMask, 0, WsSendMask.Length); _wsHandler.OnWsConnected(request); return true; } + /// + /// Fast masking / unmasking helper + /// + public static void MaskBuffer(ReadOnlySpan source, byte[] destination, int destOffset, byte[] mask, int maskOffset = 0) + { + int length = source.Length; + if (length == 0) + return; + + int i = 0; + // Process in 4-byte chunks when mask offset aligns with 4 + if ((maskOffset % 4) == 0 && length >= 4) + { + uint mask32 = (uint)(mask[0] | (mask[1] << 8) | (mask[2] << 16) | (mask[3] << 24)); + int count4 = length & ~3; + while (i < count4) + { + uint val = (uint)(source[i] | (source[i + 1] << 8) | (source[i + 2] << 16) | (source[i + 3] << 24)); + val ^= mask32; + destination[destOffset + i + 0] = (byte)(val & 0xFF); + destination[destOffset + i + 1] = (byte)((val >> 8) & 0xFF); + destination[destOffset + i + 2] = (byte)((val >> 16) & 0xFF); + destination[destOffset + i + 3] = (byte)((val >> 24) & 0xFF); + i += 4; + } + } + + for (; i < length; i++) + { + destination[destOffset + i] = (byte)(source[i] ^ mask[(i + maskOffset) % 4]); + } + } + /// /// Prepare WebSocket send frame /// @@ -305,13 +336,31 @@ public void PrepareSendFrame(byte opcode, bool mask, ReadOnlySpan buffer, if (storeStatus) { index += 2; - WsSendBuffer.Data[offset + 0] = (byte)(((status >> 8) & 0xFF) ^ WsSendMask[0]); - WsSendBuffer.Data[offset + 1] = (byte)((status & 0xFF) ^ WsSendMask[1]); + if (mask) + { + WsSendBuffer.Data[offset + 0] = (byte)(((status >> 8) & 0xFF) ^ WsSendMask[0]); + WsSendBuffer.Data[offset + 1] = (byte)((status & 0xFF) ^ WsSendMask[1]); + } + else + { + WsSendBuffer.Data[offset + 0] = (byte)((status >> 8) & 0xFF); + WsSendBuffer.Data[offset + 1] = (byte)(status & 0xFF); + } } - // Mask WebSocket frame content - for (int i = index; i < size; i++) - WsSendBuffer.Data[offset + i] = (byte)(buffer[i - index] ^ WsSendMask[i % 4]); + if (!mask) + { + // Fast path for server-to-client frames (unmasked): direct memory copy + if (buffer.Length > 0) + { + buffer.CopyTo(new Span(WsSendBuffer.Data, (int)(offset + index), buffer.Length)); + } + } + else + { + // Mask WebSocket frame content + MaskBuffer(buffer, WsSendBuffer.Data, (int)(offset + index), WsSendMask, index % 4); + } } /// @@ -361,12 +410,13 @@ public void PrepareReceiveFrame(byte[] buffer, long offset, long size) // Prepare WebSocket frame opcode and mask flag if (WsReceiveFrameBuffer.Size < 2) { - for (long i = 0; i < 2; i++, index++, size--) - { - if (size == 0) - return; - WsReceiveFrameBuffer.Append(buffer[offset + index]); - } + long need = 2 - WsReceiveFrameBuffer.Size; + long take = Math.Min(need, size); + WsReceiveFrameBuffer.Append(buffer, (int)offset + index, (int)take); + index += (int)take; + size -= take; + if (WsReceiveFrameBuffer.Size < 2) + return; } byte opcode = (byte)(WsReceiveFrameBuffer[0] & 0x0F); @@ -387,12 +437,13 @@ public void PrepareReceiveFrame(byte[] buffer, long offset, long size) { if (WsReceiveFrameBuffer.Size < 4) { - for (long i = 0; i < 2; i++, index++, size--) - { - if (size == 0) - return; - WsReceiveFrameBuffer.Append(buffer[offset + index]); - } + long need = 4 - WsReceiveFrameBuffer.Size; + long take = Math.Min(need, size); + WsReceiveFrameBuffer.Append(buffer, (int)offset + index, (int)take); + index += (int)take; + size -= take; + if (WsReceiveFrameBuffer.Size < 4) + return; } payload = (((long)WsReceiveFrameBuffer[2] << 8) | ((long)WsReceiveFrameBuffer[3] << 0)); @@ -403,12 +454,13 @@ public void PrepareReceiveFrame(byte[] buffer, long offset, long size) { if (WsReceiveFrameBuffer.Size < 10) { - for (long i = 0; i < 8; i++, index++, size--) - { - if (size == 0) - return; - WsReceiveFrameBuffer.Append(buffer[offset + index]); - } + long need = 10 - WsReceiveFrameBuffer.Size; + long take = Math.Min(need, size); + WsReceiveFrameBuffer.Append(buffer, (int)offset + index, (int)take); + index += (int)take; + size -= take; + if (WsReceiveFrameBuffer.Size < 10) + return; } payload = (((long)WsReceiveFrameBuffer[2] << 56) | ((long)WsReceiveFrameBuffer[3] << 48) | ((long)WsReceiveFrameBuffer[4] << 40) | ((long)WsReceiveFrameBuffer[5] << 32) | ((long)WsReceiveFrameBuffer[6] << 24) | ((long)WsReceiveFrameBuffer[7] << 16) | ((long)WsReceiveFrameBuffer[8] << 8) | ((long)WsReceiveFrameBuffer[9] << 0)); @@ -421,21 +473,26 @@ public void PrepareReceiveFrame(byte[] buffer, long offset, long size) { if (WsReceiveFrameBuffer.Size < WsHeaderSize) { - for (long i = 0; i < 4; i++, index++, size--) - { - if (size == 0) - return; - WsReceiveFrameBuffer.Append(buffer[offset + index]); - WsReceiveMask[i] = buffer[offset + index]; - } + long need = WsHeaderSize - WsReceiveFrameBuffer.Size; + long take = Math.Min(need, size); + WsReceiveFrameBuffer.Append(buffer, (int)offset + index, (int)take); + index += (int)take; + size -= take; + if (WsReceiveFrameBuffer.Size < WsHeaderSize) + return; } + int maskStart = (int)WsHeaderSize - 4; + WsReceiveMask[0] = WsReceiveFrameBuffer[maskStart + 0]; + WsReceiveMask[1] = WsReceiveFrameBuffer[maskStart + 1]; + WsReceiveMask[2] = WsReceiveFrameBuffer[maskStart + 2]; + WsReceiveMask[3] = WsReceiveFrameBuffer[maskStart + 3]; } long total = WsHeaderSize + WsPayloadSize; long length = Math.Min(total - WsReceiveFrameBuffer.Size, size); // Prepare WebSocket frame payload - WsReceiveFrameBuffer.Append(buffer[((int)offset + index)..((int)offset + index + (int)length)]); + WsReceiveFrameBuffer.Append(buffer, (int)offset + index, (int)length); index += (int)length; size -= length; @@ -445,11 +502,20 @@ public void PrepareReceiveFrame(byte[] buffer, long offset, long size) // Unmask WebSocket frame content if (mask) { - for (long i = 0; i < WsPayloadSize; i++) - WsReceiveFinalBuffer.Append((byte)(WsReceiveFrameBuffer[WsHeaderSize + i] ^ WsReceiveMask[i % 4])); + int destOffset = (int)WsReceiveFinalBuffer.Size; + WsReceiveFinalBuffer.Resize(destOffset + WsPayloadSize); + MaskBuffer( + WsReceiveFrameBuffer.AsSpan().Slice((int)WsHeaderSize, (int)WsPayloadSize), + WsReceiveFinalBuffer.Data, + destOffset, + WsReceiveMask, + 0 + ); } else + { WsReceiveFinalBuffer.Append(WsReceiveFrameBuffer.AsSpan().Slice((int)WsHeaderSize, (int)WsPayloadSize)); + } WsFrameReceived = true;