
net.named_data.jndn.transport.AsyncTcpTransport Maven / Gradle / Ivy
Go to download
Show more of this group Show more artifacts with this name
Show all versions of jndn-android-with-async-io Show documentation
Show all versions of jndn-android-with-async-io Show documentation
jNDN is a new implementation of a Named Data Networking client library written in Java. It is wire format compatible with the new NDN-TLV encoding, with NDNx and PARC's CCNx.
/**
* Copyright (C) 2015-2017 Regents of the University of California.
*
* @author: Jeff Thompson
* @author: Andrew Brown
* @author: Wei Yu
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Lesser General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program 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 program. If not, see .
* A copy of the GNU Lesser General Public License is in the file COPYING.
*/
package net.named_data.jndn.transport;
import java.io.IOException;
import java.net.InetSocketAddress;
import java.nio.ByteBuffer;
import java.nio.channels.AsynchronousChannelGroup;
import java.nio.channels.AsynchronousSocketChannel;
import java.nio.channels.CompletionHandler;
import java.util.concurrent.ScheduledExecutorService;
import java.util.concurrent.Semaphore;
import java.util.concurrent.TimeUnit;
import java.util.logging.Level;
import java.util.logging.Logger;
import net.named_data.jndn.encoding.ElementListener;
import net.named_data.jndn.encoding.ElementReader;
import net.named_data.jndn.encoding.EncodingException;
import net.named_data.jndn.util.Common;
/**
* AsyncTcpTransport extends Transport for async communication over TCP by
* dispatching reads from an AsynchronousSocketChannel to a
* ScheduledExecutorService. On Android, this requires Android API level 26.
*
* If enabled in the connection information, this transport implements reconnection
* logic if the:
* 1. read completion handler fails
* 2. write completion handler fails
* 3. connect fails
*
* The reconnect is scheduled with a default 5 second delay on the same thread pool
* passed in the constructor; only one reconnect will be scheduled at a time. During
* the reconnection phase, reads and writes will fail (they would in any case because
* the connection is broken). If the reconnect fails, it will schedule another reconnect
* at the default interval. Note that while reconnect will solve the connection issue
* after it succeeds, but will not re-register prefixes. This should must be handled
* appropriately by client logic.
*
* To test the reconnect logic:
* 1. launch nfd
* 2. launch client using this transport
* 3. wait prefix registered and some interests expressed successfully from client
* 4. stop nfd
* 5. client will catch IOException (broken pipe, connection refused, etc) when it tries
* to express interests
* 6. start nfd again
* 7. client will be able to express interests again normally
*/
public class AsyncTcpTransport extends Transport
{
public AsyncTcpTransport(ScheduledExecutorService threadPool) {
threadPool_ = threadPool;
// This is the CompletionHandler for asyncRead().
readCompletionHandler_ = new CompletionHandler() {
public void completed(Integer bytesRead, Void attachment) {
// Need to catch and log exceptions at this async entry point.
try {
if (bytesRead > 0) {
inputBuffer_.flip();
elementReader_.onReceivedData(inputBuffer_);
}
// Repeatedly do async read.
asyncRead();
} catch (Throwable ex) {
logger_.log(Level.SEVERE, null, ex);
}
}
public void failed(Throwable ex, Void attachment) {
logger_.log(Level.SEVERE, "Failed to read from transport", ex);
if(connectionInfo_.shouldAttemptReconnection() && acquireReconnectLock()) {
scheduleReconnect();
}
}
};
// This is the CompletionHandler for send().
writeCompletionHandler_ = new CompletionHandler() {
public void completed(Integer bytesRead, ByteBuffer data) {
// Need to catch and log exceptions at this async entry point.
try {
if (data.hasRemaining()) {
channel_.write(data, data, writeCompletionHandler_);
} else {
writeLock_.release();
}
} catch (Throwable ex) {
logger_.log(Level.SEVERE, null, ex);
}
}
public void failed(Throwable ex, ByteBuffer data) {
logger_.log(Level.SEVERE, "Failed to write to transport", ex);
writeLock_.release();
if(connectionInfo_.shouldAttemptReconnection() && acquireReconnectLock()) {
scheduleReconnect();
}
}
};
}
/**
* AsyncTcpTransport.ConnectionInfo extends Transport.ConnectionInfo to hold
* the host and port info for the TCP connection. The reconnection logic is
* disabled by default.
*/
public static class ConnectionInfo extends Transport.ConnectionInfo {
/**
* Create a ConnectionInfo with the given host and port.
* @param host The host for the connection.
* @param port The port number for the connection.
* @param attemptReconnection if true, drop packets until reconnected within {@link #DEFAULT_RECONNECT_TRY_DELAY_MS}
*/
public ConnectionInfo(String host, int port, boolean attemptReconnection) {
host_ = host;
port_ = port;
attemptReconnection_ = attemptReconnection;
}
/**
* Create a ConnectionInfo with the given host and port.
* @param host The host for the connection.
* @param port The port number for the connection.
*/
public ConnectionInfo(String host, int port) {
this(host, port, false);
}
/**
* Create a ConnectionInfo with the given host and default port 6363
* @param host The host for the connection.
*/
public ConnectionInfo(String host) {
this(host, 6363, false);
}
/**
* Get the host given to the constructor.
* @return The host.
*/
public final String
getHost() {
return host_;
}
/**
* Get the port given to the constructor.
* @return The port number.
*/
public final int
getPort() {
return port_;
}
/**
* Get attemptReconnection flag; if true, drop packets until reconnected within {@link #DEFAULT_RECONNECT_TRY_DELAY_MS}
* @return true if blockForReconnect
*/
public final boolean
shouldAttemptReconnection() {
return attemptReconnection_;
}
private final String host_;
private final int port_;
private final boolean attemptReconnection_;
}
/**
* Determine whether this transport connecting according to connectionInfo is
* to a node on the current machine; results are cached. According to
* http://redmine.named-data.net/projects/nfd/wiki/ScopeControl#local-face,
* TCP transports with a loopback address are local. If connectionInfo
* contains a host name, InetAddress will do a blocking DNS lookup; otherwise
* it will parse the IP address and examine the first octet to determine if
* it is a loopback address (e.g. first octet == 127).
* @param connectionInfo An AsyncTcpTransport.ConnectionInfo with the host to
* check.
* @return True if the host is local, false if not.
* @throws java.io.IOException
*/
public boolean
isLocal(Transport.ConnectionInfo connectionInfo) throws IOException {
synchronized (isLocalLock_) {
if (connectionInfo_ == null || !((ConnectionInfo) connectionInfo).getHost()
.equals(connectionInfo_.getHost())) {
isLocal_ = TcpTransport.getIsLocal
(((ConnectionInfo) connectionInfo).getHost());
connectionInfo_ = (ConnectionInfo) connectionInfo;
}
return isLocal_;
}
}
/**
* Override to return true since connect needs to use the onConnected callback.
* @return True.
*/
public boolean
isAsync() {
return true;
}
/**
* Connect according to the info in ConnectionInfo, and use elementListener.
* @param connectionInfo An AsyncTcpTransport.ConnectionInfo.
* @param elementListener The ElementListener must remain valid during the
* life of this object.
* @param onConnected This calls onConnected.run() when the connection is
* established. This is needed since connect is async.
* @throws IOException For I/O error.
*/
public void
connect
(Transport.ConnectionInfo connectionInfo, ElementListener elementListener,
final Runnable onConnected)
throws IOException {
logger_.log(Level.FINE, "Connecting...");
// TODO: Close a previous connection.
channelGroup_ = AsynchronousChannelGroup.withThreadPool(threadPool_);
channel_ = AsynchronousSocketChannel.open(channelGroup_);
//store other info for reconnect
this.connectionInfo_ = (ConnectionInfo) connectionInfo;
this.elementListener_ = elementListener;
this.onConnected_ = onConnected;
// connect is already async, so no need to dispatch.
channel_.connect
(new InetSocketAddress
(((ConnectionInfo) connectionInfo).getHost(),
((ConnectionInfo) connectionInfo).getPort()),
null,
new CompletionHandler() {
public void completed(Void dummy, Void attachment) {
logger_.log(Level.FINE, "Connected");
// Need to catch and log exceptions at this async entry point.
try {
if (onConnected != null)
onConnected.run();
asyncRead();
} catch (Throwable ex) {
logger_.log(Level.SEVERE, null, ex);
}
}
public void failed(Throwable ex, Void attachment) {
logger_.log(Level.SEVERE, "Failed to connect", ex);
if(connectionInfo_.shouldAttemptReconnection()){
scheduleReconnect();
}
}
});
elementReader_ = new ElementReader(elementListener);
}
/**
* Attempt to reconnect to the NFD. If successful, the reconnect lock will
* be released so that IO can continue; if the attempt fails, it will
* schedule another reconnect attempt in the future.
*/
private void
reconnect() throws IOException {
logger_.log(Level.FINE, "Reconnecting...");
channel_ = AsynchronousSocketChannel.open(channelGroup_);
// connect is already async, so no need to dispatch.
channel_.connect
(new InetSocketAddress
(connectionInfo_.getHost(),
connectionInfo_.getPort()),
null,
new CompletionHandler() {
public void completed(Void dummy, Void attachment) {
// Need to catch and log exceptions at this async entry point.
reconnectLock_.release();
try {
if (onConnected_ != null)
onConnected_.run();
asyncRead();
} catch (Throwable ex) {
logger_.log(Level.SEVERE, null, ex);
}
}
public void failed(Throwable ex, Void attachment) {
logger_.log(Level.SEVERE, null, ex);
scheduleReconnect();
}
});
elementReader_ = new ElementReader(elementListener_);
}
/**
* Helper method for acquiring the reconnect lock; any method attempting to
* schedule a reconnect must acquire this lock
*
* @return true if the lock is acquired, false otherwise
*/
private boolean
acquireReconnectLock() {
try {
return reconnectLock_.tryAcquire(0, TimeUnit.MICROSECONDS);
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
return false;
}
}
/**
* On failure, this method is called to schedule a {@link #reconnect()} for some
* configurable time in the future; any access to this method must be protected
* by the reconnect so that only one reconnect is ever scheduled at a time.
*/
private void
scheduleReconnect() {
logger_.log(Level.INFO, "Scheduled to reconnect in " + DEFAULT_RECONNECT_TRY_DELAY_MS + "ms");
threadPool_.schedule(new Runnable() {
public void run() {
try {
reconnect();
} catch (IOException e) {
logger_.log(Level.WARNING, null, e);
}
}
}, DEFAULT_RECONNECT_TRY_DELAY_MS, TimeUnit.MILLISECONDS);
}
private void
asyncRead() {
inputBuffer_.limit(inputBuffer_.capacity());
inputBuffer_.position(0);
// We only call asyncRead after a previous call, so no need to dispatch.
channel_.read(inputBuffer_, null, readCompletionHandler_);
}
/**
* Send data to the host.
* @param data The buffer of data to send. This reads from position() to
* limit(), but does not change the position.
* @throws IOException For I/O error.
*/
public void
send(ByteBuffer data) throws IOException {
if (reconnectLock_.availablePermits() < 1) {
throw new IOException("Cannot send while the socket is reconnecting...");
}
if (!getIsConnected()) {
throw new IOException("Cannot send because the socket is not open. Use connect.");
}
// This does not copy the bytes, but only duplicates the position which is
// updated by write(). We assume that the sender won't change the bytes of
// the buffer during send, so that we can avoid a costly copy operation.
data = data.duplicate();
// The completion handler will call write again if needed, or will notify
// to release the wait when finished writing.
try {
sendDataSequentially(data);
} catch (InterruptedException e) {
throw new IOException(e);
}
}
/**
* Send data buffers one-by-one; this is necessary because async IO writes
* cannot overlap without a WritePendingException (see
* https://docs.oracle.com/javase/7/docs/api/java/nio/channels/AsynchronousSocketChannel.html#write(java.nio.ByteBuffer))
* @param data the buffer to send
* @throws InterruptedException if an external agent interrupts the thread; usually this means that someone is trying
* to close the
* @throws IOException if the channel write fails
*/
private void sendDataSequentially(ByteBuffer data) throws InterruptedException, IOException {
if (!writeLock_.tryAcquire(DEFAULT_LOCK_TIMEOUT_MS, TimeUnit.MILLISECONDS)) {
throw new IOException("Failed to acquire lock on channel to write buffer");
}
channel_.write(data, data, writeCompletionHandler_);
}
/**
* Do nothing since AsynchronousSocketChannel checks for incoming data.
*/
public void
processEvents() throws IOException, EncodingException {
}
/**
* Check if the transport is connected.
* @return True if connected.
*/
public boolean
getIsConnected() throws IOException {
return channel_ != null && channel_.getRemoteAddress() != null;
}
private AsynchronousSocketChannel channel_;
private final CompletionHandler readCompletionHandler_;
private final CompletionHandler writeCompletionHandler_;
private final ScheduledExecutorService threadPool_;
private ByteBuffer inputBuffer_ = ByteBuffer.allocate(Common.MAX_NDN_PACKET_SIZE);
private ElementReader elementReader_;
private ConnectionInfo connectionInfo_;
private boolean isLocal_;
private final Object isLocalLock_ = new Object();
private final Semaphore writeLock_ = new Semaphore(1);
private static final Logger logger_ = Logger.getLogger
(AsyncTcpTransport.class.getName());
public static final int DEFAULT_LOCK_TIMEOUT_MS = 10000;
public static final int DEFAULT_RECONNECT_TRY_DELAY_MS = 5000;
private AsynchronousChannelGroup channelGroup_;
private ElementListener elementListener_;
private Runnable onConnected_;
private final Semaphore reconnectLock_ = new Semaphore(1);
}
© 2015 - 2025 Weber Informatics LLC | Privacy Policy