edu.nps.moves.disutil.NioServer Maven / Gradle / Ivy
Show all versions of open-dis Show documentation
package edu.nps.moves.disutil;
import java.util.concurrent.*;
import java.nio.channels.*;
import java.util.logging.*;
import java.beans.*;
import java.util.*;
import java.nio.*;
import java.net.*;
import java.io.*;
import java.lang.reflect.Constructor;
import java.lang.reflect.Field;
import java.lang.reflect.Method;
/**
* A robust class for establishing simultaneous TCP and UDP servers and manipulating
* their listening ports.
* The {@link Event}s and property change events make
* it an appropriate tool in a threaded, GUI application.
* It is almost identical in design to the UdpServer and TcpServer classes that
* should have accompanied this class when you downloaded it.
*
* To start a server, create a new NioServer and call start():
*
* NioServer server = new NioServer();
* server.start();
*
* You'll want to bind to a port or two:
*
* server.addTcpBinding( new InetSocketAddress( 80 ) );
* server.addUdpBinding( new InetSocketAddress( 80 ) );
*
* Of course it won't be much help unless you register as a listener
* so you'll know when data has come in:
*
* server.addNioServerListener( new NioServer.Adapter(){
* public void nioServerTcpDataReceived( NioServer.Event evt ){
* ByteBuffer buff = evt.getBuffer();
* ...
* } // end data received
*
* public void nioServerUdpDataReceived( NioServer.Event evt ){
* ByteBuffer buff = evt.getBuffer();
* ...
* } // end data received
* });
*
* The server runs on one thread, and all events are fired on that thread.
* Consider offloading heavy processing to another thread. Be aware that
* you can register multiple listeners to respond to incoming data
* so be mindful of more than one listener being around to makes calls
* on the data.
*
* The public methods are all synchronized on this, and great
* care has been taken to avoid deadlocks and race conditions. That being said,
* there may still be bugs (please contact the author if you find any), and
* you certainly still have the power to introduce these problems yourself.
*
* It's often handy to have your own class extend this one rather than
* making an instance field to hold a NioServer where you'd have to
* pass along all the setPort(...) methods and so forth.
*
* The supporting {@link Event}, {@link Listener}, and {@link Adapter}
* classes are static inner classes in this file so that you have only one
* file to copy to your project. You're welcome.
*
* Since the TcpServer.java, UdpServer.java, and NioServer.java are
* so similar, and since lots of copying and pasting was going on among them,
* you may find some comments that refer to TCP instead of UDP or vice versa.
* Please feel free to let me know, so I can correct that.
*
* This code is released into the Public Domain.
* Since this is Public Domain, you don't need to worry about
* licensing, and you can simply copy this NioServer.java file
* to your own package and use it as you like. Enjoy.
* Please consider leaving the following statement here in this code:
*
* This NioServer class was copied to this project from its source as
* found at iHarder.net.
*
* @author Robert Harder
* @author [email protected]
* @version 0.1
* @see NioServer
* @see Adapter
* @see Event
* @see Listener
*/
public class NioServer {
/** Standard Java logger. */
private final static Logger LOGGER = Logger.getLogger(NioServer.class.getName());
/** The buffer size property. */
public final static String BUFFER_SIZE_PROP = "bufferSize";
private final static int BUFFER_SIZE_DEFAULT = 4096;
private int bufferSize = BUFFER_SIZE_DEFAULT;
/**
* One of four possible states for the server to be in:
*
*
* - STARTING
* - STARTED
* - STOPPING
* - STOPPED
*
*/
public static enum State { STARTING, STARTED, STOPPING, STOPPED };
private State currentState = State.STOPPED;
public final static String STATE_PROP = "state";
public final static String LAST_EXCEPTION_PROP = "lastException";
private Throwable lastException;
private final Collection listeners = new LinkedList(); // Event listeners
private NioServer.Listener[] cachedListeners = null;
private final NioServer.Event event = new NioServer.Event(this); // Shared event
private final PropertyChangeSupport propSupport = new PropertyChangeSupport(this); // Properties
private ThreadFactory threadFactory; // Optional thread factory
private Thread ioThread; // Performs IO
private Selector selector; // Brokers all the connections
public final static String TCP_BINDINGS_PROP = "tcpBindings";
public final static String UDP_BINDINGS_PROP = "udpBindings";
public final static String SINGLE_TCP_PORT_PROP = "singleTcpPort";
public final static String SINGLE_UDP_PORT_PROP = "singleUdpPort";
private final Map tcpBindings = new HashMap();// Requested TCP bindings, e.g., "listen on port 80"
private final Map udpBindings = new HashMap();// Requested UDP bindings
private final Map multicastGroups = new HashMap();
private final Set pendingTcpAdds = new HashSet(); // TCP bindings to add to selector on next cycle
private final Set pendingUdpAdds = new HashSet(); // UDP bindings to add to selector on next cycle
private final Map pendingTcpRemoves = new HashMap(); // TCP bindings to remove from selector on next cycle
private final Map pendingUdpRemoves = new HashMap(); // UDP bindings to remove from selector on next cycle
private final Map leftoverData = new HashMap(); // Store leftovers here instead of key.attachment()
/* ******** C O N S T R U C T O R S ******** */
/**
* Constructs a new NioServer, listening to nothing, and not started.
*/
public NioServer(){
}
/**
* Constructs a new NioServer, listening to nothing, and not started.
* The provided
* ThreadFactory will be used when starting and running the server.
* @param factory the ThreadFactory to use when starting the server
*/
public NioServer( ThreadFactory factory ){
this.threadFactory = factory;
}
/* ******** R U N N I N G ******** */
/**
* Attempts to start the server listening and returns immediately.
* Listen for start events to know if the server was
* successfully started.
*
* @see Listener
*/
public synchronized void start(){
if( this.currentState == State.STOPPED ){ // Only if we're stopped now
assert ioThread == null : ioThread; // Shouldn't have a thread
Runnable run = new Runnable() {
public void run() {
runServer(); // This runs for a long time
ioThread = null;
setState( State.STOPPED ); // Clear thread
} // end run
}; // end runnable
if( this.threadFactory != null ){ // User-specified threads
this.ioThread = this.threadFactory.newThread(run);
} else { // Our own threads
this.ioThread = new Thread( run, this.getClass().getName() ); // Named
}
setState( State.STARTING ); // Update state
this.ioThread.start(); // Start thread
} // end if: currently stopped
} // end start
/**
* Attempts to stop the server, if the server is in
* the STARTED state, and returns immediately.
* Be sure to listen for stop events to know if the server was
* successfully stopped.
*
* @see Listener
*/
public synchronized void stop(){
if( this.currentState == State.STARTED || this.currentState == State.STARTING ){ // Only if already STARTED
setState( State.STOPPING ); // Mark as STOPPING
if( this.selector != null ){
try{
Set keys = this.selector.keys();
for( SelectionKey key : this.selector.keys() ){
key.channel().close();
key.cancel();
}
this.selector.close();
} catch( IOException exc ){
fireExceptionNotification(exc);
LOGGER.log(
Level.SEVERE,
"An error occurred while closing the server. " +
"This may have left the server in an undefined state.",
exc );
}
} // end if: not null
} // end if: already STARTED
} // end stop
/**
* Returns the current state of the server, one of
* STOPPED, STARTING, or STARTED.
* @return state of the server
*/
public synchronized State getState(){
return this.currentState;
}
/**
* Sets the state and fires an event. This method
* does not change what the server is doing, only
* what is reflected by the currentState variable.
* @param state the new state of the server
*/
protected synchronized void setState( State state ){
State oldVal = this.currentState;
this.currentState = state;
firePropertyChange(STATE_PROP, oldVal, state);
}
/**
* Resets the server, if it is running, otherwise does nothing.
* This is accomplished by registering as a listener, stopping
* the server, detecting the stop, unregistering, and starting
* the server again. It's a useful design pattern, and you may
* want to look at the source code for this method to check it out.
*/
public synchronized void reset(){
switch( this.currentState ){
case STARTED:
this.addPropertyChangeListener(STATE_PROP, new PropertyChangeListener() {
public void propertyChange(PropertyChangeEvent evt) {
State newState = (State)evt.getNewValue();
if( newState == State.STOPPED ){
NioServer server = (NioServer)evt.getSource();
server.removePropertyChangeListener(STATE_PROP,this);
server.start();
} // end if: stopped
} // end prop change
});
stop();
break;
} // end switch
}
/**
* This method starts up and listens indefinitely
* for TCP packets. On entering this method,
* the state is assumed to be STARTING. Upon exiting
* this method, the state will be STOPPING.
*/
protected void runServer(){
try{
ByteBuffer buff = ByteBuffer.allocateDirect(this.bufferSize);
// Add all the requested TCP and UDP bindings to the "pending" lists
synchronized( this ){
this.selector = Selector.open();
this.pendingTcpAdds.clear();
this.pendingTcpAdds.addAll(this.tcpBindings.keySet());
this.pendingUdpAdds.clear();
this.pendingUdpAdds.addAll(this.udpBindings.keySet());
this.pendingTcpRemoves.clear();
this.pendingTcpRemoves.clear();
}
setState( State.STARTED ); // Mark as started
while( this.selector.isOpen() ){
// Check to see if the server is supposed to be stopping
synchronized( this ){
if( this.currentState == State.STOPPING ){
LOGGER.finer( "Stopping server by request." );
this.selector.close();
continue;
} // end if: stopping
} // end sync
// Add and remove pending bindings
synchronized( this ){
// Pending TCP Adds
for( SocketAddress addr : this.pendingTcpAdds ){ // For each add
LOGGER.fine("Binding TCP: " + addr );
ServerSocketChannel sc = ServerSocketChannel.open(); // Open a channel
sc.socket().bind(addr); // Bind as requested
sc.configureBlocking(false); // Make non-blocking
SelectionKey acceptKey = sc.register( // Register with master Selector
this.selector, SelectionKey.OP_ACCEPT ); // We want to "accept" connections
this.tcpBindings.put(addr, acceptKey); // Save the key
} // end for: each address
this.pendingTcpAdds.clear(); // Remove list of pending adds
// Pending UDP Adds
for( SocketAddress addr : this.pendingUdpAdds ){ // Same comments as for TCP
LOGGER.fine("Binding UDP: " + addr );
DatagramChannel dc = DatagramChannel.open();
dc.socket().bind(addr);
dc.configureBlocking(false);
SelectionKey acceptKey = dc.register(
this.selector, SelectionKey.OP_READ );
this.udpBindings.put(addr, acceptKey);
// Found a weird hack to support multicast -- at least for now.
String group = this.multicastGroups.get(addr);
if( group != null && addr instanceof InetSocketAddress ){
int port = ((InetSocketAddress)addr).getPort();
InetSocketAddress groupAddr = new InetSocketAddress(group,port);
if( groupAddr.getAddress().isMulticastAddress() ){
// http://www.mernst.org/blog/archives/12-01-2006_12-31-2006.html
// UGLY UGLY HACK: multicast support for NIO
// create a temporary instanceof PlainDatagramSocket, set its fd and configure it
@SuppressWarnings("unchecked")
Constructor c =
(Constructor)
Class.forName("java.net.PlainDatagramSocketImpl").getDeclaredConstructor();
c.setAccessible(true);
DatagramSocketImpl socketImpl = c.newInstance();
Field channelFd = Class.forName("sun.nio.ch.DatagramChannelImpl").getDeclaredField("fd");
channelFd.setAccessible(true);
Field socketFd = DatagramSocketImpl.class.getDeclaredField("fd");
socketFd.setAccessible(true);
socketFd.set(socketImpl, channelFd.get(dc));
try {
Method m = DatagramSocketImpl.class.getDeclaredMethod("joinGroup", SocketAddress.class, NetworkInterface.class);
//Method m = DatagramSocketImpl.class.getDeclaredMethod("joinGroup", InetAddress.class );
m.setAccessible(true);
m.invoke(socketImpl, groupAddr, null );
}catch(Exception e){
e.printStackTrace();
}
finally {
// important, otherwise the fake socket's finalizer will nuke the fd
socketFd.set(socketImpl, null);
}
} // end if: multicast
else{
LOGGER.warning("Could not join non-multicast group: " + group);
}
} // end if: got group
} // end for: each address
this.pendingUdpAdds.clear();
// Pending TCP Removes
for( Map.Entry e : this.pendingTcpRemoves.entrySet() ){
SelectionKey key = e.getValue(); // Get the registered key
if( key != null ){ // Might be null if someone gave us bogus address
key.channel().close(); // Close the channel
key.cancel(); // And cancel the key (redundant?)
} // end if: key != null
} // end for: each remove
this.pendingTcpRemoves.clear(); // Remove from list of pending removes
// Pending UDP Removes
for( Map.Entry e : this.pendingUdpRemoves.entrySet() ){
SelectionKey key = e.getValue(); // Get the registered key
if( key != null ){ // Might be null if someone gave us bogus address
key.channel().close(); // Close the channel
key.cancel(); // And cancel the key (redundant?)
} // end if: key != null
} // end for: each remove
this.pendingUdpRemoves.clear(); // Remove from list of pending removes
} // end sync: this
//
// The meat of the matter begins now
//
//////// B L O C K S H E R E
if( this.selector.select() <= 0 ){ // Block until notified
LOGGER.finer("selector.select() <= 0"); // Possible false start
Thread.sleep(100); // Let's not run away from ourselves
}/////// B L O C K S H E R E
// Possibly resize buffer if a change was requested since last cycle
if( this.bufferSize != buff.capacity() ){ // Mismatch size means someone asked for something new
assert this.bufferSize >= 0 : this.bufferSize; // We check for this in setBufferSize(..)
buff = ByteBuffer.allocateDirect(this.bufferSize); // Resize and use direct for OS efficiencies
}
Set keys = this.selector.selectedKeys(); // These keys need attention
if( LOGGER.isLoggable(Level.FINEST ) ){ // Only report this at finest grained logging level
LOGGER.finest("Keys: " + keys ); // Which keys are being examined this round
}
Iterator iter = keys.iterator(); // Iterate over keys -- cannot use "for" loop since we remove keys
while( iter.hasNext() ){ // Each key
SelectionKey key = iter.next(); // The key
iter.remove(); // Remove from list
// Accept connections
// This should only be from the TCP bindings
if( key.isAcceptable() ){ // New, incoming connection?
handleAccept( key ); // Handle accepting connections
}
// Data to read
// This could be an ongoing TCP connection
// or a new (is there any other kind) UDP datagram
else if( key.isReadable() ){ // Existing connection has data (or is closing)
handleRead( key, buff ); // Handle data
} // end if: readable
} // end while: keys
} // end while: selector is open
} catch( Exception exc ){
synchronized( this ){
if( this.currentState == State.STOPPING ){ // User asked to stop
try{
this.selector.close();
LOGGER.info( "Server closed normally." );
} catch( IOException exc2 ){
this.lastException = exc2;
LOGGER.log(
Level.SEVERE,
"An error occurred while closing the server. " +
"This may have left the server in an undefined state.",
exc2 );
fireExceptionNotification(exc2);
} // end catch IOException
} else {
LOGGER.log( Level.WARNING, "Server closed unexpectedly: " + exc.getMessage(), exc );
} // end else
} // end sync
fireExceptionNotification(exc);
} finally {
setState( State.STOPPING );
if( this.selector != null ){
try{
this.selector.close();
LOGGER.info( "Server closed normally." );
} catch( IOException exc2 ){
LOGGER.log(
Level.SEVERE,
"An error occurred while closing the server. " +
"This may have left the server in an undefined state.",
exc2 );
fireExceptionNotification(exc2);
} // end catch IOException
} // end if: not null
this.selector = null;
} // end finally
}
/**
* Handles accepting new connections.
* @param key The OP_ACCEPT key
* @throws IOException
*/
private void handleAccept( SelectionKey key ) throws IOException{
assert key.isAcceptable() : key.readyOps(); // We know it should be acceptable
assert selector.isOpen(); // Not sure this matters. Meh.
SelectableChannel sc = key.channel(); // Channel for th key
assert sc instanceof ServerSocketChannel : sc; // Only our TCP connections have OP_ACCEPT
// if( sc instanceof ServerSocketChannel ){
ServerSocketChannel ch = (ServerSocketChannel)key.channel(); // Server channel
SocketChannel incoming = null; // Reusable for all pending connections
while( (incoming = ch.accept()) != null ){ // Iterate over all pending connections
incoming.configureBlocking(false); // Non-blocking IO
SelectionKey incomingReadKey = incoming.register( // Register new connection
this.selector, // With the Selector
SelectionKey.OP_READ);//, // Want to READ data
//(ByteBuffer)ByteBuffer.allocateDirect(128).flip() ); // Will store leftover data between read ops
fireNewConnection(incomingReadKey);
if( LOGGER.isLoggable(Level.FINEST) ){
LOGGER.finest(" " + incoming + ", key: " + incomingReadKey );
}
} // end while: each incoming connection
}
/**
* Handles reading incoming data and then firing events.
* @param key The key associated with the reading
* @param buff the ByteBuffer to hold the data
* @throws IOException
*/
private void handleRead(SelectionKey key, ByteBuffer buff ) throws IOException {
SelectableChannel sc = key.channel();
buff.clear(); // Clear input buffer
// TCP
if( sc instanceof SocketChannel ){
SocketChannel client = (SocketChannel) key.channel(); // Source socket
ByteBuffer leftover = this.leftoverData.get(key); // Leftover data from last read
if( leftover != null && leftover.remaining() > 0 ){ // Have a leftover buffer
buff.put(leftover); // Preload leftovers
} // end if: have leftover buffer
// Read into the buffer here
// If End of Stream
if( client.read(buff) == -1 ){ // End of stream?
key.cancel(); // Cancel the key
client.close(); // And cancel the client
fireConnectionClosed(key); // Fire event for connection closed
this.leftoverData.remove(key); // Remove any leftover data
if( LOGGER.isLoggable(Level.FINER) ){
LOGGER.finer("Connection closed: " + key );
}
} else {
// Not End of Stream
buff.flip(); // Flip the buffer to prepare to read
fireTcpDataReceived(key,buff); // Fire event for new data
if( buff.remaining() > 0 ){ // Did the user leave data for next time?
if( leftover == null || // Leftover buffer not yet created?
buff.remaining() > leftover.capacity() ){ // Or is too small?
leftover = ByteBuffer.allocateDirect(buff.remaining()); // Create/resize
} // end if: need to resize
leftover.clear(); // Clear old leftovers
leftover.put(buff).flip(); // Save new leftovers
} // end if: has remaining bytes
this.leftoverData.put(key,leftover); // Save leftovers for next time
} // end else: read
} // end if: SocketChannel
// Datagram
else if( sc instanceof DatagramChannel ){
DatagramChannel dc = (DatagramChannel)sc; // Cast to datagram channel
SocketAddress remote = null;
while( (remote = dc.receive(buff)) != null ){ // Loop over all pending datagrams
buff.flip(); // Flip after reading in
key.attach( buff ); // Attach buffer to key
fireUdpDataReceived(key,buff,remote); // Fire event
} // end while: each pending datagram
} // end else: UDP
} // end handleRead
/* ******** B U F F E R S I Z E ******** */
/**
* Returns the size of the ByteBuffer used to read
* from the connections. This refers to the buffer
* that will be passed along with {@link Event}
* objects as data is received and so forth.
* @return The size of the ByteBuffer
*/
public synchronized int getBufferSize(){
return this.bufferSize;
}
/**
* Sets the size of the ByteBuffer used to read
* from the connections. This refers to the buffer
* that will be passed along with {@link Event}
* objects as data is received and so forth.
* @param size The size of the ByteBuffer
*/
public synchronized void setBufferSize( int size ){
if( size <= 0 ){
throw new IllegalArgumentException( "New buffer size must be positive: " + size );
} // end if: size outside range
int oldVal = this.bufferSize;
this.bufferSize = size;
this.selector.wakeup();
firePropertyChange( BUFFER_SIZE_PROP, oldVal, size );
}
/* ******** T C P B I N D I N G S ******** */
/**
* Adds a TCP binding to the server. Effectively this is how you
* set which ports and on which interfaces you want the server
* to listen. In the simplest case, you might do the following
* to listen generically on port 80:
* addTcpBinding( new InetAddress(80) );
.
* The server can listen on multiple ports at once.
* @param addr The address on which to listen
* @return "this" to aid in chaining commands
*/
public synchronized NioServer addTcpBinding( SocketAddress addr ){
Set oldVal = this.getTcpBindings(); // Save old set for prop change event
this.tcpBindings.put(addr,null); // Add binding
Set newVal = this.getTcpBindings(); // Save new set for prop change event
this.pendingTcpAdds.add(addr); // Prepare pending add action
this.pendingTcpRemoves.remove(addr); // In case it's also pending a remove
if( this.selector != null ){ // If there's a selector...
this.selector.wakeup(); // Wake it up to handle the add action
}
firePropertyChange(TCP_BINDINGS_PROP, oldVal, newVal); // Fire prop change
return this;
}
/**
* Removes a TCP binding. Effectively stops the server from
* listening to this or that port.
* @param addr The address to stop listening to
* @return "this" to aid in chaining commands
*/
public synchronized NioServer removeTcpBinding( SocketAddress addr ){
Set oldVal = this.getTcpBindings(); // Save old set for prop change event
this.pendingTcpRemoves.put( addr, this.tcpBindings.get(addr) ); // Prepare pending remove action
this.tcpBindings.remove(addr); // Remove binding
this.pendingTcpAdds.remove(addr); // In case it's also pending an add
Set newVal = this.getTcpBindings(); // Save new set for prop change event
if( this.selector != null ){ // If there's a selector...
this.selector.wakeup(); // Wake it up to handle the remove action
}
firePropertyChange(TCP_BINDINGS_PROP, oldVal, newVal); // Fire prop change
return this;
}
/**
* Returns a set of socket addresses that the server is (or will
* be when started) bound to/listening on. This set is not
* backed by the actual data structures. Changes to this returned
* set have no effect on the server.
* @return set of tcp listening points
*/
public synchronized Set getTcpBindings(){
Set bindings = new HashSet();
bindings.addAll( this.tcpBindings.keySet() );
return bindings;
}
/**
* Sets the TCP bindings that the server should use.
* The expression setTcpBindings( getTcpBindings() )
* should result in no change to the server.
* @param newSet
* @return "this" to aid in chaining commands
*/
public synchronized NioServer setTcpBindings( Set newSet ){
Set toAdd = new HashSet();
Set toRemove = new HashSet();
toRemove.addAll( getTcpBindings() );
for( SocketAddress addr : newSet ){
if( toRemove.contains(addr) ){
toRemove.remove(addr);
} else {
toAdd.add(addr);
}
} // end for: each new addr
for( SocketAddress addr : toRemove ){
removeTcpBinding(addr);
} // end for: each new addr
for( SocketAddress addr : toAdd ){
addTcpBinding(addr);
} // end for: each new addr
return this;
}
/**
* Clears all TCP bindings.
* @return "this" to aid in chaining commands
*/
public synchronized NioServer clearTcpBindings(){
for( SocketAddress addr : getTcpBindings() ){
removeTcpBinding(addr);
}
return this;
}
/* ******** U D P B I N D I N G S ******** */
/**
* Adds a UDP binding to the server. Effectively this is how you
* set which ports and on which interfaces you want the server
* to listen. In the simplest case, you might do the following
* to listen generically on port 6997:
* addUdpBinding( new InetAddress(6997) );
.
* The server can listen on multiple ports at once.
* @param addr The address on which to listen
* @return "this" to aid in chaining commands
*/
public synchronized NioServer addUdpBinding( SocketAddress addr ){
return addUdpBinding(addr,null);
}
/**
* Experimental Hack - Adds a UDP binding to the server
* and joins the given multicast group (if group
* is not null and is a valid multicast group).
* In the simplest case, you might do the following
* to listen on port 16000 and multicast group 239.0.0.1:
* addUdpBinding( new InetAddress(16000), "239.0.0.1" );
.
* The server can listen on multiple ports at once.
*
* As of Java 6, the java.nio
"New IO" packages
* don't support multicast groups ("annoyed grunt"), however I
* found a clever hack at this gentleman's website
* (http://www.mernst.org/blog/archives/12-01-2006_12-31-2006.html)
* that makes multicast work -- for now.
*
* @param addr The address on which to listen
* @param group The multicast group to join
* @return "this" to aid in chaining commands
*/
public synchronized NioServer addUdpBinding( SocketAddress addr, String group ){
Map oldVal = this.getUdpBindings();
this.udpBindings.put(addr,null);
this.pendingUdpAdds.add(addr);
this.pendingUdpRemoves.remove(addr);
if( group != null ){
this.multicastGroups.put(addr,group);
} // end if: multicast too
Map newVal = this.getUdpBindings();
if( this.selector != null ){
this.selector.wakeup();
}
firePropertyChange(UDP_BINDINGS_PROP,oldVal,newVal);
return this;
}
/**
* Removes a UDP binding. Effectively stops the server from
* listening to this or that port.
* @param addr The address to stop listening to
* @return "this" to aid in chaining commands
*/
public synchronized NioServer removeUdpBinding( SocketAddress addr ){
Map oldVal = this.getUdpBindings(); // Save old set for prop change event
this.pendingUdpRemoves.put( addr, this.udpBindings.get(addr) ); // Prepare pending remove action
this.udpBindings.remove(addr); // Remove binding
this.multicastGroups.remove(addr); // Remove multicast note
this.pendingUdpAdds.remove(addr); // In case it's also pending an add
Map newVal = this.getUdpBindings(); // Save new set for prop change event
if( this.selector != null ){ // If there's a selector...
this.selector.wakeup(); // Wake it up to handle the remove action
}
firePropertyChange(UDP_BINDINGS_PROP, oldVal, newVal); // Fire prop change
return this;
}
/**
* Returns a map of socket addresses and multicast groups
* that the server is (or will
* be when started) bound to/listening on. This set is not
* backed by the actual data structures. Changes to this returned
* set have no effect on the server.
* The map's value portion will be null if not multicast group
* is joined for that port or it may have a String which would
* be the requested multicast group.
* @return map of udp listening points
*/
public synchronized Map getUdpBindings(){
Mapbindings = new HashMap();
for( SocketAddress addr : this.udpBindings.keySet() ){
bindings.put(addr, this.multicastGroups.get(addr) );
} // end for: each address
return bindings;
}
/**
* Sets the UDP bindings that the server should use.
* The expression setTcpBindings( getTcpBindings() )
* should result in no change to the server.
*
* The map consists of socket addresses (probably InetSocketAddress)
* and multicast addresses (the String value).
* @param newMap
* @return "this" to aid in chaining commands
*/
public synchronized NioServer setUdpBindings( Map newMap ){
Map toAdd = new HashMap();
Map toRemove = new HashMap();
toRemove.putAll( getUdpBindings() );
for( Map.Entry e : newMap.entrySet() ){
SocketAddress addr = e.getKey();
String group = e.getValue();
if( toRemove.containsKey(addr) ){
toRemove.remove(addr);
} else {
toAdd.put(addr,group);
}
} // end for: each new addr
for( Map.Entry e : toRemove.entrySet() ){
removeUdpBinding(e.getKey());
} // end for: each new addr
for( Map.Entry e : toAdd.entrySet() ){
addUdpBinding(e.getKey(),e.getValue());
} // end for: each new addr
return this;
}
/**
* Clears all UDP bindings.
* @return "this" to aid in chaining commands
*/
public synchronized NioServer clearUdpBindings(){
for( SocketAddress addr : getUdpBindings().keySet() ){
removeUdpBinding(addr);
}
return this;
}
/* ******** S I N G L E P O R T ******** */
/**
* Convenience method for clearing all bindings and
* setting up listening for TCP on the given port.
* @param port the port to listen to
* @return this
to aid in chaining
*/
public synchronized NioServer setSingleTcpPort( int port ){
int oldVal = getSingleTcpPort();
if( oldVal == port ){
return this;
}
clearTcpBindings();
addTcpBinding( new InetSocketAddress(port) );
int newVal = port;
firePropertyChange( SINGLE_TCP_PORT_PROP, oldVal, newVal );
return this;
}
/**
* Convenience method for clearing all bindings and
* setting up listening for UDP on the given port.
* @param port the port to listen to
* @return this
to aid in chaining
*/
public synchronized NioServer setSingleUdpPort( int port ){
return setSingleUdpPort( port, null );
}
/**
* Convenience method for clearing all bindings and
* setting up listening for UDP on the given port
* and joining the provided multicast group.
* @param port the port to listen to
* @param group
* @return this
to aid in chaining
*/
public synchronized NioServer setSingleUdpPort( int port, String group ){
int oldVal = getSingleUdpPort();
if( oldVal == port ){
return this;
}
clearUdpBindings();
addUdpBinding( new InetSocketAddress(port), group );
int newVal = port;
firePropertyChange( SINGLE_UDP_PORT_PROP, oldVal, newVal );
return this;
}
/**
* Returns the port for the single TCP binding in effect,
* or -1 (minus one) if there are no or multiple TCP
* bindings or some other error.
* @return TCP listening port or -1
*/
public synchronized int getSingleTcpPort(){
int port = -1;
Set bindings = getTcpBindings();
if( bindings.size() == 1 ){
SocketAddress sa = bindings.iterator().next();
if( sa instanceof InetSocketAddress ){
port = ((InetSocketAddress)sa).getPort();
} // end if: inet
} // end if: only one binding
return port;
}
/**
* Returns the port for the single UDP binding in effect,
* or -1 (minus one) if there are no or multiple UDP
* bindings or some other error.
* @return UDP listening port or -1
*/
public synchronized int getSingleUdpPort(){
int port = -1;
Map bindings = getUdpBindings();
if( bindings.size() == 1 ){
SocketAddress sa = bindings.keySet().iterator().next();
if( sa instanceof InetSocketAddress ){
port = ((InetSocketAddress)sa).getPort();
} // end if: inet
} // end if: only one binding
return port;
}
/* ******** E V E N T S ******** */
/** Adds a {@link Listener}.
* @param l the listener
*/
public synchronized void addNioServerListener(NioServer.Listener l) {
listeners.add(l);
cachedListeners = null;
}
/** Removes a {@link Listener}.
* @param l the listener
*/
public synchronized void removeNioServerListener(NioServer.Listener l) {
listeners.remove(l);
cachedListeners = null;
}
/**
* Fire when data is received.
* @param key the SelectionKey associated with the data
* @param buffer the buffer containing the new (and possibly leftover) data
*/
protected synchronized void fireTcpDataReceived(SelectionKey key, ByteBuffer buffer) {
if( cachedListeners == null ){
cachedListeners = listeners.toArray(new NioServer.Listener[ listeners.size() ] );
}
this.event.reset(key,buffer,null);
// Make a Runnable object to execute the calls to listeners.
// In the event we don't have an Executor, this results in
// an unnecessary object instantiation, but it also makes
// the code more maintainable.
for( NioServer.Listener l : cachedListeners ){
try{
l.nioServerTcpDataReceived(event);
} catch( Exception exc ){
LOGGER.warning("NioServer.Listener " + l + " threw an exception: " + exc.getMessage() );
fireExceptionNotification(exc);
} // end catch
} // end for: each listener
}
/**
* Fire when data is received.
* @param key the SelectionKey associated with the data
* @param buffer the buffer containing the data
* @param remote the source address of the datagram or null if not available
*/
protected synchronized void fireUdpDataReceived(SelectionKey key, ByteBuffer buffer, SocketAddress remote) {
if( cachedListeners == null ){
cachedListeners = listeners.toArray(new NioServer.Listener[ listeners.size() ] );
}
this.event.reset(key,buffer,remote);
// Make a Runnable object to execute the calls to listeners.
// In the event we don't have an Executor, this results in
// an unnecessary object instantiation, but it also makes
// the code more maintainable.
for( NioServer.Listener l : cachedListeners ){
try{
l.nioServerUdpDataReceived(event);
} catch( Exception exc ){
LOGGER.warning("NioServer.Listener " + l + " threw an exception: " + exc.getMessage() );
fireExceptionNotification(exc);
} // end catch
} // end for: each listener
} // end fireNioServerPacketReceived
/**
* Fire when a connection is closed remotely.
* @param key The key for the closed connection.
*/
protected synchronized void fireConnectionClosed(SelectionKey key) {
if( cachedListeners == null ){
cachedListeners = listeners.toArray(new NioServer.Listener[ listeners.size() ] );
}
this.event.reset(key,null,null);
// Make a Runnable object to execute the calls to listeners.
// In the event we don't have an Executor, this results in
// an unnecessary object instantiation, but it also makes
// the code more maintainable.
for( NioServer.Listener l : cachedListeners ){
try{
l.nioServerConnectionClosed(event);
} catch( Exception exc ){
LOGGER.warning("NioServer.Listener " + l + " threw an exception: " + exc.getMessage() );
fireExceptionNotification(exc);
} // end catch
} // end for: each listener
} // end fireNioServerPacketReceived
/**
* Fire when a new connection is established.
* @param key the SelectionKey associated with the connection
*/
protected synchronized void fireNewConnection(SelectionKey key) {
if( cachedListeners == null ){
cachedListeners = listeners.toArray(new NioServer.Listener[ listeners.size() ] );
}
this.event.reset(key,null,null);
// Make a Runnable object to execute the calls to listeners.
// In the event we don't have an Executor, this results in
// an unnecessary object instantiation, but it also makes
// the code more maintainable.
for( NioServer.Listener l : cachedListeners ){
try{
l.nioServerNewConnectionReceived(event);
} catch( Exception exc ){
LOGGER.warning("NioServer.Listener " + l + " threw an exception: " + exc.getMessage() );
fireExceptionNotification(exc);
} // end catch
} // end for: each listener
} // end fireNioServerPacketReceived
/* ******** P R O P E R T Y C H A N G E ******** */
/**
* Fires property chagne events for all current values
* setting the old value to null and new value to the current.
*/
public synchronized void fireProperties(){
firePropertyChange( STATE_PROP, null, getState() );
firePropertyChange( BUFFER_SIZE_PROP, null, getBufferSize() );
}
/**
* Fire a property change event on the current thread.
*
* @param prop name of property
* @param oldVal old value
* @param newVal new value
*/
protected synchronized void firePropertyChange( final String prop, final Object oldVal, final Object newVal ){
try{
propSupport.firePropertyChange(prop,oldVal,newVal);
} catch( Exception exc ){
LOGGER.log(Level.WARNING,
"A property change listener threw an exception: " + exc.getMessage()
,exc);
fireExceptionNotification(exc);
} // end catch
} // end fire
/**
* Add a property listener.
* @param listener the listener
*/
public synchronized void addPropertyChangeListener( PropertyChangeListener listener ){
propSupport.addPropertyChangeListener(listener);
}
/**
* Add a property listener for the named property.
* @param property the property name
* @param listener the listener
*/
public synchronized void addPropertyChangeListener( String property, PropertyChangeListener listener ){
propSupport.addPropertyChangeListener(property,listener);
}
/**
* Remove a property listener.
* @param listener the listener
*/
public synchronized void removePropertyChangeListener( PropertyChangeListener listener ){
propSupport.removePropertyChangeListener(listener);
}
/**
* Remove a property listener for the named property.
* @param property the property name
* @param listener the listener
*/
public synchronized void removePropertyChangeListener( String property, PropertyChangeListener listener ){
propSupport.removePropertyChangeListener(property,listener);
}
/* ******** E X C E P T I O N S ******** */
/**
* Returns the last exception (Throwable, actually)
* that the server encountered.
* @return last exception
*/
public synchronized Throwable getLastException(){
return this.lastException;
}
/**
* Fires a property change event with the new exception.
* @param t
*/
protected void fireExceptionNotification( Throwable t ){
Throwable oldVal = this.lastException;
this.lastException = t;
firePropertyChange( LAST_EXCEPTION_PROP, oldVal, t );
}
/* ******** L O G G I N G ******** */
/**
* Static method to set the logging level using Java's
* java.util.logging package. Example:
* NioServer.setLoggingLevel(Level.OFF);
.
*
* @param level the new logging level
*/
public static void setLoggingLevel( Level level ){
LOGGER.setLevel(level);
}
/**
* Static method returning the logging level using Java's
* java.util.logging package.
* @return the logging level
*/
public static Level getLoggingLevel(){
return LOGGER.getLevel();
}
/* ******** ******** */
/* ******** ******** */
/* ******** S T A T I C I N N E R C L A S S L I S T E N E R ******** */
/* ******** ******** */
/* ******** ******** */
/**
* An interface for listening to events from a {@link NioServer}.
* A single {@link Event} is shared for all invocations
* of these methods.
*
* This code is released into the Public Domain.
* Since this is Public Domain, you don't need to worry about
* licensing, and you can simply copy this NioServer.java file
* to your own package and use it as you like. Enjoy.
* Please consider leaving the following statement here in this code:
*
* This NioServer class was copied to this project from its source as
* found at iHarder.net.
*
* @author Robert Harder
* @author [email protected]
* @version 0.1
* @see NioServer
* @see Adapter
* @see Event
*/
public static interface Listener extends java.util.EventListener {
/**
* Called when a new connection is received. The SelectionKey associated
* with the event (an OP_READ
key), is the key that will be
* used with the data received event. In this way, you can seed a
* Map
or other data structure and associate this very
* key with the connection. You will only get new connection
* events for TCP connections (not UDP).
*
* The key's attachment mechanism is unused by NioServer and is
* available for you to store whatever you like.
*
* If your protocol requires the server to respond to a client
* upon connection, this sample code demonstrates such an arrangement:
*
*
* public void nioServerNewConnectionReceived(NioServer.Event evt) {
* SocketChannel ch = (SocketChannel) evt.getKey().channel();
* try {
* ch.write(ByteBuffer.wrap("Greetings\r\n".getBytes()));
* } catch (IOException ex) {
* ex.printStackTrace(); // Please don't do printStackTrace in production code
* }
* }
*
* @param evt the shared event
*/
public abstract void nioServerNewConnectionReceived( NioServer.Event evt );
/**
* Called when TCP data is received. Retrieve the associated ByteBuffer
* with evt.getBuffer()
. This is the source ByteBuffer
* used by the server directly to receive the data. It is a
* "direct" ByteBuffer (created with ByteBuffer.allocateDirect(..)
).
* Read from it as much as
* you can. Any data that remains on or after the value
* of position()
will be saved for the next
* time an event is fired. In this way, you can defer
* processing incomplete data until everything arrives.
* Be careful that you don't leave the buffer full
* or you won't be able to receive anything next time around
* (unless you call {@link #setBufferSize} to resize buffer).
*
* The key's attachment mechanism is unused by NioServer and is
* available for you to store whatever you like.
*
* Example: You are receiving lines of text. The ByteBuffer
* returned here contains one and a half lines of text.
* When you realize this, you process the first line as you
* like, but you leave this buffer's position at the beginning
* of the second line. In this way, The beginning of the second
* line will be the start of the buffer the next time around.
*
* @param evt the shared event
*/
public abstract void nioServerTcpDataReceived( NioServer.Event evt );
/**
* Called when UDP data is received. Retrieve the associated ByteBuffer
* with evt.getBuffer()
. This is the source ByteBuffer
* used by the server directly to receive the data. It is a
* "direct" ByteBuffer (created with ByteBuffer.allocateDirect(..)
).
* The contents of the ByteBuffer will be the entire contents
* received from the UDP datagram.
*
* @param evt the shared event
*/
public abstract void nioServerUdpDataReceived( NioServer.Event evt );
/**
* Called when a connection is closed remotely. If you close the connection
* somewhere in your own code, this event probably won't be fired.
* @param evt the shared event
*/
public abstract void nioServerConnectionClosed( NioServer.Event evt );
} // end inner static class Listener
/* ******** ******** */
/* ******** ******** */
/* ******** S T A T I C I N N E R C L A S S A D A P T E R ******** */
/* ******** ******** */
/* ******** ******** */
/**
* A helper class that implements all methods of the
* {@link NioServer.Listener} interface with empty methods.
*
* This code is released into the Public Domain.
* Since this is Public Domain, you don't need to worry about
* licensing, and you can simply copy this NioServer.java file
* to your own package and use it as you like. Enjoy.
* Please consider leaving the following statement here in this code:
*
* This NioServer class was copied to this project from its source as
* found at iHarder.net.
*
* @author Robert Harder
* @author [email protected]
* @version 0.1
* @see NioServer
* @see Listener
* @see Event
*/
public class Adapter implements Listener {
/**
* Empty method.
* @see Listener
* @param evt the shared event
*/
public void nioServerTcpDataReceived(NioServer.Event evt) {}
/**
* Empty method.
* @see Listener
* @param evt the shared event
*/
public void nioServerUdpDataReceived(NioServer.Event evt) {}
/**
* Empty method.
* @see Listener
* @param evt the shared event
*/
public void nioServerNewConnectionReceived(NioServer.Event evt) {}
/**
* Empty method.
* @see Listener
* @param evt the shared event
*/
public void nioServerConnectionClosed(NioServer.Event evt) {}
} // end static inner class Adapter
/* ******** ******** */
/* ******** ******** */
/* ******** S T A T I C I N N E R C L A S S E V E N T ******** */
/* ******** ******** */
/* ******** ******** */
/**
* An event representing activity by a {@link NioServer}.
*
* This code is released into the Public Domain.
* Since this is Public Domain, you don't need to worry about
* licensing, and you can simply copy this NioServer.java file
* to your own package and use it as you like. Enjoy.
* Please consider leaving the following statement here in this code:
*
* This NioServer class was copied to this project from its source as
* found at iHarder.net.
*
* @author Robert Harder
* @author [email protected]
* @version 0.1
* @see NioServer
* @see Adapter
* @see Listener
*/
public static class Event extends java.util.EventObject {
private final static long serialVersionUID = 1;
/**
* The key associated with this (reusable) event.
* Use setKey(..) to change the key between firings.
*/
private SelectionKey key;
/**
* The buffer that holds the data, for some events.
*/
private ByteBuffer buffer;
/**
* The source address for incoming UDP datagrams.
* The {@link #getRemoteSocketAddress} method
* will return this value if data is from UDP.
*/
private SocketAddress remoteUdp;
/**
* Creates a Event based on the given {@link NioServer}.
* @param src the source of the event
*/
public Event( NioServer src ){
super(src);
}
/**
* Returns the source of the event, a {@link NioServer}.
* Shorthand for (NioServer)getSource().
* @return the server
*/
public NioServer getNioServer(){
return (NioServer)getSource();
}
/**
* Shorthand for getNioServer().getState().
* @return the state of the server
* @see NioServer.State
*/
public NioServer.State getState(){
return getNioServer().getState();
}
/**
* Returns the SelectionKey associated with this event.
*
* @return the SelectionKey
*/
public SelectionKey getKey(){
return this.key;
}
/**
* Resets an event between firings by updating the parameters
* that change.
* @param key The SelectionKey for the event
* @param buffer
* @param remoteUdp the remote UDP source or null for TCP
*/
protected void reset( SelectionKey key, ByteBuffer buffer, SocketAddress remoteUdp ){
this.key = key;
this.buffer = buffer;
this.remoteUdp = remoteUdp;
}
/**
* Returns the {@link java.nio.ByteBuffer} that contains
* the data for this connection. Read from it as much as
* you can. Any data that remains on or after the value
* of position()
will be saved for the next
* time an event is fired. In this way, you can defer
* processing incomplete data until everything arrives.
*
* Example: You are receiving lines of text. The ByteBuffer
* returned here contains one and a half lines of text.
* When you realize this, you process the first line as you
* like, but you leave this buffer's position at the beginning
* of the second line. In this way, The beginning of the second
* line will be the start of the buffer the next time around.
* @return buffer with the data
*/
public ByteBuffer getBuffer(){
return this.buffer;
}
/**
* Returns the local address/port to which this connection
* is bound. That is, if you are listening on port 80, then
* this might return something like an InetSocketAddress
* (probably) that indicated /127.0.0.1:80.
* This is
* essentially a convenience method for returning the same-name
* methods from the key's channel after checking the type
* of channel (SocketChannel or DatagramChannel).
*
* @return local address that server is bound to for this connection
*/
public SocketAddress getLocalSocketAddress(){
SocketAddress addr = null;
if( this.key != null ){
SelectableChannel sc = this.key.channel();
if( sc instanceof SocketChannel ){
addr = ((SocketChannel)sc).socket().getLocalSocketAddress();
} else if( sc instanceof DatagramChannel ){
addr = ((DatagramChannel)sc).socket().getLocalSocketAddress();
}
}
return addr;
}
/**
* Returns the address of the endpoint this socket is
* connected to, or null if it is unconnected.
* This is
* essentially a convenience method for returning the same-name
* methods from the key's channel after checking the type
* of channel (SocketChannel or DatagramChannel).
*
* @return remote address from which connection came
*/
public SocketAddress getRemoteSocketAddress(){
SocketAddress addr = null;
if( this.key != null ){
SelectableChannel sc = this.key.channel();
if( sc instanceof SocketChannel ){
addr = ((SocketChannel)sc).socket().getRemoteSocketAddress();
} else if( sc instanceof DatagramChannel ){
addr = this.remoteUdp;
}
}
return addr;
}
/**
* Convenience method for checking
* getKey().channel() instanceof SocketChannel
.
* @return true if a TCP connection
*/
public boolean isTcp(){
return this.key == null ? false : this.key.channel() instanceof SocketChannel;
}
/**
* Convenience method for checking
* getKey().channel() instanceof DatagramChannel
.
* @return true if a UDP connection
*/
public boolean isUdp(){
return this.key == null ? false : this.key.channel() instanceof DatagramChannel;
}
} // end static inner class Event
} // end class NioServer