package com.esotericsoftware.kryonet; import java.lang.reflect.InvocationTargetException; import java.lang.reflect.Method; import java.util.HashMap; import java.util.LinkedList; import java.util.concurrent.ExecutorService; import java.util.concurrent.Executors; import java.util.concurrent.ScheduledExecutorService; import java.util.concurrent.TimeUnit; import static com.esotericsoftware.minlog.Log.*; /** Used to be notified about connection events. */ public class Listener { /** Called when the remote end has been connected. This will be invoked before any objects are received by * {@link #received(Connection, Object)}. This will be invoked on the same thread as {@link Client#update(int)} and * {@link Server#update(int)}. This method should not block for long periods as other network activity will not be processed * until it returns. */ public void connected (Connection connection) { } /** Called when the remote end is no longer connected. There is no guarantee as to what thread will invoke this method. */ public void disconnected (Connection connection) { } /** Called when an object has been received from the remote end of the connection. This will be invoked on the same thread as * {@link Client#update(int)} and {@link Server#update(int)}. This method should not block for long periods as other network * activity will not be processed until it returns. */ public void received (Connection connection, Object object) { } /** Called when the connection is below the {@link Connection#setIdleThreshold(float) idle threshold}. */ public void idle (Connection connection) { } /** Uses reflection to called "received(Connection, XXX)" on the listener, where XXX is the received object type. Note this * class uses a HashMap lookup and (cached) reflection, so is not as efficient as writing a series of "instanceof" statements. */ static public class ReflectionListener extends Listener { private final HashMap<Class, Method> classToMethod = new HashMap(); public void received (Connection connection, Object object) { Class type = object.getClass(); Method method = classToMethod.get(type); if (method == null) { if (classToMethod.containsKey(type)) return; // Only fail on the first attempt to find the method. try { method = getClass().getMethod("received", new Class[] {Connection.class, type}); } catch (SecurityException ex) { if (ERROR) error("kryonet", "Unable to access method: received(Connection, " + type.getName() + ")", ex); return; } catch (NoSuchMethodException ex) { if (DEBUG) debug("kryonet", "Unable to find listener method: " + getClass().getName() + "#received(Connection, " + type.getName() + ")"); return; } finally { classToMethod.put(type, method); } } try { method.invoke(this, connection, object); } catch (Throwable ex) { if (ex instanceof InvocationTargetException && ex.getCause() != null) ex = ex.getCause(); if (ex instanceof RuntimeException) throw (RuntimeException)ex; throw new RuntimeException("Error invoking method: " + getClass().getName() + "#received(Connection, " + type.getName() + ")", ex); } } } /** Wraps a listener and queues notifications as {@link Runnable runnables}. This allows the runnables to be processed on a * different thread, preventing the connection's update thread from being blocked. */ static public abstract class QueuedListener extends Listener { final Listener listener; public QueuedListener (Listener listener) { if (listener == null) throw new IllegalArgumentException("listener cannot be null."); this.listener = listener; } public void connected (final Connection connection) { queue(new Runnable() { public void run () { listener.connected(connection); } }); } public void disconnected (final Connection connection) { queue(new Runnable() { public void run () { listener.disconnected(connection); } }); } public void received (final Connection connection, final Object object) { queue(new Runnable() { public void run () { listener.received(connection, object); } }); } public void idle (final Connection connection) { queue(new Runnable() { public void run () { listener.idle(connection); } }); } abstract protected void queue (Runnable runnable); } /** Wraps a listener and processes notification events on a separate thread. */ static public class ThreadedListener extends QueuedListener { protected final ExecutorService threadPool; /** Creates a single thread to process notification events. */ public ThreadedListener (Listener listener) { this(listener, Executors.newFixedThreadPool(1)); } /** Uses the specified threadPool to process notification events. */ public ThreadedListener (Listener listener, ExecutorService threadPool) { super(listener); if (threadPool == null) throw new IllegalArgumentException("threadPool cannot be null."); this.threadPool = threadPool; } public void queue (Runnable runnable) { threadPool.execute(runnable); } } /** Delays the notification of the wrapped listener to simulate lag on incoming objects. Notification events are processed on a * separate thread after a delay. Note that only incoming objects are delayed. To delay outgoing objects, use a LagListener at * the other end of the connection. */ static public class LagListener extends QueuedListener { private final ScheduledExecutorService threadPool; private final int lagMillisMin, lagMillisMax; final LinkedList<Runnable> runnables = new LinkedList(); public LagListener (int lagMillisMin, int lagMillisMax, Listener listener) { super(listener); this.lagMillisMin = lagMillisMin; this.lagMillisMax = lagMillisMax; threadPool = Executors.newScheduledThreadPool(1); } public void queue (Runnable runnable) { synchronized (runnables) { runnables.addFirst(runnable); } int lag = lagMillisMin + (int)(Math.random() * (lagMillisMax - lagMillisMin)); threadPool.schedule(new Runnable() { public void run () { Runnable runnable; synchronized (runnables) { runnable = runnables.removeLast(); } runnable.run(); } }, lag, TimeUnit.MILLISECONDS); } } }