/* * Digital Audio Access Protocol (DAAP) Library * Copyright (C) 2004-2010 Roger Kapsi * * 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 org.ardverk.daap.nio; import java.io.IOException; import java.net.BindException; import java.net.ServerSocket; import java.net.Socket; import java.net.SocketAddress; import java.net.SocketException; import java.nio.channels.CancelledKeyException; import java.nio.channels.ClosedChannelException; import java.nio.channels.SelectableChannel; import java.nio.channels.SelectionKey; import java.nio.channels.Selector; import java.nio.channels.ServerSocketChannel; import java.nio.channels.SocketChannel; import java.util.Iterator; import org.ardverk.daap.DaapConfig; import org.ardverk.daap.DaapConnection; import org.ardverk.daap.DaapServer; import org.ardverk.daap.DaapSession; import org.ardverk.daap.DaapStreamException; import org.ardverk.daap.Library; import org.ardverk.daap.SessionId; import org.slf4j.Logger; import org.slf4j.LoggerFactory; /** * A DAAP server written with NIO and a single Thread. * * @author Roger Kapsi */ public class DaapServerNIO extends DaapServer<DaapConnectionNIO> { private static final Logger LOG = LoggerFactory .getLogger(DaapServerNIO.class); /** Selector.select() timeout */ private static final long TIMEOUT = 250; /** The ServerSocket */ private ServerSocketChannel ssc = null; /** Selector for ServerSocket and Sockets */ private Selector selector = null; /** Flag to indicate that all clients shall be disconnected */ private boolean disconnectAll = false; /** * Flag to indicate there are Library updates available in the queue */ private boolean update = false; /** * Creates a new DAAP server with Library and {@see SimpleConfig} * * @param library * a Library */ public DaapServerNIO(Library library) { this(library, new DaapConfig()); } /** * Creates a new DAAP server with Library and DaapConfig * * @param library * a Library * @param config * a DaapConfig */ public DaapServerNIO(Library library, DaapConfig config) { super(library, config); } /** * Binds this server to the SocketAddress supplied by DaapConfig * * @throws IOException */ public void bind() throws IOException { SocketAddress bindAddr = config.getInetSocketAddress(); int backlog = config.getBacklog(); try { ssc = ServerSocketChannel.open(); ServerSocket socket = ssc.socket(); // BugID: 4546610 // On Win2k, Mac OS X, XYZ it is possible to bind // the same address without rising a SocketException // (the Documentation lies) socket.setReuseAddress(false); try { socket.bind(bindAddr, backlog); } catch (SocketException err) { throw new BindException(err.getMessage()); } ssc.configureBlocking(false); if (LOG.isInfoEnabled()) { LOG.info("DaapServerNIO bound to " + bindAddr); } } catch (IOException err) { close(); throw err; } } protected synchronized void update() { update = true; } /** * Stops the DAAP Server */ public synchronized void stop() { running = false; } /** * Cloeses the server and releases all resources */ private synchronized void close() { running = false; update = false; disconnectAll = false; if (selector != null) { for (SelectionKey key : selector.keys()) cancel(key); try { // Note: throws on OSX always "IOEx: Bad file descriptor" selector.close(); } catch (IOException err) { LOG.error("Selector.close()", err); } selector = null; } if (ssc != null) { try { ssc.close(); } catch (IOException err) { LOG.error("ServerSocketChannel.close()", err); } ssc = null; } sessionIds.clear(); connections.clear(); libraryQueue.clear(); } /** * Disconnects all DAAP and Stream connections */ public synchronized void disconnectAll() { disconnectAll = true; } /** * Cancel SelesctionKey, close Channel and "free" the attachment */ private void cancel(SelectionKey sk) { sk.cancel(); SelectableChannel channel = sk.channel(); try { channel.close(); } catch (IOException err) { LOG.error("Channel.close()", err); } DaapConnection connection = (DaapConnection) sk.attachment(); if (connection != null) { closeConnection(connection); } } protected void closeConnection(DaapConnection connection) { DaapSession session = connection.getSession(false); if (session != null) { destroySessionId(session.getSessionId()); } connection.close(); try { removeConnection(connection); } catch (IllegalStateException err) { // Shouldn't happen LOG.error("IllegalStateException", err); } } /** * Accept an icoming connection * * @throws IOException */ private void processAccept(SelectionKey sk) throws IOException { if (!sk.isValid()) return; ServerSocketChannel ssc = (ServerSocketChannel) sk.channel(); SocketChannel channel = ssc.accept(); if (channel == null) return; try { Socket socket = channel.socket(); if (channel.isOpen() && accept(socket.getInetAddress())) { channel.configureBlocking(false); DaapConnectionNIO connection = new DaapConnectionNIO(this, channel); channel.register(selector, SelectionKey.OP_READ, connection); addPendingConnection(connection); } else { channel.close(); } } catch (IOException err) { LOG.error("IOException", err); try { channel.close(); } catch (IOException iox) { } } } /** * Read data * * @throws IOException */ private void processRead(SelectionKey sk) throws IOException { if (!sk.isValid()) return; DaapConnectionNIO connection = (DaapConnectionNIO) sk.attachment(); boolean keepAlive = false; keepAlive = connection.read(); if (keepAlive) { sk.interestOps(connection.interrestOps()); } else { cancel(sk); } } /** * Write data * * @throws IOException */ private void processWrite(SelectionKey sk) throws IOException { if (!sk.isValid()) return; DaapConnectionNIO connection = (DaapConnectionNIO) sk.attachment(); boolean keepAlive = false; try { keepAlive = connection.write(); } catch (DaapStreamException err) { // Broken pipe: User pressed Pause, fast-foward // or whatever. Just close the connection and go // ahead keepAlive = false; LOG.error("DaapStreamException", err); } if (keepAlive) { sk.interestOps(connection.interrestOps()); } else { cancel(sk); } } /** * Disconnects all clients from this server */ private void processDisconnectAll() { for (SelectionKey sk : selector.keys()) { SelectableChannel channel = sk.channel(); if (channel instanceof SocketChannel) { cancel(sk); } } libraryQueue.clear(); } /** * Notify all clients about an update of the Library */ private void processUpdate() { for (DaapConnectionNIO connection : getDaapConnections()) { SelectionKey sk = connection.getChannel().keyFor(selector); try { for (int i = 0; i < libraryQueue.size(); i++) { connection.enqueueLibrary(libraryQueue.get(i)); } connection.update(); if (sk.isValid()) { try { sk.interestOps(SelectionKey.OP_READ | SelectionKey.OP_WRITE); } catch (CancelledKeyException err) { cancel(sk); LOG.error("SelectionKey.interestOps()", err); } } } catch (ClosedChannelException err) { cancel(sk); LOG.error("DaapConnection.update()", err); } catch (IOException err) { cancel(sk); LOG.error("DaapConnection.update()", err); } } libraryQueue.clear(); } /** * 1) Disconnect all connections that are in undefined state and that have * exceeded their timeout. * * 2) Empty the libraryQueue of daap connections if they've exceeded their * timeout. Some clients do not support live updates and this will prevent * us from running out of memory if the client doesn't fetch its updates). */ protected void processTimeout() { for (DaapConnectionNIO connection : getPendingConnections()) { if (connection.timeout()) { cancelConnection(connection); } } for (DaapConnectionNIO connection : getDaapConnections()) { if (connection.timeout()) { connection.clearLibraryQueue(); } } } protected void cancelConnection(DaapConnectionNIO connection) { SelectionKey sk = connection.getChannel().keyFor(selector); cancel(sk); } /** * The actual NIO run loop * * @throws IOException */ private void process() throws IOException { int n = -1; running = true; update = false; disconnectAll = false; while (running) { try { n = selector.select(TIMEOUT); } catch (NullPointerException err) { continue; } catch (CancelledKeyException err) { continue; } synchronized (this) { if (!running) { break; } if (disconnectAll) { processDisconnectAll(); disconnectAll = false; continue; // as all clients were disconnected // there is nothing more to do } if (update) { processUpdate(); update = false; } if (n > 0) { for (Iterator<SelectionKey> it = selector.selectedKeys() .iterator(); it.hasNext() && running;) { SelectionKey sk = it.next(); it.remove(); try { if (sk.isAcceptable()) { processAccept(sk); } else { if (sk.isReadable()) { try { processRead(sk); } catch (IOException err) { cancel(sk); LOG .error( "An exception occured in processRead()", err); } } if (sk.isWritable()) { try { processWrite(sk); } catch (IOException err) { cancel(sk); LOG .error( "An exception occured in processWrite()", err); } } } } catch (CancelledKeyException err) { continue; } } } // Kill the gremlins processTimeout(); } } // close() is in finally of run() {} } /** * The run loop */ public void run() { try { if (running) { LOG.error("DaapServerNIO is already running."); return; } selector = Selector.open(); ssc.register(selector, SelectionKey.OP_ACCEPT); process(); } catch (IOException err) { LOG.error("IOException", err); throw new RuntimeException(err); } finally { close(); } } /* Make them accessible for classes in this package */ protected synchronized DaapConnectionNIO getAudioConnection( SessionId sessionId) { return super.getAudioConnection(sessionId); } /* Make them accessible for classes in this package */ protected synchronized DaapConnectionNIO getDaapConnection( SessionId sessionId) { return super.getDaapConnection(sessionId); } /* Make them accessible for classes in this package */ protected synchronized boolean isSessionIdValid(SessionId sessionId) { return super.isSessionIdValid(sessionId); } /* Make them accessible for classes in this package */ protected synchronized boolean updateConnection(DaapConnectionNIO connection) { return super.updateConnection(connection); } }