//****************************************************************************************************** // TcpSimpleClient.cs - Gbtc // // Copyright © 2020, Grid Protection Alliance. All Rights Reserved. // // Licensed to the Grid Protection Alliance (GPA) under one or more contributor license agreements. See // the NOTICE file distributed with this work for additional information regarding copyright ownership. // The GPA licenses this file to you under the MIT License (MIT), the "License"; you may not use this // file except in compliance with the License. You may obtain a copy of the License at: // // http://opensource.org/licenses/MIT // // Unless agreed to in writing, the subject software distributed under the License is distributed on an // "AS-IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. Refer to the // License for the specific language governing permissions and limitations. // // Code Modification History: // ---------------------------------------------------------------------------------------------------- // 05/13/2020 - J. Ritchie Carroll // Generated original version of source code. // //****************************************************************************************************** using System; using System.Collections.Generic; using System.ComponentModel; using System.Linq; using System.Net.Sockets; using System.Text.RegularExpressions; using System.Threading; using System.Threading.Tasks; using GSF.Configuration; using GSF.Diagnostics; using TcpClientSocket = System.Net.Sockets.TcpClient; namespace GSF.Communication { /// /// Represents a simple implementation of a TCP-based communication client. /// public class TcpSimpleClient : ClientBase { #region [ Members ] // Nested Types private class TcpClientProvider : TransportProvider { public int Offset; public int PayloadLength = -1; } // Constants /// /// Specifies the default value for the property. /// public const bool DefaultPayloadAware = false; /// /// Specifies the default value for the property. /// public const bool DefaultNoDelay = false; /// /// Specifies the default value for the property. /// public const string DefaultConnectionString = "Server=localhost:8888"; // Fields private readonly TcpClientProvider m_tcpClient; private byte[] m_payloadMarker; private EndianOrder m_payloadEndianOrder; private IPStack m_ipStack; private string[] m_serverList; private Dictionary m_connectData; private ManualResetEvent m_connectWaitHandle; private Func m_cancelReadAsync; private bool m_disposed; #endregion #region [ Constructors ] /// /// Initializes a new instance of the class. /// public TcpSimpleClient() : this(DefaultConnectionString) { } /// /// Initializes a new instance of the class. /// /// Connect string of the . See for format. public TcpSimpleClient(string connectString) : base(TransportProtocol.Tcp, connectString) { m_tcpClient = new TcpClientProvider(); PayloadAware = DefaultPayloadAware; m_payloadMarker = Payload.DefaultMarker; m_payloadEndianOrder = EndianOrder.LittleEndian; NoDelay = DefaultNoDelay; } /// /// Initializes a new instance of the class. /// /// object that contains the . public TcpSimpleClient(IContainer container) : this() => container?.Add(this); #endregion #region [ Properties ] /// /// Gets or sets a boolean value that indicates whether the payload boundaries are to be preserved during transmission. /// [Category("Data")] [DefaultValue(DefaultPayloadAware)] [Description("Indicates whether the payload boundaries are to be preserved during transmission.")] public bool PayloadAware { get; set; } /// /// Gets or sets the byte sequence used to mark the beginning of a payload in a transmission. /// /// /// Setting property to null will create a zero-length payload marker. /// [Browsable(false)] [DesignerSerializationVisibility(DesignerSerializationVisibility.Hidden)] public byte[] PayloadMarker { get => m_payloadMarker; set => m_payloadMarker = value ?? Array.Empty(); } /// /// Gets or sets the endian order to apply for encoding and decoding payload size in a transmission. /// /// /// Setting property to null will force use of little-endian encoding. /// [Browsable(false)] [DesignerSerializationVisibility(DesignerSerializationVisibility.Hidden)] public EndianOrder PayloadEndianOrder { get => m_payloadEndianOrder; set => m_payloadEndianOrder = value ?? EndianOrder.LittleEndian; } /// /// Gets or sets a boolean value that determines if small packets are delivered to the remote host without delay. /// [Category("Settings")] [DefaultValue(DefaultNoDelay)] [Description("Determines if small packets are delivered to the remote host without delay.")] public bool NoDelay { get; set; } /// /// Gets the object for the . /// [Browsable(false)] public Socket Client => m_tcpClient.Provider?.Client; /// /// Gets the server URI of the . /// [Browsable(false)] public override string ServerUri => $"{TransportProtocol}://{ServerList[ServerIndex]}".ToLower(); /// /// Determines whether the base class should track statistics. /// protected override bool TrackStatistics => false; // Gets server connect data as an array - will always be at least one empty string, not null private string[] ServerList { get { if (!(m_serverList is null)) return m_serverList; if (m_connectData is null || !m_connectData.TryGetValue("server", out string serverList) || string.IsNullOrWhiteSpace(serverList)) return Array.Empty(); return m_serverList = serverList.Split(new[] { ',' }, StringSplitOptions.RemoveEmptyEntries).Select(server => server.Trim()).ToArray(); } } #endregion #region [ Methods ] /// /// Releases the unmanaged resources used by the and optionally releases the managed resources. /// /// true to release both managed and unmanaged resources; false to release only unmanaged resources. protected override void Dispose(bool disposing) { if (m_disposed) return; try { if (!disposing) return; if (m_connectWaitHandle != null) { m_connectWaitHandle.Set(); m_connectWaitHandle.Dispose(); m_connectWaitHandle = null; } Task readTask = m_cancelReadAsync?.Invoke(); if (!s_onReadThread) readTask?.Wait(TimeSpan.FromSeconds(5.0D)); } finally { m_disposed = true; // Prevent duplicate dispose. base.Dispose(disposing); // Call base class Dispose(). } } /// /// Saves settings to the config file if the property is set to true. /// public override void SaveSettings() { base.SaveSettings(); if (!PersistSettings) return; // Save settings under the specified category. ConfigurationFile config = ConfigurationFile.Current; CategorizedSettingsElementCollection settings = config.Settings[SettingsCategory]; settings["PayloadAware", true].Update(PayloadAware); settings["NoDelay", true].Update(NoDelay); config.Save(); } /// /// Loads saved settings from the config file if the property is set to true. /// public override void LoadSettings() { base.LoadSettings(); if (!PersistSettings) return; // Load settings from the specified category. ConfigurationFile config = ConfigurationFile.Current; CategorizedSettingsElementCollection settings = config.Settings[SettingsCategory]; settings.Add("PayloadAware", PayloadAware, "True if payload boundaries are to be preserved during transmission, otherwise False."); settings.Add("NoDelay", NoDelay, "True to disable Nagle so that small packets are delivered to the remote host without delay, otherwise False."); PayloadAware = settings["PayloadAware"].ValueAs(PayloadAware); NoDelay = settings["NoDelay"].ValueAs(NoDelay); } /// /// Connects the to the server asynchronously. /// /// for the asynchronous operation. public override WaitHandle ConnectAsync() { TcpClientSocket tcpClient = null; if (CurrentState != ClientState.Disconnected || m_disposed) return m_connectWaitHandle; void fail(Exception ex) { // Log exception during connection attempt OnConnectionException(ex); // ReSharper disable once AccessToDisposedClosure tcpClient?.Close(); // Notify of terminated connection OnConnectionTerminated(); // Ensure that the wait handle is set so that operations waiting // for completion of the asynchronous connection loop can continue m_connectWaitHandle?.Set(); } try { // If we do not already have a wait handle to use for connections, get one from the base class if (m_connectWaitHandle == null) m_connectWaitHandle = (ManualResetEvent)base.ConnectAsync(); m_tcpClient.SetReceiveBuffer(ReceiveBufferSize); tcpClient = new TcpClientSocket(Transport.CreateEndPoint(m_connectData["interface"], 0, m_ipStack)); using (m_tcpClient.Provider) m_tcpClient.Provider = tcpClient; OnConnectionAttempt(); m_connectWaitHandle.Reset(); // Overwrite config file value if "noDelay" option exists in connection string if (m_connectData.TryGetValue("noDelay", out string noDelaySetting)) NoDelay = noDelaySetting.ParseBoolean(); tcpClient.Client.NoDelay = NoDelay; Match endpoint = Regex.Match(ServerList[ServerIndex], Transport.EndpointFormatRegex); // Initiate the asynchronous connection loop tcpClient.ConnectAsync(endpoint.Groups["host"].Value, int.Parse(endpoint.Groups["port"].Value)).ContinueWith(task => { try { // Notify of established connection m_connectWaitHandle?.Set(); OnConnectionEstablished(); // Set up read cancellation before starting the read loop CancellationTokenSource cancellationTokenSource = new CancellationTokenSource(); TaskCompletionSource taskCompletionSource = new TaskCompletionSource(); CancellationToken cancellationToken = cancellationTokenSource.Token; int isCancelled = 0; Task cancelAsync() { if (Interlocked.Exchange(ref isCancelled, 1) == 0) cancellationTokenSource.Cancel(); return taskCompletionSource.Task; } // This ensures that no orphaned read loops can // persist regardless of potential race conditions. // If a previous read loop is replaced, we cancel it here. Func cancelPreviousReadAsync = Interlocked.Exchange(ref m_cancelReadAsync, cancelAsync); cancelPreviousReadAsync?.Invoke(); // Start continuous read loop // ReSharper disable once MethodSupportsCancellation Task.Run(async () => { try { while (!cancellationToken.IsCancellationRequested && CurrentState == ClientState.Connected) await ReadDataAsync(cancellationToken); } catch (Exception ex) { if (!(ex is ThreadAbortException)) OnReceiveDataException(ex); } finally { taskCompletionSource.SetResult(null); cancellationTokenSource.Dispose(); } }); } catch (Exception ex) { fail(ex); } }); } catch (Exception ex) { fail(ex); } finally { // If the operation was cancelled during execution, // make sure to dispose of erroneously allocated resources if (CurrentState == ClientState.Disconnected || m_disposed) tcpClient?.Dispose(); } // Return the wait handle that signals completion // of the asynchronous connection loop return m_connectWaitHandle; } /// /// Reads a number of bytes from the current received data buffer and writes those bytes into a byte array at the specified offset. /// /// Destination buffer used to hold copied bytes. /// 0-based starting index into destination to begin writing data. /// The number of bytes to read from current received data buffer and write into . /// The number of bytes read. /// /// This function should only be called from within the event handler. Calling this method outside /// this event will have unexpected results. /// /// Failed to get network stream. /// is null. /// /// or is less than 0 -or- /// and will exceed length. /// public override int Read(byte[] buffer, int startIndex, int length) { buffer.ValidateParameters(startIndex, length); if (m_tcpClient.ReceiveBuffer == null) throw new InvalidOperationException("No received data buffer has been defined to read."); int sourceLength = m_tcpClient.PayloadLength - ReadIndex; int readBytes = length > sourceLength ? sourceLength : length; Buffer.BlockCopy(m_tcpClient.ReceiveBuffer, ReadIndex, buffer, startIndex, readBytes); // Update read index for next call ReadIndex += readBytes; if (ReadIndex >= m_tcpClient.PayloadLength) ReadIndex = 0; return readBytes; } /// /// Requests that the client attempt to move to the next . /// /// true if request succeeded; otherwise, false. /// /// Return value will only be true if changed. /// public override bool RequestNextServerIndex() { int serverListLength = ServerList.Length; if (serverListLength < 2) return false; // When multiple servers are available, move to next server connection ServerIndex++; if (ServerIndex >= serverListLength) ServerIndex = 0; return true; } // Reads data from the TcpClient stream. private async Task ReadDataAsync(CancellationToken cancellationToken) { try { NetworkStream stream = m_tcpClient.Provider?.GetStream(); if (stream == null) throw new InvalidOperationException("Failed to get network stream."); while (!cancellationToken.IsCancellationRequested) { int length; if (PayloadAware) { if (m_tcpClient.PayloadLength < 0) length = m_payloadMarker.Length + Payload.LengthSegment; else length = m_tcpClient.PayloadLength; } else { length = m_tcpClient.ReceiveBufferSize; } // Retrieve data from the socket m_tcpClient.BytesReceived = await stream.ReadAsync(m_tcpClient.ReceiveBuffer, m_tcpClient.Offset, length - m_tcpClient.Offset, cancellationToken); if (cancellationToken.IsCancellationRequested) return; if (PayloadAware) m_tcpClient.Offset += m_tcpClient.BytesReceived; if (PayloadAware) { if (m_tcpClient.PayloadLength < 0) { m_tcpClient.PayloadLength = Payload.ExtractLength(m_tcpClient.ReceiveBuffer, m_tcpClient.Offset, m_payloadMarker, m_payloadEndianOrder); if (m_tcpClient.PayloadLength > 0) { m_tcpClient.Offset = 0; if (m_tcpClient.ReceiveBuffer.Length < m_tcpClient.PayloadLength) m_tcpClient.SetReceiveBuffer(m_tcpClient.PayloadLength); } } else if (m_tcpClient.Offset == m_tcpClient.PayloadLength) { // Notify consumer that entire data payload has been received OnReceiveDataComplete(m_tcpClient.ReceiveBuffer, m_tcpClient.PayloadLength); // Reset payload length m_tcpClient.Offset = 0; m_tcpClient.PayloadLength = -1; } } else { // Notify consumer that data has been received OnReceiveDataComplete(m_tcpClient.ReceiveBuffer, m_tcpClient.BytesReceived); } } } catch (Exception ex) { // Notify of the exception. if (!(ex is NullReferenceException)) OnReceiveDataException(ex); // Terminate connection on read exceptions TerminateConnectionOnReadThread(); } } /// /// Sends data to the server asynchronously. /// /// The buffer that contains the binary data to be sent. /// The zero-based position in the at which to begin sending data. /// The number of bytes to be sent from starting at the . /// for the asynchronous operation. /// Failed to get network stream. protected override WaitHandle SendDataAsync(byte[] data, int offset, int length) { if (CurrentState == ClientState.Disconnected) throw new InvalidOperationException("Cannot send on disconnected client"); // Prepare for payload-aware transmission. if (PayloadAware) Payload.AddHeader(ref data, ref offset, ref length, m_payloadMarker, m_payloadEndianOrder); NetworkStream stream = m_tcpClient.Provider?.GetStream(); if (stream == null) throw new InvalidOperationException("Failed to get network stream."); Task result = stream.WriteAsync(data, offset, length); return ((IAsyncResult)result).AsyncWaitHandle; } /// /// Disconnects the from the connected server synchronously. /// public override void Disconnect() { try { if (CurrentState == ClientState.Disconnected) return; m_tcpClient.Provider?.Dispose(); m_connectWaitHandle?.Set(); Task readTask = m_cancelReadAsync?.Invoke(); if (!s_onReadThread) { if (!readTask?.Wait(TimeSpan.FromSeconds(15.0D)) ?? false) throw new TimeoutException("Timeout waiting for read cancellation."); } } catch (ObjectDisposedException) { // This can be safely ignored } catch (Exception ex) { OnSendDataException(new InvalidOperationException($"Disconnect exception: {ex.Message}", ex)); } } /// /// Validates the specified . /// /// Connection string to be validated. /// Server property is missing. /// Server property is invalid. /// Server port value is not between and . protected override void ValidateConnectionString(string connectionString) { m_connectData = connectionString.ParseKeyValuePairs(); // Derive desired IP stack based on specified "interface" setting, adding setting if it's not defined m_ipStack = Transport.GetInterfaceIPStack(m_connectData); // Check if 'server' property is missing. if (!m_connectData.ContainsKey("server") || string.IsNullOrWhiteSpace(m_connectData["server"])) throw new ArgumentException($"Server property is missing (Example: {DefaultConnectionString})"); // Backwards compatibility adjustments. // New Format: Server=localhost:8888 // Old Format: Server=localhost; Port=8888 if (m_connectData.ContainsKey("port") && !m_connectData["server"].Contains(',')) m_connectData["server"] = $"{m_connectData["server"]}:{m_connectData["port"]}"; m_serverList = null; foreach (string server in ServerList) { // Check if 'server' property is valid. Match endpoint = Regex.Match(server, Transport.EndpointFormatRegex); if (endpoint == Match.Empty) throw new FormatException($"Server property is invalid (Example: {DefaultConnectionString})"); if (!Transport.IsPortNumberValid(endpoint.Groups["port"].Value)) throw new ArgumentOutOfRangeException(nameof(connectionString), $"Server port must between {Transport.PortRangeLow} and {Transport.PortRangeHigh}"); } } /// /// Raises the event. /// /// Exception to send to event. protected override void OnSendDataException(Exception ex) { if (CurrentState != ClientState.Disconnected) base.OnSendDataException(ex); else Logger.SwallowException(ex, $"{nameof(TcpSimpleClient)}: The client state was disconnected"); } /// /// Raises the event. /// /// Data received from the client. /// Number of bytes received from the client. protected override void OnReceiveDataComplete(byte[] data, int size) { s_onReadThread = true; try { base.OnReceiveDataComplete(data, size); } finally { s_onReadThread = false; } } /// /// Raises the event for . /// /// Exception to send to event. protected void OnReceiveDataException(SocketException ex) { if (ex.SocketErrorCode != SocketError.Disconnecting) OnReceiveDataException((Exception)ex); else Logger.SwallowException(ex, $"{nameof(TcpSimpleClient)}: The socket was disconnecting"); } /// /// Raises the event. /// /// Exception to send to event. protected override void OnReceiveDataException(Exception ex) { s_onReadThread = true; try { if (CurrentState != ClientState.Disconnected) base.OnReceiveDataException(ex); else Logger.SwallowException(ex, $"{nameof(TcpSimpleClient)}: The socket was disconnected"); } finally { s_onReadThread = false; } } private void TerminateConnectionOnReadThread() { s_onReadThread = true; try { OnConnectionTerminated(); } finally { s_onReadThread = false; } } #endregion #region [ Static ] // Static Fields [ThreadStatic] private static bool s_onReadThread; #endregion } }