protected override async Task ProtectedWriteAsync(
            byte[] buffer,
            int offset,
            int count,
            CancellationToken cancellationToken)
        {
            if (count > this.MaxWriteSize)
            {
                throw new IndexOutOfRangeException(
                          $"Write buffer too large ({count}), must be at most {this.MaxWriteSize}");
            }

            while (true)
            {
                try
                {
                    var connection = await ConnectAsync(cancellationToken).ConfigureAwait(false);

                    // Take care of outstanding ACKs.
                    var bytesToAck = Thread.VolatileRead(ref this.bytesReceived);
                    if (this.lastAck < bytesToAck)
                    {
                        var ackMessage = new MessageBuffer(
                            new byte[AckMessage.ExpectedLength]).AsAckMessage();
                        ackMessage.Tag = MessageTag.ACK;
                        ackMessage.Ack = bytesToAck;

                        TraceLine($"Sending ACK #{ackMessage.Ack}...");

                        await connection.WriteAsync(
                            ackMessage.Buffer,
                            0,
                            (int)AckMessage.ExpectedLength,
                            cancellationToken).ConfigureAwait(false);

                        this.lastAck = ackMessage.Ack;
                    }

                    // Send data.

                    var newMessage = new DataMessage((uint)count)
                    {
                        Tag            = MessageTag.DATA,
                        DataLength     = (uint)count,
                        SequenceNumber = Thread.VolatileRead(ref this.bytesSent)
                    };
                    Array.Copy(buffer, offset, newMessage.Buffer, DataMessage.DataOffset, count);

                    TraceLine($"Sending DATA #{newMessage.SequenceNumber}...");

                    // Update bytesSent before we write the data to the wire,
                    // otherwise we might see an ACK before bytesSent even reflects
                    // that the data has been sent.
                    this.bytesSent = Thread.VolatileRead(ref this.bytesSent) + (ulong)count;

                    await connection.WriteAsync(
                        newMessage.Buffer,
                        0,
                        newMessage.BufferLength,
                        cancellationToken).ConfigureAwait(false);

                    // We should get an ACK for this message.
                    lock (this.sentButUnacknoledgedQueueLock)
                    {
                        this.sentButUnacknoledgedQueue.Enqueue(newMessage);
                    }

                    return;
                }
                catch (WebSocketStreamClosedByServerException e)
                {
                    if (e.CloseStatus == WebSocketCloseStatus.NormalClosure ||
                        (CloseCode)e.CloseStatus == CloseCode.DESTINATION_READ_FAILED ||
                        (CloseCode)e.CloseStatus == CloseCode.DESTINATION_WRITE_FAILED)
                    {
                        // The server closed the connection and us sending more data
                        // really seems unexpected.
                        throw;
                    }
                    else if ((CloseCode)e.CloseStatus == CloseCode.NOT_AUTHORIZED)
                    {
                        TraceLine("NOT_AUTHORIZED");

                        throw new UnauthorizedException(e.CloseStatusDescription);
                    }
                    else
                    {
                        TraceLine($"Connection closed abnormally: {(CloseCode)e.CloseStatus}");

                        TraceLine("Disconnecting, preparing to reconnect");
                        await DisconnectAsync(cancellationToken).ConfigureAwait(false);
                    }
                }
            }
        }
        protected override async Task <int> ProtectedReadAsync(
            byte[] buffer,
            int offset,
            int count,
            CancellationToken cancellationToken)
        {
            if (count < this.MinReadSize)
            {
                throw new IndexOutOfRangeException(
                          $"Read buffer too small ({count}), must be at least {this.MinReadSize}");
            }

            MessageBuffer receiveBuffer = new MessageBuffer(new byte[count + DataMessage.DataOffset]);

            while (true)
            {
                try
                {
                    var connection = await ConnectAsync(cancellationToken).ConfigureAwait(false);

                    int bytesRead = await connection.ReadAsync(
                        receiveBuffer.Buffer,
                        0,
                        receiveBuffer.Buffer.Length,
                        cancellationToken).ConfigureAwait(false);

                    if (bytesRead == 0)
                    {
                        TraceLine("Server closed connection");

                        // We are done here
                        return(0);
                    }
                    else if (bytesRead < 2)
                    {
                        throw new InvalidServerResponseException("Truncated message received");
                    }

                    var tag = receiveBuffer.PeekMessageTag();
                    switch (tag)
                    {
                    case MessageTag.CONNECT_SUCCESS_SID:
                    {
                        this.Sid = receiveBuffer.AsSidMessage().Sid;

                        TraceLine($"Connected session <{this.Sid}>");

                        // No data to return to client yet.
                        break;
                    }

                    case MessageTag.RECONNECT_SUCCESS_ACK:
                    {
                        var reconnectMessage = receiveBuffer.AsAckMessage();
                        var lastAckReceived  = Thread.VolatileRead(ref this.bytesSentAndAcknoledged);
                        if (lastAckReceived < reconnectMessage.Ack)
                        {
                            TraceLine("Last ACK sent by server was not received");

                            bytesSentAndAcknoledged = reconnectMessage.Ack;
                        }
                        else if (lastAckReceived > reconnectMessage.Ack)
                        {
                            Debug.Assert(false, "Server acked backwards");
                            throw new Exception("Server acked backwards");
                        }
                        else
                        {
                            TraceLine($"Reconnected session <{this.Sid}>");
                        }

                        // No data to return to client yet.
                        break;
                    }

                    case MessageTag.ACK:
                    {
                        var ackMessage = receiveBuffer.AsAckMessage();
                        if (ackMessage.Ack <= 0 || ackMessage.Ack > Thread.VolatileRead(ref this.bytesSent))
                        {
                            throw new InvalidServerResponseException("Received invalid ACK");
                        }

                        this.bytesSentAndAcknoledged = ackMessage.Ack;

                        lock (this.sentButUnacknoledgedQueueLock)
                        {
                            // The server might be acknolodging multiple messages at once.
                            while (this.sentButUnacknoledgedQueue.Count > 0 &&
                                   this.sentButUnacknoledgedQueue.Peek().ExpectedAck
                                   <= Thread.VolatileRead(ref this.bytesSentAndAcknoledged))
                            {
                                this.sentButUnacknoledgedQueue.Dequeue();
                            }
                        }

                        TraceLine($"Received ACK #{ackMessage.Ack}");

                        // No data to return to client yet.
                        break;
                    }

                    case MessageTag.DATA:
                    {
                        var dataMessage = receiveBuffer.AsDataMessage();

                        TraceLine($"Received data ({dataMessage.DataLength} bytes)");

                        bytesReceived += dataMessage.DataLength;

                        // Copy data to caller's buffer.
                        Debug.Assert(dataMessage.DataLength < count);
                        Array.Copy(
                            receiveBuffer.Buffer,
                            DataMessage.DataOffset,
                            buffer,
                            offset,
                            dataMessage.DataLength);
                        return((int)dataMessage.DataLength);
                    }

                    default:
                        if (this.Sid == null)
                        {
                            // An unrecognized tag at the start of a connection means that we are
                            // essentially reading junk, so bail out.
                            throw new InvalidServerResponseException($"Unknown tag: {tag}");
                        }
                        else
                        {
                            // The connection was properly opened - an unrecognized tag merely
                            // means that the server uses a feature that we do not support (yet).
                            // In accordance with the protocol specification, ignore this tag.
                            break;
                        }
                    }
                }
                catch (WebSocketStreamClosedByClientException)
                {
                    TraceLine("Detected attempt to read after close");
                    throw;
                }
                catch (WebSocketStreamClosedByServerException e)
                {
                    if (e.CloseStatus == WebSocketCloseStatus.NormalClosure ||
                        (CloseCode)e.CloseStatus == CloseCode.DESTINATION_READ_FAILED ||
                        (CloseCode)e.CloseStatus == CloseCode.DESTINATION_WRITE_FAILED)
                    {
                        TraceLine("Server closed connection");

                        // Server closed the connection normally, we are done here

                        return(0);
                    }
                    else if ((CloseCode)e.CloseStatus == CloseCode.NOT_AUTHORIZED)
                    {
                        TraceLine("Not authorized");

                        throw new UnauthorizedException(e.CloseStatusDescription);
                    }
                    else if ((CloseCode)e.CloseStatus == CloseCode.SID_UNKNOWN ||
                             (CloseCode)e.CloseStatus == CloseCode.SID_IN_USE)
                    {
                        // Failed reconect attempt - do not try again.
                        TraceLine("Sid unknown or in use");

                        throw new WebSocketStreamClosedByServerException(
                                  e.CloseStatus,
                                  "Connection losed abnormally and an attempt to reconnect was rejected");
                    }
                    else if ((CloseCode)e.CloseStatus == CloseCode.FAILED_TO_CONNECT_TO_BACKEND)
                    {
                        // Server probably not listening.
                        TraceLine("Failed to connect to backend");

                        throw new WebSocketStreamClosedByServerException(
                                  e.CloseStatus,
                                  "Failed to connect to backend");
                    }
                    else
                    {
                        TraceLine($"Connection closed abnormally: {(CloseCode)e.CloseStatus}");

                        TraceLine("Disconnecting, preparing to reconnect");
                        await DisconnectAsync(cancellationToken).ConfigureAwait(false);
                    }
                }
            }
        }