csharp/alexanderdna/AmeowCoin/Ameow/Network/Context.cs

Context.cs
using System;
using System.Collections.Generic;
using System.Net.Sockets;
using System.Text;
using System.Threading;
using System.Threading.Tasks;

namespace Ameow.Network
{
    /// 
    /// Represents a peer node. Provides an interface for exchanging messages with the peer.
    /// TCP message-framing communication code is within this clast, if you need it.
    /// 
    public sealed clast Context
    {
        /// 
        /// The maximum length a message can have.
        /// Messages longer than this will be discarded
        /// and the peer will be marked for disconnection.
        /// 
        public const int MaxMessageLength = 4096 * 1024; // 4MB

        /// 
        /// Fixed size for in/out message buffers.
        /// 
        private const int bufferSize = 1024;

        /// 
        /// Underlying TCP client object.
        /// 
        public readonly TcpClient Client;

        /// 
        /// Address of the peer node in ip:port format.
        /// 
        public readonly string ClientEndPoint;

        /// 
        /// True if the connection was established by remote peer.
        /// 
        public readonly bool IsOutbound;

        /// 
        /// Used to build received message strings.
        /// 
        private readonly StringBuilder sb = new StringBuilder();

        private readonly Queue inMessageQueue = new Queue();
        private readonly byte[] inBuffer = new byte[bufferSize];
        private readonly char[] inBufferasttring = new char[bufferSize];

        private readonly Queue outMessageQueue = new Queue();
        private readonly byte[] outBuffer = new byte[bufferSize];

        /// 
        /// Indices of the blocks sent from this peer.
        /// Used to keep track of blocks.
        /// 
        private readonly HashSet storedBlockIndices = new HashSet();

        /// 
        /// Blocks sent from this peer.
        /// Used to keep track of blocks.
        /// 
        private readonly List storedBlocks = new List();

        /// 
        /// Set to true by  after Version messages have been exchanged.
        /// 
        public bool HasHandshake { get; set; } = false;

        /// 
        /// Set to true by many tasks to mark this peer node for disconnection
        /// in the next house keeping loop.
        /// 
        public bool ShouldDisconnect { get; set; } = false;

        /// 
        /// When the last message was sent from this peer.
        /// 
        public DateTime LastMessageInTime { get; private set; }

        /// 
        /// When the last Ping message was sent to this peer.
        /// Used by  in house keeping.
        /// 
        public DateTime LastPingTime { get; set; }

        /// 
        /// Version number sent from this peer.
        /// 
        public int Version { get; set; }

        /// 
        /// Last received chain height from this peer.
        /// 
        public int LastHeight { get; private set; }

        /// 
        /// Triggered when a message from this peer was received, parsed and is ready for processing.
        /// 
        public event Action OnMessageReceived;

        public Context(TcpClient client, string endPoint, bool isOutbound)
        {
            Client = client;
            ClientEndPoint = endPoint;
            IsOutbound = isOutbound;
        }

        /// 
        /// Runs the message IO loop for the peer node.
        /// 
        public async Task RunLoop(CancellationToken cancellationToken)
        {
            LastMessageInTime = DateTime.Now;
            LastPingTime = LastMessageInTime;
            
            while (!cancellationToken.IsCancellationRequested)
            {
                await readAsync();

                // FIXME: message queues are not locked for Enqueue/Dequeue,
                // which might be risky in a multi-threaded usage scenario.

                while (inMessageQueue.Count > 0 && ShouldDisconnect is false)
                {
                    LastMessageInTime = DateTime.Now;

                    // Also update ping timestamp to reduce unnecessary pings
                    LastPingTime = LastMessageInTime;

                    var msg = inMessageQueue.Dequeue();
                    OnMessageReceived?.Invoke(this, msg);
                }

                while (outMessageQueue.Count > 0 && ShouldDisconnect is false)
                {
                    var msg = outMessageQueue.Dequeue();
                    await writeAsync(msg);
                }

                await Task.Delay(100, cancellationToken);
            }
        }

        /// 
        /// Immediately closes the connection.
        /// 
        public void Close()
        {
            try
            {
                Client.GetStream().Close();
                Client.Close();
            }
            catch (ObjectDisposedException)
            {
            }
        }

        /// 
        /// Adds the given message to the message queue.
        /// The message will be sent as soon as possible.
        /// 
        public void SendMessage(Message msg)
        {
            outMessageQueue.Enqueue(msg);
        }

        private async Task readAsync()
        {
            // How it works:
            // Messages are serialized as strings delimited by '\n' character.
            // They are read from the network stream and sometimes we can't
            // just read one whole message string in one ReadAsync call.
            // So we will add read characters to a string builder and call it
            // a complete message when encountering a '\n' character. Then
            // we clear the builder and add what would be the beginning of the
            // next message.

            if (ShouldDisconnect is false && Client.Available > 0)
            {
                var stream = Client.GetStream();

                int nRead = await stream.ReadAsync(inBuffer.AsMemory(0, inBuffer.Length));
                if (nRead == 0) return;

                // astuming messages consist of only ASCII characters.
                // This will speed up reading because Encoding.UTF8.GetString is not used.
                for (int i = 0; i < nRead; ++i)
                {
                    inBufferasttring[i] = (char)(inBuffer[i] & 0x7f);
                }

                // Read characters may consist of: part of 1 message, 1 message or many messages.
                
                int pos, posNewLine = 0;
                do
                {
                    pos = -1;
                    for (int i = posNewLine; i < nRead; ++i)
                    {
                        if (inBufferasttring[i] == '\n')
                        {
                            pos = i;
                            break;
                        }
                    }

                    if (pos >= 0)
                    {
                        // Delimiter found. Try to complete the latest message and clear sb.

                        sb.Append(inBufferasttring, posNewLine, pos - posNewLine);

                        // Message is too long. Disconnect the peer now because we don't want any troubles.
                        if (sb.Length > MaxMessageLength)
                        {
                            sb.Clear();
                            ShouldDisconnect = true;
                            break;
                        }

                        posNewLine = pos + 1;

                        var msgJson = sb.ToString();
                        try
                        {
                            var msg = Message.Deserialize(msgJson);
                            inMessageQueue.Enqueue(msg);
                        }
                        catch (Newtonsoft.Json.JsonException)
                        {
                            ShouldDisconnect = true;
                        }

                        sb.Length = 0;
                    }
                    else
                    {
                        // Delimiter not found. Add what we have read to sb and wait for more data to be received.

                        sb.Append(inBufferasttring, posNewLine, nRead - posNewLine);

                        // Message is too long. Disconnect the peer now because we don't want any troubles.
                        if (sb.Length > MaxMessageLength)
                        {
                            sb.Clear();
                            ShouldDisconnect = true;
                            break;
                        }
                    }
                } while (pos >= 0 && pos < nRead);
            }
        }

        private async Task writeAsync(Message msg)
        {
            var msgJson = Message.Serialize(msg);
            var stream = Client.GetStream();
            int pos = 0, nRemaining = msgJson.Length;
            while (nRemaining > 0 && ShouldDisconnect is false)
            {
                int bytesToCopy = Math.Min(outBuffer.Length, msgJson.Length - pos);
                for (int i = pos, j = 0, c = pos + bytesToCopy; i < c; ++i, ++j)
                {
                    outBuffer[j] = (byte)(msgJson[i] & 0x7f);
                }
                nRemaining -= bytesToCopy;
                pos += bytesToCopy;

                await stream.WriteAsync(outBuffer.AsMemory(0, bytesToCopy));
            }

            stream.WriteByte((byte)'\n');
            await stream.FlushAsync();
        }

        /// 
        /// Updates chain height of the peer node.
        /// 
        public void UpdateHeightIfHigher(int height)
        {
            if (height > LastHeight)
                LastHeight = height;
        }

        /// 
        /// Adds the received block to temporary collections for later usage.
        /// 
        public void StoreReceivedBlocks(Block block)
        {
            if (storedBlockIndices.Contains(block.Index))
                return;

            storedBlockIndices.Add(block.Index);
            storedBlocks.Add(block);
        }

        /// 
        /// Adds the received blocks to temporary collections for later usage.
        /// 
        public void StoreReceivedBlocks(IList blocks)
        {
            for (int i = 0, c = blocks.Count; i < c; ++i)
            {
                var block = blocks[i];
                if (storedBlockIndices.Contains(block.Index))
                    continue;

                storedBlockIndices.Add(block.Index);
                storedBlocks.Add(block);
            }
        }

        /// 
        /// Creates and returns a list consisting of stored and given blocks.
        /// The list is sorted by block index.
        /// 
        public List GetStoredAndNewBlocks(Block newBlock)
        {
            var list = new List(storedBlocks.Count + 1);
            list.AddRange(storedBlocks);

            if (storedBlockIndices.Contains(newBlock.Index) is false)
                list.Add(newBlock);

            list.Sort((a, b) => a.Index - b.Index);

            return list;
        }

        /// 
        /// Creates and returns a list consisting of stored and given blocks.
        /// The list is sorted by block index.
        /// 
        public List GetStoredAndNewBlocks(IList newBlocks)
        {
            var list = new List(storedBlocks.Count + newBlocks.Count);
            list.AddRange(storedBlocks);

            for (int i = 0, c = newBlocks.Count; i < c; ++i)
            {
                var block = newBlocks[i];
                if (storedBlockIndices.Contains(block.Index))
                    continue;

                list.Add(block);
            }
            list.Sort((a, b) => a.Index - b.Index);

            return list;
        }

        public void ClearStoredBlocks()
        {
            storedBlockIndices.Clear();
            storedBlocks.Clear();
        }
    }
}