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); newMessage.Tag = MessageTag.DATA; newMessage.DataLength = (uint)count; newMessage.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. 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; // 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); } } } }