/**
* Part of the CCNx Java Library.
*
* Copyright (C) 2010-2013 Palo Alto Research Center, Inc.
*
* This library is free software; you can redistribute it and/or modify it
* under the terms of the GNU Lesser General Public License version 2.1
* as published by the Free Software Foundation.
* This library is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
* Lesser General Public License for more details. You should have received
* a copy of the GNU Lesser General Public License along with this library;
* if not, write to the Free Software Foundation, Inc., 51 Franklin Street,
* Fifth Floor, Boston, MA 02110-1301 USA.
*/
package org.ccnx.ccn.impl;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.net.InetSocketAddress;
import java.net.PortUnreachableException;
import java.nio.BufferUnderflowException;
import java.nio.ByteBuffer;
import java.nio.channels.ClosedChannelException;
import java.nio.channels.DatagramChannel;
import java.nio.channels.SelectionKey;
import java.nio.channels.Selector;
import java.nio.channels.SocketChannel;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.logging.Level;
import org.ccnx.ccn.config.SystemConfiguration;
import org.ccnx.ccn.impl.CCNNetworkManager.NetworkProtocol;
import org.ccnx.ccn.impl.encoding.BinaryXMLDecoder;
import org.ccnx.ccn.impl.encoding.XMLEncodable;
import org.ccnx.ccn.impl.support.Log;
/**
* This guy manages all of the access to the network connection.
* It is capable of supporting both UDP and TCP transport protocols
*
* It also creates a stream interface for input to the decoders. It is necessary to
* create our own input stream for TCP because the stream that can be obtained via the
* socket interface is not markable. Originally the UDP code used to translate the UDP
* input data into a ByteArrayInputStream after reading it in, but we now an use the
* same input stream for both transports.
*/
public class CCNNetworkChannel extends InputStream {
public static final int HEARTBEAT_PERIOD = 3500;
public static final int SOCKET_TIMEOUT = SystemConfiguration.MEDIUM_TIMEOUT; // period to wait in ms.
// public static final int DOWN_DELAY = SystemConfiguration.MEDIUM_TIMEOUT; // Wait period for retry when ccnd is down
public static final int LINGER_TIME = 10; // In seconds
// This is to make log messages intelligible
protected final static AtomicInteger _channelIdCounter = new AtomicInteger(0);
protected final int _channelId;
// These are set in the constructor
protected final String _ncHost;
protected final int _ncPort;
protected final NetworkProtocol _ncProto;
protected final FileOutputStream _ncTapStreamIn;
protected int _ncLocalPort;
protected DatagramChannel _ncDGrmChannel = null;
protected SocketChannel _ncSockChannel = null;
// This lock provides exclusion between calls to open() and close()
protected Object _opencloseLock = new Object();
protected Selector _ncReadSelector = null;
protected Selector _ncWriteSelector = null; // Not needed for UDP
protected int _downDelay = 250;
// This lock (maybe unnecessary now?), if used with _openCloseLock, should be contained inside it.
protected Object _ncConnectedLock = new Object();
protected boolean _ncConnected = false; // Actually asking the channel if its connected doesn't appear to be reliable
protected boolean _retry = true; // Attempt to reconnect
protected boolean _ncInitialized = false;
protected Boolean _ncStarted = false;
protected BinaryXMLDecoder _decoder = null;
// Allocate datagram buffer
protected ByteBuffer _datagram = ByteBuffer.allocateDirect(CCNNetworkManager.MAX_PAYLOAD);
// The following lines can be uncommented to help with debugging (i.e. you can't easily look at
// what's in the buffer when an allocateDirect is done).
// TODO - this should be under the control of a debugging flag instead
//private byte[] buffer = new byte[CCNNetworkManager.MAX_PAYLOAD];
//protected ByteBuffer _datagram = ByteBuffer.wrap(buffer);
private int _mark = -1;
private int _readLimit = 0;
private int _lastMark = 0;
public CCNNetworkChannel(String host, int port, NetworkProtocol proto, FileOutputStream tapStreamIn) throws IOException {
_ncHost = host;
_ncPort = port;
_ncProto = proto;
_ncTapStreamIn = tapStreamIn;
_channelId = _channelIdCounter.incrementAndGet();
_decoder = new BinaryXMLDecoder();
_decoder.setResyncable(true);
if (Log.isLoggable(Log.FAC_NETMANAGER, Level.INFO))
Log.info(Log.FAC_NETMANAGER, "NetworkChannel {0}: Starting up CCNNetworkChannel using {1}.", _channelId, proto.toString());
}
/**
* Open the channel to ccnd depending on the protocol, connect on the ccnd port and
* set up the selector
*
* @throws IOException
*/
public void open() throws IOException {
synchronized(_opencloseLock) {
if (Log.isLoggable(Log.FAC_NETMANAGER, Level.INFO))
Log.info(Log.FAC_NETMANAGER, "NetworkChannel {0}: open()", _channelId);
if( _ncConnected ) {
Log.severe(Log.FAC_NETMANAGER, "NetworkChannel {0}: Calling open on an already connected channel!", _channelId);
throw new IOException("NetworkChannel " + _channelId + ": channel already connected");
}
_ncReadSelector = Selector.open();
if (_ncProto == NetworkProtocol.UDP) {
try {
_ncDGrmChannel = DatagramChannel.open();
_ncDGrmChannel.connect(new InetSocketAddress(_ncHost, _ncPort));
_ncDGrmChannel.configureBlocking(false);
// For some weird reason we seem to have to test writing twice when ccnd is down
// before the channel actually notices. There might be some kind of timing/locking
// problem responsible for this but I can't figure out what it is.
ByteBuffer test = ByteBuffer.allocate(1);
if (_ncInitialized)
_ncDGrmChannel.write(test);
wakeup();
_ncDGrmChannel.register(_ncReadSelector, SelectionKey.OP_READ);
_ncLocalPort = _ncDGrmChannel.socket().getLocalPort();
if (_ncInitialized) {
test.flip();
_ncDGrmChannel.write(test);
}
} catch (NullPointerException npe) {
Log.warning(Log.FAC_NETMANAGER, "NetworkChannel {0}: UDP open exception {1}", _channelId, npe.getMessage());
npe.printStackTrace();
return;
} catch (IOException ioe) {
Log.warning(Log.FAC_NETMANAGER, "NetworkChannel {0}: UDP open exception {1}", _channelId, ioe.getMessage());
ioe.printStackTrace();
return;
}
} else if (_ncProto == NetworkProtocol.TCP) {
_ncSockChannel = SocketChannel.open();
try {
_ncSockChannel.connect(new InetSocketAddress(_ncHost, _ncPort));
} catch (IOException ioe) {
if (!_ncInitialized) {
Log.warning(Log.FAC_NETMANAGER, "NetworkChannel {0}: TCP open exception {1}", _channelId, ioe.getMessage());
throw ioe;
}
Log.info(Log.FAC_NETMANAGER, "NetworkChannel {0}: TCP (re)open exception {1}", _channelId, ioe.getMessage());
return;
}
_ncSockChannel.configureBlocking(false);
_ncSockChannel.register(_ncReadSelector, SelectionKey.OP_READ);
_ncWriteSelector = Selector.open();
_ncSockChannel.register(_ncWriteSelector, SelectionKey.OP_WRITE);
_ncLocalPort = _ncSockChannel.socket().getLocalPort();
//_ncSockChannel.socket().setSoLinger(true, LINGER_TIME);
} else {
throw new IOException("NetworkChannel " + _channelId + ": invalid protocol specified");
}
if (Log.isLoggable(Log.FAC_NETMANAGER, Level.INFO)) {
String connecting = (_ncInitialized ? "Reconnecting to" : "Contacting");
Log.info(Log.FAC_NETMANAGER, "NetworkChannel {0}: {1} CCN agent at {2}:{3} on local port {4}",
_channelId,
connecting,
_ncHost,
_ncPort,
_ncLocalPort
);
}
initStream();
_ncInitialized = true;
_downDelay = _ncPort * 17 % 199 + 101; // randomize a bit on backoff times
synchronized (_ncConnectedLock) {
_ncConnected = true;
}
}
}
/**
* Get the next packet from the network. It could be either an interest or data. If ccnd is
* down this is where we do a sleep to avoid a busy wait. We go ahead and try to read in
* the initial data here also because if there isn't any we want to find out here, not in the middle
* of thinking we might be able to decode something. Also since this is supposed to happen
* on packet boundaries, we reset the data buffer to its start during the initial read. We only do
* the initial read if there's nothing already in the buffer though because in TCP we could have
* read in some or all of a preceding packet during the last reading.
*
* Also it should be noted that we are relying on ccnd to guarantee that all packets sent
* to us are complete ccn packets. This code does not have the ability to recover from
* receiving a partial ccn packet followed by correctly formed ones.
*
* @return a ContentObject, an Interest, or null if there's no data waiting
* @throws IOException
*/
public XMLEncodable getPacket() throws IOException {
if (isConnected()) {
_mark = -1;
_readLimit = 0;
if (! _datagram.hasRemaining()) {
int ret = doReadIn(0);
if (ret <= 0 || !isConnected())
return null;
}
_decoder.beginDecoding(this);
return _decoder.getPacket();
}
try {
if (_retry) {
synchronized (_opencloseLock) {
_opencloseLock.wait(_downDelay);
if (! _ncConnected) {
if (_downDelay < HEARTBEAT_PERIOD)
_downDelay = _downDelay * 2 + 1;
open();
}
}
} else {
// We do not want to spin without a delay
Thread.sleep(_downDelay);
}
} catch (InterruptedException e) {
Log.info(Log.FAC_NETMANAGER, "NetworkChannel {0}: interrupted", _channelId);
}
return null;
}
/**
* Close the channel depending on the protocol
* @throws IOException
*/
@Override
public void close() throws IOException {
close(false);
}
private void close(boolean retry) throws IOException {
synchronized(_opencloseLock) {
if (Log.isLoggable(Log.FAC_NETMANAGER, Level.INFO))
Log.info(Log.FAC_NETMANAGER, "NetworkChannel {0}: close({1})", _channelId, retry);
_retry &= retry;
synchronized (_ncConnectedLock) {
_ncConnected = false;
}
_ncReadSelector.close();
if (_ncWriteSelector != null)
_ncWriteSelector.close();
if (_ncDGrmChannel != null) {
_ncDGrmChannel.close();
}
if (_ncSockChannel != null) {
_ncSockChannel.close();
}
}
}
/**
* Check whether the channel is currently connected. This is really a test
* to see whether ccnd is running. If it isn't the channel is not connected.
* @return true if connected
*/
public boolean isConnected() {
synchronized (_ncConnectedLock) {
return _ncConnected;
}
}
/**
* Write to ccnd using methods based on the protocol type
* @param src - ByteBuffer to write
* @return - number of bytes written
* @throws IOException
*/
public int write(ByteBuffer src) throws IOException {
if (! isConnected())
return -1; // XXX - is this documented?
if (Log.isLoggable(Log.FAC_NETMANAGER, Level.FINEST))
Log.finest(Log.FAC_NETMANAGER,
"NetworkChannel {0}: write() on port {1}", _channelId, _ncLocalPort);
try {
if (_ncDGrmChannel != null) {
return (_ncDGrmChannel.write(src));
} else {
// XXX -this depends on synchronization in caller, which is less than ideal.
// Need to handle partial writes
int written = 0;
while (src.hasRemaining()) {
if (! isConnected())
return -1;
int b = _ncSockChannel.write(src);
if (b > 0) {
written += b;
} else {
_ncWriteSelector.selectedKeys().clear();
_ncWriteSelector.select();
}
}
return written;
}
} catch (PortUnreachableException pue) {}
catch (ClosedChannelException cce) {}
Log.info(Log.FAC_NETMANAGER, "NetworkChannel {0}: closing due to error on write", _channelId);
close(true);
return -1;
}
/**
* Force wakeup from a select
* @return the selector
*/
public Selector wakeup() {
return (_ncReadSelector.wakeup());
}
/**
* Initialize the channel at the point when we are actually ready to create faces
* with ccnd
* @throws IOException
*/
public void init() throws IOException {
}
private void initStream() {
_datagram.clear();
_datagram.limit(0);
}
@Override
public int read() throws IOException {
while (true) {
try {
if (_datagram.hasRemaining()) {
int ret = _datagram.get();
return ret & 0xff;
}
} catch (BufferUnderflowException bfe) {}
int ret = fill();
if (ret < 0) {
return ret;
}
}
}
@Override
public int read(byte[] b) throws IOException {
return read(b, 0, b.length);
}
@Override
public int read(byte[] b, int off, int len) throws IOException {
int ret = 0;
if (len > b.length - off) {
throw new IndexOutOfBoundsException();
}
if (! _datagram.hasRemaining()) {
int tmpRet = fill();
if (tmpRet <= 0) {
return tmpRet;
}
}
ret = _datagram.remaining() > len ? len : _datagram.remaining();
_datagram.get(b, off, ret);
return ret;
}
@Override
public boolean markSupported() {
return true;
}
@Override
public void mark(int readlimit) {
_readLimit = readlimit;
_mark = _datagram.position();
}
@Override
public void reset() throws IOException {
if (_mark < 0)
throw new IOException("Reset called with no mark set - readlimit: " + _readLimit + " lastMark: " + _lastMark);
if ((_datagram.position() - _mark) > _readLimit) {
throw new IOException("Invalid reset called past readlimit");
}
_datagram.position(_mark);
}
/**
* Refill the buffer. We don't reset the start of it unless necessary (i.e. we have
* reached the end of the buffer). If the start is reset and a mark has been set within
* "readLimit" bytes of the end, we need to copy the end of the previous buffer out
* to the start so that a reset is possible.
*
* @return
* @throws IOException
*/
private int fill() throws IOException {
int position = _datagram.position();
if (position >= _datagram.capacity()) {
byte[] b = null;
boolean doCopy = false;
int checkPosition = position - 1;
doCopy = _mark >= 0 && _mark + _readLimit >= checkPosition;
if (doCopy) {
b = new byte[checkPosition - (_mark - 1)];
_datagram.position(_mark);
_datagram.get(b);
}
_datagram.clear();
if (doCopy) {
_datagram.put(b);
_mark = 0;
} else {
_lastMark = _mark;
_mark = -1;
}
position = _datagram.position();
}
return doReadIn(position);
}
/**
* Read in data to the buffer starting at the specified position.
* @param position
* @return
* @throws IOException
*/
private int doReadIn(int position) throws IOException {
int ret = 0;
_ncReadSelector.selectedKeys().clear();
if (_ncReadSelector.select() != 0) {
if (! isConnected())
return -1;
// Note that we must set limit first before setting position because setting
// position larger than limit causes an exception.
_datagram.limit(_datagram.capacity());
_datagram.position(position);
if (_ncDGrmChannel != null) {
ret = _ncDGrmChannel.read(_datagram);
} else {
ret = _ncSockChannel.read(_datagram);
}
if (ret >= 0) {
// The following is the equivalent to doing a flip except we don't
// want to reset the position to 0 as flip would do (because we
// potentially want to preserve a mark). But the read positions
// the buffer to end of the read and we want to reposition to the start
// of the data just read in.
_datagram.limit(position + ret);
_datagram.position(position);
if (null != _ncTapStreamIn) {
byte [] b = new byte[ret];
_datagram.get(b);
_ncTapStreamIn.write(b);
// Got the data so we have to redo the "hand flip" to read from the
// correct position.
_datagram.limit(position + ret);
_datagram.position(position);
}
} else
close(true);
}
return ret;
}
/**
* @return true if heartbeat sent
*/
public boolean heartbeat() {
try {
ByteBuffer heartbeat = ByteBuffer.allocate(1);
_ncDGrmChannel.write(heartbeat);
return true;
} catch (IOException io) {
// We do not see errors on send typically even if
// agent is gone, so log each but do not track
Log.warning(Log.FAC_NETMANAGER,
"NetworkChannel {0}: Error sending heartbeat packet: {1}", _channelId, io.getMessage());
try {
close(true);
} catch (IOException e) {}
}
return false;
}
} /* NetworkChannel */