/*************************************************************************** * (C) Copyright 2003-2010 - Marauroa * *************************************************************************** *************************************************************************** * * * This program is free software; you can redistribute it and/or modify * * it under the terms of the GNU General Public License as published by * * the Free Software Foundation; either version 2 of the License, or * * (at your option) any later version. * * * ***************************************************************************/ package marauroa.server.net.nio; import java.io.IOException; import java.net.InetAddress; import java.net.InetSocketAddress; import java.nio.ByteBuffer; import java.nio.channels.SelectionKey; import java.nio.channels.Selector; import java.nio.channels.ServerSocketChannel; import java.nio.channels.SocketChannel; import java.nio.channels.spi.SelectorProvider; import java.util.ArrayList; import java.util.HashMap; import java.util.Iterator; import java.util.LinkedList; import java.util.List; import java.util.Map; import marauroa.common.Log4J; /** * This class is the basic schema for a nio server. It works in a pattern of * master/slave. * * @author miguel * */ class NioServer extends Thread { private static final int BACKLOG_WARNING_SIZE = 10; /** the logger instance. */ private static final marauroa.common.Logger logger = Log4J.getLogger(NioServer.class); /** The host:port combination to listen on */ private final InetAddress hostAddress; private final int port; /** While keepRunning is true, we keep receiving messages */ private boolean keepRunning; /** isFinished is true when the thread has really exited. */ private boolean isFinished; /** The channel on which we'll accept connections */ private ServerSocketChannel serverChannel; /** The selector we'll be monitoring */ private final Selector selector; /** The buffer into which we'll read data when it's available */ private final ByteBuffer readBuffer = ByteBuffer.allocate(8192); /** * This is the slave associated with this master. As it is a simple thread, * we only need one slave. */ private final IWorker worker; /** A list of PendingChange instances */ private final List<ChangeRequest> pendingChanges = new LinkedList<ChangeRequest>(); private final List<ChangeRequest> pendingClosed; /** Maps a SocketChannel to a list of ByteBuffer instances */ private final Map<SocketChannel, List<ByteBuffer>> pendingData = new HashMap<SocketChannel, List<ByteBuffer>>(); public NioServer(InetAddress hostAddress, int port, IWorker worker) throws IOException { super("NioServer"); keepRunning = true; isFinished = false; this.hostAddress = hostAddress; this.port = port; this.selector = this.initSelector(); this.worker = worker; this.worker.setServer(this); pendingClosed = new LinkedList<ChangeRequest>(); } /** * This method closes a channel. It also notify any listener about the * event. * * @param channel * the channel to close. */ public void close(SocketChannel channel) { worker.onDisconnect(channel); // We ask the server to close the channel synchronized (this.pendingClosed) { pendingClosed.add(new ChangeRequest(channel, ChangeRequest.CLOSE, 0)); } // Wake up to make the closure effective. selector.wakeup(); } /** * This method is used to send data on a socket. * * @param socket * the socketchannel to use. * @param data * a byte array of data to send */ public void send(SocketChannel socket, byte[] data) { synchronized (this.pendingChanges) { // Indicate we want the interest ops set changed this.pendingChanges.add(new ChangeRequest(socket, ChangeRequest.CHANGEOPS, SelectionKey.OP_WRITE)); // And queue the data we want written synchronized (this.pendingData) { List<ByteBuffer> queue = this.pendingData.get(socket); if (queue == null) { queue = new ArrayList<ByteBuffer>(); this.pendingData.put(socket, queue); } queue.add(ByteBuffer.wrap(data)); if (queue.size() > BACKLOG_WARNING_SIZE) { logger.debug(socket + ": " + queue.size()); } } } // Finally, wake up our selecting thread so it can make the required // changes this.selector.wakeup(); } /** * Finish this thread in a correct way. */ public void finish() { keepRunning = false; selector.wakeup(); while (!(isFinished)) { Thread.yield(); } try { selector.close(); } catch (IOException e) { // We really don't care about the exception. } } @Override public void run() { while (keepRunning) { try { // Process any pending changes synchronized (this.pendingChanges) { Iterator<?> changes = this.pendingChanges.iterator(); while (changes.hasNext()) { ChangeRequest change = (ChangeRequest) changes.next(); if (change.socket.isConnected()) { if (change.type == ChangeRequest.CHANGEOPS) { SelectionKey key = change.socket.keyFor(this.selector); if ((key != null) && key.isValid()) { key.interestOps(change.ops); } } } } this.pendingChanges.clear(); } synchronized (this.pendingClosed) { Iterator<?> it = pendingClosed.iterator(); while (it.hasNext()) { ChangeRequest change = (ChangeRequest) it.next(); if (change.socket.isConnected()) { if (change.type == ChangeRequest.CLOSE) { try { // Force data to be sent if there is data waiting. if (pendingData.containsKey(change.socket)) { SelectionKey key = change.socket.keyFor(selector); if (key.isValid()) { write(key); } } // Close the socket change.socket.close(); } catch (Exception e) { logger.info("Exception happened when closing socket", e); } break; } } else { logger.info("Closing a not connected socket"); } } pendingClosed.clear(); } // Wait for an event one of the registered channels this.selector.select(); // Iterate over the set of keys for which events are available Iterator<?> selectedKeys = this.selector.selectedKeys().iterator(); while (selectedKeys.hasNext()) { SelectionKey key = (SelectionKey) selectedKeys.next(); selectedKeys.remove(); if (!key.isValid()) { continue; } // Check what event is available and deal with it if (key.isAcceptable()) { this.accept(key); } else if (key.isReadable()) { this.read(key); } else if (key.isWritable()) { this.write(key); } } } catch (IOException e) { logger.error("Error on NIOServer", e); } catch (RuntimeException e) { logger.error("Error on NIOServer", e); } } isFinished = true; } private void accept(SelectionKey key) throws IOException { // For an accept to be pending the channel must be a server socket // channel. ServerSocketChannel serverSocketChannel = (ServerSocketChannel) key.channel(); // Accept the connection and make it non-blocking SocketChannel socketChannel = serverSocketChannel.accept(); socketChannel.configureBlocking(false); // Register the new SocketChannel with our Selector, indicating // we'd like to be notified when there's data waiting to be read socketChannel.register(this.selector, SelectionKey.OP_READ); worker.onConnect(socketChannel); } private void read(SelectionKey key) { SocketChannel socketChannel = (SocketChannel) key.channel(); // Clear out our read buffer so it's ready for new data this.readBuffer.clear(); // Attempt to read off the channel int numRead; try { numRead = socketChannel.read(this.readBuffer); } catch (IOException e) { // The remote forcibly closed the connection, cancel // the selection key and close the channel. logger.debug("Remote closed connnection", e); key.cancel(); close(socketChannel); return; } if (numRead == -1) { // Remote entity shut the socket down cleanly. Do the // same from our end and cancel the channel. logger.debug("Remote closed connnection cleanly"); close((SocketChannel) key.channel()); key.cancel(); return; } // Hand the data off to our worker thread this.worker.onData(this, socketChannel, this.readBuffer.array(), numRead); } private void write(SelectionKey key) { SocketChannel socketChannel = (SocketChannel) key.channel(); synchronized (this.pendingData) { List<ByteBuffer> queue = this.pendingData.get(socketChannel); try { // Write until there's not more data ... while (!queue.isEmpty()) { ByteBuffer buf = queue.get(0); socketChannel.write(buf); if (buf.remaining() > 0) { // ... or the socket's buffer fills up break; } queue.remove(0); } if (queue.isEmpty()) { // We wrote away all data, so we're no longer interested // in writing on this socket. Switch back to waiting for // data. key.interestOps(SelectionKey.OP_READ); } } catch (IOException e) { // The remote forcibly closed the connection, cancel // the selection key and close the channel. logger.debug("Remote closed connnection", e); queue.clear(); key.cancel(); close(socketChannel); return; } } } private Selector initSelector() throws IOException { // Create a new selector Selector socketSelector = SelectorProvider.provider().openSelector(); // Create a new non-blocking server socket channel this.serverChannel = ServerSocketChannel.open(); serverChannel.configureBlocking(false); // Bind the server socket to the specified address and port InetSocketAddress isa = new InetSocketAddress(this.hostAddress, this.port); serverChannel.socket().bind(isa); serverChannel.socket().setPerformancePreferences(0, 2, 1); // Register the server socket channel, indicating an interest in // accepting new connections serverChannel.register(socketSelector, SelectionKey.OP_ACCEPT); return socketSelector; } }