701 lines · 28.8 KB · C#
RawDownload
//******************************************************************************************************//  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{    /// <summary>    /// Represents a simple implementation of a TCP-based communication client.    /// </summary>    public class TcpSimpleClient : ClientBase    {        #region [ Members ]         // Nested Types        private class TcpClientProvider : TransportProvider<TcpClientSocket>        {            public int Offset;            public int PayloadLength = -1;        }         // Constants         /// <summary>        /// Specifies the default value for the <see cref="PayloadAware"/> property.        /// </summary>        public const bool DefaultPayloadAware = false;         /// <summary>        /// Specifies the default value for the <see cref="NoDelay"/> property.        /// </summary>        public const bool DefaultNoDelay = false;         /// <summary>        /// Specifies the default value for the <see cref="ClientBase.ConnectionString"/> property.        /// </summary>        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<string, string> m_connectData;        private ManualResetEvent m_connectWaitHandle;        private Func<Task> m_cancelReadAsync;        private bool m_disposed;         #endregion         #region [ Constructors ]         /// <summary>        /// Initializes a new instance of the <see cref="TcpSimpleClient"/> class.        /// </summary>        public TcpSimpleClient() : this(DefaultConnectionString)        {        }         /// <summary>        /// Initializes a new instance of the <see cref="TcpSimpleClient"/> class.        /// </summary>        /// <param name="connectString">Connect string of the <see cref="TcpSimpleClient"/>. See <see cref="DefaultConnectionString"/> for format.</param>        public TcpSimpleClient(string connectString) : base(TransportProtocol.Tcp, connectString)        {            m_tcpClient = new TcpClientProvider();            PayloadAware = DefaultPayloadAware;            m_payloadMarker = Payload.DefaultMarker;            m_payloadEndianOrder = EndianOrder.LittleEndian;            NoDelay = DefaultNoDelay;        }         /// <summary>        /// Initializes a new instance of the <see cref="TcpSimpleClient"/> class.        /// </summary>        /// <param name="container"><see cref="IContainer"/> object that contains the <see cref="TcpSimpleClient"/>.</param>        public TcpSimpleClient(IContainer container) : this() => container?.Add(this);         #endregion         #region [ Properties ]         /// <summary>        /// Gets or sets a boolean value that indicates whether the payload boundaries are to be preserved during transmission.        /// </summary>        [Category("Data")]        [DefaultValue(DefaultPayloadAware)]        [Description("Indicates whether the payload boundaries are to be preserved during transmission.")]        public bool PayloadAware { get; set; }         /// <summary>        /// Gets or sets the byte sequence used to mark the beginning of a payload in a <see cref="PayloadAware"/> transmission.        /// </summary>        /// <remarks>        /// Setting property to <c>null</c> will create a zero-length payload marker.        /// </remarks>        [Browsable(false)]        [DesignerSerializationVisibility(DesignerSerializationVisibility.Hidden)]        public byte[] PayloadMarker        {            get => m_payloadMarker;            set => m_payloadMarker = value ?? Array.Empty<byte>();        }         /// <summary>        /// Gets or sets the endian order to apply for encoding and decoding payload size in a <see cref="PayloadAware"/> transmission.        /// </summary>        /// <remarks>        /// Setting property to <c>null</c> will force use of little-endian encoding.        /// </remarks>        [Browsable(false)]        [DesignerSerializationVisibility(DesignerSerializationVisibility.Hidden)]        public EndianOrder PayloadEndianOrder        {            get => m_payloadEndianOrder;            set => m_payloadEndianOrder = value ?? EndianOrder.LittleEndian;        }         /// <summary>        /// Gets or sets a boolean value that determines if small packets are delivered to the remote host without delay.        /// </summary>        [Category("Settings")]        [DefaultValue(DefaultNoDelay)]        [Description("Determines if small packets are delivered to the remote host without delay.")]        public bool NoDelay { get; set; }         /// <summary>        /// Gets the <see cref="Socket"/> object for the <see cref="TcpSimpleClient"/>.        /// </summary>        [Browsable(false)]        public Socket Client => m_tcpClient.Provider?.Client;         /// <summary>        /// Gets the server URI of the <see cref="TcpSimpleClient"/>.        /// </summary>        [Browsable(false)]        public override string ServerUri => $"{TransportProtocol}://{ServerList[ServerIndex]}".ToLower();         /// <summary>        /// Determines whether the base class should track statistics.        /// </summary>        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<string>();                 return m_serverList = serverList.Split(new[] { ',' }, StringSplitOptions.RemoveEmptyEntries).Select(server => server.Trim()).ToArray();            }        }         #endregion         #region [ Methods ]         /// <summary>        /// Releases the unmanaged resources used by the <see cref="TcpClient"/> and optionally releases the managed resources.        /// </summary>        /// <param name="disposing">true to release both managed and unmanaged resources; false to release only unmanaged resources.</param>        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().            }        }         /// <summary>        /// Saves <see cref="TcpSimpleClient"/> settings to the config file if the <see cref="ClientBase.PersistSettings"/> property is set to true.        /// </summary>        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();        }         /// <summary>        /// Loads saved <see cref="TcpSimpleClient"/> settings from the config file if the <see cref="ClientBase.PersistSettings"/> property is set to true.        /// </summary>        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);        }         /// <summary>        /// Connects the <see cref="TcpSimpleClient"/> to the server asynchronously.        /// </summary>        /// <returns><see cref="WaitHandle"/> for the asynchronous operation.</returns>        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<object> taskCompletionSource = new TaskCompletionSource<object>();                        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<Task> 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;        }         /// <summary>        /// Reads a number of bytes from the current received data buffer and writes those bytes into a byte array at the specified offset.        /// </summary>        /// <param name="buffer">Destination buffer used to hold copied bytes.</param>        /// <param name="startIndex">0-based starting index into destination <paramref name="buffer"/> to begin writing data.</param>        /// <param name="length">The number of bytes to read from current received data buffer and write into <paramref name="buffer"/>.</param>        /// <returns>The number of bytes read.</returns>        /// <remarks>        /// This function should only be called from within the <see cref="ClientBase.ReceiveData"/> event handler. Calling this method outside        /// this event will have unexpected results.        /// </remarks>        /// <exception cref="InvalidOperationException">Failed to get network stream.</exception>        /// <exception cref="ArgumentNullException"><paramref name="buffer"/> is null.</exception>        /// <exception cref="ArgumentOutOfRangeException">        /// <paramref name="startIndex"/> or <paramref name="length"/> is less than 0 -or-         /// <paramref name="startIndex"/> and <paramref name="length"/> will exceed <paramref name="buffer"/> length.        /// </exception>        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;        }         /// <summary>        /// Requests that the client attempt to move to the next <see cref="ClientBase.ServerIndex"/>.        /// </summary>        /// <returns><c>true</c> if request succeeded; otherwise, <c>false</c>.</returns>        /// <remarks>        /// Return value will only be <c>true</c> if <see cref="ClientBase.ServerIndex"/> changed.        /// </remarks>        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);                            }                        }

Lines 1–500 of 701.

Keyboard shortcuts

General

Show keyboard shortcuts
?
Focus the current search
/

Issues and pull requests

Open filters
f

Issues, pull requests and Inbox

Select next item
j↓
Select previous item
k↑
Open selected item
Enter
Dismiss filters or clear selection
Esc

Repository code

Go to file
t