net.sf.eBus.client.EReliableUDPConnection Maven / Gradle / Ivy
//
// Copyright 2023 Charles W. Rapp
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
//
package net.sf.eBus.client;
import java.io.IOException;
import java.lang.invoke.MethodHandle;
import java.net.InetSocketAddress;
import java.net.ProtocolException;
import java.net.SocketAddress;
import java.nio.ByteBuffer;
import java.time.Duration;
import java.util.concurrent.atomic.AtomicReference;
import javax.net.ssl.SSLContext;
import static net.sf.eBus.client.EAbstractConnection.HEARTBEAT;
import static net.sf.eBus.client.EAbstractConnection.HEARTBEAT_REPLY;
import static net.sf.eBus.client.EAbstractConnection.MAX_MESSAGE_SIZE;
import static net.sf.eBus.client.EAbstractConnection.MESSAGE_HEADER_SIZE;
import static net.sf.eBus.client.EAbstractConnection.MESSAGE_SIZE_SIZE;
import static net.sf.eBus.client.EClient.sCoreExecutor;
import net.sf.eBus.config.EConfigure;
import net.sf.eBus.config.EConfigure.AbstractConfig;
import net.sf.eBus.config.EConfigure.ConnectionRole;
import net.sf.eBus.config.EConfigure.ConnectionType;
import net.sf.eBus.config.EConfigure.RemoteConnection;
import net.sf.eBus.config.EConfigure.Service;
import net.sf.eBus.messages.EMessage;
import net.sf.eBus.messages.EMessageHeader;
import static net.sf.eBus.messages.EMessageHeader.NO_ID;
import net.sf.eBus.messages.type.DataType;
import net.sf.eBus.messages.type.MessageType;
import net.sf.eBus.net.AsyncDatagramSocket;
import net.sf.eBus.net.DatagramBufferWriter;
import net.sf.eBus.timer.EScheduledExecutor.IETimer;
import net.sf.eBus.util.HexDump;
import net.sf.eBus.util.LazyString;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/**
* Extends {@link EUDPConnection} providing reliability to UDP
* packet transmission. Using UDP to communicate has two
* problems:
*
* -
* lost packets (low probability in today's networks) and
*
* -
* packets arriving out-of-order (real possibility).
*
*
*
* This connection solves these two problems using the simplest
* type of reliability: allowing only one packet in flight at a
* time. A second packet is held in a queue and transmitted only
* when the first packet is acknowledged. This solution is
* definitely simpler than TCP reliability but avoids the
* non-trivial TCP's complexity. The simple solution was chosen
* because message transmission rate is on the magnitude of
* one message or less per millisecond which nothing compared to
* TCP segments.
*
*
* Given this connections limitations it is recommended that
* this connection type not be used for high rate
* messages as the reliability technique is low throughput.
*
*
* This connection type provides both secure and insecure
* communication via the {@code EUDPConnection} super class.
*
*
* @author Charles W. Rapp
*/
/* package */ final class EReliableUDPConnection
extends EUDPConnection
{
//---------------------------------------------------------------
// Member data.
//
//-----------------------------------------------------------
// Constants.
//
/**
* Reliable UDP acknowledgement messages are denoted by this
* negative "message length" value.
*/
private static final int ACK_ID = 0xeb050000;
/**
* Sequence number position in the acknowledge message is
* at 4.
*/
private static final int SEQ_NUM_POSITION = Integer.BYTES;
//-----------------------------------------------------------
// Statics.
//
/**
* Access point for logging subsystem.
*/
private static final Logger sLogger =
LoggerFactory.getLogger(EReliableUDPConnection.class);
//-----------------------------------------------------------
// Locals.
//
/**
* Expected sequence number of the next inbound application
* message. Initialized to zero.
*/
private int mNextInboundSequenceNumber;
/**
* When this task expires, retransmit application on front
* of transmit queue if it matches the sequence
* number stored in the task.
*/
private final AtomicReference mRetransmitTask;
/**
* Serialized reliable UDP acknowledgement message.
*/
private final byte[] mAckIdData;
/**
* Wrap {@link #mAckIdData} in this buffer and use it to set
* the acknowledged sequence number.
*/
private final ByteBuffer mAckBuffer;
/**
* Re-transmit application messages at this millisecond
* rate.
*/
private Duration mRetransmitDelay;
/**
* Re-transmit a single application message this many times
* before declaring a disconnect.
*/
private int mRetransmitLimit;
//---------------------------------------------------------------
// Member methods.
//
//-----------------------------------------------------------
// Constructors.
//
/**
* Creates a new reliable UDP connection instance of the
* given connection type.
* @param connType reliable UDP connection type.
* @param connRole either accepted or initiated.
* @param dtlsContext DTLS context used for a secure UDP
* connection type.
* @param remoteApp eBus remote application instance owning
* this connection.
*/
private EReliableUDPConnection(final ConnectionType connType,
final ConnectionRole connRole,
final SSLContext dtlsContext,
final ERemoteApp remoteApp)
{
super (connType, connRole, dtlsContext, remoteApp);
mNextInboundSequenceNumber = 0;
mRetransmitTask = new AtomicReference<>();
final int ackSize = (Integer.BYTES * 2);
mAckIdData = new byte[ackSize];
mAckBuffer = ByteBuffer.wrap(mAckIdData);
} // end of EReliableUDPConnection(...)
//
// end of Constructors.
//-----------------------------------------------------------
//-----------------------------------------------------------
// Abstract Method Overrides.
//
/**
* Retrieves the re-transmit delay and limit from connection
* configuration.
* @param config reliable UDP connection configuration.
*/
@Override
protected void doInitialize(final RemoteConnection config)
{
mRetransmitDelay = config.retransmitDelay();
mRetransmitLimit = config.retransmitLimit();
// Set the message acknowledgement byte order.
mAckBuffer.order(config.byteOrder());
mAckBuffer.putInt(ACK_ID);
super.doInitialize(config);
} // end of doInitialize(RemoteConnection)
/**
* Retrieves the re-transmit delay and limit from connection
* configuration.
* @param config reliable UDP service configuration.
*/
@Override
protected void doInitialize(final Service config)
{
mRetransmitDelay = config.retransmitDelay();
mRetransmitLimit = config.retransmitLimit();
// Set the message acknowledgement byte order.
mAckBuffer.order(config.byteOrder());
mAckBuffer.putInt(ACK_ID);
super.doInitialize(config);
} // end of doInitialize(final Service config)
/**
* Returns output writer for this connection type.
* @param config remote connection configuration.
* @return connection output writer.
*/
@Override
protected AbstractMessageWriter createWriter(
final AbstractConfig config)
{
return (new ReliableMessageWriter(config, this));
} // end of createWriter(AbstractConfig)
//
// end of Abstract Method Overrides.
//-----------------------------------------------------------
//-----------------------------------------------------------
// Timeout Handlers.
//
/**
* Re-transmits first application message on transmit queue
* if:
*
* -
* re-transmit timer is still running and
*
* -
* first application message's sequence number matches
* the re-transmit timer's sequence number.
*
*
* Otherwise this task does nothing.
* @param task expired re-transmit timer task.
*/
@SuppressWarnings({"java:S1172"})
private void onRetransmitTimeout(final int seqNum)
{
// Get the latest re-transmit timer and set to null
// since this is a one shot timer.
final IETimer timer = mRetransmitTask.getAndSet(null);
// Was the timer set?
if (timer != null)
{
// Yes, the timer is valid.
final ReliableMessageWriter writer =
(ReliableMessageWriter) mOutputWriter;
try
{
// Is there an application message enqueued with
// a matching sequence number?
// Note: the message writer will throw an I/O
// exception if the message transmit limit has
// been exceeded.
if (writer.okToRetransmit(seqNum,
mRetransmitLimit))
{
sLogger.trace(
"{}: re-transmitting sequence number {}.",
mRemoteAddress,
seqNum);
doSend(writer);
}
}
catch (IOException ioex)
{
// Either re-transmit limit was reached or
// re-transmit failed. Drop the connection.
dropConnection(ioex);
}
}
} // end of onRetransmitTimeout(TimerEvent)
//
// end of Timeout Handlers.
//-----------------------------------------------------------
/**
* Returns a new, closed eBus reliable UDP connection
* configured according to {@code config}.
* @param config remote connection configuration.
* @param remoteApp pass messages to this eBus remote
* application interface.
* @return a closed reliable UDP connection.
*
* @see #create(InetSocketAddress, Service, ERemoteApp)
*/
/* package */ static EReliableUDPConnection create(final RemoteConnection config,
final ERemoteApp remoteApp)
{
final EReliableUDPConnection retval =
new EReliableUDPConnection(config.connectionType(),
ConnectionRole.INITIATOR,
config.sslContext(),
remoteApp);
// Initialize the socket, message writer, and message
// readers map.
retval.initialize(config);
return (retval);
} // end of create(RemoteConnection, ERemoteApp)
/**
* Returns a new, open eBus reliable UDP connection for the
* given "accepted" connection from {@code address}. This
* means that the UDP "server" received a connect request
* from a UDP connection at that address and port.
* @param address far end UDP connection.
* @param config eBus service configuration.
* @param remoteApp connection belongs to this remote
* application proxy.
* @return open eBus reliable UDP connection.
*
* @see #create(RemoteConnection, ERemoteApp)
*/
/* package */ static EReliableUDPConnection create(final InetSocketAddress address,
final Service config,
final ERemoteApp remoteApp)
{
final EReliableUDPConnection retval =
new EReliableUDPConnection(config.connectionType(),
ConnectionRole.ACCEPTOR,
config.sslContext(),
remoteApp);
// Initialize the socket, message writer, and message
// readers map.
retval.initialize(config);
retval.mTarget = address;
return (retval);
} // end of create(InetSocketAddress, Service, ERemoteApp)
/**
* Returns input reader for given message type and subject
* used to decode inbound messages. Returned reader is for
* reliable UDP message header.
* @param keyId unique message class identifier from remote
* eBus application.
* @param subject message key subject.
* @param mt message type, used for de-serialization.
* @param mh callback target method.
* @param mn callback method name.
* @return reliable UDP message input message decoder.
*/
@Override
protected MessageReader createReader(final int keyId,
final String subject,
final MessageType mt,
final MethodHandle mh,
final String mn)
{
return (
new ReliableMessageReader(
keyId, subject, mt, mh, mn));
} // end of createReader(...)
@Override
protected void processInput(final ByteBuffer buffer,
final InetSocketAddress address)
{
// Stop whichever heart timer is running while processing
// input.
stopHeartbeatTimer();
// The fact that we received input of any kind serves
// purpose of a heartbeat reply. We know that the
// connection is still alive. So turn off the heartbeat
// reply flag.
mHeartbeatReplyFlag = false;
if (sLogger.isTraceEnabled())
{
final int rem = buffer.remaining();
final int pos = buffer.position();
final int ms = buffer.getInt(pos);
final byte[] data = new byte[rem];
buffer.mark();
buffer.get(data);
buffer.reset();
sLogger.trace(
"{}: {} bytes available (start={}, msg size={}):\n{}",
address,
rem,
pos,
ms,
new LazyString(
() -> HexDump.dump(data, 0, rem, " ")));
}
else
{
sLogger.debug("{}: {} bytes available.",
address,
buffer.remaining());
}
final int messageSize = buffer.getInt();
boolean hbReplyFlag = false;
// Heartbeat and heartbeat reply messages are
// simply two-byte integers that are < 0.
if (messageSize == HEARTBEAT)
{
hbReplyFlag = true;
}
else if (messageSize == HEARTBEAT_REPLY)
{
// That's nice. A reply to our heartbeat.
// But the heartbeat flag is already turned off.
// So there is nothing else to do.
}
// Is this a reliable UDP message acknowledgement?
else if (messageSize == ACK_ID)
{
// Yes. Get the sequence number and verify this
// acknowledgement.
receiveAck(buffer.getInt());
}
// The message size must be at least big enough for
// the message header or is within the maximum
// allowed message size. If not, disconnect; there is
// not way to recover from this protocol error.
else if (messageSize < MESSAGE_HEADER_SIZE ||
messageSize > MAX_MESSAGE_SIZE)
{
logError(
new ProtocolException(
"invalid message size - " +
Integer.toString(messageSize)));
}
else
{
// Have the message reader parse the message and
// forward it to the remote application interface.
decode(buffer, address);
}
// Do we need to send a heartbeat reply?
if (hbReplyFlag)
{
// Yes, send the reply on its way.
try
{
sLogger.trace("{}: sending heartbeat reply.",
mRemoteAddress);
((AsyncDatagramSocket) mAsocket).send(
mHeartbeatReplyData,
0,
mHeartbeatReplyData.length);
}
catch (IOException bufex)
{
// Ignore.
// Why?
// Because a full output buffer means that we
// have output waiting to be sent and that output
// will serve as a heartbeat reply. See the top
// of this method.
// On the other hand, if the other side is
// sending us a heartbeat while our output buffer
// is full up, then something is desparately
// wrong. We're pitching bytes and they keep
// missing them. If max output message queue is
// set, we will likely hit that max and doClose
// the connection. If not set, then this JVM will
// run out of heap and crash.
}
}
// Now that we have finished processing input,
// start the appropriate heartbeat timer.
startHeartbeatTimer();
} // end of processInput(ByteBuffer, InetSocketAddress)
/**
* Handles {@link ReliableUdpAck} system messages. If this
* is an application message, then checks if inbound sequence
* number matches expected value, and then acknowledges
* message receipt to the far-end. If a reliable UDP protocol
* violation is detected (acknowledge message or inbound
* application message contains an invalid sequence number),
* then the reliable UDP connection is dropped.
* @param keyId message key identifier extracted from message
* header.
* @param address far-end address.
* @param header decoded message header.
* @param reader message reader used to decode message.
*/
@Override
protected void processMessage(final int keyId,
final InetSocketAddress address,
final EMessageHeader header,
final MessageReader reader)
{
final boolean isSystemMessage = header.isSystemMessage();
final int sequenceNumber = header.sequenceNumber();
// Always forward system messages.
boolean forwardFlag = isSystemMessage;
// Is this an application message?
if (!isSystemMessage)
{
// This is an application message.
// Does the sequence number match the expected value?
forwardFlag =
(sequenceNumber == mNextInboundSequenceNumber);
if (forwardFlag)
{
// Yes. Acknowledge receipt and advance the
// expected inbound sequence number.
sendAck(sequenceNumber);
++mNextInboundSequenceNumber;
}
// Is this the previously received message (that is,
// sequence number is one less than expected)?
else if (sequenceNumber == (mNextInboundSequenceNumber - 1))
{
// Yes. Far-end may have missed our
// acknowledgement. Re-send that ack.
sendAck(sequenceNumber);
}
// No. Either the far-end acknowledged a much
// older message or a future message. Drop the
// connection.
else
{
dropConnection(
new ProtocolException(
String.format(
"received application message with invalid seq num (expected %,d, received %,d)",
mNextInboundSequenceNumber,
sequenceNumber)));
}
}
// Good to forward this message?
// Does is sequence number match the expected value?
if (forwardFlag)
{
// Yes.
super.processMessage(keyId, address, header, reader);
}
} // end of processMessage(...)
/**
* Processes a reliable UDP application message
* acknowledgement. If the acknowledged sequence number
* matches the expected sequence number and there is
* another message in the queue, then sends next message.
* @param sequenceNumber acknowledge sequence number.
*/
private void receiveAck(final int sequenceNumber)
{
sLogger.trace("{}: received UDP ack seq num {}.",
mRemoteAddress,
sequenceNumber);
try
{
final ReliableMessageWriter writer =
(ReliableMessageWriter) mOutputWriter;
// Is there another message to send?
if (writer.acknowledgeReceived(sequenceNumber))
{
// Yes. Send pending message now.
doSend(writer);
++mMsgOutCount;
++sTotalOutCount;
}
}
catch (IOException ioex)
{
// If transmit fails, then close UDP connection,
// and let ERemoteApp know the connection is
// gone. Attempt to reconnect if so configured.
dropConnection(ioex);
}
} // end of receiveAck(int)
/**
* Sends a reliable UDP acknowledgement to far-end with the
* given sequence number. Since an acknowledgement is a
* system message, it may be sent immediately.
* @param sequenceNumber acknowledging receipt of application
* message with this sequence number.
*/
private void sendAck(final int sequenceNumber)
{
sLogger.trace("{}: acknowledging seq num {}.",
mRemoteAddress,
sequenceNumber);
try
{
mAckBuffer.position(0)
.putInt(SEQ_NUM_POSITION, sequenceNumber);
((AsyncDatagramSocket) mAsocket).send(
mAckIdData, 0, mAckIdData.length);
}
catch(IOException ioex)
{
// If transmit fails, then close UDP connection,
// and let ERemoteApp know the connection is
// gone. Attempt to reconnect if so configured.
dropConnection(ioex);
}
} // end of sendAck(final int)
/**
* Starts application message re-transmit timer. When timer
* expires and the next message on the transmit queue
* matches the given sequence number, then send that message
* again.
* @param seqNum when time expires, re-transmit application
* message with this sequence number. If first message on
* transmit queue does not have a matching sequence number,
* then do not transmit message.
*/
@SuppressWarnings({"java:S3398"})
private void startRetransmitTimer(final int seqNum)
{
mRetransmitTask.set(
sCoreExecutor.schedule(
() -> onRetransmitTimeout(seqNum),
this,
mRetransmitDelay));
} // end of startRetransmitTimer(int)
/**
* Stops re-transmit timer if currently set.
*/
private void stopRetransmitTimer()
{
final IETimer timer = mRetransmitTask.getAndSet(null);
if (timer != null)
{
try
{
timer.close();
}
catch (Exception jex)
{}
}
} // end of stopRetransmitTimer()
/**
* Stops re-transmit timer, closes underlying UDP socket, and
* passes exception to
* {@link EAbstractConnection#disconnected(Throwable)}.
* @param t exception causing disconnect.
*/
private void dropConnection(final Throwable t)
{
// Stop re-transmit timer since we are no longer
// connected.
stopRetransmitTimer();
mAsocket.closeNow();
disconnected(t);
} // end of dropConnection(Throwable)
//---------------------------------------------------------------
// Inner classes.
//
/**
* Extends {@code EAbstractConnection.MessageReader} and
* includes 4-byte sequence number extraction from reliable
* UDP message header.
*/
private final class ReliableMessageReader
extends MessageReader
{
//-----------------------------------------------------------
// Member data.
//
//-------------------------------------------------------
// Constants.
//
/**
* Reliable UDP header size sans message size is
* {@value} bytes.
*/
private static final int HEADER_SIZE = 12; // bytes.
//-----------------------------------------------------------
// Member methods.
//
//-------------------------------------------------------
// Constructors.
//
/**
* Creates a new reliable message reader for the given
* parameters.
* @param keyId unique message class identifier from
* remote eBus application.
* @param subject message key subject.
* @param mt message type, used for de-serialization.
* @param mh callback target method.
* @param mn callback method name.
*/
private ReliableMessageReader(final int keyId,
final String subject,
final MessageType mt,
final MethodHandle mh,
final String mn)
{
super (keyId, subject, mt, mh, mn, HEADER_SIZE);
} // end of ReliableMessageReader(...)
//
// end of Constructors.
//-------------------------------------------------------
//-------------------------------------------------------
// MessageWriter Method Overrides.
//
@Override
protected EMessageHeader extractHeader(final ByteBuffer buffer,
final SocketAddress address,
final EMessage msg)
{
final int fromFeedId = buffer.getInt();
final int toFeedId = buffer.getInt();
final int sequenceNumber = buffer.getInt();
return (new EMessageHeader(mKeyId,
fromFeedId,
toFeedId,
sequenceNumber,
address,
msg));
} // end of extractHeader(...)
//
// end of MessageWriter Method Overrides.
//-------------------------------------------------------
} // end of class ReliableMessageReader
/**
* Responsible for encoding outbound eBus messages to the
* underlying {@link AsyncDatagramSocket} outbound buffer.
* Unlike {@code EUDPConnection.MessageWriter} the eBus
* message header sequence number is encoded.
*/
private final class ReliableMessageWriter
extends AbstractMessageWriter
implements DatagramBufferWriter
{
//-----------------------------------------------------------
// Member data.
//
//-------------------------------------------------------
// Locals.
//
/**
* Set outbound application message's sequence number to
* this value and increment after. Note that system
* messages do not have a sequence number. Initialized to
* zero.
*/
private int mNextSequenceNumber;
/**
* Sequence number of latest unacknowledged application
* transmitted. If there is no expected acknowledgement,
* then set to a value < zero.
*/
private int mUnackedSequenceNumber;
/**
* Tracks the number of times a particular application
* message has been transmitted. If the count exceeds the
* re-transmit limit, then the reliable UDP connection is
* declared lost.
*/
private int mRetransmitCount;
//-----------------------------------------------------------
// Member methods.
//
//-------------------------------------------------------
// Constructors.
//
/**
* Creates reliable datagram buffer message writer for
* the reliable UDP connection.
* @param config connection configuration.
* @param connection reliable UDP connection.
*/
private ReliableMessageWriter(final EConfigure.AbstractConfig config,
final EReliableUDPConnection connection)
{
super (config, connection);
mNextSequenceNumber = 0;
mUnackedSequenceNumber = NO_ID;
} // end of ReliableMessageWriter()
//
// end of Constructors.
//-------------------------------------------------------
//-------------------------------------------------------
// DatagramBufferWriter Interface Implementation.
//
@Override
public SocketAddress fill(final ByteBuffer buffer)
{
final EMessageHeader header = mTransmitQueue.peek();
final DataType msgType = header.dataType();
final int sizePosition = buffer.position();
// Move past the message size bytes and fill in the
// rest of the header.
// Note: buffer.position() returns Buffer and not
// ByteBuffer.
(buffer.position(sizePosition + MESSAGE_SIZE_SIZE))
.putInt(header.classId())
.putInt(header.fromFeedId())
.putInt(header.toFeedId())
.putInt(header.sequenceNumber());
msgType.serialize(header.message(), null, buffer);
// Now write out the message size.
buffer.putInt(sizePosition,
(buffer.position() - sizePosition));
// If this is system message, then remove it from
// the queue because system messages are not
// acknowledged.
if (header.isSystemMessage())
{
mTransmitQueue.poll();
// Is the transmit queue now empty?
if (mTransmitQueue.isEmpty())
{
// Yes. Set unacknowledge message sequence
// number to < zero because we are not
// expecting an ack.
mUnackedSequenceNumber = NO_ID;
mRetransmitCount = 0;
}
// No, not empty. That means the transmit queue
// contains an application message waiting to be
// acknowledged. Do *NOT* update the un-acked
// sequence number or re-transmit count. They
// still apply to the enqueued application
// message.
}
// This is an application message. Store away the
// message sequence number and start the re-transmit
// timer.
else
{
mUnackedSequenceNumber = header.sequenceNumber();
++mRetransmitCount;
startRetransmitTimer(mUnackedSequenceNumber);
}
++mTransmitCount;
sLogger.trace(
"{}: queued message sent to {} (seq num={}, re-transmit count={} size={}, transmited={}, discarded={}).",
mConnection.remoteSocketAddress(),
(mTarget == null ?
"connected" :
mTarget),
header.sequenceNumber(),
mRetransmitCount,
mTransmitQueueSize.get(),
mTransmitCount,
mDiscardCount);
return (mTarget);
} // end of fill(ByteBuffer)
//
// end of DatagramBufferWriter Interface Implementation.
//-------------------------------------------------------
/**
* Returns {@code true} if {@code header} contains a
* system message or this is the sole application message
* on the transmit queue. Place system message at the
* transmit queue's front and application message at the
* transmit queue's read.
* @param header enqueue this message.
* @param queueSize message queue size as a result of
* adding message.
* @return {@code true} if clear to transmit.
*/
@Override
protected boolean enqueue(final EMessageHeader header,
final int queueSize)
{
final boolean retcode;
// Is this a system message?
if (header.isSystemMessage())
{
// Yes. Place this message at transmit queue's
// *front* and return ready-to-transmit.
mTransmitQueue.addFirst(header);
retcode = true;
}
// This is an application message. Set message's
// sequence number and place message at transmit
// queue's rear.
// Return true if this is the only message on the
// queue.
else
{
header.sequenceNumber(mNextSequenceNumber);
++mNextSequenceNumber;
mTransmitQueue.addLast(header);
retcode = (queueSize == 1);
}
return (retcode);
} // end of enqueue(EMessageHeader, int)
/**
* In addition to {@link AbstractMessageWriter#closed()}
* resets next sequence number to zero and unacknowledged
* sequence number to {@link #NO_ID}.
*/
@Override
protected void closed()
{
super.closed();
mNextSequenceNumber = 0;
mUnackedSequenceNumber = NO_ID;
mRetransmitCount = 0;
} // end of closed()
/**
* Returns {@code true} if given sequence number matches
* expected value and there is another message on the
* transmit queue.
* @param seqNum acknowledge sequence number.
* @return {@code true} if previously transmitted
* application message is now acknowledged and there is
* another message waiting to be transmitted.
* @throws ProtocolException
* if acknowledged sequence number is either unexpected
* or is > expected value.
*/
private boolean acknowledgeReceived(final int seqNum)
throws ProtocolException
{
boolean retcode = false;
// Is an acknowledgement expected?
if (mUnackedSequenceNumber < 0)
{
// No. Is this an application message sequence
// number?
if (seqNum >= 0)
{
// Yes. Log this error.
throw (
new ProtocolException(
String.format(
"received unexpected ack (seq num %,d)",
seqNum)));
}
// A system message was acknowledged. Report
// this mistake.
else
{
throw (
new ProtocolException(
"received ack for system message"));
}
}
// Yes, an ackowledgement is expected.
// Is it the expected sequence number?
else if (mUnackedSequenceNumber == seqNum)
{
final int queueSize;
// Yes. Then:
// 1) Stop re-transmit timer.
// 2) Decrement transmit queue size.
// 3) Remove acknowledged application message
// from the queue.
// 4) Reset the unacked sequence number.
stopRetransmitTimer();
queueSize = mTransmitQueueSize.decrementAndGet();
mTransmitQueue.poll();
mUnackedSequenceNumber = NO_ID;
mRetransmitCount = 0;
sLogger.trace(
"{}: received acknowledge seq num {}, queue size={}.",
mConnection.remoteSocketAddress(),
seqNum,
queueSize);
// Is there another enqueued message waiting to
// be transmitted?
// If so, send this message now.
retcode = (queueSize > 0);
}
// Is this for a previous message?
else if (seqNum < mUnackedSequenceNumber)
{
// Yes, it is less than the expected sequence
// number but that is OK. It may be a message ack
// re-transmit. Log this but do nothing else.
sLogger.warn(
"{}: received re-transmitted acknowledement, expected {}, received {}.",
mConnection.remoteSocketAddress(),
mUnackedSequenceNumber,
seqNum);
}
// Acknowledged sequence number is for a future,
// yet-to-be-transmitted message.
else
{
throw (
new ProtocolException(
String.format(
"ack seq num > expected (expected %,d, received %,d)",
mUnackedSequenceNumber,
seqNum)));
}
return (retcode);
} // end of acknowledgeReceived(int)
/**
* Returns {@code true} if:
*
* -
* transmit queue is not empty,
*
* -
* first message on the queue is an application
* message with a sequence number matching
* {@code seqNum}, and
*
* -
* application message's re-transmit count has not
* exceeded {@code retransmitLimit}.
*
*
* @param seqNum re-transmit message with this sequence
* number.
* @param retransmitLimit message may be re-transmitted
* at most this many times.
* @return if there is an application message ready for
* re-transmit with the given sequence number and within
* re-transmit limit.
* @throws IOException
* if application message with given sequence number has
* exceeded re-transmit limit.
*/
private boolean okToRetransmit(final int seqNum,
final int retransmitLimit)
throws IOException
{
final EMessageHeader header = mTransmitQueue.peek();
boolean retcode = false;
// Is there a message to re-transmit?
if (header != null)
{
// Yes.
retcode = (header.sequenceNumber() == seqNum);
// Does the message's sequence number match the
// desired sequence number?
// Has the re-transmit limit been exceeded?
if (retcode &&
mRetransmitCount > retransmitLimit)
{
// Yes. Throw an I/O exception to let
// the reliable UDP connection know that
// this connection is down.
throw (
new IOException(
String.format(
"re-transmit seq num %,d reached %,d limit",
seqNum,
retransmitLimit)));
}
}
// No message on the transmit queue.
return (retcode);
} // end of okToRetransmit(int, int)
} // end of class ReliableMessageWriter
} // end of class EReliableUDPConnection