package com.netifera.platform.net.sockets.internal; import java.io.IOException; import java.net.InetSocketAddress; import java.nio.ByteBuffer; import java.nio.channels.CancelledKeyException; import java.nio.channels.ClosedChannelException; import java.nio.channels.DatagramChannel; import java.nio.channels.ReadableByteChannel; import java.nio.channels.SelectionKey; import java.nio.channels.Selector; import java.nio.channels.SocketChannel; import java.nio.channels.WritableByteChannel; import java.util.Collections; import java.util.HashMap; import java.util.Map; import java.util.concurrent.BlockingQueue; import java.util.concurrent.Callable; import java.util.concurrent.ExecutorService; import java.util.concurrent.Executors; import java.util.concurrent.Future; import java.util.concurrent.LinkedBlockingQueue; import java.util.concurrent.TimeUnit; import java.util.concurrent.atomic.AtomicInteger; import com.netifera.platform.api.log.ILogManager; import com.netifera.platform.api.log.ILogger; import com.netifera.platform.net.sockets.AsynchronousSelectableChannel; import com.netifera.platform.net.sockets.CompletionHandler; import com.netifera.platform.net.sockets.ISocketEngineService; import com.netifera.platform.net.sockets.TCPChannel; import com.netifera.platform.net.sockets.UDPChannel; import com.netifera.platform.util.addresses.inet.InternetAddress; import com.netifera.platform.util.locators.TCPSocketLocator; import com.netifera.platform.util.locators.UDPSocketLocator; public class SocketEngineService implements ISocketEngineService { /** The limit for outstanding incomplete socket connections */ private int maxConnectingSockets = 100; /** The limit for open sockets (including connecting sockets) */ private int maxOpenSockets = 200; /** Count of currently connecting sockets */ final private AtomicInteger currentlyConnectingSockets = new AtomicInteger(0); /** Count of all open sockets including connecting sockets */ final private AtomicInteger currentlyOpenSockets = new AtomicInteger(0); /** Selector */ private Selector selector; /** Thread for selector loop */ private Thread selectThread; // XXX document please final private BlockingQueue<SelectionContext> registrationQueue = new LinkedBlockingQueue<SelectionContext>(); final private Map<AsynchronousSelectableChannel, SelectionContext> contextMap = Collections.synchronizedMap(new HashMap<AsynchronousSelectableChannel, SelectionContext>()); private ILogger logger; /** * We use a cached thread pool because the thread resources are bound by the * maximum open socket count. */ private final ExecutorService executor = Executors.newCachedThreadPool(); public int getMaxConnectingSockets() { return maxConnectingSockets; } public void setMaxConnectingSockets(int limit) { maxConnectingSockets = limit; } public int getMaxOpenSockets() { return maxOpenSockets; } public void setMaxOpenSockets(int limit) { maxOpenSockets = limit; } public TCPChannel openTCP() throws IOException { if (selector == null) startSelector(); TCPChannel channel = new TCPChannel(this, SocketChannel.open()); channel.getWrappedChannel().configureBlocking(false); SelectionContext context = new SelectionContext(this, channel, logger); registerChannel(channel, context); return channel; } public UDPChannel openUDP() throws IOException { if (selector == null) startSelector(); UDPChannel channel = new UDPChannel(this, DatagramChannel.open()); channel.getWrappedChannel().configureBlocking(false); SelectionContext context = new SelectionContext(this, channel, logger); registerChannel(channel, context); return channel; } public <A> Future<Void> asynchronousConnect(TCPChannel channel, TCPSocketLocator remote, long timeout, TimeUnit unit, final A attachment, final CompletionHandler<Void, ? super A> handler) throws IOException, InterruptedException { if (selector == null) startSelector(); synchronized (this) { while (!canConnect()) this.wait(); // check and handle timeout countConnectingSocket(); } final SocketChannel socket = channel.getWrappedChannel(); /* * Wrap the handler in another completion handler that performs outstanding connection accounting. */ final CompletionHandler<Void, A> connectCompletion = new CompletionHandler<Void, A>() { public void cancelled(A a) { handler.cancelled(a); countConnectFinished(); } public void completed(Void result, A a) { handler.completed(result, a); countConnectFinished(); } public void failed(Throwable exc, A a) { handler.failed(exc, a); countConnectFinished(); } }; long deadline = System.currentTimeMillis() + unit.toMillis(timeout); SelectionFuture<Void,? super A> future = new SelectionFuture<Void,A>(connectCompletion, attachment, deadline, logger, new Callable<Void>() { public Void call() throws Exception { socket.finishConnect(); return null; } }); InetSocketAddress sockaddr = new InetSocketAddress(remote.getAddress().toInetAddress(), remote.getPort()); socket.configureBlocking(false); try { socket.connect(sockaddr); } catch(IOException e) { countConnectFinished(); throw e; } SelectionContext context = contextMap.get(channel); if (context == null) { logger.error("context not found on connect() "+channel); handler.cancelled(attachment); return null; } context.enqueueConnect(future); registrationQueue.add(context); selector.wakeup(); return future; } public <A> Future<Integer> asynchronousRead(final AsynchronousSelectableChannel channel, final ByteBuffer dst, long timeout, TimeUnit unit, final A attachment, final CompletionHandler<Integer,? super A> handler) { long deadline = System.currentTimeMillis() + unit.toMillis(timeout); SelectionFuture<Integer,A> future = new SelectionFuture<Integer,A>(handler, attachment, deadline, logger, new Callable<Integer>() { public Integer call() throws Exception { Integer count = ((ReadableByteChannel)channel.getWrappedChannel()).read(dst); if (count <= 0) throw new ClosedChannelException(); return count; } }); SelectionContext context = contextMap.get(channel); if (context == null) { logger.error("Context not found on read() "+channel); handler.cancelled(attachment); return null; } // if (context.reader != null) throw new PendingReadException(); context.enqueueRead(future); registrationQueue.add(context); selector.wakeup(); return future; } public <A> Future<Integer> asynchronousWrite(final AsynchronousSelectableChannel channel, final ByteBuffer src, long timeout, TimeUnit unit, final A attachment, final CompletionHandler<Integer,? super A> handler) { long deadline = System.currentTimeMillis() + unit.toMillis(timeout); SelectionFuture<Integer,A> future = new SelectionFuture<Integer,A>(handler, attachment, deadline, logger, new Callable<Integer>() { public Integer call() throws Exception { Integer count = ((WritableByteChannel)channel.getWrappedChannel()).write(src); if (count <= 0) throw new ClosedChannelException(); return count; } }); SelectionContext context = contextMap.get(channel); if (context == null) { logger.error("Context not found on write() "+channel); handler.cancelled(attachment); return null; } // if (context.reader != null) throw new PendingReadException(); context.enqueueWrite(future); registrationQueue.add(context); selector.wakeup(); return future; } public <A> Future<UDPSocketLocator> asynchronousReceive(final UDPChannel channel, final ByteBuffer dst, long timeout, TimeUnit unit, final A attachment, final CompletionHandler<UDPSocketLocator,? super A> handler) { long deadline = System.currentTimeMillis() + unit.toMillis(timeout); SelectionFuture<UDPSocketLocator,A> future = new SelectionFuture<UDPSocketLocator,A>(handler, attachment, deadline, logger, new Callable<UDPSocketLocator>() { public UDPSocketLocator call() throws Exception { InetSocketAddress address = (InetSocketAddress) channel.getWrappedChannel().receive(dst); return new UDPSocketLocator(InternetAddress.fromInetAddress(address.getAddress()),address.getPort()); } }); SelectionContext context = contextMap.get(channel); if (context == null) { logger.error("Context not found on recv() for "+channel); handler.cancelled(attachment); return null; } context.enqueueRead(future); registrationQueue.add(context); selector.wakeup(); return future; } public <A> Future<Integer> asynchronousSend(final UDPChannel channel, final ByteBuffer src, final UDPSocketLocator target, long timeout, TimeUnit unit, final A attachment, final CompletionHandler<Integer,? super A> handler) { long deadline = System.currentTimeMillis() + unit.toMillis(timeout); SelectionFuture<Integer,A> future = new SelectionFuture<Integer,A>(handler, attachment, deadline, logger, new Callable<Integer>() { public Integer call() throws Exception { return channel.getWrappedChannel().send(src, new InetSocketAddress(target.getAddress().toInetAddress(),target.getPort())); } }); SelectionContext context = contextMap.get(channel); if (context == null) { logger.error("Context not found on send() "+channel); handler.cancelled(attachment); return null; } context.enqueueWrite(future); registrationQueue.add(context); selector.wakeup(); return future; } private void countOpenSocket() { currentlyOpenSockets.incrementAndGet(); } /** * Count a new open connection by incrementing * <code>currentlyConnectingSockets</code> */ private void countConnectingSocket() { currentlyConnectingSockets.incrementAndGet(); } /** * Count a completed (or failed) connection by decrementing the * <code>currentlyConnectingSockets</code> counter and wake up threads * that may be sleeping while waiting for socket resources to become * available. */ private void countConnectFinished() { currentlyConnectingSockets.decrementAndGet(); synchronized (this) { this.notifyAll(); } } /** * Count a closed socket by decrementing the * <code>currentlyOpenSockets</code> and wake up threads that may be * sleeping while waiting for socket resources to become available. */ private void countSocketClose() { currentlyOpenSockets.decrementAndGet(); synchronized (this) { this.notifyAll(); } } /** * Test counters to verify that resources are available to create a new * connecting sockets. * * @return True if socket resources are below limits, otherwise false. */ private boolean canConnect() { return (currentlyConnectingSockets.get() < maxConnectingSockets) && (currentlyOpenSockets.get() < maxOpenSockets); } /** * Create {@link Selector} and start select loop thread. * * @throws IOException * Error creating Selector. */ private void startSelector() { assert (selector == null); try { selector = Selector.open(); } catch(IOException e) { assert logger != null; logger.error("I/O error, cannot open selector", e); return; } selectThread = new Thread(new Runnable() { public void run() { selectLoop(); try { selector.close(); } catch (IOException e) { assert logger != null; logger.error("I/O error closing selector", e); } selector = null; } }); selectThread.setDaemon(true); selectThread.setName("Socket Connect Engine Selector thread"); selectThread.start(); } private void registerPending() { SelectionContext context; while ((context = registrationQueue.poll()) != null) context.register(); } /** * The select loop multiplexes the connection status of multiple sockets and * detects completed connections and expired connection timeout values. */ private void selectLoop() { long timeout = 0; // wait indefinitely registerPending(); while (!Thread.interrupted()) { if (contextMap.isEmpty() && currentlyOpenSockets.get() == 0 && currentlyConnectingSockets.get() == 0) { assert logger != null; logger.debug("SocketEngineService clean"); timeout = 0; } else { // System.out.println("active contexts: "+contextMap.size()+" selection keys: "+selector.keys().size()); // System.out.println("open sockets: "+currentlyOpenSockets.get()+" connecting sockets: "+currentlyConnectingSockets.get()); timeout = Math.max(timeout, 500); // XXX } try { selector.select(timeout); assert selector != null; if (selector.isOpen() == false) { return; } } catch (IOException e) { assert logger != null; logger.error("I/O error in Selector#select()", e); return; } registerPending(); for (SelectionKey key : selector.selectedKeys()) { SelectionContext context = (SelectionContext)key.attachment(); try { context.testKey(key); } catch (CancelledKeyException e) { // a selected key is cancelled //logger.warning("Cancelled selector key (on selected key)", e); // do something about it context.close(); } } long now = System.currentTimeMillis(); timeout = Long.MAX_VALUE; for (SelectionKey key : selector.keys()) { SelectionContext context = (SelectionContext)key.attachment(); try { timeout = Math.min(timeout, context.testTimeOut(key, now)); } catch (CancelledKeyException e) { //logger.warning("Cancelled selector key (when testing timeout of unselected key)", e); // do something about it. should close? } } if (timeout == Long.MAX_VALUE) timeout = 0; // 0 means wait indefinitely } } /** * Close the socket connect engine. This method is for testing that * resources such as socket handles are correctly freed and should be * removed later. */ // TODO called by deactivate() void close() { //selectThread.interrupt(); try { selector.close(); } catch (IOException e) { logger.error("I/O error closing selector", e); } executor.shutdownNow(); } private synchronized void registerChannel(AsynchronousSelectableChannel channel, SelectionContext context) { contextMap.put(channel, context); countOpenSocket(); } public synchronized void unregisterChannel(AsynchronousSelectableChannel channel) { if (null != contextMap.remove(channel)) countSocketClose(); } /* boolean hasSelectionContextFor(AsynchronousSelectableChannel channel) { return contextMap.containsKey(channel); } */ Selector getSelector() { return selector; } public ExecutorService getExecutor() { return executor; } protected void setLogManager(ILogManager logManager) { logger = logManager.getLogger("Socket Engine"); } protected void unsetLogManager(ILogManager logManager) { // FIXME commented: workaround for #159 //logger = null; } }