From 92bf1c822a60a5b509a563f065d3fc967b5c16ad Mon Sep 17 00:00:00 2001 From: Abby Beizer Date: Thu, 19 Dec 2024 15:55:13 -0500 Subject: [PATCH 1/3] create, edit, delete segment. message frame fix --- .../Scripts/ClientDashboard/RGTcpManager.cs | 138 +++++++++++++++++- .../Scripts/ClientDashboard/RGTcpServer.cs | 49 +++++-- .../Scripts/ClientDashboard/TcpMessage.cs | 122 +++++++++++----- .../TcpMessageDataJsonConverter.cs | 10 ++ .../Scripts/ClientDashboard/TcpMessageType.cs | 46 ++++++ .../ClientDashboard/TcpMessageType.cs.meta | 3 + 6 files changed, 316 insertions(+), 52 deletions(-) create mode 100644 src/gg.regression.unity.bots/Runtime/Scripts/ClientDashboard/TcpMessageType.cs create mode 100644 src/gg.regression.unity.bots/Runtime/Scripts/ClientDashboard/TcpMessageType.cs.meta diff --git a/src/gg.regression.unity.bots/Runtime/Scripts/ClientDashboard/RGTcpManager.cs b/src/gg.regression.unity.bots/Runtime/Scripts/ClientDashboard/RGTcpManager.cs index ece03aaf..d1db6c4c 100644 --- a/src/gg.regression.unity.bots/Runtime/Scripts/ClientDashboard/RGTcpManager.cs +++ b/src/gg.regression.unity.bots/Runtime/Scripts/ClientDashboard/RGTcpManager.cs @@ -1,3 +1,4 @@ +using System; using System.Collections.Generic; using System.Diagnostics; using System.IO; @@ -236,6 +237,30 @@ private static void ProcessClientMessage(TcpClient client, TcpMessage message) m_shouldStopReplay = true; break; } + case TcpMessageType.RequestSequenceJson: + { + var payload = (RequestResourceContentsTcpMessageData) message.payload; + SendSequenceJson(payload.resourcePath, client); + break; + } + case TcpMessageType.RequestSegmentJson: + { + var payload = (RequestResourceContentsTcpMessageData) message.payload; + SendSegmentJson(payload.resourcePath, client); + break; + } + case TcpMessageType.SaveSegment: + { + var payload = (SaveSegmentListTcpMessageData) message.payload; + SaveSegment(payload.segmentList, payload.resourcePath); + break; + } + case TcpMessageType.DeleteSegment: + { + var payload = (DeleteSegmentTcpMessageData) message.payload; + DeleteSegment(payload.resourcePath); + break; + } } } @@ -253,12 +278,6 @@ private static void ProcessAndSendSequences(IDictionary seg.Item2).ToList(); - SendAvailableSegments(); - } - /// /// Returns the active bot sequence, if there is one /// @@ -298,6 +317,74 @@ private static ActiveSequence GetActiveBotSequence() #endregion + #region Bot Segments + + private static void ProcessAndSendSegments() + { + m_availableBotSegments = BotSegment.LoadAllSegments().Values.Select(seg => seg.Item2).ToList(); + SendAvailableSegments(); + } + + private static void SaveSegment(BotSegmentList segmentList, [CanBeNull] string resourcePath) + { + string directoryPath = null; + +#if UNITY_EDITOR + directoryPath = "Assets/RegressionGames/Resources/BotSegments/"; +#else + directoryPath = Application.persistentDataPath + "/RegressionGames/Resources/BotSegments/"; +#endif + Directory.CreateDirectory(directoryPath); + + if (resourcePath != null) + { + // remove all text up to BotSegments/ + var index = resourcePath.IndexOf("BotSegments/"); + if (index != -1) + { + resourcePath = resourcePath.Substring(index + "BotSegments/".Length); + } + resourcePath = directoryPath + resourcePath + ".json"; + + File.Delete(resourcePath); + using var sw = File.CreateText(resourcePath); + sw.Write(segmentList.ToJsonString()); + sw.Close(); + } + else + { + // generate a path for this new SegmentList + var filepath = string.Join("-", segmentList.name.Split(" ")); + foreach (var c in Path.GetInvalidPathChars()) + { + filepath = filepath.Replace(c, '-'); + } + filepath = directoryPath + "/" + filepath + ".json"; + + using var sw = File.CreateText(filepath); + sw.Write(segmentList.ToJsonString()); + sw.Close(); + } + + // then refresh the list of available segments + ProcessAndSendSegments(); + } + + private static void DeleteSegment(string resourcePath) + { + resourcePath = resourcePath.Replace('\\', '/'); + if (!resourcePath.StartsWith("Assets/")) + { + resourcePath = "Assets/RegressionGames/Resources/" + resourcePath; + } + resourcePath += ".json"; + + File.Delete(resourcePath); + ProcessAndSendSegments(); + } + + #endregion + #region Send Messages private static void SendAvailableSequences([CanBeNull] TcpClient client = null) @@ -339,6 +426,45 @@ private static void SendActiveSequence([CanBeNull] TcpClient client = null) RGTcpServer.QueueMessage(message, client); } + private static void SendSequenceJson(string resourcePath, TcpClient client) + { + var botSequenceJson = BotSequence.LoadJsonResource(resourcePath).Item3; + var message = new TcpMessage + { + type = TcpMessageType.SendSequenceJson, + payload = new SendJsonTcpMessageData + { + jsonContent = botSequenceJson + } + }; + RGTcpServer.QueueMessage(message, client); + } + + private static void SendSegmentJson(string resourcePath, TcpClient client) + { + var botSegment = BotSequence.LoadBotSegmentOrBotSegmentListFromPath(resourcePath); + + var jsonContent = ""; + if (botSegment.Item3 is BotSegment segment) + { + jsonContent = segment.ToJsonString(); + } + else if (botSegment.Item3 is BotSegmentList segmentList) + { + jsonContent = segmentList.ToJsonString(); + } + + var message = new TcpMessage + { + type = TcpMessageType.SendSegmentJson, + payload = new SendJsonTcpMessageData + { + jsonContent = jsonContent, + } + }; + RGTcpServer.QueueMessage(message, client); + } + #endregion } } diff --git a/src/gg.regression.unity.bots/Runtime/Scripts/ClientDashboard/RGTcpServer.cs b/src/gg.regression.unity.bots/Runtime/Scripts/ClientDashboard/RGTcpServer.cs index 6185a34b..b420bb72 100644 --- a/src/gg.regression.unity.bots/Runtime/Scripts/ClientDashboard/RGTcpServer.cs +++ b/src/gg.regression.unity.bots/Runtime/Scripts/ClientDashboard/RGTcpServer.cs @@ -1,5 +1,7 @@ using System; using System.Collections.Concurrent; +using System.Collections.Generic; +using System.Linq; using System.Net; using System.Net.Sockets; using System.Text; @@ -8,6 +10,7 @@ using JetBrains.Annotations; using Newtonsoft.Json; using RegressionGames.StateRecorder; +using UnityEngine; namespace RegressionGames.ClientDashboard { @@ -320,7 +323,7 @@ private static string DecodeReceivedMessage(TcpClient client) // whether the full message has been sent from the client // currently not used here, but may need to consider partial frames // if we end up accepting large messages from client - bool fin = (bytes[0] & 0b10000000) != 0; + bool fin = (bytes[0] & 0b10000000) != 0; // must be true, "All messages from the client to the server have this bit set" bool mask = (bytes[1] & 0b10000000) != 0; @@ -368,10 +371,29 @@ private static string DecodeReceivedMessage(TcpClient client) byte[] decoded = new byte[msglen]; byte[] masks = new byte[4] { bytes[offset], bytes[offset + 1], bytes[offset + 2], bytes[offset + 3] }; offset += 4; - - for (ulong i = 0; i < msglen; ++i) + + if ((int)msglen > bytes.Length) + { + var buffer = new List(); + buffer.AddRange(bytes); + while (buffer.Count < (int)msglen) + { + byte[] temp = new byte[client.Available]; + client.GetStream().Read(temp, 0, temp.Length); + buffer.AddRange(temp); + } + + for (ulong i = 0; i < msglen; ++i) + { + decoded[i] = (byte)(buffer.ElementAt((int)offset + (int)i) ^ masks[i % 4]); + } + } + else { - decoded[i] = (byte)(bytes[offset + i] ^ masks[i % 4]); + for (ulong i = 0; i < msglen; ++i) + { + decoded[i] = (byte)(bytes[offset + i] ^ masks[i % 4]); + } } string decodedMessage = Encoding.UTF8.GetString(decoded); @@ -408,15 +430,16 @@ private static byte[] EncodeMessageToSend(string message) } else { - frame[1] = (byte)127; - frame[2] = (byte)((length >> 56) & 255); - frame[3] = (byte)((length >> 48) & 255); - frame[4] = (byte)((length >> 40) & 255); - frame[5] = (byte)((length >> 32) & 255); - frame[6] = (byte)((length >> 24) & 255); - frame[7] = (byte)((length >> 16) & 255); - frame[8] = (byte)((length >> 8) & 255); - frame[9] = (byte)(length & 255); + var lengthAsULong = Convert.ToUInt64(length); + frame[1] = 127; + frame[2] = (byte)((lengthAsULong >> 56) & 255); + frame[3] = (byte)((lengthAsULong >> 48) & 255); + frame[4] = (byte)((lengthAsULong >> 40) & 255); + frame[5] = (byte)((lengthAsULong >> 32) & 255); + frame[6] = (byte)((lengthAsULong >> 24) & 255); + frame[7] = (byte)((lengthAsULong >> 16) & 255); + frame[8] = (byte)((lengthAsULong >> 8) & 255); + frame[9] = (byte)(lengthAsULong & 255); indexStartRawData = 10; } diff --git a/src/gg.regression.unity.bots/Runtime/Scripts/ClientDashboard/TcpMessage.cs b/src/gg.regression.unity.bots/Runtime/Scripts/ClientDashboard/TcpMessage.cs index 8bc7cfc7..91e95329 100644 --- a/src/gg.regression.unity.bots/Runtime/Scripts/ClientDashboard/TcpMessage.cs +++ b/src/gg.regression.unity.bots/Runtime/Scripts/ClientDashboard/TcpMessage.cs @@ -10,39 +10,6 @@ namespace RegressionGames.ClientDashboard { - public enum TcpMessageType - { - // ===================== - // client -> server - // ===================== - - Ping, - - // client requests to play a resource with the given resourcePath - PlaySequence, - PlaySegment, - - // stops any currently-running sequence/segments - StopReplay, - - // ===================== - // server -> client - // ===================== - - Pong, - - // info about the available file-based resources for this game instance - AvailableSequences, - AvailableSegments, - - // info about the currently-running sequence (or segment) - ActiveSequence, - - // sent prior to closing Unity windows. - // this tells any running client windows to also close. - CloseConnection - } - public interface ITcpMessageData : IStringBuilderWriteable { } /// @@ -148,6 +115,8 @@ public void WriteToStringBuilder(StringBuilder stringBuilder) IntJsonConverter.WriteToStringBuilder(stringBuilder, currentSegment.apiVersion); // not normally serialized + stringBuilder.Append(",\"path\":"); + StringJsonConverter.WriteToStringBuilder(stringBuilder, currentSegment.path); stringBuilder.Append(",\"resourcePath\":"); StringJsonConverter.WriteToStringBuilder(stringBuilder, currentSegment.resourcePath); stringBuilder.Append(",\"type\":"); @@ -195,4 +164,91 @@ public override string ToString() } } + [Serializable] + public class RequestResourceContentsTcpMessageData : ITcpMessageData + { + public string resourcePath; + + public void WriteToStringBuilder(StringBuilder stringBuilder) + { + stringBuilder.Append("{\"resourcePath\":"); + StringJsonConverter.WriteToStringBuilder(stringBuilder, resourcePath); + stringBuilder.Append("}"); + } + + public override string ToString() + { + StringBuilder sb = new StringBuilder(1000); + WriteToStringBuilder(sb); + return sb.ToString(); + } + } + + [Serializable] + public class SendJsonTcpMessageData : ITcpMessageData + { + /** + * The JSON object to send. + * This should be a string that has already been serialized using a WriteToStringBuilder implementation + */ + public string jsonContent; + + public void WriteToStringBuilder(StringBuilder stringBuilder) + { + stringBuilder.Append("{\"jsonObject\":"); + stringBuilder.Append(jsonContent); + stringBuilder.Append("}"); + } + + public override string ToString() + { + StringBuilder sb = new StringBuilder(1000); + WriteToStringBuilder(sb); + return sb.ToString(); + } + } + + [Serializable] + public class SaveSegmentListTcpMessageData : ITcpMessageData + { + public BotSegmentList segmentList; + [CanBeNull] public string resourcePath; + + public void WriteToStringBuilder(StringBuilder stringBuilder) + { + stringBuilder.Append("{\"segmentList\":"); + segmentList.WriteToStringBuilder(stringBuilder); + stringBuilder.Append(",\"resourcePath\":"); + StringJsonConverter.WriteToStringBuilder(stringBuilder, resourcePath); + stringBuilder.Append("}"); + } + + public override string ToString() + { + StringBuilder sb = new StringBuilder(1000); + WriteToStringBuilder(sb); + return sb.ToString(); + } + } + + [Serializable] + public class DeleteSegmentTcpMessageData : ITcpMessageData + { + public string resourcePath; + + public void WriteToStringBuilder(StringBuilder stringBuilder) + { + stringBuilder.Append("{\"resourcePath\":"); + StringJsonConverter.WriteToStringBuilder(stringBuilder, resourcePath); + stringBuilder.Append("}"); + } + + public override string ToString() + { + StringBuilder sb = new StringBuilder(1000); + WriteToStringBuilder(sb); + return sb.ToString(); + } + } + } \ No newline at end of file diff --git a/src/gg.regression.unity.bots/Runtime/Scripts/ClientDashboard/TcpMessageDataJsonConverter.cs b/src/gg.regression.unity.bots/Runtime/Scripts/ClientDashboard/TcpMessageDataJsonConverter.cs index 9d737ae1..cfddcf72 100644 --- a/src/gg.regression.unity.bots/Runtime/Scripts/ClientDashboard/TcpMessageDataJsonConverter.cs +++ b/src/gg.regression.unity.bots/Runtime/Scripts/ClientDashboard/TcpMessageDataJsonConverter.cs @@ -39,6 +39,16 @@ public override object ReadJson(JsonReader reader, Type objectType, object exist case TcpMessageType.PlaySegment: payload = jObject["payload"].ToObject(serializer); break; + case TcpMessageType.RequestSequenceJson: + case TcpMessageType.RequestSegmentJson: + payload = jObject["payload"].ToObject(serializer); + break; + case TcpMessageType.SaveSegment: + payload = jObject["payload"].ToObject(serializer); + break; + case TcpMessageType.DeleteSegment: + payload = jObject["payload"].ToObject(serializer); + break; default: throw new JsonSerializationException($"Unsupported TcpMessage type: '{message.type}'"); } diff --git a/src/gg.regression.unity.bots/Runtime/Scripts/ClientDashboard/TcpMessageType.cs b/src/gg.regression.unity.bots/Runtime/Scripts/ClientDashboard/TcpMessageType.cs new file mode 100644 index 00000000..d52f84bd --- /dev/null +++ b/src/gg.regression.unity.bots/Runtime/Scripts/ClientDashboard/TcpMessageType.cs @@ -0,0 +1,46 @@ +namespace RegressionGames.ClientDashboard +{ + public enum TcpMessageType + { + // ===================== + // client -> server + // ===================== + + Ping, + + // client requests to play a resource with the given resourcePath + PlaySequence, + PlaySegment, + + // stops any currently-running sequence/segments + StopReplay, + + // request JSON contents for a file-based resource + RequestSequenceJson, + RequestSegmentJson, + + SaveSegment, + DeleteSegment, + + // ===================== + // server -> client + // ===================== + + Pong, + + // info about the available file-based resources for this game instance + AvailableSequences, + AvailableSegments, + + // info about the currently-running sequence (or segment) + ActiveSequence, + + // send JSON contents for a file-based resource + SendSequenceJson, + SendSegmentJson, + + // sent prior to closing Unity windows. + // this tells any running client windows to also close. + CloseConnection + } +} \ No newline at end of file diff --git a/src/gg.regression.unity.bots/Runtime/Scripts/ClientDashboard/TcpMessageType.cs.meta b/src/gg.regression.unity.bots/Runtime/Scripts/ClientDashboard/TcpMessageType.cs.meta new file mode 100644 index 00000000..ae2bd5de --- /dev/null +++ b/src/gg.regression.unity.bots/Runtime/Scripts/ClientDashboard/TcpMessageType.cs.meta @@ -0,0 +1,3 @@ +fileFormatVersion: 2 +guid: 1f292d14db764738aaa5d7865ab4f116 +timeCreated: 1734370041 \ No newline at end of file From 748f20852bbe57bc9a5c0bdb4014c622ef80e7c9 Mon Sep 17 00:00:00 2001 From: Abby Beizer Date: Fri, 20 Dec 2024 11:43:10 -0500 Subject: [PATCH 2/3] segment crud --- .../Scripts/ClientDashboard/RGTcpManager.cs | 64 ++--------------- .../BotSegments/Models/BotSegment.cs | 69 ++++++++++++++++++- 2 files changed, 72 insertions(+), 61 deletions(-) diff --git a/src/gg.regression.unity.bots/Runtime/Scripts/ClientDashboard/RGTcpManager.cs b/src/gg.regression.unity.bots/Runtime/Scripts/ClientDashboard/RGTcpManager.cs index d1db6c4c..cd838870 100644 --- a/src/gg.regression.unity.bots/Runtime/Scripts/ClientDashboard/RGTcpManager.cs +++ b/src/gg.regression.unity.bots/Runtime/Scripts/ClientDashboard/RGTcpManager.cs @@ -252,13 +252,15 @@ private static void ProcessClientMessage(TcpClient client, TcpMessage message) case TcpMessageType.SaveSegment: { var payload = (SaveSegmentListTcpMessageData) message.payload; - SaveSegment(payload.segmentList, payload.resourcePath); + BotSegment.SaveSegmentListAsJson(payload.segmentList, payload.resourcePath); + ProcessAndSendSegments(); break; } case TcpMessageType.DeleteSegment: { var payload = (DeleteSegmentTcpMessageData) message.payload; - DeleteSegment(payload.resourcePath); + BotSegment.Delete(payload.resourcePath); + ProcessAndSendSegments(); break; } } @@ -324,64 +326,6 @@ private static void ProcessAndSendSegments() m_availableBotSegments = BotSegment.LoadAllSegments().Values.Select(seg => seg.Item2).ToList(); SendAvailableSegments(); } - - private static void SaveSegment(BotSegmentList segmentList, [CanBeNull] string resourcePath) - { - string directoryPath = null; - -#if UNITY_EDITOR - directoryPath = "Assets/RegressionGames/Resources/BotSegments/"; -#else - directoryPath = Application.persistentDataPath + "/RegressionGames/Resources/BotSegments/"; -#endif - Directory.CreateDirectory(directoryPath); - - if (resourcePath != null) - { - // remove all text up to BotSegments/ - var index = resourcePath.IndexOf("BotSegments/"); - if (index != -1) - { - resourcePath = resourcePath.Substring(index + "BotSegments/".Length); - } - resourcePath = directoryPath + resourcePath + ".json"; - - File.Delete(resourcePath); - using var sw = File.CreateText(resourcePath); - sw.Write(segmentList.ToJsonString()); - sw.Close(); - } - else - { - // generate a path for this new SegmentList - var filepath = string.Join("-", segmentList.name.Split(" ")); - foreach (var c in Path.GetInvalidPathChars()) - { - filepath = filepath.Replace(c, '-'); - } - filepath = directoryPath + "/" + filepath + ".json"; - - using var sw = File.CreateText(filepath); - sw.Write(segmentList.ToJsonString()); - sw.Close(); - } - - // then refresh the list of available segments - ProcessAndSendSegments(); - } - - private static void DeleteSegment(string resourcePath) - { - resourcePath = resourcePath.Replace('\\', '/'); - if (!resourcePath.StartsWith("Assets/")) - { - resourcePath = "Assets/RegressionGames/Resources/" + resourcePath; - } - resourcePath += ".json"; - - File.Delete(resourcePath); - ProcessAndSendSegments(); - } #endregion diff --git a/src/gg.regression.unity.bots/Runtime/Scripts/StateRecorder/BotSegments/Models/BotSegment.cs b/src/gg.regression.unity.bots/Runtime/Scripts/StateRecorder/BotSegments/Models/BotSegment.cs index 43bfa4e4..451990bf 100644 --- a/src/gg.regression.unity.bots/Runtime/Scripts/StateRecorder/BotSegments/Models/BotSegment.cs +++ b/src/gg.regression.unity.bots/Runtime/Scripts/StateRecorder/BotSegments/Models/BotSegment.cs @@ -4,6 +4,7 @@ using System.Linq; using System.Text; using System.Threading; +using JetBrains.Annotations; using RegressionGames.StateRecorder.BotSegments.Models.BotCriteria; using RegressionGames.StateRecorder.JsonConverters; using RegressionGames.StateRecorder.Models; @@ -272,6 +273,18 @@ public void WriteToStringBuilder(StringBuilder stringBuilder) stringBuilder.Append("}"); } + private static string SegmentsDirectoryPath + { + get + { +#if UNITY_EDITOR + return "Assets/RegressionGames/Resources/BotSegments/"; +#else + return Application.persistentDataPath + "/RegressionGames/Resources/BotSegments/" +#endif + } + } + /** * * Loads all the Segments that exist in this project (for use in the Editor or in a build) @@ -336,7 +349,7 @@ public void WriteToStringBuilder(StringBuilder stringBuilder) return segments; } - + /** * * Recursively look through directories for Segment and segment list files, and load them @@ -360,6 +373,60 @@ public void WriteToStringBuilder(StringBuilder stringBuilder) return results; } + + /** + * + * Saves a BotSegmentList to a file + * + * The BotSegmentList to save to file + * The segment's resourcePath, if it already exists. If null, a new path will be generated for it. + */ + public static void SaveSegmentListAsJson(BotSegmentList segmentList, [CanBeNull] string resourcePath) + { + Directory.CreateDirectory(SegmentsDirectoryPath); + if (resourcePath != null) + { + // remove all text up to BotSegments/ + var index = resourcePath.IndexOf("BotSegments/"); + if (index != -1) + { + resourcePath = resourcePath.Substring(index + "BotSegments/".Length); + } + resourcePath = SegmentsDirectoryPath + resourcePath + ".json"; + File.Delete(resourcePath); + } + else + { + // generate a path for this new SegmentList + var generatedName = string.Join("-", segmentList.name.Split(" ")); + foreach (var c in Path.GetInvalidPathChars()) + { + generatedName = generatedName.Replace(c, '-'); + } + resourcePath = SegmentsDirectoryPath + generatedName + ".json"; + } + + using var sw = File.CreateText(resourcePath); + sw.Write(segmentList.ToJsonString()); + sw.Close(); + } + + /** + * + * Delete the BotSegment or BotSegmentList with the given resourcePath + * + */ + public static void Delete(string resourcePath) + { + resourcePath = resourcePath.Replace('\\', '/'); + if (!resourcePath.StartsWith("Assets/")) + { + resourcePath = "Assets/RegressionGames/Resources/" + resourcePath; + } + resourcePath += ".json"; + + File.Delete(resourcePath); + } } } From 8b38fdae260de366bad3bbffedbe7a8a5a0f3f53 Mon Sep 17 00:00:00 2001 From: Abby Beizer Date: Fri, 20 Dec 2024 15:25:09 -0500 Subject: [PATCH 3/3] overhaul tcp message decode --- .../Scripts/ClientDashboard/RGTcpServer.cs | 166 +++++++++++------- 1 file changed, 98 insertions(+), 68 deletions(-) diff --git a/src/gg.regression.unity.bots/Runtime/Scripts/ClientDashboard/RGTcpServer.cs b/src/gg.regression.unity.bots/Runtime/Scripts/ClientDashboard/RGTcpServer.cs index b420bb72..37299337 100644 --- a/src/gg.regression.unity.bots/Runtime/Scripts/ClientDashboard/RGTcpServer.cs +++ b/src/gg.regression.unity.bots/Runtime/Scripts/ClientDashboard/RGTcpServer.cs @@ -1,6 +1,7 @@ using System; using System.Collections.Concurrent; using System.Collections.Generic; +using System.IO; using System.Linq; using System.Net; using System.Net.Sockets; @@ -313,91 +314,120 @@ private static void SendMessage(TcpClient client, TcpMessage message) /// /// Decode message from client so we can process it + /// https://developer.mozilla.org/en-US/docs/Web/API/WebSockets_API/Writing_WebSocket_servers#exchanging_data_frames /// https://developer.mozilla.org/en-US/docs/Web/API/WebSockets_API/Writing_WebSocket_server#decoding_messages /// private static string DecodeReceivedMessage(TcpClient client) { - byte[] bytes = new byte[client.Available]; - client.GetStream().Read(bytes, 0, bytes.Length); - - // whether the full message has been sent from the client - // currently not used here, but may need to consider partial frames - // if we end up accepting large messages from client - bool fin = (bytes[0] & 0b10000000) != 0; - - // must be true, "All messages from the client to the server have this bit set" - bool mask = (bytes[1] & 0b10000000) != 0; - int opcode = bytes[0] & 0b00001111; + var clientStream = client.GetStream(); + var decodedBytes = new List(); - if (opcode == 8) + while (client.Connected) { - // this is a close frame - if (m_connectedClients.TryGetValue(client, out var clientActions)) + if (client.Available == 0) { - clientActions.ShouldClose = true; + // we check this condition before calling this method, + // but we may be waiting on a continuation frame here + continue; } - return null; - } - - if (opcode != 1) - { - // not a text message - return null; - } - - ulong offset = 2; - ulong msglen = bytes[1] & (ulong)0b01111111; - - if (msglen == 126) - { - // bytes are reversed because websocket will print them in Big-Endian, whereas - // BitConverter will want them arranged in little-endian on windows - msglen = BitConverter.ToUInt16(new byte[] { bytes[3], bytes[2] }, 0); - offset = 4; - } - else if (msglen == 127) - { - // To test the below code, we need to manually buffer larger messages — since the NIC's autobuffering - // may be too latency-friendly for this code to run (that is, we may have only some of the bytes in this - // websocket frame available through client.Available). - msglen = BitConverter.ToUInt64(new byte[] { - bytes[9], bytes[8], bytes[7], bytes[6], bytes[5], bytes[4], bytes[3], bytes[2] - }, 0); - offset = 10; - } - - if (msglen > 0 && mask) - { - byte[] decoded = new byte[msglen]; - byte[] masks = new byte[4] { bytes[offset], bytes[offset + 1], bytes[offset + 2], bytes[offset + 3] }; - offset += 4; - if ((int)msglen > bytes.Length) + // read the first byte of the message + // bit 0 -> fin, whether the full message has been received + // bit 1-3 -> reserved, we don't care about this right now + // bit 4-7 -> opcode, the type of message + var firstByte = (byte)clientStream.ReadByte(); + bool fin = (firstByte & 0b10000000) != 0; + int frameOpcode = firstByte & 0b00001111; + + switch (frameOpcode) { - var buffer = new List(); - buffer.AddRange(bytes); - while (buffer.Count < (int)msglen) + case 0x0: // Continuation Frame + case 0x1: // Text + case 0x2: // Binary + break; + case 0x8: // Connection close { - byte[] temp = new byte[client.Available]; - client.GetStream().Read(temp, 0, temp.Length); - buffer.AddRange(temp); + if (m_connectedClients.TryGetValue(client, out var clientActions)) + { + clientActions.ShouldClose = true; + } + return null; } + default: // Ping/Pong/Reserved + continue; + } + + // read the second byte of the message + // bit 0 -> mask, should be true for client > server messages + // bit 1-7 -> payload length + var secondByte = (byte)clientStream.ReadByte(); + bool mask = (secondByte & 0b10000000) != 0; + var psuedoLength = secondByte - 128; + + // to get the actual length of the payload, we need to interpret the psuedo length from the header + int payloadLength = 0; + + if (psuedoLength > 0 && psuedoLength <= 125) + { + // if length doesn't exceed 125, + // then this is the actual length of the payload + payloadLength = psuedoLength; + } + else if (psuedoLength == 126) + { + // Actual length will be the next 2 bytes + var lengthBytes = new byte[2]; + clientStream.Read(lengthBytes, 0, lengthBytes.Length); - for (ulong i = 0; i < msglen; ++i) - { - decoded[i] = (byte)(buffer.ElementAt((int)offset + (int)i) ^ masks[i % 4]); - } + // bytes are reversed because websocket will print them in Big-Endian, whereas + // BitConverter will want them arranged in little-endian on windows + payloadLength = BitConverter.ToUInt16(new byte[] { lengthBytes[1], lengthBytes[0] }, 0); + } + else if (psuedoLength == 127) + { + // Actual length will be the next 8 bytes + var lengthBytes = new byte[8]; + clientStream.Read(lengthBytes, 0, lengthBytes.Length); + + // bytes are reversed because websocket will print them in Big-Endian, whereas + // BitConverter will want them arranged in little-endian on windows + payloadLength = (int)BitConverter.ToUInt64(new byte[] { + lengthBytes[7], lengthBytes[6], lengthBytes[5], lengthBytes[4], + lengthBytes[3], lengthBytes[2], lengthBytes[1], lengthBytes[0] + }, 0); } - else + + // for now we're assuming client-sent messages are masked + // determine the masking key from the next 4 bytes + // this will be used to decode the message + var maskingKey = new byte[4]; + clientStream.Read(maskingKey, 0, maskingKey.Length); + + if (payloadLength == 0) { - for (ulong i = 0; i < msglen; ++i) - { - decoded[i] = (byte)(bytes[offset + i] ^ masks[i % 4]); - } + // no payload, so we're done + return null; + } + + // read the payload + // and decode it using the masking key + var payload = new byte[payloadLength]; + clientStream.Read(payload, 0, payloadLength); + + for (int i = 0; i < payload.Length; ++i) + { + decodedBytes.Add((byte)(payload[i] ^ maskingKey[i % 4])); } - string decodedMessage = Encoding.UTF8.GetString(decoded); - return decodedMessage; + if (!fin) + { + // if this isn't the complete message, + // then we need to wait for the continuation frame + continue; + } + + // we have the full message, so return it as a string + return Encoding.UTF8.GetString(decodedBytes.ToArray()); } return null;