package com.limegroup.gnutella.udpconnect; import java.io.IOException; import java.io.InputStream; import java.io.OutputStream; import java.net.InetAddress; import java.net.SocketException; import java.net.UnknownHostException; import org.apache.commons.logging.Log; import org.apache.commons.logging.LogFactory; import com.limegroup.gnutella.Acceptor; import com.limegroup.gnutella.ErrorService; import com.limegroup.gnutella.RouterService; import com.limegroup.gnutella.UDPService; import com.limegroup.gnutella.messages.BadPacketException; import com.limegroup.gnutella.settings.DownloadSettings; import com.limegroup.gnutella.util.NetworkUtils; /** * Manage a reliable udp connection for the transfer of data. */ public class UDPConnectionProcessor { private static final Log LOG = LogFactory.getLog(UDPConnectionProcessor.class); /** Define the chunk size used for data bytes */ public static final int DATA_CHUNK_SIZE = 512; /** Define the maximum chunk size read for data bytes before we will blow out the connection */ public static final int MAX_DATA_SIZE = 4096; /** Handle to the output stream that is the input to this connection */ private UDPBufferedOutputStream _inputFromOutputStream; /** Handle to the input stream that is the output of this connection */ private UDPBufferedInputStream _outputToInputStream; /** A leftover chunk of data from an incoming data message. These will always be present with a data message because the first data chunk will be from the GUID and the second chunk will be the payload. */ private Chunk _trailingChunk; /** The limit on space for data to be written out */ private volatile int _chunkLimit; /** The receivers windowSpace defining amount of data that receiver can accept */ private volatile int _receiverWindowSpace; /** Record the desired connection timeout on the connection */ private long _connectTimeOut = MAX_CONNECT_WAIT_TIME; /** Record the desired read timeout on the connection, defaults to 1 minute */ private int _readTimeOut = 1 * 60 * 1000; /** Predefine a common exception if the user can't receive UDP */ private static final IOException CANT_RECEIVE_UDP = new IOException("Can't receive UDP"); /** Predefine a common exception if the connection times out on creation */ private static final IOException CONNECTION_TIMEOUT = new IOException("Connection timed out"); /** Define the size of the data window */ private static final int DATA_WINDOW_SIZE = 20; /** Define the maximum accepted write ahead packet */ private static final int DATA_WRITE_AHEAD_MAX = DATA_WINDOW_SIZE + 5; /** The maximum number of times to try and send a data message */ private static final int MAX_SEND_TRIES = 8; // Handle to various singleton objects in our architecture private UDPService _udpService; private UDPMultiplexor _multiplexor; private UDPScheduler _scheduler; private Acceptor _acceptor; // Define WAIT TIMES // /** Define the wait time between SYN messages */ private static final long SYN_WAIT_TIME = 400; /** Define the maximum wait time to connect */ private static final long MAX_CONNECT_WAIT_TIME = 20*1000; /** Define the maximum wait time before sending a message in order to keep the connection alive (and firewalls open). */ private static final long KEEPALIVE_WAIT_TIME = (3*1000 - 500); /** Define the startup time before starting to send data. Note that on the receivers end, they may not be setup initially. */ private static final long WRITE_STARTUP_WAIT_TIME = 400; /** Define the default time to check for an ack to a data message */ private static final long DEFAULT_RTO_WAIT_TIME = 400; /** Define the maximum time that a connection will stay open without a message being received */ private static final long MAX_MESSAGE_WAIT_TIME = 20 * 1000; /** Define the minimum wait time between ack timeout events */ private static final long MIN_ACK_WAIT_TIME = 5; /** Define the size of a small send window for increasing wait time */ private static final long SMALL_SEND_WINDOW = 2; /** Ensure that writing takes a break every 4 writes so other synchronized activity can take place */ private static final long MAX_WRITE_WITHOUT_SLEEP = 4; /** Delay the write wakeup event a little so that it isn't constantly firing - This should achieve part of nagles algorithm. */ private static final long WRITE_WAKEUP_DELAY_TIME = 10; /** Delay the write events by one second if there is nothing to do */ private static final long NOTHING_TO_DO_DELAY = 1000; /** Time to wait after a close before everything is totally shutdown. */ private static final long SHUTDOWN_DELAY_TIME = 400; // Define Connection states // /** The state on first creation before connection is established */ private static final int PRECONNECT_STATE = 0; /** The state after a connection is established */ private static final int CONNECT_STATE = 1; /** The state after user communication during shutdown */ private static final int FIN_STATE = 2; /** The ip of the host connected to */ private final InetAddress _ip; /** The port of the host connected to */ private final int _port; /** The Window for sending and acking data */ private DataWindow _sendWindow; /** The WriteRegulator controls the amount of waiting time between writes */ private WriteRegulator _writeRegulator; /** The Window for receiving data */ private DataWindow _receiveWindow; /** The connectionID of this end of connection. Used for routing */ private byte _myConnectionID; /** The connectionID of the other end of connection. Used for routing */ private volatile byte _theirConnectionID; /** The status of the connection */ private int _connectionState; /** Scheduled event for keeping connection alive */ private UDPTimerEvent _keepaliveEvent; /** Scheduled event for writing data appropriately over time */ private UDPTimerEvent _writeDataEvent; /** Scheduled event for cleaning up at end of connection life */ private UDPTimerEvent _closedCleanupEvent; /** Flag that the writeEvent is shutdown waiting for space to write */ private boolean _waitingForDataSpace; /** Flag that the writeEvent is shutdown waiting for data to write */ private volatile boolean _waitingForDataAvailable; /** Flag saying that a Fin packet has been acked on shutdown */ private boolean _waitingForFinAck; /** Scheduled event for ensuring that data is acked or resent */ private UDPTimerEvent _ackTimeoutEvent; /** Adhoc event for waking up the writing of data */ private SafeWriteWakeupTimerEvent _safeWriteWakeup; /** The current sequence number of messages originated here */ private long _sequenceNumber; /** The sequence number of a pending fin message */ private long _finSeqNo; /** Transformer for mapping 2 byte sequenceNumbers of incoming ACK messages to 8 byte longs of essentially infinite size - note Acks echo our seqNo */ private SequenceNumberExtender _localExtender; /** Transformer for mapping 2 byte sequenceNumbers of incoming messages to 8 byte longs of essentially infinite size */ private SequenceNumberExtender _extender; /** The last time that a message was sent to other host */ private long _lastSendTime; /** The last time that data was sent to other host */ private long _lastDataSendTime; /** The last time that a message was received from the other host */ private long _lastReceivedTime; /** The number of resends to take into account when scheduling ack wait */ private int _ackResendCount; /** Skip a Data Write if this flag is true */ private boolean _skipADataWrite; /** Keep track of the reason for shutting down */ private byte _closeReasonCode; //////////////////////////////////////////// // Some settings related to skipping acks /////////////////////////////////////////// /** Whether to skip any acks at all */ private final boolean _skipAcks = DownloadSettings.SKIP_ACKS.getValue(); /** How long each measuring period is */ private final int _period = DownloadSettings.PERIOD_LENGTH.getValue(); /** How many periods to keep track of */ private static final int _periodHistory = DownloadSettings.PERIOD_LENGTH.getValue(); /** * By how much does the current period need to deviate from the average * before we start acking. */ private final float _deviation = DownloadSettings.DEVIATION.getValue(); /** Do not skip more than this many acks in a row */ private static final int _maxSkipAck = DownloadSettings.MAX_SKIP_ACKS.getValue(); /** how many data packets we got each second */ private final int [] _periods = new int[_periodHistory]; /** index within that array, points to the last period */ private int _currentPeriodId; /** How many data packets we received this period */ private int _packetsThisPeriod; /** whether we have enough data */ private boolean _enoughData; /** when the current second started */ private long _lastPeriod; /** how many acks we skipped in a row vs. total */ private int _skippedAcks, _skippedAcksTotal; /** how many packets we got in total */ private int _totalDataPackets; /** Allow a testing stub version of UDPService to be used */ private static UDPService _testingUDPService; /** * For testing only, allow UDPService to be overridden */ public static void setUDPServiceForTesting(UDPService udpService) { _testingUDPService = udpService; } /** * Try to kickoff a reliable udp connection. This method blocks until it * either sucessfully establishes a connection or it times out and throws * an IOException. */ public UDPConnectionProcessor(InetAddress ip, int port) throws IOException { // Record their address _ip = ip; _port = port; if(LOG.isDebugEnabled()) { LOG.debug("Creating UDPConn ip:"+ip+" port:"+port); } // Init default state _theirConnectionID = UDPMultiplexor.UNASSIGNED_SLOT; _connectionState = PRECONNECT_STATE; _lastSendTime = 0l; _lastDataSendTime = 0l; _chunkLimit = DATA_WINDOW_SIZE; _receiverWindowSpace = DATA_WINDOW_SIZE; _waitingForDataSpace = false; _waitingForDataAvailable = false; _waitingForFinAck = false; _skipADataWrite = false; _ackResendCount = 0; _closeReasonCode = FinMessage.REASON_NORMAL_CLOSE; // Allow UDPService to be overridden for testing if ( _testingUDPService == null ) _udpService = UDPService.instance(); else _udpService = _testingUDPService; // If UDP is not running or not workable, barf if ( !_udpService.isListening() || !_udpService.canDoFWT() ) { throw CANT_RECEIVE_UDP; } // Only wake these guys up if the service is okay _multiplexor = UDPMultiplexor.instance(); _scheduler = UDPScheduler.instance(); _acceptor = RouterService.getAcceptor(); // Precreate the receive window for responce reporting _receiveWindow = new DataWindow(DATA_WINDOW_SIZE, 1); // All incoming seqNo and windowStarts get extended // Acks seqNo need to be extended separately _localExtender = new SequenceNumberExtender(); _extender = new SequenceNumberExtender(); // Register yourself for incoming messages _myConnectionID = _multiplexor.register(this); // Throw an exception if udp connection limit hit if ( _myConnectionID == UDPMultiplexor.UNASSIGNED_SLOT) throw new IOException("no room for connection"); // See if you can establish a pseudo connection // which means each side can send/receive a SYN and ACK tryToConnect(); } public InputStream getInputStream() throws IOException { if (_outputToInputStream == null) { _outputToInputStream = new UDPBufferedInputStream(this); } return _outputToInputStream; } /** * Create a special output stream that feeds byte array chunks * into this connection. */ public OutputStream getOutputStream() throws IOException { if ( _inputFromOutputStream == null ) { // Start looking for data to write after an initial startup time // Note: the caller needs to open the output connection and write // some data before we can do anything. scheduleWriteDataEvent(WRITE_STARTUP_WAIT_TIME); _inputFromOutputStream = new UDPBufferedOutputStream(this); } return _inputFromOutputStream; } /** * Set the read timeout for the associated input stream. */ public void setSoTimeout(int timeout) throws SocketException { _readTimeOut = timeout; } public synchronized void close() throws IOException { if (LOG.isDebugEnabled()) LOG.debug("closing connection",new Exception()); // If closed then done if ( _connectionState == FIN_STATE ) throw new IOException("already closed"); // Shutdown keepalive event callbacks if ( _keepaliveEvent != null ) _keepaliveEvent.unregister(); // Shutdown write event callbacks if ( _writeDataEvent != null ) _writeDataEvent.unregister(); // Shutdown ack timeout event callbacks if ( _ackTimeoutEvent != null ) _ackTimeoutEvent.unregister(); // Unregister the safeWriteWakeup handler if ( _safeWriteWakeup != null ) _safeWriteWakeup.unregister(); // Register that the connection is closed _connectionState = FIN_STATE; // Track incoming ACKS for an ack of FinMessage _waitingForFinAck = true; // Tell the receiver that we are shutting down safeSendFin(); // Wakeup any sleeping readers if ( _outputToInputStream != null ) _outputToInputStream.wakeup(); // Wakeup any sleeping writers if ( _inputFromOutputStream != null ) _inputFromOutputStream.connectionClosed(); // Register for a full cleanup after a slight delay if (_closedCleanupEvent==null) { _closedCleanupEvent = new ClosedConnectionCleanupTimerEvent( System.currentTimeMillis() + SHUTDOWN_DELAY_TIME,this); LOG.debug("registering a closedCleanupEvent"); _scheduler.register(_closedCleanupEvent); } } private synchronized void finalClose() { // Send one final Fin message if not acked. if (_waitingForFinAck) safeSendFin(); // Unregister for message multiplexing _multiplexor.unregister(this); // Clean up my caller _closedCleanupEvent.unregister(); // TODO: Clear up state to streams? Might need more time. Anything else? } /** * Return the InetAddress. */ public InetAddress getInetAddress() { return _ip; } /** * Do some magic to get the local address if available. */ public InetAddress getLocalAddress() { InetAddress lip = null; try { lip = InetAddress.getByName( NetworkUtils.ip2string(_acceptor.getAddress(false))); } catch (UnknownHostException uhe) { try { lip = InetAddress.getLocalHost(); } catch (UnknownHostException uhe2) { lip = null; } } return lip; } int getPort() { return _port; } /** * Prepare for handling an open connection. */ private void prepareOpenConnection() { _connectionState = CONNECT_STATE; _sequenceNumber=1; scheduleKeepAlive(); // Create the delayed connection components _sendWindow = new DataWindow(DATA_WINDOW_SIZE, 1); _writeRegulator = new WriteRegulator(_sendWindow); // Precreate the event for rescheduling writing to allow // thread safety and faster writing _safeWriteWakeup = new SafeWriteWakeupTimerEvent(Long.MAX_VALUE,this); _scheduler.register(_safeWriteWakeup); // Keep chunkLimit in sync with window space _chunkLimit = _sendWindow.getWindowSpace(); } /** * Make sure any firewall or nat stays open by scheduling a keepalive * message before the connection should close. * * This just fires and reschedules itself appropriately so that we * don't need to worry about rescheduling as every new message is sent. */ private synchronized void scheduleKeepAlive() { // Create event with initial time _keepaliveEvent = new KeepAliveTimerEvent(_lastSendTime + KEEPALIVE_WAIT_TIME,this); // Register keepalive event for future event callbacks _scheduler.register(_keepaliveEvent); // Schedule the first keepalive event callback _scheduler.scheduleEvent(_keepaliveEvent); } /** * Setup and schedule the callback event for writing data. */ private synchronized void scheduleWriteDataEvent(long time) { if ( isConnected() ) { if ( _writeDataEvent == null ) { _writeDataEvent = new WriteDataTimerEvent(time,this); // Register writeData event for future use _scheduler.register(_writeDataEvent); } else { _writeDataEvent.updateTime(time); } // Notify the scheduler that there is a new write event/time _scheduler.scheduleEvent(_writeDataEvent); if(LOG.isDebugEnabled()) { LOG.debug("scheduleWriteDataEvent :"+time); } } } /** * Activate writing if we were waiting for space */ private synchronized void writeSpaceActivation() { if ( _waitingForDataSpace ) { _waitingForDataSpace = false; // Schedule immediately scheduleWriteDataEvent(0); } } /** * Activate writing if we were waiting for data to write */ public synchronized void writeDataActivation() { // Schedule at a reasonable time long rto = (long)_sendWindow.getRTO(); scheduleWriteDataEvent( _lastDataSendTime + (rto/4) ); } /** * Hand off the wakeup of data writing to the scheduler */ public void wakeupWriteEvent() { if ( _waitingForDataAvailable ) { LOG.debug("wakupWriteEvent"); if (_safeWriteWakeup.getEventTime() == Long.MAX_VALUE) { _safeWriteWakeup.updateTime(System.currentTimeMillis()+ WRITE_WAKEUP_DELAY_TIME); _scheduler.scheduleEvent(_safeWriteWakeup); } } } /** * Setup and schedule the callback event for ensuring data gets acked. */ private synchronized void scheduleAckTimeoutEvent(long time) { if ( isConnected() ) { if ( _ackTimeoutEvent == null ) { _ackTimeoutEvent = new AckTimeoutTimerEvent(time,this); // Register ackTimout event for future use _scheduler.register(_ackTimeoutEvent); } else { _ackTimeoutEvent.updateTime(time); } // Notify the scheduler that there is a new ack timeout event _scheduler.scheduleEvent(_ackTimeoutEvent); } } /** * Suppress ack timeout events for now */ private synchronized void unscheduleAckTimeoutEvent() { // Nothing required if not initialized if ( _ackTimeoutEvent == null ) return; // Set an existing event to an infinite wait // Note: No need to explicitly inform scheduler. _ackTimeoutEvent.updateTime(Long.MAX_VALUE); } /** * Determine if an ackTimeout should be rescheduled */ private synchronized boolean isAckTimeoutUpdateRequired() { // If ack timeout not yet created then yes. if ( _ackTimeoutEvent == null ) return true; // If ack timeout exists but is infinite then yes an update is required. return (_ackTimeoutEvent.getEventTime() == Long.MAX_VALUE); } /** * Test whether the connection is in connecting mode */ public synchronized boolean isConnected() { return (_connectionState == CONNECT_STATE && _theirConnectionID != UDPMultiplexor.UNASSIGNED_SLOT); } /** * Test whether the connection is closed */ public synchronized boolean isClosed() { return (_connectionState == FIN_STATE); } /** * Test whether the connection is not fully setup */ public synchronized boolean isConnecting() { return !isClosed() && (_connectionState == PRECONNECT_STATE || _theirConnectionID == UDPMultiplexor.UNASSIGNED_SLOT); } /** * Test whether the ip and ports match */ public boolean matchAddress(InetAddress ip, int port) { return (_ip.equals(ip) && _port == port); } /** * Return the connections connectionID identifier. */ public byte getConnectionID() { return _myConnectionID; } /** * Return the room for new local incoming data in chunks. This should * remain equal to the space available in the sender and receiver * data window. */ public int getChunkLimit() { return Math.min(_chunkLimit, _receiverWindowSpace); } /** * Return a chunk of data from the incoming data container. */ public Chunk getIncomingChunk() { Chunk chunk; if ( _trailingChunk != null ) { chunk = _trailingChunk; _trailingChunk = null; return chunk; } // Fetch a block from the receiving window. DataRecord drec = _receiveWindow.getWritableBlock(); if ( drec == null ) return null; drec.written = true; DataMessage dmsg = (DataMessage) drec.msg; // Record the second chunk of the message for the next read. _trailingChunk = dmsg.getData2Chunk(); // Record how much space was previously available in the receive window int priorSpace = _receiveWindow.getWindowSpace(); // Remove this record from the receiving window _receiveWindow.clearEarlyWrittenBlocks(); // If the receive window opened up then send a special // KeepAliveMessage so that the window state can be // communicated. if ( priorSpace == 0 || (priorSpace <= SMALL_SEND_WINDOW && _receiveWindow.getWindowSpace() > SMALL_SEND_WINDOW) ) { sendKeepAlive(); } // Return the first small chunk of data from the GUID return dmsg.getData1Chunk(); } public int getReadTimeout() { return _readTimeOut; } /** * Convenience method for sending keepalive message since we might fire * these off before waiting */ private void sendKeepAlive() { KeepAliveMessage keepalive = null; try { keepalive = new KeepAliveMessage(_theirConnectionID, _receiveWindow.getWindowStart(), _receiveWindow.getWindowSpace()); send(keepalive); } catch(IllegalArgumentException iae) { // Report an error since this shouldn't ever happen ErrorService.error(iae); closeAndCleanup(FinMessage.REASON_SEND_EXCEPTION); } } /** * Convenience method for sending data. */ private synchronized void sendData(Chunk chunk) { try { // TODO: Should really verify that chunk starts at zero. It does // by design. DataMessage dm = new DataMessage(_theirConnectionID, _sequenceNumber, chunk.data, chunk.length); send(dm); DataRecord drec = _sendWindow.addData(dm); drec.sentTime = _lastSendTime; drec.sends++; if( LOG.isDebugEnabled() && (_lastSendTime - _lastDataSendTime) > 2000) { LOG.debug("SendData lag = "+ (_lastSendTime - _lastDataSendTime)); } // Record when data was sent for future scheduling _lastDataSendTime = _lastSendTime; // Update the chunk limit for fast (nonlocking) access _chunkLimit = _sendWindow.getWindowSpace(); _sequenceNumber++; // If Acking check needs to be woken up then do it if ( isAckTimeoutUpdateRequired()) scheduleAckIfNeeded(); // Predecrement the other sides window until I here otherwise. // This prevents a cascade of sends before an Ack if ( _receiverWindowSpace > 0 ) _receiverWindowSpace--; } catch(IllegalArgumentException iae) { // Report an error since this shouldn't ever happen ErrorService.error(iae); closeAndCleanup(FinMessage.REASON_SEND_EXCEPTION); } } /** * Build and send an ack with default error handling with * the messages sequenceNumber, receive window start and * receive window space. */ private synchronized void safeSendAck(UDPConnectionMessage msg) { // Ack the message AckMessage ack = null; try { ack = new AckMessage( _theirConnectionID, msg.getSequenceNumber(), _receiveWindow.getWindowStart(), _receiveWindow.getWindowSpace()); if (LOG.isDebugEnabled()) { LOG.debug("total data packets "+_totalDataPackets+ " total acks skipped "+_skippedAcksTotal+ " skipped this session "+ _skippedAcks); } _skippedAcks=0; send(ack); } catch (BadPacketException bpe) { // This would not be good. ErrorService.error(bpe); closeAndCleanup(FinMessage.REASON_SEND_EXCEPTION); } catch(IllegalArgumentException iae) { // Report an error since this shouldn't ever happen ErrorService.error(iae); closeAndCleanup(FinMessage.REASON_SEND_EXCEPTION); } } /** * Build and send a fin message with default error handling. */ private synchronized void safeSendFin() { // Ack the message FinMessage fin = null; try { // Record sequence number for ack monitoring // Not that it should increment anymore anyways _finSeqNo = _sequenceNumber; // Send the FinMessage fin = new FinMessage(_theirConnectionID, _sequenceNumber, _closeReasonCode); send(fin); } catch(IllegalArgumentException iae) { // Report an error since this shouldn't ever happen ErrorService.error(iae); LOG.warn("calling recursively closeAndCleanup"); closeAndCleanup(FinMessage.REASON_SEND_EXCEPTION); } } /** * Send a message on to the UDPService */ private synchronized void safeSend(UDPConnectionMessage msg) { try { send(msg); } catch(IllegalArgumentException iae) { // Report an error since this shouldn't ever happen ErrorService.error(iae); closeAndCleanup(FinMessage.REASON_SEND_EXCEPTION); } } /** * Send a message on to the UDPService */ private synchronized void send(UDPConnectionMessage msg) throws IllegalArgumentException { _lastSendTime = System.currentTimeMillis(); if(LOG.isDebugEnabled()) { LOG.debug("send :"+msg+" ip:"+_ip+" p:"+_port+" t:"+ _lastSendTime); if ( msg instanceof FinMessage ) { Exception ex = new Exception(); LOG.debug("", ex); } } _udpService.send(msg, _ip, _port); } /** * Schedule an ack timeout for the oldest unacked data. * If no acks are pending, then do nothing. */ private synchronized void scheduleAckIfNeeded() { DataRecord drec = _sendWindow.getOldestUnackedBlock(); if ( drec != null ) { int rto = _sendWindow.getRTO(); if (rto == 0) rto = (int) DEFAULT_RTO_WAIT_TIME; long waitTime = drec.sentTime + ((long)rto); // If there was a resend then base the wait off of current time if ( _ackResendCount > 0 ) { waitTime = _lastSendTime + ((long)rto); _ackResendCount = 0; } // Enforce a mimimum waitTime from now long minTime = System.currentTimeMillis() + MIN_ACK_WAIT_TIME; waitTime = Math.max(waitTime, minTime); scheduleAckTimeoutEvent(waitTime); } else { unscheduleAckTimeoutEvent(); } } /** * Ensure that data is getting acked. If not within an appropriate time, * then resend. */ private synchronized void validateAckedData() { long currTime = System.currentTimeMillis(); if (_sendWindow.acksAppearToBeMissing(currTime, 1)) { // if the older blocks ack have been missing for a while // resend them. // Calculate a good maximum time to wait int rto = _sendWindow.getRTO(); long start = _sendWindow.getWindowStart(); if(LOG.isDebugEnabled()) LOG.debug("Soft resend check:"+ start+ " rto:"+rto+ " uS:"+_sendWindow.getUsedSpots()+" localSeq:"+_sequenceNumber); DataRecord drec; DataRecord drecNext; int numResent = 0; // Resend up to 1 packet at a time resend: { // Get the oldest unacked block out of storage drec = _sendWindow.getOldestUnackedBlock(); int expRTO = (rto * (int)Math.pow(2,drec.sends-1)); if (LOG.isDebugEnabled()) LOG.debug(" exponential backoff is now "+expRTO); // Check if the next drec is acked if(_sendWindow.countHigherAckBlocks() >0){ expRTO*=0.75; if (LOG.isDebugEnabled()) LOG.debug(" higher acked blocks, adjusting exponential backoff is now "+ expRTO); } // The assumption is that this record has not been acked if ( drec == null || drec.acks > 0) break resend; // If too many sends then abort connection if ( drec.sends > MAX_SEND_TRIES+1 ) { if(LOG.isDebugEnabled()) LOG.debug("Tried too many send on:"+ drec.msg.getSequenceNumber()); closeAndCleanup(FinMessage.REASON_TOO_MANY_RESENDS); return; } int currentWait = (int)(currTime - drec.sentTime); // If it looks like we waited too long then speculatively resend // Case 1: We waited 150% of RTO and next packet had been acked // Case 2: We waited 200% of RTO if ( currentWait > expRTO) { if(LOG.isDebugEnabled()) LOG.debug("Soft resending message:"+ drec.msg.getSequenceNumber()); safeSend(drec.msg); // Scale back on the writing speed if you are hitting limits _writeRegulator.addMessageFailure(); _writeRegulator.hitResendTimeout(); currTime = _lastSendTime; drec.sentTime = currTime; drec.sends++; numResent++; } else LOG.debug(" not resending message "); } // Delay subsequent resends of data based on number resent _ackResendCount = numResent; if ( numResent > 0 ) _skipADataWrite = true; } scheduleAckIfNeeded(); } /** * Close and cleanup by unregistering this connection and sending a Fin. */ private synchronized void closeAndCleanup(byte reasonCode) { _closeReasonCode = reasonCode; try { close(); } catch (IOException ioe) {} } // ------------------ Connection Handling Logic ------------------- // /** * Send SYN messages to desired host and wait for Acks and their * SYN message. Block connector while trying to connect. */ private void tryToConnect() throws IOException { try { _sequenceNumber = 0; // Keep track of how long you are waiting on connection long waitTime = 0; // Build SYN message with my connectionID in it SynMessage synMsg = new SynMessage(_myConnectionID); // Keep sending and waiting until you get a Syn and an Ack from // the other side of the connection. while ( true ) { // If we have received their connectionID then use it synchronized(this){ if (!isConnecting()) break; if ( waitTime > _connectTimeOut ) { _connectionState = FIN_STATE; _multiplexor.unregister(this); throw CONNECTION_TIMEOUT; } if (_theirConnectionID != UDPMultiplexor.UNASSIGNED_SLOT && _theirConnectionID != synMsg.getConnectionID()) { synMsg = new SynMessage(_myConnectionID, _theirConnectionID); } } // Send a SYN packet with our connectionID send(synMsg); // Wait for some kind of response try { Thread.sleep(SYN_WAIT_TIME); } catch(InterruptedException e) {} waitTime += SYN_WAIT_TIME; } } catch (IllegalArgumentException iae) { throw new IOException(iae.getMessage()); } } /** * Take action on a received message. */ public void handleMessage(UDPConnectionMessage msg) { boolean doYield = false; // Trigger a yield at the end if 1k available synchronized (this) { // Record when the last message was received _lastReceivedTime = System.currentTimeMillis(); if(LOG.isDebugEnabled()) LOG.debug("handleMessage :"+msg+" t:"+_lastReceivedTime); if (msg instanceof SynMessage) { // Extend the msgs sequenceNumber to 8 bytes based on past state msg.extendSequenceNumber( _extender.extendSequenceNumber( msg.getSequenceNumber()) ); // First Message from other host - get his connectionID. SynMessage smsg = (SynMessage) msg; byte theirConnID = smsg.getSenderConnectionID(); if ( _theirConnectionID == UDPMultiplexor.UNASSIGNED_SLOT ) { // Keep track of their connectionID _theirConnectionID = theirConnID; } else if ( _theirConnectionID == theirConnID ) { // Getting a duplicate SYN so just ack it again. } else { // Unmatching SYN so just ignore it return; } // Ack their SYN message safeSendAck(msg); } else if (msg instanceof AckMessage) { // Extend the msgs sequenceNumber to 8 bytes based on past state // Note that this sequence number is of local origin msg.extendSequenceNumber( _localExtender.extendSequenceNumber( msg.getSequenceNumber()) ); AckMessage amsg = (AckMessage) msg; // Extend the windowStart to 8 bytes the same as the // sequenceNumber amsg.extendWindowStart( _localExtender.extendSequenceNumber(amsg.getWindowStart()) ); long seqNo = amsg.getSequenceNumber(); long wStart = amsg.getWindowStart(); int priorR = _receiverWindowSpace; _receiverWindowSpace = amsg.getWindowSpace(); // Adjust the receivers window space with knowledge of // how many extra messages we have sent since this ack if ( _sequenceNumber > wStart ) _receiverWindowSpace = DATA_WINDOW_SIZE + (int) (wStart - _sequenceNumber); //_receiverWindowSpace += (wStart - _sequenceNumber); // Reactivate writing if required if ( (priorR == 0 || _waitingForDataSpace) && _receiverWindowSpace > 0 ) { if(LOG.isDebugEnabled()) LOG.debug(" -- ACK wakeup"); writeSpaceActivation(); } // If they are Acking our SYN message, advance the state if ( seqNo == 0 && isConnecting() && _connectionState == PRECONNECT_STATE ) { // The connection should be successful assuming that I // receive their SYN so move state to CONNECT_STATE // and get ready for activity prepareOpenConnection(); } else if ( _waitingForFinAck && seqNo == _finSeqNo ) { // A fin message has been acked on shutdown _waitingForFinAck = false; } else if (_connectionState == CONNECT_STATE) { // Record the ack _sendWindow.ackBlock(seqNo); _writeRegulator.addMessageSuccess(); // Ensure that all messages up to sent windowStart are acked _sendWindow.pseudoAckToReceiverWindow(amsg.getWindowStart()); // Clear out the acked blocks at window start _sendWindow.clearLowAckedBlocks(); // Update the chunk limit for fast (nonlocking) access _chunkLimit = _sendWindow.getWindowSpace(); } } else if (msg instanceof DataMessage) { // Extend the msgs sequenceNumber to 8 bytes based on past state msg.extendSequenceNumber( _extender.extendSequenceNumber( msg.getSequenceNumber()) ); // Pass the data message to the output window DataMessage dmsg = (DataMessage) msg; // If message is more than limit beyond window, // then throw it away long seqNo = dmsg.getSequenceNumber(); long baseSeqNo = _receiveWindow.getWindowStart(); // If data is too large then blow out the connection // before any damage is done if (dmsg.getDataLength() > MAX_DATA_SIZE) { closeAndCleanup(FinMessage.REASON_LARGE_PACKET); return; } if ( seqNo > (baseSeqNo + DATA_WRITE_AHEAD_MAX) ) { if(LOG.isDebugEnabled()) LOG.debug("Received block num too far ahead: "+ seqNo); return; } // Make sure the data is not before the window start if ( seqNo >= baseSeqNo ) { // Record the receipt of the data in the receive window DataRecord drec = _receiveWindow.addData(dmsg); drec.ackTime = System.currentTimeMillis(); drec.acks++; // Notify InputStream that data is available for reading if ( _outputToInputStream != null && seqNo==baseSeqNo) { _outputToInputStream.wakeup(); // Get the reader moving after 1k received if ( (seqNo % 2) == 0) doYield = true; } } else { if(LOG.isDebugEnabled()) LOG.debug("Received duplicate block num: "+ dmsg.getSequenceNumber()); } //if this is the first data message we get, start the period now if (_lastPeriod == 0) _lastPeriod = _lastReceivedTime; _packetsThisPeriod++; _totalDataPackets++; //if we have enough history, see if we should skip an ack if (_skipAcks && _enoughData && _skippedAcks < _maxSkipAck) { float average = 0; for (int i = 0;i < _periodHistory;i++) average+=_periods[i]; average /= _periodHistory; // skip an ack if the rate at which we receive data has not dropped sharply if (_periods[_currentPeriodId] > average / _deviation) { _skippedAcks++; _skippedAcksTotal++; } else safeSendAck(msg); } else safeSendAck(msg); // if this is the end of a period, record how many data packets we got if (_lastReceivedTime - _lastPeriod >= _period) { _lastPeriod = _lastReceivedTime; _currentPeriodId++; if (_currentPeriodId >= _periodHistory) { _currentPeriodId=0; _enoughData=true; } _periods[_currentPeriodId]=_packetsThisPeriod; _packetsThisPeriod=0; } } else if (msg instanceof KeepAliveMessage) { // No need to extend seqNo on KeepAliveMessage since it is zero KeepAliveMessage kmsg = (KeepAliveMessage) msg; // Extend the windowStart to 8 bytes the same // as the Ack kmsg.extendWindowStart( _localExtender.extendSequenceNumber(kmsg.getWindowStart()) ); long seqNo = kmsg.getSequenceNumber(); long wStart = kmsg.getWindowStart(); int priorR = _receiverWindowSpace; _receiverWindowSpace = kmsg.getWindowSpace(); // Adjust the receivers window space with knowledge of // how many extra messages we have sent since this ack if ( _sequenceNumber > wStart ) _receiverWindowSpace = DATA_WINDOW_SIZE + (int) (wStart - _sequenceNumber); //_receiverWindowSpace += (wStart - _sequenceNumber); // If receiving KeepAlives when closed, send another FinMessage if ( isClosed() ) { safeSendFin(); } // Ensure that all messages up to sent windowStart are acked // Note, you could get here preinitialization - in which case, // do nothing. if ( _sendWindow != null ) { _sendWindow.pseudoAckToReceiverWindow(wStart); // Reactivate writing if required if ( (priorR == 0 || _waitingForDataSpace) && _receiverWindowSpace > 0 ) { if(LOG.isDebugEnabled()) LOG.debug(" -- KA wakeup"); writeSpaceActivation(); } } } else if (msg instanceof FinMessage) { // Extend the msgs sequenceNumber to 8 bytes based on past state msg.extendSequenceNumber( _extender.extendSequenceNumber( msg.getSequenceNumber()) ); // Stop sending data _receiverWindowSpace = 0; // Ack the Fin message safeSendAck(msg); // If a fin message is received then close connection if ( !isClosed() ) closeAndCleanup(FinMessage.REASON_YOU_CLOSED); } } // Yield to the reading thread if it has been woken up // in the hope that it will start reading immediately // rather than getting backlogged if ( doYield ) Thread.yield(); } /** * If there is data to be written then write it * and schedule next write time. */ public synchronized void writeData() { // Make sure we don't write without a break for too long int noSleepCount = 0; while (true) { // If the input has not been started then wait again if ( _inputFromOutputStream == null ) { scheduleWriteDataEvent(WRITE_STARTUP_WAIT_TIME); return; } // Reset special flags for long wait times _waitingForDataAvailable = false; _waitingForDataSpace = false; // If someone wanted us to wait a bit then don't send data now if ( _skipADataWrite ) { _skipADataWrite = false; } else { // Otherwise, it is safe to send some data // If there is room to send something then send data // if available if ( getChunkLimit() > 0 ) { // Get data and send it Chunk chunk = _inputFromOutputStream.getChunk(); if ( chunk != null ) sendData(chunk); } else { // if no room to send data then wait for the window to Open // Don't wait more than 1 second for sanity checking scheduleWriteDataEvent( System.currentTimeMillis() + NOTHING_TO_DO_DELAY); _waitingForDataSpace = true; if(LOG.isDebugEnabled()) LOG.debug("Shutdown SendData cL:"+_chunkLimit+ " rWS:"+ _receiverWindowSpace); } } // Don't wait for next write if there is no chunk available. // Writes will get rescheduled if a chunk becomes available. synchronized(_inputFromOutputStream) { if ( _inputFromOutputStream.getPendingChunks() == 0 ) { // Don't wait more than 1 second for sanity checking scheduleWriteDataEvent( System.currentTimeMillis() + NOTHING_TO_DO_DELAY); _waitingForDataAvailable = true; if(LOG.isDebugEnabled()) LOG.debug("Shutdown SendData no pending"); return; } } // Compute how long to wait // TODO: Simplify experimental algorithm and plug it in //long waitTime = (long)_sendWindow.getRTO() / 6l; long currTime = System.currentTimeMillis(); long waitTime = _writeRegulator.getSleepTime(currTime, _receiverWindowSpace); // If we are getting too close to the end of window, make a note if ( _receiverWindowSpace <= SMALL_SEND_WINDOW ) { // Scale back on the writing speed if you are hitting limits if ( _receiverWindowSpace <= 1 ) _writeRegulator.hitZeroWindow(); } // Initially ensure waitTime is not too low if (waitTime == 0 && _sequenceNumber < 10 ) waitTime = DEFAULT_RTO_WAIT_TIME; // Enforce some minimal sleep time if we have been in tight loop // This will allow handleMessages to get done if pending if (noSleepCount >= MAX_WRITE_WITHOUT_SLEEP) { waitTime += 1; } // Only wait if the waitTime is more than zero if ( waitTime > 0 ) { long time = System.currentTimeMillis() + waitTime; scheduleWriteDataEvent(time); break; } // Count how long we are sending without a sleep noSleepCount++; } } /** * Define what happens when a keepalive timer fires. */ static class KeepAliveTimerEvent extends UDPTimerEvent { public KeepAliveTimerEvent(long time,UDPConnectionProcessor proc) { super(time,proc); } protected void doActualEvent(UDPConnectionProcessor udpCon) { long time = System.currentTimeMillis(); if(LOG.isDebugEnabled()) LOG.debug("keepalive: "+ time); // If connection closed, then make sure that keepalives have ended if (udpCon.isClosed() ) { udpCon._keepaliveEvent.unregister(); } // Make sure that some messages are received within timeframe if ( udpCon.isConnected() && udpCon._lastReceivedTime + MAX_MESSAGE_WAIT_TIME < time ) { LOG.debug("Keepalive generated shutdown"); // If no incoming messages for very long time then // close connection udpCon.closeAndCleanup(FinMessage.REASON_TIMEOUT); return; } // If reevaluation of the time still requires a keepalive then send if ( time+1 >= (udpCon._lastSendTime + KEEPALIVE_WAIT_TIME) ) { if ( udpCon.isConnected() ) { udpCon.sendKeepAlive(); } else { return; } } // Reschedule keepalive timer _eventTime = udpCon._lastSendTime + KEEPALIVE_WAIT_TIME; udpCon._scheduler.scheduleEvent(this); if(LOG.isDebugEnabled()) LOG.debug("end keepalive: "+ System.currentTimeMillis()); } } /** * Define what happens when a WriteData timer event fires. */ static class WriteDataTimerEvent extends UDPTimerEvent { public WriteDataTimerEvent(long time,UDPConnectionProcessor proc) { super(time,proc); } protected void doActualEvent(UDPConnectionProcessor udpCon) { if(LOG.isDebugEnabled()) LOG.debug("data timeout :"+ System.currentTimeMillis()); long time = System.currentTimeMillis(); // Make sure that some messages are received within timeframe if ( udpCon.isConnected() && udpCon._lastReceivedTime + MAX_MESSAGE_WAIT_TIME < time ) { // If no incoming messages for very long time then // close connection udpCon.closeAndCleanup(FinMessage.REASON_TIMEOUT); return; } // If still connected then handle then try to write some data if ( udpCon.isConnected() ) { udpCon.writeData(); } if(LOG.isDebugEnabled()) LOG.debug("end data timeout: "+ System.currentTimeMillis()); } } /** * Define what happens when an ack timeout occurs */ static class AckTimeoutTimerEvent extends UDPTimerEvent { public AckTimeoutTimerEvent(long time,UDPConnectionProcessor proc) { super(time,proc); } protected void doActualEvent(UDPConnectionProcessor udpCon) { if(LOG.isDebugEnabled()) LOG.debug("ack timeout: "+ System.currentTimeMillis()); if ( udpCon.isConnected() ) { udpCon.validateAckedData(); } if(LOG.isDebugEnabled()) LOG.debug("end ack timeout: "+ System.currentTimeMillis()); } } /** * This is an event that wakes up writing with a given delay */ static class SafeWriteWakeupTimerEvent extends UDPTimerEvent { public SafeWriteWakeupTimerEvent(long time,UDPConnectionProcessor proc) { super(time,proc); } protected void doActualEvent(UDPConnectionProcessor udpCon) { if(LOG.isDebugEnabled()) LOG.debug("write wakeup timeout: "+ System.currentTimeMillis()); if ( udpCon.isConnected() ) { udpCon.writeDataActivation(); } _eventTime = Long.MAX_VALUE; udpCon._scheduler.scheduleEvent(this); if(LOG.isDebugEnabled()) LOG.debug("write wakeup timeout: "+ System.currentTimeMillis()); } } /** * Do final cleanup and shutdown after connection is closed. */ static class ClosedConnectionCleanupTimerEvent extends UDPTimerEvent { public ClosedConnectionCleanupTimerEvent(long time, UDPConnectionProcessor proc) { super(time,proc ); } protected void doActualEvent(UDPConnectionProcessor udpCon) { if(LOG.isDebugEnabled()) LOG.debug("Closed connection timeout: "+ System.currentTimeMillis()); udpCon.finalClose(); if(LOG.isDebugEnabled()) LOG.debug("Closed connection done: "+ System.currentTimeMillis()); unregister(); } } // // ----------------------------------------------------------------- protected void finalize() { if (!isClosed()) { LOG.warn("finalizing an open UDPConnectionProcessor!"); try { close(); }catch (IOException ignored) {} } } }