package com.koushikdutta.async;
import android.os.Build;
import android.os.Handler;
import android.util.Log;
import com.koushikdutta.async.callback.CompletedCallback;
import com.koushikdutta.async.callback.ConnectCallback;
import com.koushikdutta.async.callback.ListenCallback;
import com.koushikdutta.async.future.Cancellable;
import com.koushikdutta.async.future.Future;
import com.koushikdutta.async.future.SimpleFuture;
import com.koushikdutta.async.future.TransformFuture;
import com.koushikdutta.async.util.StreamUtility;
import java.io.IOException;
import java.net.InetAddress;
import java.net.InetSocketAddress;
import java.net.SocketAddress;
import java.nio.channels.CancelledKeyException;
import java.nio.channels.ClosedChannelException;
import java.nio.channels.ClosedSelectorException;
import java.nio.channels.DatagramChannel;
import java.nio.channels.SelectionKey;
import java.nio.channels.ServerSocketChannel;
import java.nio.channels.SocketChannel;
import java.nio.channels.spi.SelectorProvider;
import java.util.Comparator;
import java.util.PriorityQueue;
import java.util.Set;
import java.util.WeakHashMap;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.Semaphore;
public class AsyncServer {
public static final String LOGTAG = "NIO";
private static class RunnableWrapper implements Runnable {
boolean hasRun;
Runnable runnable;
ThreadQueue threadQueue;
Handler handler;
@Override
public void run() {
synchronized (this) {
if (hasRun)
return;
hasRun = true;
}
try {
runnable.run();
}
finally {
threadQueue.remove(this);
handler.removeCallbacks(this);
threadQueue = null;
handler = null;
runnable = null;
}
}
}
public static void post(Handler handler, Runnable runnable) {
RunnableWrapper wrapper = new RunnableWrapper();
ThreadQueue threadQueue = ThreadQueue.getOrCreateThreadQueue(handler.getLooper().getThread());
wrapper.threadQueue = threadQueue;
wrapper.handler = handler;
wrapper.runnable = runnable;
threadQueue.add(wrapper);
handler.post(wrapper);
// run the queue if the thread is blocking
threadQueue.queueSemaphore.release();
}
static {
try {
if (Build.VERSION.SDK_INT <= Build.VERSION_CODES.FROYO) {
java.lang.System.setProperty("java.net.preferIPv4Stack", "true");
java.lang.System.setProperty("java.net.preferIPv6Addresses", "false");
}
}
catch (Throwable ex) {
}
}
static AsyncServer mInstance = new AsyncServer();
public static AsyncServer getDefault() {
return mInstance;
}
private SelectorWrapper mSelector;
public boolean isRunning() {
return mSelector != null;
}
public AsyncServer() {
}
private void handleSocket(final AsyncNetworkSocket handler) throws ClosedChannelException {
final ChannelWrapper sc = handler.getChannel();
SelectionKey ckey = sc.register(mSelector.getSelector());
ckey.attach(handler);
handler.setup(this, ckey);
}
public void removeAllCallbacks(Object scheduled) {
synchronized (this) {
mQueue.remove(scheduled);
}
}
private static void wakeup(final SelectorWrapper selector) {
synchronousWorkers.execute(new Runnable() {
@Override
public void run() {
selector.wakeupOnce();
}
});
}
public Object postDelayed(Runnable runnable, long delay) {
Scheduled s;
synchronized (this) {
// Calculate when to run this queue item:
// If there is a delay (non-zero), add it to the current time
// When delay is zero, ensure that this follows all other
// zero-delay queue items. This is done by setting the
// "time" to the queue size. This will make sure it is before
// all time-delayed queue items (for all real world scenarios)
// as it will always be less than the current time and also remain
// behind all other immediately run queue items.
long time;
if (delay != 0)
time = System.currentTimeMillis() + delay;
else
time = mQueue.size();
mQueue.add(s = new Scheduled(runnable, time));
// start the server up if necessary
if (mSelector == null)
run(false, true);
if (!isAffinityThread()) {
wakeup(mSelector);
}
}
return s;
}
public Object post(Runnable runnable) {
return postDelayed(runnable, 0);
}
public Object post(final CompletedCallback callback, final Exception e) {
return post(new Runnable() {
@Override
public void run() {
callback.onCompleted(e);
}
});
}
public void run(final Runnable runnable) {
if (Thread.currentThread() == mAffinity) {
post(runnable);
lockAndRunQueue(this, mQueue);
return;
}
final Semaphore semaphore = new Semaphore(0);
post(new Runnable() {
@Override
public void run() {
runnable.run();
semaphore.release();
}
});
try {
semaphore.acquire();
}
catch (InterruptedException e) {
Log.e(LOGTAG, "run", e);
}
}
private static class Scheduled {
public Scheduled(Runnable runnable, long time) {
this.runnable = runnable;
this.time = time;
}
public Runnable runnable;
public long time;
}
PriorityQueue<Scheduled> mQueue = new PriorityQueue<Scheduled>(1, Scheduler.INSTANCE);
static class Scheduler implements Comparator<Scheduled> {
public static Scheduler INSTANCE = new Scheduler();
private Scheduler() {
}
@Override
public int compare(Scheduled s1, Scheduled s2) {
// keep the smaller ones at the head, so they get tossed out quicker
if (s1.time == s2.time)
return 0;
if (s1.time > s2.time)
return 1;
return -1;
}
}
public void stop() {
// Log.i(LOGTAG, "****AsyncServer is shutting down.****");
final SelectorWrapper currentSelector;
final Semaphore semaphore;
synchronized (this) {
currentSelector = mSelector;
if (currentSelector == null)
return;
synchronized (mServers) {
mServers.remove(mAffinity);
}
semaphore = new Semaphore(0);
// post a shutdown and wait
mQueue.add(new Scheduled(new Runnable() {
@Override
public void run() {
shutdownEverything(currentSelector);
semaphore.release();
}
}, 0));
currentSelector.wakeupOnce();
// force any existing connections to die
shutdownKeys(currentSelector);
mQueue = new PriorityQueue<Scheduled>(1, Scheduler.INSTANCE);
mSelector = null;
mAffinity = null;
}
try {
semaphore.acquire();
}
catch (Exception e) {
}
}
protected void onDataTransmitted(int transmitted) {
}
private static class ObjectHolder<T> {
T held;
}
public AsyncServerSocket listen(final InetAddress host, final int port, final ListenCallback handler) {
final ObjectHolder<AsyncServerSocket> holder = new ObjectHolder<AsyncServerSocket>();
run(new Runnable() {
@Override
public void run() {
ServerSocketChannel closeableServer = null;
ServerSocketChannelWrapper closeableWrapper = null;
try {
closeableServer = ServerSocketChannel.open();
closeableWrapper = new ServerSocketChannelWrapper(
closeableServer);
final ServerSocketChannel server = closeableServer;
final ServerSocketChannelWrapper wrapper = closeableWrapper;
InetSocketAddress isa;
if (host == null)
isa = new InetSocketAddress(port);
else
isa = new InetSocketAddress(host, port);
server.socket().bind(isa);
final SelectionKey key = wrapper.register(mSelector.getSelector());
key.attach(handler);
handler.onListening(holder.held = new AsyncServerSocket() {
@Override
public int getLocalPort() {
return server.socket().getLocalPort();
}
@Override
public void stop() {
StreamUtility.closeQuietly(wrapper);
try {
key.cancel();
}
catch (Exception e) {
}
}
});
}
catch (Exception e) {
StreamUtility.closeQuietly(closeableWrapper, closeableServer);
handler.onCompleted(e);
}
}
});
return holder.held;
}
private class ConnectFuture extends SimpleFuture<AsyncNetworkSocket> {
@Override
protected void cancelCleanup() {
super.cancelCleanup();
try {
if (socket != null)
socket.close();
}
catch (IOException e) {
}
}
SocketChannel socket;
ConnectCallback callback;
}
private ConnectFuture connectResolvedInetSocketAddress(final InetSocketAddress address, final ConnectCallback callback) {
final ConnectFuture cancel = new ConnectFuture();
assert !address.isUnresolved();
post(new Runnable() {
@Override
public void run() {
if (cancel.isCancelled())
return;
cancel.callback = callback;
SelectionKey ckey = null;
SocketChannel socket = null;
try {
socket = cancel.socket = SocketChannel.open();
socket.configureBlocking(false);
ckey = socket.register(mSelector.getSelector(), SelectionKey.OP_CONNECT);
ckey.attach(cancel);
socket.connect(address);
}
catch (Exception e) {
if (ckey != null)
ckey.cancel();
try {
if (socket != null)
socket.close();
}
catch (Exception ignored) {
}
cancel.setComplete(e);
}
}
});
return cancel;
}
public Cancellable connectSocket(final InetSocketAddress remote, final ConnectCallback callback) {
if (!remote.isUnresolved())
return connectResolvedInetSocketAddress(remote, callback);
return getByName(remote.getHostName())
.then(new TransformFuture<AsyncSocket, InetAddress>() {
@Override
protected void transform(InetAddress result) throws Exception {
setParent(connectResolvedInetSocketAddress(new InetSocketAddress(remote.getHostName(), remote.getPort()), callback));
}
});
}
public Cancellable connectSocket(final String host, final int port, final ConnectCallback callback) {
return connectSocket(InetSocketAddress.createUnresolved(host, port), callback);
}
private static ExecutorService synchronousWorkers = Executors.newFixedThreadPool(4);
public Future<InetAddress[]> getAllByName(final String host) {
final SimpleFuture<InetAddress[]> ret = new SimpleFuture<InetAddress[]>();
synchronousWorkers.execute(new Runnable() {
@Override
public void run() {
try {
final InetAddress[] result = InetAddress.getAllByName(host);
if (result == null || result.length == 0)
throw new HostnameResolutionException("no addresses for host");
post(new Runnable() {
@Override
public void run() {
ret.setComplete(null, result);
}
});
} catch (final Exception e) {
post(new Runnable() {
@Override
public void run() {
ret.setComplete(e, null);
}
});
}
}
});
return ret;
}
public Future<InetAddress> getByName(String host) {
return getAllByName(host)
.then(new TransformFuture<InetAddress, InetAddress[]>() {
@Override
protected void transform(InetAddress[] result) throws Exception {
setComplete(result[0]);
}
});
}
public AsyncDatagramSocket connectDatagram(final String host, final int port) throws IOException {
final DatagramChannel socket = DatagramChannel.open();
final AsyncDatagramSocket handler = new AsyncDatagramSocket();
handler.attach(socket);
// ugh.. this should really be post to make it nonblocking...
// but i want datagrams to be immediately writable.
// they're not really used anyways.
run(new Runnable() {
@Override
public void run() {
try {
final SocketAddress remote = new InetSocketAddress(host, port);
handleSocket(handler);
socket.connect(remote);
}
catch (Exception e) {
Log.e(LOGTAG, "Datagram error", e);
}
}
});
return handler;
}
public AsyncDatagramSocket openDatagram() throws IOException {
return openDatagram(null, false);
}
public AsyncDatagramSocket openDatagram(final SocketAddress address, final boolean reuseAddress) throws IOException {
final DatagramChannel socket = DatagramChannel.open();
final AsyncDatagramSocket handler = new AsyncDatagramSocket();
handler.attach(socket);
// ugh.. this should really be post to make it nonblocking...
// but i want datagrams to be immediately writable.
// they're not really used anyways.
run(new Runnable() {
@Override
public void run() {
try {
if (reuseAddress)
socket.socket().setReuseAddress(reuseAddress);
socket.socket().bind(address);
handleSocket(handler);
}
catch (Exception e) {
Log.e(LOGTAG, "Datagram error", e);
}
}
});
return handler;
}
public AsyncDatagramSocket connectDatagram(final SocketAddress remote) throws IOException {
final DatagramChannel socket = DatagramChannel.open();
final AsyncDatagramSocket handler = new AsyncDatagramSocket();
handler.attach(socket);
// ugh.. this should really be post to make it nonblocking...
// but i want datagrams to be immediately writable.
// they're not really used anyways.
run(new Runnable() {
@Override
public void run() {
try {
handleSocket(handler);
socket.connect(remote);
}
catch (Exception e) {
}
}
});
return handler;
}
final static WeakHashMap<Thread, AsyncServer> mServers = new WeakHashMap<Thread, AsyncServer>();
private boolean addMe() {
synchronized (mServers) {
AsyncServer current = mServers.get(mAffinity);
if (current != null) {
// Log.e(LOGTAG, "****AsyncServer already running on this thread.****");
return false;
}
mServers.put(mAffinity, this);
}
return true;
}
public static AsyncServer getCurrentThreadServer() {
return mServers.get(Thread.currentThread());
}
Thread mAffinity;
public void run() {
run(false, false);
}
public void run(final boolean keepRunning, boolean newThread) {
final SelectorWrapper selector;
final PriorityQueue<Scheduled> queue;
boolean reentrant = false;
synchronized (this) {
if (mSelector != null) {
Log.i(LOGTAG, "Reentrant call");
assert Thread.currentThread() == mAffinity;
// this is reentrant
reentrant = true;
selector = mSelector;
queue = mQueue;
}
else {
try {
selector = mSelector = new SelectorWrapper(SelectorProvider.provider().openSelector());
queue = mQueue;
}
catch (IOException e) {
return;
}
if (newThread) {
mAffinity = new Thread("AsyncServer") {
public void run() {
AsyncServer.run(AsyncServer.this, selector, queue, keepRunning);
}
};
}
else {
mAffinity = Thread.currentThread();
}
if (!addMe()) {
try {
mSelector.close();
}
catch (Exception e) {
}
mSelector = null;
mAffinity = null;
return;
}
if (newThread) {
mAffinity.start();
// kicked off the new thread, let's bail.
return;
}
// fall through to outside of the synchronization scope
// to allow the thread to run without locking.
}
}
if (reentrant) {
try {
runLoop(this, selector, queue, false);
}
catch (Exception e) {
Log.e(LOGTAG, "exception?", e);
}
return;
}
run(this, selector, queue, keepRunning);
}
private static void run(final AsyncServer server, final SelectorWrapper selector, final PriorityQueue<Scheduled> queue, final boolean keepRunning) {
// Log.i(LOGTAG, "****AsyncServer is starting.****");
// at this point, this local queue and selector are owned
// by this thread.
// if a stop is called, the instance queue and selector
// will be replaced and nulled respectively.
// this will allow the old queue and selector to shut down
// gracefully, while also allowing a new selector thread
// to start up while the old one is still shutting down.
while(true) {
try {
runLoop(server, selector, queue, keepRunning);
}
catch (ClosedSelectorException e) {
}
catch (Exception e) {
Log.e(LOGTAG, "exception?", e);
}
// see if we keep looping, this must be in a synchronized block since the queue is accessed.
synchronized (server) {
if (selector.isOpen() && (selector.keys().size() > 0 || keepRunning || queue.size() > 0))
continue;
shutdownEverything(selector);
if (server.mSelector == selector) {
server.mQueue = new PriorityQueue<Scheduled>(1, Scheduler.INSTANCE);
server.mSelector = null;
server.mAffinity = null;
}
break;
}
}
synchronized (mServers) {
mServers.remove(Thread.currentThread());
}
// Log.i(LOGTAG, "****AsyncServer has shut down.****");
}
private static void shutdownKeys(SelectorWrapper selector) {
try {
for (SelectionKey key: selector.keys()) {
try {
key.channel().close();
}
catch (Exception e) {
}
try {
key.cancel();
}
catch (Exception e) {
}
}
}
catch (Exception ex) {
}
}
private static void shutdownEverything(SelectorWrapper selector) {
shutdownKeys(selector);
// SHUT. DOWN. EVERYTHING.
try {
selector.close();
}
catch (Exception e) {
}
}
private static final long QUEUE_EMPTY = Long.MAX_VALUE;
private static long lockAndRunQueue(final AsyncServer server, final PriorityQueue<Scheduled> queue) {
long wait = QUEUE_EMPTY;
// find the first item we can actually run
while (true) {
Scheduled run = null;
synchronized (server) {
long now = System.currentTimeMillis();
if (queue.size() > 0) {
Scheduled s = queue.remove();
if (s.time <= now) {
run = s;
}
else {
wait = s.time - now;
queue.add(s);
}
}
}
if (run == null)
break;
run.runnable.run();
}
return wait;
}
private static void runLoop(final AsyncServer server, final SelectorWrapper selector, final PriorityQueue<Scheduled> queue, final boolean keepRunning) throws IOException {
// Log.i(LOGTAG, "Keys: " + selector.keys().size());
boolean needsSelect = true;
// run the queue to populate the selector with keys
long wait = lockAndRunQueue(server, queue);
synchronized (server) {
// select now to see if anything is ready immediately. this
// also clears the canceled key queue.
int readyNow = selector.selectNow();
if (readyNow == 0) {
// if there is nothing to select now, make sure we don't have an empty key set
// which means it would be time to turn this thread off.
if (selector.keys().size() == 0 && !keepRunning && wait == QUEUE_EMPTY) {
// Log.i(LOGTAG, "Shutting down. keys: " + selector.keys().size() + " keepRunning: " + keepRunning);
return;
}
}
else {
needsSelect = false;
}
}
if (needsSelect) {
if (wait == QUEUE_EMPTY) {
// wait until woken up
selector.select();
}
else {
// nothing to select immediately but there's something pending so let's block that duration and wait.
selector.select(wait);
}
}
// process whatever keys are ready
Set<SelectionKey> readyKeys = selector.selectedKeys();
for (SelectionKey key : readyKeys) {
try {
if (key.isAcceptable()) {
ServerSocketChannel nextReady = (ServerSocketChannel) key.channel();
SocketChannel sc = nextReady.accept();
if (sc == null)
continue;
sc.configureBlocking(false);
SelectionKey ckey = sc.register(selector.getSelector(), SelectionKey.OP_READ);
ListenCallback serverHandler = (ListenCallback) key.attachment();
AsyncNetworkSocket handler = new AsyncNetworkSocket();
handler.attach(sc, (InetSocketAddress)sc.socket().getRemoteSocketAddress());
handler.setup(server, ckey);
ckey.attach(handler);
serverHandler.onAccepted(handler);
}
else if (key.isReadable()) {
AsyncNetworkSocket handler = (AsyncNetworkSocket) key.attachment();
int transmitted = handler.onReadable();
server.onDataTransmitted(transmitted);
}
else if (key.isWritable()) {
AsyncNetworkSocket handler = (AsyncNetworkSocket) key.attachment();
handler.onDataWritable();
}
else if (key.isConnectable()) {
ConnectFuture cancel = (ConnectFuture) key.attachment();
SocketChannel sc = (SocketChannel) key.channel();
key.interestOps(SelectionKey.OP_READ);
try {
sc.finishConnect();
AsyncNetworkSocket newHandler = new AsyncNetworkSocket();
newHandler.setup(server, key);
newHandler.attach(sc, (InetSocketAddress)sc.socket().getRemoteSocketAddress());
key.attach(newHandler);
if (cancel.setComplete(newHandler))
cancel.callback.onConnectCompleted(null, newHandler);
}
catch (Exception ex) {
key.cancel();
sc.close();
if (cancel.setComplete(ex))
cancel.callback.onConnectCompleted(ex, null);
}
}
else {
Log.i(LOGTAG, "wtf");
assert false;
}
}
catch (CancelledKeyException ex) {
}
catch (Exception ex) {
Log.e(LOGTAG, "inner loop exception", ex);
}
}
readyKeys.clear();
}
public void dump() {
post(new Runnable() {
@Override
public void run() {
if (mSelector == null) {
Log.i(LOGTAG, "Server dump not possible. No selector?");
return;
}
Log.i(LOGTAG, "Key Count: " + mSelector.keys().size());
for (SelectionKey key: mSelector.keys()) {
Log.i(LOGTAG, "Key: " + key);
}
}
});
}
public Thread getAffinity() {
return mAffinity;
}
public boolean isAffinityThread() {
return mAffinity == Thread.currentThread();
}
}