All Downloads are FREE. Search and download functionalities are using the official Maven repository.

eu.dariolucia.ccsds.sle.utl.network.tml.TmlChannel Maven / Gradle / Ivy

The newest version!
/*
 *  Copyright 2018-2019 Dario Lucia (https://www.dariolucia.eu)
 *
 *  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 eu.dariolucia.ccsds.sle.utl.network.tml;

import eu.dariolucia.ccsds.sle.utl.si.PeerAbortReasonEnum;
import eu.dariolucia.ccsds.sle.utl.util.DataRateCalculator;
import eu.dariolucia.ccsds.sle.utl.util.DataRateSample;

import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.net.Socket;
import java.nio.ByteBuffer;
import java.util.Arrays;
import java.util.Timer;
import java.util.TimerTask;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;
import java.util.logging.Level;
import java.util.logging.Logger;

/**
 * This class implements all the required TML features as specified by CCSDS 913.1-B-2:
 * 
    *
  • TML context, pdu and heartbeat message;
  • *
  • TX and RX heartbeat timers and dead factor;
  • *
  • Server and client modes.
  • *
* Given the nature of the defined protocol, interactions with instances of this class have an asynchronous nature: * a callback object must be provided in the constructor and the relevant methods will be called, depending on the * type of data received from the underlying transport layer. * * The class includes support for rate calculation, but relies on an external polling mechanism. */ public abstract class TmlChannel { /** * Static creation function to instantiate a client TML channel, i.e. a channel that can be used by the SLE User * Test Library to connect to a remote SLE service instance and initiate a session. * * @param host the hostname to connect to * @param port the TCP port to connect to * @param heartbeatTimer the heartbeat interval to propose in the TML context message and use later on, in seconds * @param deadFactor the dead factor to propose in the TML context message and use later on * @param observer the callback interface * @param txBuffer the number of bytes to be set for the TCP transmission buffer * @param rxBuffer the number of bytes to be set for the TCP reception buffer * @return the TML channel, ready to be connected */ public static TmlChannel createClientTmlChannel(String host, int port, int heartbeatTimer, int deadFactor, ITmlChannelObserver observer, int txBuffer, int rxBuffer) { return new TmlChannelClient(host, port, heartbeatTimer, deadFactor, observer, txBuffer, rxBuffer); } /** * Static creation function to instantiate a server TML channel, i.e. a channel that can be used by the SLE User * Test Library to wait for connections from a remote SLE service instance. * * @param port the TCP port to open, to wait for incoming connections * @param observer the callback interface * @param txBuffer the number of bytes to be set for the TCP transmission buffer * @param rxBuffer the number of bytes to be set for the TCP reception buffer * @return the TML channel, ready to be put in listen mode */ public static TmlChannel createServerTmlChannel(int port, ITmlChannelObserver observer, int txBuffer, int rxBuffer) { return new TmlChannelServer(port, observer, txBuffer, rxBuffer); } private static final Logger LOG = Logger.getLogger(TmlChannel.class.getName()); protected static final byte[] PDU_MESSAGE_HDR = new byte[] {0x01, 0x00, 0x00, 0x00}; protected static final byte[] HBT_MESSAGE = new byte[] {0x03, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00}; protected static final byte[] CTX_MESSAGE_HDR = new byte[] {0x02, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x0C, 0x49, 0x53, 0x50, 0x31, 0x00, 0x00, 0x00, 0x01}; protected final AtomicInteger heartbeatTimer; protected final AtomicInteger deadFactor; protected final ITmlChannelObserver observer; protected final Lock lock = new ReentrantLock(); protected final Lock writeLock = new ReentrantLock(); // To handle concurrent writing to the socket between HBT and PDU private final Timer hbtScheduler = new Timer(true); protected final int txBuffer; protected final int rxBuffer; protected Socket sock; protected InputStream rxStream; protected OutputStream txStream; protected Thread readingThread; private TimerTask hbtRxTimer; private TimerTask hbtTxTimer; protected volatile boolean aboutToDisconnect = false; protected volatile boolean running = false; protected final DataRateCalculator statsCounter = new DataRateCalculator(); /** * Initialise a TML channel. * * @param observer the callback interface * @param txBuffer the TCP transmission buffer in bytes (less or equal 0 means 'do not set') * @param rxBuffer the TCP reception buffer in bytes (less or equal 0 means 'do not set') */ protected TmlChannel(ITmlChannelObserver observer, int txBuffer, int rxBuffer) { if(observer == null) { throw new NullPointerException("Channel observer cannot be null"); } this.heartbeatTimer = new AtomicInteger(60); // Will be overwritten this.deadFactor = new AtomicInteger(4); // Will be overwritten this.observer = observer; this.txBuffer = txBuffer; this.rxBuffer = rxBuffer; } /** * Depending on the channel mode, invoking this method will cause either the connection attempt to the provider, or * the opening of a server port ready to accept incoming connections. This method returns upon establishment of * the connection to the provider, or as soon as the server port is bound and listening. * * @throws TmlChannelException in case of error when establishing the connection */ public void connect() throws TmlChannelException { this.aboutToDisconnect = false; performConnect(); } protected abstract void performConnect() throws TmlChannelException; protected final void notifyChannelConnected() { try { this.observer.onChannelConnected(this); } catch (Exception e) { LOG.log(Level.WARNING, String.format("Notification of connection on channel %s threw exception on observer", toString()), e); } } /** * This method is called by the SLE User Test Library service instance when a positive unbind return is received. * It avoids detecting subsequent disconnection as a critical error. */ public void aboutToDisconnect() { this.aboutToDisconnect = true; } @SuppressWarnings("resource") protected void startReadingThread(String name) { this.running = true; this.readingThread = new Thread(this::readingThreadMain); this.readingThread.setName(name); this.readingThread.start(); } protected abstract boolean isTmlContextMsgExpected(); protected abstract boolean performPreConnectionOperations(); private void readingThreadMain() { // Perform preliminary operations to obtain the socket, if any if(!performPreConnectionOperations()) { return; } // Check if you need to wait for a TML context message boolean tmlContextMsgReceived = !isTmlContextMsgExpected(); byte[] headerBuffer = new byte[8]; InputStream is = getRxStream(); // Start of the reading cycle while(is != null && this.running) { // Read TML message or HB int read = 0; try { // Read at least 8 octets (ref. CCSDS 913.1-B-2 3.3.4.2.3.2) while(read < 8 && this.running) { int readOther = is.read(headerBuffer, read, 8 - read); if(readOther <= 0) { throw new IOException("end of stream (TML header) detected"); } read += readOther; } this.statsCounter.addIn(read); } catch (IOException e) { if(!this.aboutToDisconnect) { // Inform remote disconnection: at this stage, the disconnection could be because a peer abort // or other reasons. If it is because of a peer abort, headerBuffer should contain a single // byte. If this is the case, then the peer abort can be decoded. if(read == 1) { if(LOG.isLoggable(Level.FINE)) { LOG.fine(String.format("Reading thread on channel %s detected remote disconnection with a single byte %d", toString(), read)); } remotePeerAbortDetected(e, headerBuffer[0]); } else { if(LOG.isLoggable(Level.FINE)) { LOG.fine(String.format("Reading thread on channel %s detected remote disconnection", toString())); } remoteDisconnectionDetected(e); } } return; } if(!this.running) { if(LOG.isLoggable(Level.WARNING)) { LOG.warning(String.format("Reading thread on channel %s stopped", toString())); } return; } if(!tmlContextMsgReceived) { // If TML context message is not received and the message is a TML context message, process it // (ref. CCSDS 913.1-B-2 3.3.4.2.3.2.2) if(isTmlContextMsg(headerBuffer)) { // Read other 12 bytes (protocol ID, spare + version, HBT, DF) byte[] msg = new byte[12]; int read2 = 0; try { while(read2 < 12 && this.running) { int readOther = is.read(msg, read2, 12 - read2); if(readOther <= 0) { throw new IOException("end of stream (CTX) detected"); } read2 += readOther; } this.statsCounter.addIn(read2); } catch (IOException e) { if(!this.aboutToDisconnect) { // inform remote disconnection remoteDisconnectionDetected(e); } return; } ByteBuffer reader = ByteBuffer.wrap(msg, 8, 4); this.heartbeatTimer.set(reader.getShort()); this.deadFactor.set(reader.getShort()); if(LOG.isLoggable(Level.FINE)) { LOG.fine(String.format("HB interval set to %d, dead factor set to %s", this.heartbeatTimer.get(), this.deadFactor.get())); } // start HBT timers, if needed startHbtTimers(); // notify notifyChannelConnected(); tmlContextMsgReceived = true; } else { if(LOG.isLoggable(Level.WARNING)) { LOG.warning(String.format("Expecting TML context message on channel %s but received %s", toString(), Arrays.toString(headerBuffer))); } protocolErrorDetected(headerBuffer, TmlDisconnectionReasonEnum.PROTOCOL_ERROR); return; } } else if(isTmlHbt(headerBuffer)) { if(LOG.isLoggable(Level.FINE)) { LOG.fine(String.format("HB on channel %s received", toString())); } // If TML HBT, restart Rx timer restartHbtRxTimer(); } else if(isTmlPdu(headerBuffer)) { // Restart Rx timer restartHbtRxTimer(); // The last 4 bytes of the header buffer are the length ByteBuffer reader = ByteBuffer.wrap(headerBuffer, 4, 4); int length = reader.getInt(); byte[] pdu = new byte[length]; int read2 = 0; try { while(read2 < length && this.running) { int readOther = is.read(pdu, read2, length - read2); if(readOther <= 0) { throw new IOException("end of stream (PDU) detected"); } read2 += readOther; } this.statsCounter.addIn(read2); } catch (IOException e) { if(!this.aboutToDisconnect) { // inform remote disconnection remoteDisconnectionDetected(e); } return; } tmlPduReceived(pdu); } else { // Ref. CCSDS 913.1-B-2 3.3.2.2.2, point a) protocolErrorDetected(headerBuffer, TmlDisconnectionReasonEnum.UNKNOWN_TYPE_ID); return; } is = getRxStream(); } if(LOG.isLoggable(Level.WARNING)) { LOG.warning(String.format("Reading thread on channel %s has null inputstream, thread returns", toString())); } } private boolean isTmlContextMsg(byte[] headerBuffer) { return headerBuffer != null && headerBuffer.length > 0 && headerBuffer[0] == 0x02; } private void tmlPduReceived(byte[] pdu) { if(LOG.isLoggable(Level.FINE)) { LOG.fine(String.format("PDU received on channel %s", toString())); } try { this.observer.onPduReceived(this, pdu); } catch(Exception e) { LOG.log(Level.SEVERE, String.format("Exception while forwarding PDU from channel %s to observer", toString()), e); } } private void protocolErrorDetected(byte[] headerBuffer, TmlDisconnectionReasonEnum reason) { if(LOG.isLoggable(Level.SEVERE)) { LOG.log(Level.SEVERE, String.format("Protocol error detected on channel %s with reason %s, header=%s", toString(), reason, Arrays.toString(headerBuffer))); } this.lock.lock(); try { // stop the channel performChannelStop(reason, null); // return if(LOG.isLoggable(Level.FINE)) { LOG.fine(String.format("Channel disconnected: %s via protocolErrorDetected()", toString())); } } finally { this.lock.unlock(); } } private boolean isTmlPdu(byte[] headerBuffer) { return headerBuffer != null && headerBuffer.length > 0 && headerBuffer[0] == 0x01; } private void restartHbtRxTimer() { if(LOG.isLoggable(Level.FINER)) { LOG.finer(String.format("Starting HBT RX timer of %s to %d seconds", toString(), this.heartbeatTimer.get() * this.deadFactor.get())); } this.lock.lock(); try { if(this.hbtRxTimer != null) { this.hbtRxTimer.cancel(); this.hbtRxTimer = null; } if(this.heartbeatTimer.get() > 0) { this.hbtRxTimer = new TimerTask() { @Override public void run() { hbtRxTimerExpired(); } }; this.hbtScheduler.schedule(this.hbtRxTimer, this.heartbeatTimer.get() * 1000L * this.deadFactor.get()); } } finally { this.lock.unlock(); } } private void hbtRxTimerExpired() { if(LOG.isLoggable(Level.SEVERE)) { LOG.log(Level.SEVERE, String.format("HBT Rx expired detected on channel %s", toString())); } this.lock.lock(); try { // stop the channel performChannelStop(TmlDisconnectionReasonEnum.RX_HBT_EXPIRED, null); // return if(LOG.isLoggable(Level.FINE)) { LOG.fine(String.format("Channel disconnected: %s via hbtRxTimerExpired()", toString())); } } finally { this.lock.unlock(); } } private void restartHbtTxTimer() { if(LOG.isLoggable(Level.FINE)) { LOG.fine(String.format("Starting HBT TX timer of %s to %d seconds", toString(), this.heartbeatTimer.get())); } this.lock.lock(); try { if(this.hbtTxTimer != null) { this.hbtTxTimer.cancel(); this.hbtTxTimer = null; } if(this.heartbeatTimer.get() > 0) { this.hbtTxTimer = new TimerTask() { @Override public void run() { sendHbtMessage(); } }; this.hbtScheduler.schedule(this.hbtTxTimer, this.heartbeatTimer.get() * 1000L); } } finally { this.lock.unlock(); } } private boolean isTmlHbt(byte[] headerBuffer) { return headerBuffer != null && headerBuffer.length > 0 && headerBuffer[0] == 0x03; } private void remotePeerAbortDetected(IOException e, byte code) { String exceptionReason = e != null ? e.getMessage() : "N/A"; if(exceptionReason == null) { exceptionReason = "null"; } if(LOG.isLoggable(Level.SEVERE)) { LOG.log(Level.SEVERE, String.format("Remote peer abort detected (%s) on channel %s, code %s", exceptionReason, toString(), PeerAbortReasonEnum.fromCode(code))); } this.lock.lock(); try { // stop the channel performChannelStop(TmlDisconnectionReasonEnum.REMOTE_PEER_ABORT, PeerAbortReasonEnum.fromCode(code)); // return if(LOG.isLoggable(Level.FINE)) { LOG.fine(String.format("Channel disconnected: %s via remotePeerAbortDetected()", toString())); } } finally { this.lock.unlock(); } } protected void remoteDisconnectionDetected(IOException e) { String exceptionReason = e != null ? e.getMessage() : "N/A"; if(exceptionReason == null) { exceptionReason = "null"; } if(LOG.isLoggable(Level.SEVERE)) { LOG.log(Level.SEVERE, String.format("Remote disconnection detected (%s) on channel %s", exceptionReason, toString())); } this.lock.lock(); try { // stop the channel performChannelStop(TmlDisconnectionReasonEnum.REMOTE_DISCONNECT, null); // return if(LOG.isLoggable(Level.FINE)) { LOG.fine(String.format("Channel disconnected: %s via remoteDisconnectionDetected()", toString())); } } finally { this.lock.unlock(); } } private void performChannelStop(TmlDisconnectionReasonEnum disconnectionReason, PeerAbortReasonEnum peerAbortReason) { // stop read thread stopReadingThread(); // stop HBT timers, if needed stopHbtTimers(); // disconnect from endpoint disconnectEndpoint(disconnectionReason, peerAbortReason); // cleanup cleanup(); } private InputStream getRxStream() { this.lock.lock(); try { return this.rxStream; } finally { this.lock.unlock(); } } protected void startHbtTimers() { restartHbtRxTimer(); restartHbtTxTimer(); } private void sendHbtMessage() { if(LOG.isLoggable(Level.FINE)) { LOG.fine(String.format("Sending HBT from %s via sendHbtMessage()", toString())); } OutputStream os = getTxStream(); if(os == null) { if(LOG.isLoggable(Level.WARNING)) { LOG.warning(String.format("Cannot send HBT on channel %s, disconnected", toString())); } return; } this.writeLock.lock(); // Acquire try { os.write(HBT_MESSAGE); if(LOG.isLoggable(Level.FINE)) { LOG.fine(String.format("HB sent from channel %s", toString())); } this.statsCounter.addOut(HBT_MESSAGE.length); this.writeLock.unlock(); // Release OK } catch (IOException e) { this.writeLock.unlock(); // Release fail LOG.log(Level.SEVERE, String.format("Exception while sending HBT on channel %s", toString()), e); this.lock.lock(); try { // stop the channel performChannelStop(TmlDisconnectionReasonEnum.HBT_TX_SEND_ERROR, null); // return if(LOG.isLoggable(Level.FINE)) { LOG.fine(String.format("Channel disconnected: %s via sendHbtMessage()", toString())); } } finally { this.lock.unlock(); } } restartHbtTxTimer(); } protected void cleanup() { this.sock = null; this.rxStream = null; this.txStream = null; this.readingThread = null; } /** * This method sends a PEER-ABORT to the other end using the urgent byte, and disconnects the channel. * * @param reason the reason of the PEER-ABORT */ public void abort(byte reason) { this.lock.lock(); try { // send urgent data try { if (this.sock != null) { this.sock.sendUrgentData(reason); } else { if(LOG.isLoggable(Level.INFO)) { LOG.info(String.format("Aborting channel %s but no connection is established, urgent data %s not sent", toString(), PeerAbortReasonEnum.fromCode(reason))); } } } catch (IOException e) { LOG.log(Level.WARNING, String.format("Exception while aborting channel %s with reason %s", toString(), PeerAbortReasonEnum.fromCode(reason)), e); } // stop the channel performChannelStop(TmlDisconnectionReasonEnum.PEER_ABORT, PeerAbortReasonEnum.fromCode(reason)); // return if(LOG.isLoggable(Level.FINE)) { LOG.fine(String.format("Channel disconnected: %s via abort()", toString())); } } finally { this.lock.unlock(); } } /** * This method disconnects the channel. If the channel is already disconnected, it does not do anything. */ public void disconnect() { if(LOG.isLoggable(Level.FINEST)) { LOG.log(Level.FINEST, String.format("Socket on channel %s received a disconnect request", toString())); } this.lock.lock(); try { boolean alreadyDisconnected = checkIfAlreadyDisconnected(); if (alreadyDisconnected) { if(LOG.isLoggable(Level.FINE)) { LOG.fine(String.format("Disconnecting channel %s but it is already disconnected", toString())); } return; } // stop the channel performChannelStop(TmlDisconnectionReasonEnum.LOCAL_DISCONNECT, null); // return if(LOG.isLoggable(Level.FINE)) { LOG.fine(String.format("Channel disconnected: %s via disconnect()", toString())); } } finally { this.lock.unlock(); } } private void disconnectEndpoint(TmlDisconnectionReasonEnum reason, PeerAbortReasonEnum peerAbortReason) { // Remember if the channel was already disconnected: a TML channel client is already disconnected if this.sock // is null; in addition, a TML channel server is already disconnected if this.serverSocket is closed or null. boolean wasAlreadyDisconnected = checkIfAlreadyDisconnected(); // Close the thread try { this.aboutToDisconnect = true; if(this.sock != null) { this.sock.close(); } if(this.rxStream != null) { this.rxStream.close(); } if(this.txStream != null) { this.txStream.close(); } performPostDisconnectionOperations(); } catch (IOException e) { LOG.log(Level.FINE, String.format("Socket/stream on channel %s threw exception on close()", toString()), e); } // Send the notification only if the channel has been disconnected now if(!wasAlreadyDisconnected) { try { this.observer.onChannelDisconnected(this, reason, peerAbortReason); } catch (Exception e) { LOG.log(Level.WARNING, String.format("Notification of disconnection on channel %s threw exception on observer", toString()), e); } } } protected abstract boolean checkIfAlreadyDisconnected(); protected abstract void performPostDisconnectionOperations() throws IOException; private void stopHbtTimers() { if(this.hbtRxTimer != null) { this.hbtRxTimer.cancel(); this.hbtRxTimer = null; } if(this.hbtTxTimer != null) { this.hbtTxTimer.cancel(); this.hbtTxTimer = null; } } private void stopReadingThread() { this.running = false; } /** * This method allows to send a PDU to the other endpoint. * * @param pdu the PDU to be sent * @throws TmlChannelException if the channel is not connected */ public void sendPdu(byte[] pdu) throws TmlChannelException { OutputStream os = getTxStream(); if(os == null) { throw new TmlChannelException("Channel " + toString() + " not connected"); } this.writeLock.lock(); try { byte[] toSend = ByteBuffer.allocate(8 + pdu.length).put(PDU_MESSAGE_HDR).putInt(pdu.length).put(pdu).array(); os.write(toSend); this.statsCounter.addOut(toSend.length); } catch (IOException e) { throw new TmlChannelException("Exception while writing on channel " + toString(), e); } finally { this.writeLock.unlock(); } } protected OutputStream getTxStream() { this.lock.lock(); try { return this.txStream; } finally { this.lock.unlock(); } } /** * This method is used to compute the current data rate in bytes/seconds. The sampling time is driven by the * frequency used to call this method. * * @return the current data rate (and other ancillary information) */ public DataRateSample getDataRate() { return this.statsCounter.sample(); } /** * This method is used to check whether this TML channel is running. A channel is considered running if it is * actively trying to read data from a TCP/IP connection. Disconnected channels are by definition not running. * * @return true if the channel is running */ public boolean isRunning() { return this.running; } }




© 2015 - 2025 Weber Informatics LLC | Privacy Policy