From 74cd2ff992a9a372491babdf7ae600b68220bc7e Mon Sep 17 00:00:00 2001 From: Andrew Golledge Date: Thu, 24 Jun 2021 10:25:07 +0200 Subject: [PATCH] The BasicMessage class now takes a byte array as a parameter instead of a UTF-8 string. This byte array is then used for further parsing in the derived EventMessage class to save converting back again to a byte array from a UTF-8 string. Tests have been extended to include a DetectedSpeech event. There are two variants of this event which are used in the tests, one containing a response with English text in the body and another containing German text and umlaut characters. Add Logger initializer to MessageParsingTests class to allow its test cases to be run independently. Add new test case to MessageParsingTests to test for the successful extraction of body payload from the EventMessage. --- .../Sockets/MessageParsingTests.cs | 127 +++++++++++---- .../TestSupport/TestMessages.cs | 149 ++++++++++++++++++ NEventSocket/FreeSwitch/BasicMessage.cs | 9 +- NEventSocket/FreeSwitch/EventMessage.cs | 29 +++- NEventSocket/NEventSocket.csproj | 2 +- NEventSocket/Sockets/EventSocket.cs | 4 +- NEventSocket/Sockets/Parser.cs | 50 +++--- 7 files changed, 300 insertions(+), 70 deletions(-) diff --git a/NEventSocket.Tests/Sockets/MessageParsingTests.cs b/NEventSocket.Tests/Sockets/MessageParsingTests.cs index 13cd8ed..3abcb0b 100644 --- a/NEventSocket.Tests/Sockets/MessageParsingTests.cs +++ b/NEventSocket.Tests/Sockets/MessageParsingTests.cs @@ -1,24 +1,41 @@ using System; +using System.Text; using System.Collections.Generic; using System.Reactive.Linq; using NEventSocket.FreeSwitch; +using NEventSocket.Logging; using NEventSocket.Sockets; using NEventSocket.Tests.Properties; using NEventSocket.Tests.TestSupport; using NEventSocket.Util; using Xunit; +using Microsoft.Extensions.Logging; + namespace NEventSocket.Tests.Sockets { public class MessageParsingTests { - [Theory, MemberData(nameof(ExampleMessages))] + public MessageParsingTests() + { + PreventThreadPoolStarvation.Init(); + Logger.Configure(LoggerFactory.Create(builder => + { + builder + .AddFilter("Microsoft", LogLevel.Warning) + .AddFilter("System", LogLevel.Warning) + .AddFilter("LoggingConsoleApp.Program", LogLevel.Debug) + .AddConsole(); + })); + } + +[Theory, MemberData(nameof(ExampleMessages))] public void it_should_parse_the_expected_messages_from_a_stream(int expectedMessageCount, string exampleInput) { int parsedMessageCount = 0; - - exampleInput.ToObservable() - .AggregateUntil(() => new Parser(), (builder, ch) => builder.Append(ch), builder => builder.Completed) + byte[] exampleByteInput = Encoding.UTF8.GetBytes(exampleInput); + exampleByteInput.ToObservable() + .AggregateUntil(() => new Parser(), (builder, b) => builder.Append(b), builder => builder.Completed) .Select(parser => parser.ExtractMessage()) .Subscribe(_ => parsedMessageCount++); @@ -31,14 +48,17 @@ public void it_should_parse_the_expected_messages_from_a_stream(int expectedMess [InlineData(TestMessages.ConnectEvent)] [InlineData(TestMessages.DisconnectEvent)] [InlineData(TestMessages.PlaybackComplete)] + [InlineData(TestMessages.DetectedSpeech)] + [InlineData(TestMessages.DetectedSpeechEnglish)] public void can_parse_test_messages(string input) { var parser = new Parser(); var rawInput = input.Replace("\r\n", "\n") + "\n\n"; + byte[] byteInput = Encoding.UTF8.GetBytes(rawInput); - foreach (char c in rawInput) + foreach (byte b in byteInput) { - parser.Append(c); + parser.Append(b); } Assert.True(parser.Completed); @@ -51,13 +71,17 @@ public void can_parse_test_messages(string input) [Theory] [InlineData(TestMessages.BackgroundJob)] [InlineData(TestMessages.CallState)] + [InlineData(TestMessages.DetectedSpeech)] + [InlineData(TestMessages.DetectedSpeechEnglish)] public void it_should_extract_the_body_from_a_message(string input) { var parser = new Parser(); var rawInput = input.Replace("\r\n", "\n") + "\n\n"; - foreach (char c in rawInput) + byte[] byteInput = Encoding.UTF8.GetBytes(rawInput); + + foreach (byte b in byteInput) { - parser.Append(c); + parser.Append(b); } Assert.True(parser.Completed); @@ -65,7 +89,7 @@ public void it_should_extract_the_body_from_a_message(string input) BasicMessage payload = parser.ExtractMessage(); Assert.Equal(ContentTypes.EventPlain, payload.ContentType); Assert.NotNull(payload.BodyText); - Assert.Equal(payload.ContentLength, payload.BodyText.Length); + Assert.Equal(payload.ContentLength, payload.BodyBytes.Length); Console.WriteLine(payload.ToString()); } @@ -73,13 +97,17 @@ public void it_should_extract_the_body_from_a_message(string input) [Theory] [InlineData(TestMessages.BackgroundJob, EventName.BackgroundJob)] [InlineData(TestMessages.CallState, EventName.ChannelCallstate)] + [InlineData(TestMessages.DetectedSpeech, EventName.DetectedSpeech)] + [InlineData(TestMessages.DetectedSpeechEnglish, EventName.DetectedSpeech)] public void it_should_parse_event_messages(string input, EventName eventName) { var parser = new Parser(); var rawInput = input.Replace("\r\n", "\n") + "\n\n"; - foreach (char c in rawInput) + byte[] byteInput = Encoding.UTF8.GetBytes(rawInput); + + foreach (byte b in byteInput) { - parser.Append(c); + parser.Append(b); } Assert.True(parser.Completed); @@ -91,16 +119,44 @@ public void it_should_parse_event_messages(string input, EventName eventName) Console.WriteLine(eventMessage.ToString()); } + [Theory] + [InlineData(TestMessages.DetectedSpeech, EventName.DetectedSpeech)] + [InlineData(TestMessages.DetectedSpeechEnglish, EventName.DetectedSpeech)] + public void it_should_parse_event_messages_and_extract_body_payload(string input, EventName eventName) + { + var parser = new Parser(); + var rawInput = input.Replace("\r\n", "\n") + "\n\n"; + byte[] byteInput = Encoding.UTF8.GetBytes(rawInput); + + foreach (byte b in byteInput) + { + parser.Append(b); + } + + Assert.True(parser.Completed); + + var eventMessage = new EventMessage(parser.ExtractMessage()); + Assert.NotNull(eventMessage); + Assert.Equal(eventName, eventMessage.EventName); + + var contentLength = int.Parse(eventMessage.Headers[HeaderNames.ContentLength]); + + Assert.Equal(contentLength, Encoding.UTF8.GetByteCount(eventMessage.BodyText)); + + Console.WriteLine(eventMessage.ToString()); + } + [Fact] public void it_should_parse_BackgroundJobResult_OK() { var input = TestMessages.BackgroundJob; var parser = new Parser(); var rawInput = input.Replace("\r\n", "\n") + "\n\n"; + byte[] byteInput = Encoding.UTF8.GetBytes(rawInput); - foreach (char c in rawInput) + foreach (byte b in byteInput) { - parser.Append(c); + parser.Append(b); } Assert.True(parser.Completed); @@ -118,10 +174,11 @@ public void it_should_parse_BackgroundJobResult_ERR() var input = TestMessages.BackgroundJobError; var parser = new Parser(); var rawInput = input.Replace("\r\n", "\n") + "\n\n"; + byte[] byteInput = Encoding.UTF8.GetBytes(rawInput); - foreach (char c in rawInput) + foreach (byte b in byteInput) { - parser.Append(c); + parser.Append(b); } Assert.True(parser.Completed); @@ -139,10 +196,11 @@ public void it_should_parse_Command_Reply_OK() { var parser = new Parser(); var rawInput = "Content-Type: command/reply\nReply-Text: +OK\n\n"; + byte[] byteInput = Encoding.UTF8.GetBytes(rawInput); - foreach (char c in rawInput) + foreach (byte b in byteInput) { - parser.Append(c); + parser.Append(b); } Assert.True(parser.Completed); @@ -159,10 +217,11 @@ public void it_should_parse_Command_Reply_ERR() { var parser = new Parser(); var rawInput = "Content-Type: command/reply\nReply-Text: -ERR Error\n\n"; + byte[] byteInput = Encoding.UTF8.GetBytes(rawInput); - foreach (char c in rawInput) + foreach (byte b in byteInput) { - parser.Append(c); + parser.Append(b); } Assert.True(parser.Completed); @@ -180,10 +239,11 @@ public void it_should_parse_Api_Response_OK() { var parser = new Parser(); var rawInput = "Content-Type: api/response\nContent-Length: 3\n\n+OK"; + byte[] byteInput = Encoding.UTF8.GetBytes(rawInput); - foreach (char c in rawInput) + foreach (byte b in byteInput) { - parser.Append(c); + parser.Append(b); } Assert.True(parser.Completed); @@ -200,10 +260,11 @@ public void it_should_parse_Api_Response_ERR() { var parser = new Parser(); var rawInput = "Content-Type: api/response\nContent-Length: 10\n\n-ERR Error"; + byte[] byteInput = Encoding.UTF8.GetBytes(rawInput); - foreach (char c in rawInput) + foreach (byte b in byteInput) { - parser.Append(c); + parser.Append(b); } Assert.True(parser.Completed); @@ -221,10 +282,11 @@ public void it_should_treat_Api_Response_ERR_no_reply_as_Success() { var parser = new Parser(); var rawInput = "Content-Type: api/response\nContent-Length: 13\n\n-ERR no reply"; + byte[] byteInput = Encoding.UTF8.GetBytes(rawInput); - foreach (char c in rawInput) + foreach (byte b in byteInput) { - parser.Append(c); + parser.Append(b); } Assert.True(parser.Completed); @@ -242,10 +304,11 @@ public void it_should_trim_new_lines_from__the_end_of_ApiResponse_Body_text() { var parser = new Parser(); var rawInput = "Content-Type: api/response\nContent-Length: 14\n\n-ERR no reply\n"; + byte[] byteInput = Encoding.UTF8.GetBytes(rawInput); - foreach (char c in rawInput) + foreach (byte b in byteInput) { - parser.Append(c); + parser.Append(b); } Assert.True(parser.Completed); @@ -269,9 +332,10 @@ public void Can_parse_example_sessions_to_completion(string input) } bool gotDisconnectNotice = false; + byte[] byteInput = Encoding.UTF8.GetBytes(input); - input.ToObservable() - .AggregateUntil(() => new Parser(), (builder, ch) => builder.Append(ch), builder => builder.Completed) + byteInput.ToObservable() + .AggregateUntil(() => new Parser(), (builder, b) => builder.Append(b), builder => builder.Completed) .Select(parser => parser.ExtractMessage()) .Subscribe( m => @@ -297,8 +361,9 @@ public void Can_parse_disconnect_notice() Disconnected, goodbye. See you at ClueCon! http://www.cluecon.com/ "; - msg.ToObservable() - .AggregateUntil(() => new Parser(), (builder, ch) => builder.Append(ch), builder => builder.Completed) + byte[] byteMsg = Encoding.UTF8.GetBytes(msg); + byteMsg.ToObservable() + .AggregateUntil(() => new Parser(), (builder, b) => builder.Append(b), builder => builder.Completed) .Select(parser => parser.ExtractMessage()) .Subscribe( Console.WriteLine); diff --git a/NEventSocket.Tests/TestSupport/TestMessages.cs b/NEventSocket.Tests/TestSupport/TestMessages.cs index 4d7981a..1c07f18 100644 --- a/NEventSocket.Tests/TestSupport/TestMessages.cs +++ b/NEventSocket.Tests/TestSupport/TestMessages.cs @@ -278,6 +278,155 @@ public class TestMessages Disconnected, goodbye. See you at ClueCon! http://www.cluecon.com/"; + public const string DetectedSpeech = @"Content-Length: 2471 +Content-Type: text/event-plain + +Event-Name: DETECTED_SPEECH +Core-UUID: ec530c4e-484d-473e-9bd0-073863f752eb +FreeSWITCH-Hostname: ser +FreeSWITCH-Switchname: ser +FreeSWITCH-IPv4: 192.168.1.104 +FreeSWITCH-IPv6: %3A%3A1 +Event-Date-Local: 2021-06-28%2023%3A20%3A38 +Event-Date-GMT: Mon,%2028%20Jun%202021%2021%3A20%3A38%20GMT +Event-Date-Timestamp: 1624915238513391 +Event-Calling-File: switch_ivr_async.c +Event-Calling-Function: speech_thread +Event-Calling-Line-Number: 4947 +Event-Sequence: 251969 +Speech-Type: detected-speech +ASR-Completion-Cause: 0 +Channel-State: CS_EXECUTE +Channel-Call-State: ACTIVE +Channel-State-Number: 4 +Channel-Name: sofia/internal/%2B491734064561%40172.16.50.128 +Unique-ID: f5228692-9715-4b6f-b54b-9e35be1a7335 +Call-Direction: inbound +Presence-Call-Direction: inbound +Channel-HIT-Dialplan: true +Channel-Presence-ID: %2B491734064561%40172.16.50.128 +Channel-Call-UUID: f5228692-9715-4b6f-b54b-9e35be1a7335 +Answer-State: answered +Channel-Read-Codec-Name: G722 +Channel-Read-Codec-Rate: 16000 +Channel-Read-Codec-Bit-Rate: 64000 +Channel-Write-Codec-Name: G722 +Channel-Write-Codec-Rate: 16000 +Channel-Write-Codec-Bit-Rate: 64000 +Caller-Direction: inbound +Caller-Logical-Direction: inbound +Caller-Username: %2B491734000000 +Caller-Dialplan: XML +Caller-Caller-ID-Name: %2B491734000000 +Caller-Caller-ID-Number: %2B491734000000 +Caller-Orig-Caller-ID-Name: %2B491734000000 +Caller-Orig-Caller-ID-Number: %2B491734000000 +Caller-Network-Addr: 10.1.10.100 +Caller-ANI: %2B491734000000 +Caller-Destination-Number: 493000000000000 +Caller-Unique-ID: f5228692-9715-4b6f-b54b-9e35be1a7335 +Caller-Source: mod_sofia +Caller-Context: public +Caller-Channel-Name: sofia/internal/%2B491734064561%40172.16.50.128 +Caller-Profile-Index: 1 +Caller-Profile-Created-Time: 1624915212053386 +Caller-Channel-Created-Time: 1624915212053386 +Caller-Channel-Answered-Time: 1624915222293356 +Caller-Channel-Progress-Time: 0 +Caller-Channel-Progress-Media-Time: 1624915216013390 +Caller-Channel-Hangup-Time: 0 +Caller-Channel-Transfer-Time: 0 +Caller-Channel-Resurrect-Time: 0 +Caller-Channel-Bridged-Time: 0 +Caller-Channel-Last-Hold: 0 +Caller-Channel-Hold-Accum: 0 +Caller-Screen-Bit: true +Caller-Privacy-Hide-Name: false +Caller-Privacy-Hide-Number: false +Content-Length: 261 + + + + + Verlängerung Störung Bestätigung + Verlängerung Störung Bestätigung + +"; + + public const string DetectedSpeechEnglish = @"Content-Length: 2465 +Content-Type: text/event-plain + +Event-Name: DETECTED_SPEECH +Core-UUID: ec530c4e-484d-473e-9bd0-073863f752eb +FreeSWITCH-Hostname: ser +FreeSWITCH-Switchname: ser +FreeSWITCH-IPv4: 192.168.1.104 +FreeSWITCH-IPv6: %3A%3A1 +Event-Date-Local: 2021-06-28%2023%3A20%3A38 +Event-Date-GMT: Mon,%2028%20Jun%202021%2021%3A20%3A38%20GMT +Event-Date-Timestamp: 1624915238513391 +Event-Calling-File: switch_ivr_async.c +Event-Calling-Function: speech_thread +Event-Calling-Line-Number: 4947 +Event-Sequence: 251969 +Speech-Type: detected-speech +ASR-Completion-Cause: 0 +Channel-State: CS_EXECUTE +Channel-Call-State: ACTIVE +Channel-State-Number: 4 +Channel-Name: sofia/internal/%2B491734064561%40172.16.50.128 +Unique-ID: f5228692-9715-4b6f-b54b-9e35be1a7335 +Call-Direction: inbound +Presence-Call-Direction: inbound +Channel-HIT-Dialplan: true +Channel-Presence-ID: %2B491734064561%40172.16.50.128 +Channel-Call-UUID: f5228692-9715-4b6f-b54b-9e35be1a7335 +Answer-State: answered +Channel-Read-Codec-Name: G722 +Channel-Read-Codec-Rate: 16000 +Channel-Read-Codec-Bit-Rate: 64000 +Channel-Write-Codec-Name: G722 +Channel-Write-Codec-Rate: 16000 +Channel-Write-Codec-Bit-Rate: 64000 +Caller-Direction: inbound +Caller-Logical-Direction: inbound +Caller-Username: %2B491734000000 +Caller-Dialplan: XML +Caller-Caller-ID-Name: %2B491734000000 +Caller-Caller-ID-Number: %2B491734000000 +Caller-Orig-Caller-ID-Name: %2B491734000000 +Caller-Orig-Caller-ID-Number: %2B491734000000 +Caller-Network-Addr: 10.1.10.100 +Caller-ANI: %2B491734000000 +Caller-Destination-Number: 493000000000000 +Caller-Unique-ID: f5228692-9715-4b6f-b54b-9e35be1a7335 +Caller-Source: mod_sofia +Caller-Context: public +Caller-Channel-Name: sofia/internal/%2B491734064561%40172.16.50.128 +Caller-Profile-Index: 1 +Caller-Profile-Created-Time: 1624915212053386 +Caller-Channel-Created-Time: 1624915212053386 +Caller-Channel-Answered-Time: 1624915222293356 +Caller-Channel-Progress-Time: 0 +Caller-Channel-Progress-Media-Time: 1624915216013390 +Caller-Channel-Hangup-Time: 0 +Caller-Channel-Transfer-Time: 0 +Caller-Channel-Resurrect-Time: 0 +Caller-Channel-Bridged-Time: 0 +Caller-Channel-Last-Hold: 0 +Caller-Channel-Hold-Accum: 0 +Caller-Screen-Bit: true +Caller-Privacy-Hide-Name: false +Caller-Privacy-Hide-Number: false +Content-Length: 255 + + + + + Extensions Problems Confirmation + Extensions Problems Confirmation + +"; public const string PlaybackComplete = @"Content-Length: 7209 Content-Type: text/event-plain diff --git a/NEventSocket/FreeSwitch/BasicMessage.cs b/NEventSocket/FreeSwitch/BasicMessage.cs index b985200..afa0e53 100644 --- a/NEventSocket/FreeSwitch/BasicMessage.cs +++ b/NEventSocket/FreeSwitch/BasicMessage.cs @@ -9,7 +9,7 @@ namespace NEventSocket.FreeSwitch using System; using System.Collections.Generic; using System.Linq; - + using System.Text; using NEventSocket.Util; using NEventSocket.Util.ObjectPooling; @@ -25,10 +25,11 @@ internal BasicMessage(IDictionary headers) Headers = new Dictionary(headers, StringComparer.OrdinalIgnoreCase); } - internal BasicMessage(IDictionary headers, string body) + internal BasicMessage(IDictionary headers, byte[] bodyBytes) { Headers = new Dictionary(headers, StringComparer.OrdinalIgnoreCase); - BodyText = body; + BodyText = Encoding.UTF8.GetString(bodyBytes); + BodyBytes = bodyBytes; } /// @@ -48,6 +49,8 @@ protected BasicMessage() /// public string BodyText { get; protected set; } + public byte[] BodyBytes { get; protected set; } + /// /// Gets the Content Type header. /// diff --git a/NEventSocket/FreeSwitch/EventMessage.cs b/NEventSocket/FreeSwitch/EventMessage.cs index 8ce0d0c..86d1b53 100644 --- a/NEventSocket/FreeSwitch/EventMessage.cs +++ b/NEventSocket/FreeSwitch/EventMessage.cs @@ -7,9 +7,10 @@ namespace NEventSocket.FreeSwitch { using System; + using System.Collections.Generic; using System.Diagnostics; using System.Linq; - + using System.Text; using Microsoft.Extensions.Logging; using NEventSocket.Logging; @@ -22,6 +23,7 @@ namespace NEventSocket.FreeSwitch [Serializable] public class EventMessage : BasicMessage { + public static readonly byte[] delimiterBytes = Encoding.UTF8.GetBytes("\n\n"); private static readonly ILogger log = Logger.Get(); internal EventMessage(BasicMessage basicMessage) @@ -60,8 +62,8 @@ internal EventMessage(BasicMessage basicMessage) try { - var delimiterIndex = basicMessage.BodyText.IndexOf("\n\n", StringComparison.Ordinal); - if (delimiterIndex == -1 || delimiterIndex == basicMessage.BodyText.Length - 2) + var delimiterIndex = PatternAt(basicMessage.BodyBytes, delimiterBytes); + if (delimiterIndex == -1 || delimiterIndex == basicMessage.BodyBytes.Length - 2) { // body text consists of key-value-pair event headers, no body Headers = basicMessage.BodyText.ParseKeyValuePairs(": "); @@ -71,13 +73,16 @@ internal EventMessage(BasicMessage basicMessage) { // ...but some Event Messages also carry a body payload, eg. a BACKGROUND_JOB event // which is a message carried inside an EventMessage carried inside a BasicMessage.. - Headers = basicMessage.BodyText.Substring(0, delimiterIndex).ParseKeyValuePairs(": "); + string headersSection = Encoding.UTF8.GetString(basicMessage.BodyBytes, 0, delimiterIndex); + Headers = headersSection.ParseKeyValuePairs(": "); Debug.Assert(Headers.ContainsKey(HeaderNames.ContentLength)); var contentLength = int.Parse(Headers[HeaderNames.ContentLength]); - Debug.Assert(delimiterIndex + 2 + contentLength <= basicMessage.BodyText.Length, "Message cut off mid-transmission"); - var body = basicMessage.BodyText.Substring(delimiterIndex + 2, contentLength); + Debug.Assert(Headers.ContainsKey(HeaderNames.EventName)); + + Debug.Assert(delimiterIndex + 2 + contentLength <= basicMessage.BodyBytes.Length, "Message cut off mid-transmission"); + var body = Encoding.UTF8.GetString(basicMessage.BodyBytes, delimiterIndex + 2, contentLength); //remove any \n\n if any var index = body.IndexOf("\n\n", StringComparison.Ordinal); @@ -99,6 +104,18 @@ protected EventMessage() { } + private static int PatternAt(byte[] source, byte[] pattern) + { + for (int i = 0; i < source.Length; i++) + { + if (source.Skip(i).Take(pattern.Length).SequenceEqual(pattern)) + { + return i; + } + } + return -1; + } + /// /// Gets the of this instance. /// diff --git a/NEventSocket/NEventSocket.csproj b/NEventSocket/NEventSocket.csproj index b850a7f..9e81c14 100644 --- a/NEventSocket/NEventSocket.csproj +++ b/NEventSocket/NEventSocket.csproj @@ -7,7 +7,7 @@ iamkinetic NEventSocket.DotNetCore .Net Core port of NEventSocket - 2.0.2 + 2.0.3 https://github.com/iamkinetic/NEventSocket FreeSwitch NEventSocket DotNetCore - .Net Standard 2.0 support. diff --git a/NEventSocket/Sockets/EventSocket.cs b/NEventSocket/Sockets/EventSocket.cs index 29c664a..47e5f6c 100644 --- a/NEventSocket/Sockets/EventSocket.cs +++ b/NEventSocket/Sockets/EventSocket.cs @@ -50,8 +50,8 @@ protected EventSocket(TcpClient tcpClient, TimeSpan? responseTimeOut = null) : b ResponseTimeOut = responseTimeOut ?? TimeSpan.FromSeconds(5); messages = - Receiver.SelectMany(x => Encoding.UTF8.GetString(x)) - .AggregateUntil(() => new Parser(), (builder, ch) => builder.Append(ch), builder => builder.Completed) + Receiver.SelectMany(x => x) + .AggregateUntil(() => new Parser(), (builder, b) => builder.Append(b), builder => builder.Completed) .Select(builder => builder.ExtractMessage()) .Do( x => Log.LogTrace("Messages Received [{0}].".Fmt(x.ContentType)), diff --git a/NEventSocket/Sockets/Parser.cs b/NEventSocket/Sockets/Parser.cs index df85376..9584351 100644 --- a/NEventSocket/Sockets/Parser.cs +++ b/NEventSocket/Sockets/Parser.cs @@ -8,27 +8,28 @@ namespace NEventSocket.Sockets using System; using System.Collections.Generic; using System.Diagnostics; + using System.IO; using System.Text; - using NEventSocket.FreeSwitch; using NEventSocket.Util; - using NEventSocket.Util.ObjectPooling; /// /// A parser for converting a stream of strings or chars into a stream of s from FreeSwitch. /// public class Parser : IDisposable { - private StringBuilder buffer = StringBuilderPool.Allocate(); - - private char previous; + private byte previous; private int? contentLength; private IDictionary headers; + private MemoryStream headerBytes = new MemoryStream(); + private MemoryStream bodyBytes; + private readonly InterlockedBoolean disposed = new InterlockedBoolean(); + private const byte headerEndDelimiter = (byte)'\n'; ~Parser() { Dispose(false); @@ -45,26 +46,25 @@ public class Parser : IDisposable public bool HasBody => contentLength.HasValue && contentLength > 0; /// - /// Appends the given to the message. + /// Appends the given to the message. /// - /// The next of the message. + /// The next of the message. /// The same instance of the . - public Parser Append(char next) + public Parser Append(byte next) { if (Completed) { return new Parser().Append(next); } - buffer.Append(next); - if (!HasBody) { + headerBytes.WriteByte(next); // we're parsing the headers - if (previous == '\n' && next == '\n') + if (previous == headerEndDelimiter && next == headerEndDelimiter) { // \n\n denotes the end of the Headers - var headerString = buffer.ToString(); + var headerString = Encoding.UTF8.GetString(headerBytes.ToArray()); headers = headerString.ParseKeyValuePairs(": "); @@ -79,10 +79,7 @@ public Parser Append(char next) else { // start parsing the body content - buffer.Clear(); - - // allocate the buffer up front given that we now know the expected size - buffer.EnsureCapacity(contentLength.Value); + bodyBytes = new MemoryStream(contentLength.Value); } } else @@ -98,8 +95,11 @@ public Parser Append(char next) } else { + Debug.Assert(bodyBytes != null); + bodyBytes.WriteByte(next); // if we've read the Content-Length amount of bytes then we're done - Completed = buffer.Length == contentLength.GetValueOrDefault() || contentLength == 0; + Debug.Assert(contentLength > 0); + Completed = bodyBytes.Length == contentLength.GetValueOrDefault(); } return this; @@ -108,11 +108,11 @@ public Parser Append(char next) /// /// Appends the provided string to the internal buffer. /// - public Parser Append(string next) + public Parser Append(byte[] next) { var parser = this; - foreach (var c in next) + foreach (var b in next) { parser = parser.Append(next); } @@ -141,17 +141,17 @@ public BasicMessage ExtractMessage() if (HasBody) { - errorMessage += "expected a body with length {0}, got {1} instead.".Fmt(contentLength, buffer.Length); + errorMessage += "expected a body with length {0}, got {1} instead.".Fmt(contentLength, bodyBytes.Length); } throw new InvalidOperationException(errorMessage); } - var result = HasBody ? new BasicMessage(headers, buffer.ToString()) : new BasicMessage(headers); + var result = HasBody ? new BasicMessage(headers, bodyBytes.ToArray()) : new BasicMessage(headers); if (HasBody) { - Debug.Assert(result.BodyText.Length == result.ContentLength); + Debug.Assert(bodyBytes.Length == result.ContentLength); } Dispose(); @@ -168,11 +168,7 @@ protected virtual void Dispose(bool disposing) { if (disposed != null && !disposed.EnsureCalledOnce()) { - if (buffer != null) - { - StringBuilderPool.Free(buffer); - buffer = null; - } + bodyBytes = null; } } }