Java tutorial
/* * Digital Audio Access Protocol (DAAP) * Copyright (C) 2004 Roger Kapsi, info at kapsi dot de * * 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. * * 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 General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program; if not, write to the Free Software * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA */ package de.kapsi.net.daap.nio; import java.io.IOException; import java.net.BindException; import java.net.InetAddress; import java.net.ServerSocket; 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.HashSet; import java.util.Iterator; import java.util.Set; import org.apache.commons.logging.Log; import org.apache.commons.logging.LogFactory; import de.kapsi.net.daap.DaapAuthenticator; import de.kapsi.net.daap.DaapConfig; import de.kapsi.net.daap.DaapConnection; import de.kapsi.net.daap.DaapFilter; import de.kapsi.net.daap.DaapServer; import de.kapsi.net.daap.DaapSession; import de.kapsi.net.daap.DaapStreamException; import de.kapsi.net.daap.DaapStreamSource; import de.kapsi.net.daap.DaapThreadFactory; import de.kapsi.net.daap.DaapUtil; import de.kapsi.net.daap.Library; import de.kapsi.net.daap.SimpleConfig; /** * A DAAP server written with NIO and a single Thread. * * @author Roger Kapsi */ public class DaapServerNIO implements DaapServer { private static final Log LOG = LogFactory.getLog(DaapServerNIO.class); private static final long TIMEOUT = 50; private ServerSocketChannel ssc = null; private Selector selector = null; private Library library; private HashSet streams; private HashSet connections; private HashSet sessionIds; private DaapConfig config; private DaapFilter filter; private DaapStreamSource streamSource; private DaapAuthenticator authenticator; private boolean running = false; private boolean disconnectAll = false; 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 SimpleConfig()); } /** * Creates new DAAP server with Library, a {@see SimpleConfig} and * the Port * * @param library a Library * @param port a Port used by SimpleConfig */ public DaapServerNIO(Library library, int port) { this(library, new SimpleConfig(port)); } /** * Creates a new DAAP server with Library and DaapConfig * * @param library a Library * @param config a DaapConfig */ public DaapServerNIO(Library library, DaapConfig config) { this.library = library; this.config = config; } /** * Returns the Library of this server * * @return Library */ public Library getLibrary() { return library; } /** * Sets the DaapConfig for this server * * @param config DaapConfig */ public void setConfig(DaapConfig config) { this.config = config; } /** * Returns the DaapConfig of this server * * @return DaapConfig of this server */ public DaapConfig getConfig() { return config; } /** * Sets the DaapAuthenticator for this server * * @param authenticator a DaapAuthenticator */ public void setAuthenticator(DaapAuthenticator authenticator) { this.authenticator = authenticator; } /** * Retrieves the DaapAuthenticator of this server * * @return DaapAuthenticator or <code>null</code> */ public DaapAuthenticator getAuthenticator() { return authenticator; } /** * Sets the DaapStreamSource for this server * * @param streamSource a DaapStreamSource */ public void setStreamSource(DaapStreamSource streamSource) { this.streamSource = streamSource; } /** * Retrieves the DaapStreamSource of this server * * @return DaapStreamSource or <code>null</code> */ public DaapStreamSource getStreamSource() { return streamSource; } /** * Sets a DaapFilter for this server * * @param filter a DaapFilter */ public void setFilter(DaapFilter filter) { this.filter = filter; } /** * Returns a DaapFilter * * @return a DaapFilter or <code>null</code> */ public DaapFilter getFilter() { return filter; } /** * Throws an {@see java.lang.UnsupportedOperationException} as * the NIO server is implemented with a single Thread. */ public void setThreadFactory(DaapThreadFactory factory) { throw new UnsupportedOperationException(); } /** * Returns <code>true</code> if DAAP Server * accepts incoming connections. */ public boolean isRunning() { return running; } /** * 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); } streams = new HashSet(); connections = new HashSet(); sessionIds = new HashSet(); } catch (IOException err) { close(); throw err; } } /** * Call this to notify the server that Library has changed */ public void update() { update = true; } /** * Returns <code>true</code> if sessionId is known and valid */ synchronized boolean isSessionIdValid(int sessionId) { return isSessionIdValid(new Integer(sessionId)); } /** * Returns <code>true</code> if sessionId is known and valid * * <p>DO NOT CALL THIS METHOD! THIS METHOD IS ONLY PUBLIC * DUE TO SOME DESIGN ISSUES!</p> */ public synchronized boolean isSessionIdValid(Integer sessionId) { return (sessionIds != null) ? sessionIds.contains(sessionId) : false; } /** * Returns a DaapConnection for sessionId or <code>null</code> * if sessionId is unknown. * * <p>DO NOT CALL THIS METHOD! THIS METHOD IS ONLY PUBLIC * DUE TO SOME DESIGN ISSUES!</p> */ public DaapConnection getConnection(Integer sessionId) { if (connections == null) return null; Iterator it = connections.iterator(); while (it.hasNext()) { DaapConnection connection = (DaapConnection) it.next(); DaapSession session = connection.getSession(false); if (session != null) { Integer sid = session.getSessionId(); if (sid.equals(sessionId)) { return connection; } } } return null; } /** * Creates an unique sessionId and retuns it. * * <p>DO NOT CALL THIS METHOD! THIS METHOD IS ONLY PUBLIC * DUE TO SOME DESIGN ISSUES!</p> */ public Integer createSessionId() { if (sessionIds == null) return null; Integer sid = DaapUtil.createSessionId(sessionIds); sessionIds.add(sid); return sid; } /** * Returns the number of connections */ public synchronized int getNumberOfConnections() { return (connections != null) ? connections.size() : 0; } /** * Returns the number of streams */ public synchronized int getNumberOfStreams() { return (streams != null) ? streams.size() : 0; } /** * Stops the DAAP Server */ public void stop() { running = false; } /** * Cloeses the server and releases all resources */ private synchronized void close() { running = false; update = false; disconnectAll = false; if (selector != null) { Iterator it = selector.keys().iterator(); while (it.hasNext()) { SelectionKey sk = (SelectionKey) it.next(); cancel(sk); } 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; } if (sessionIds != null) { sessionIds.clear(); sessionIds = null; } if (streams != null) { streams.clear(); streams = null; } if (connections != null) { connections.clear(); connections = null; } } /** * Disconnects all DAAP and Stream connections */ public void disconnectAll() { disconnectAll = true; } /** * Cancel SelesctionKey, close Channel and "free" the attachment */ private void cancel(SelectionKey sk) { sk.cancel(); SelectableChannel channel = (SelectableChannel) sk.channel(); try { channel.close(); } catch (IOException err) { LOG.error("Channel.close()", err); } DaapConnection connection = (DaapConnection) sk.attachment(); if (connection != null) { DaapSession session = connection.getSession(false); if (session != null) { sessionIds.remove(session.getSessionId()); } connection.close(); if (connection.isDaapConnection()) { connections.remove(connection); } else if (connection.isAudioStream()) { streams.remove(connection); } } } /** * * @throws IOException */ void registerConnection(DaapConnection connection) throws IOException { if (connection.isAudioStream()) { if (streams.size() < config.getMaxConnections()) { streams.add(connection); return; } } else if (connection.isDaapConnection()) { if (connections.size() < config.getMaxConnections()) { connections.add(connection); return; } } throw new IOException("Too many connections"); } /** * Returns <code>true</code> if host with <code>addr</code> is * allowed to connect to this DAAP server. * * @return true host with <code>addr</code> is allowed to connect */ private boolean accept(InetAddress addr) { if (filter != null && filter.accept(addr) == false) { if (LOG.isInfoEnabled()) { LOG.info("DaapFilter refused connection from " + addr); } return false; } return true; } /** * 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; if (channel.isOpen() && accept(channel.socket().getInetAddress())) { channel.configureBlocking(false); DaapConnection connection = new DaapConnectionNIO(this, channel); SelectionKey key = channel.register(selector, SelectionKey.OP_READ, connection); } else { try { channel.close(); } catch (IOException err) { LOG.error("SocketChannel.close()", err); } } } /** * Read data * * @throws IOException */ private void processRead(SelectionKey sk) throws IOException { if (!sk.isValid()) return; DaapConnectionNIO connection = (DaapConnectionNIO) sk.attachment(); SocketChannel channel = (SocketChannel) sk.channel(); 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(); SocketChannel channel = (SocketChannel) sk.channel(); 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(err); } if (keepAlive) { sk.interestOps(connection.interrestOps()); } else { cancel(sk); } } /** * Disconnects all clients from this server */ private void processDisconnect() { Iterator it = selector.keys().iterator(); while (it.hasNext()) { SelectionKey sk = (SelectionKey) it.next(); SelectableChannel channel = (SelectableChannel) sk.channel(); if (channel instanceof SocketChannel) { cancel(sk); } } } /** * Notify all clients about an update of the Library */ private void processUpdate() { Set keys = selector.keys(); Iterator it = keys.iterator(); while (it.hasNext()) { SelectionKey sk = (SelectionKey) it.next(); SelectableChannel channel = (SelectableChannel) sk.channel(); if (channel instanceof SocketChannel) { DaapConnection connection = (DaapConnection) sk.attachment(); if (connection.isDaapConnection()) { try { 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); } } } } } /** * 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; } if (!running) { break; } if (disconnectAll) { processDisconnect(); disconnectAll = false; continue; // as all clients were disconnected // there is nothing more to do } if (update) { processUpdate(); update = false; } if (n == 0) continue; Iterator it = selector.selectedKeys().iterator(); while (it.hasNext() && running) { SelectionKey sk = (SelectionKey) 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); } } else if (sk.isWritable()) { try { processWrite(sk); } catch (IOException err) { cancel(sk); LOG.error("An exception occured in processWrite()", err); } } } } catch (CancelledKeyException err) { continue; } } } // 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(); SelectionKey sk = ssc.register(selector, SelectionKey.OP_ACCEPT); process(); } catch (IOException err) { LOG.error(err); throw new RuntimeException(err); } finally { close(); } } public String toString() { StringBuffer buffer = new StringBuffer(); buffer.append("Name: ").append(config.getServerName()).append("\n"); buffer.append("Address: ").append(config.getInetSocketAddress()).append("\n"); buffer.append("Backlog: ").append(config.getBacklog()).append("\n"); buffer.append("Max connections: ").append(config.getMaxConnections()).append("\n"); buffer.append("IsRunning: ").append(isRunning()).append("\n"); if (isRunning()) { buffer.append("Connections: ").append(getNumberOfConnections()).append("\n"); buffer.append("Streams: ").append(getNumberOfStreams()).append("\n"); } return buffer.toString(); } }