package com.vdom.comms;
import java.io.EOFException;
import java.io.ObjectInputStream;
import java.io.ObjectOutputStream;
import java.io.IOException;
import java.io.OptionalDataException;
import java.io.StreamCorruptedException;
import java.net.ServerSocket;
import java.net.Socket;
import java.net.SocketTimeoutException;
import java.net.UnknownHostException;
import java.util.concurrent.LinkedBlockingQueue;
import java.util.concurrent.TimeUnit;
import com.vdom.comms.Event.EType;
/**
* Ok, I reworked this class a little.
* Originally, this was a class for a hybrid between asynchronous and event-driven communication.
* One could communicate using public get and put asynchronously and / or run this class as a thread.
*
* Now this class spawn a thread upon creation and communicate with it in a threadsafe manner.
* Use doWait() in place of get(); put() keeps its functionality.
*
*/
public class Comms {
final static int TIMEOUT = 15000; // 15 seconds in ms
final static boolean DEBUGGING = false;
String host;
int port;
EventHandler parent;
private boolean isServer = true;
public class MonitorObject{};
private Socket pclient = null;
private SocketThread networkThread;
LinkedBlockingQueue<Event> latestEvents = new LinkedBlockingQueue<Event>();
/*
* The following functions (doWait, get_ts, poll, doWaitTimeout) are used to synchronously receive packets
* in a thread-safe manner (unlike the horrible mess we had before).
* !!! They receive everything for which the message-handler returned false. !!!
*/
public Event doWait(){
return doWaitTimeout(TIMEOUT);
}
public Event get_ts() { // the old get, but threadsafe
return doWaitTimeout(-1);
}
public Event poll() { // Look if we received something but don't block
return doWaitTimeout(0);
}
/**
* Returns the latest Event or null if none received within timeout
* @param timeout in milliseconds. May be 0 if we don't wait (poll), or negative for infinite wait
*/
public Event doWaitTimeout(long timeout) {
Event e = null;
long towait = 0;
long endtime = -1;
boolean usetimeout = (timeout > 0);
if (usetimeout) {
endtime = System.nanoTime() + timeout * 1000 * 1000; // Don't use System.currentTimeMillis, it fails on timezone change etc.
}
if (networkThread.getDone()) // dispatch loop has finished
return null;
while(e == null){
if (usetimeout) {
towait = (endtime - System.nanoTime());
if (towait <= 0) {
break;
}
} else {
if (timeout == 0) { // return instantly
return latestEvents.poll();
}
}
try{
if (usetimeout) {
e = latestEvents.poll(towait, TimeUnit.NANOSECONDS);
} else {
e = latestEvents.take(); // wait indefinitely
}
} catch(InterruptedException e1){
}
if (e != null && e.t == EType.KILLSENDER) { // We need the possibility to cancel a get_ts in case we had a put_ts error.
return null;
}
if (networkThread.getDone()) // dispatch loop has finished
return null;
}
return e;
}
public void put_ts(Event e) {
if (!networkThread.getDone())
networkThread.put(e);
}
/**
* Wait for the created thread to finish initializing, so that we know
* if we want to throw an exception.
* TODO: This is not the android-way of doing things, but it fits better into the existing codebase.
*/
private void waitForThreadInit(long timeout) {
synchronized (networkThread.exceptionMonitorObject) {
long endtime = System.nanoTime() + timeout * 1000 * 1000; // Don't use System.currentTimeMillis, it fails on timezone change etc.
long towait = timeout; // wait 2 seconds max
while (!networkThread.socketThreadInitialized) {
try {
networkThread.exceptionMonitorObject.wait(towait);
} catch (InterruptedException e) {
// don't care really
}
towait = (endtime - System.nanoTime()) / (1000 * 1000);
if (towait <= 0) {
break;
}
}
}
}
/**
* Initialize this as server.
*
* This starts the network thread, which does the initialization
* of the socket and starts listening.
* This also waits until the network thread is done initializing and
* throws an error. TODO: This could block if the socket initialization
* takes long (does that happen?)
* @param parent In the example it's the VDomServer object
* @param port
* @throws IOException
*/
public Comms(EventHandler parent, int port) throws IOException {
this.parent = parent;
this.isServer = true;
this.port = port;
parent.debug("Creating server");
networkThread = new SocketThread();
new Thread(networkThread).start();
waitForThreadInit(2000);
if (networkThread.thrownIOException != null) { throw networkThread.thrownIOException; }
}
/**
* Initialize as client
*
* This starts the network thread, which does the initialization
* of the socket and connects.
* TODO: Do callers rely on the socket being connected as soon
* as this returns?
* @param parent (GameActivity)
* @param host
* @param port
*/
public Comms(EventHandler parent, String host, int port) throws StreamCorruptedException, IOException, UnknownHostException {
this.parent = parent;
this.isServer = false;
this.host = host;
this.port = port;
parent.debug("Creating client");
networkThread = new SocketThread();
new Thread(networkThread).start();
waitForThreadInit(2000);
if (networkThread.thrownUnknownHostException != null ) {throw networkThread.thrownUnknownHostException; }
if (networkThread.thrownStreamCorruptedException != null) {throw networkThread.thrownStreamCorruptedException; }
if (networkThread.thrownIOException != null) { throw networkThread.thrownIOException; }
}
public String getHost() {
return host;
}
public int getPort() {
return port;
}
private class SocketThread implements Runnable {
private ServerSocket pserver = null;
private ObjectInputStream ois = null;
private ObjectOutputStream oos = null;
private volatile boolean done = false; // this is true if and only if the dispatchLoop is working
/**
* Exception can be UnknownHostException, IOException, or StreamCorruptedException
*/
public IOException thrownIOException = null; // this is set to something in case creating the server fails
public UnknownHostException thrownUnknownHostException = null;
public StreamCorruptedException thrownStreamCorruptedException = null;
public boolean socketThreadInitialized = false;
private MonitorObject exceptionMonitorObject = new MonitorObject();
private SendingThread sendingThread = null;
/**
* Wake up threads waiting for us to receive something with get or doWait
* @param e the event we received
*/
private void doNotify(Event e){
latestEvents.offer(e);
}
/**
* doWait will return NULL once
*/
public void injectNullReceived() {
doNotify(new Event(EType.KILLSENDER));
}
/**
* Set 'done' to true and wake up threads waiting for us to receive something.
*/
public void setDoneTrue() {
done = true;
injectNullReceived();
}
public boolean getDone() {
return done;
}
/**
* Check if we are currently connected
* @return true if connected, false otherwise
*/
private boolean isConnected() {
return (pclient == null ? false : pclient.isConnected());
}
private void CreateServer() throws IOException {
debug("Opening server socket...");
pserver = new ServerSocket(port);
host = pserver.getInetAddress().getHostAddress();
debug("Opened: " + host + " / " + port);
}
public SocketThread()
{
}
private void debug(String s) {
s = host + ":" + port + " -- " + s;
// System.err.println (":: Androminion :: " + s);
if (DEBUGGING)
parent.debug(s);
}
/**
* This function blocks until it received something from the network.
* This function should only be called from the network thread!
* @return the received object, casted to an Event
* @throws IOException
*/
private Event get() throws IOException {
Event p = null;
try {
// debug("Trying to get...");
p = (Event) ois.readObject();
debug("Got: " + p.toString());
} catch (OptionalDataException e) {
debug("OptionalDataException in Comms.get() -- ignoring.");
} catch (ClassNotFoundException e) {
debug("ClassNotFoundException in Comms.get() -- ignoring.");
} catch (NullPointerException e) {
debug("NullPointerException in Comms.get() -- ignoring.");
}
return p;
}
/**
* This function assumes that done is set to true.
* @return
*/
private boolean disconnect() {
if (!done) {
debug("Comms::SocketThread: 'disconnect' executed, but 'done' is not true.");
}
boolean clean = true;
debug("Shutting down...");
debug("Waiting for sendqueue to drain");
put(new Event(EType.KILLSENDER));
while (sendingThread != null && sendingThread.isAlive()) {
try {
sendingThread.join();
} catch (InterruptedException e) {
// go on waiting
}
}
try {
// close I/O streams
pclient.shutdownInput();
pclient.shutdownOutput();
debug("Streams shutdown.");
} catch (Exception e) {
clean = false;
}
try {
oos.close();
ois.close();
debug("Streams closed.");
} catch (Exception e) {
clean = false;
}
try {
// close socket connection
pclient.close();
debug("Socket closed.");
} catch (Exception e) {
clean = false;
}
if (isServer) {
// close server
// debug ("Stopping server...");
try {
pserver.close();
debug("Server stopped");
} catch (Exception e) {
clean = false;
}
}
ois = null;
oos = null;
pclient = null;
pserver = null;
debug(clean? "Disconnected cleanly" : "No clean disconnect. Apparently was already partially disconnected.");
return clean;
}
private LinkedBlockingQueue<Event> toSendQueue = new LinkedBlockingQueue<Event>();
private class SendingThread extends Thread {
@Override
public void run() {
Event toSend;
while (true) {
try {
toSend = toSendQueue.take();
} catch (InterruptedException e) {
continue;
}
debug("sending event " + toSend.toString());
if (toSend.t == EType.KILLSENDER) {
break;
}
try {
oos.writeObject(toSend);
} catch (IOException e) {
if (!done) { // done is volatile, so this works
parent.sendErrorHandler(e);
}
}
} // while (true)
debug("sending thread dying");
}
}
public void put(Event p) {
debug("Put: " + p.toString());
if (!toSendQueue.offer(p)) {
debug("Send Queue is full. Since the capacity of the queue is MAX_VALUE, you will not see this.");
System.exit(1); // TODO: let the user know in some nicer way that something is seriously broken
}
}
private Event ping() {
// the try-block is not necessary; if there is an error, we will notice
// since we aren't receiving anything.
// try {
put(new Event(Event.EType.PING));
// } catch (Exception e) {
// debug("Exception in Comms.ping() while sending -- quitting.");
// e.printStackTrace();
// return true;
// }
Event p;
try {
p = get(); // This is legal, we are in the receiving thread
} catch (SocketTimeoutException e) {
debug("Timed out in Comms.ping() -- quitting.");
return null;
} catch (Exception e) {
debug("Exception in Comms.ping() while recving -- quitting.");
e.printStackTrace();
return null;
}
if (p == null) {
debug("Invalid packet in Comms.ping() -- quitting.");
}
return p;
}
private void connect() throws UnknownHostException, IOException, StreamCorruptedException {
if (isConnected())
return;
// open a socket connection
if (isServer)
pclient = pserver.accept();
else
pclient = new Socket(host, port);
// Set read timeout, double for servers than for clients
pclient.setSoTimeout(TIMEOUT * (isServer ? 2 : 1));
// open I/O streams for objects
oos = new ObjectOutputStream(pclient.getOutputStream());
ois = new ObjectInputStream(pclient.getInputStream());
toSendQueue.clear();
sendingThread = new SendingThread();
sendingThread.start();
}
@Override
public void run() {
synchronized (exceptionMonitorObject) {
try {
if (isServer) {
CreateServer();
} else {
try {
connect();
} catch (UnknownHostException e) {
thrownUnknownHostException = e;
} catch (StreamCorruptedException e) {
thrownStreamCorruptedException = e;
}
}
} catch (IOException e) {
thrownIOException = e;
} finally {
socketThreadInitialized = true;
exceptionMonitorObject.notify();
}
}
dispatchLoop();
}
private void dispatchLoop() {
done = false;
if (!isConnected()) {
if (isServer) {
try {
connect();
} catch (Exception e) {
debug("Failed to connect in run: " + e.getMessage());
setDoneTrue();
return;
}
} else {
setDoneTrue();
return;
}
}
boolean timeout, disconnect = false;
Event p = null;
while (!done) {
timeout = false;
if (p == null) { // if p is not null, we handle p without receiving
try {
p = get();
} catch (SocketTimeoutException e) {
debug("Connection timed out...");
timeout = true;
} catch (EOFException e) {
debug("Socket externally closed in Comms.run() -- quitting.");
disconnect = true;
} catch (Exception e) {
debug("Other exception in Comms.run() -- quitting.");
e.printStackTrace();
disconnect = true;
}
}
if (done) break;
if (p != null && p.t == EType.SLEEP) {
try { Thread.sleep(p.i); }
catch (InterruptedException e) {}
p = null;
continue;
}
if ((p != null) && (p.t == Event.EType.PING))
try {
put(new Event(Event.EType.PONG));
} catch (Exception e) {
debug("Could not pong in Comms.run() -- quitting.");
e.printStackTrace();
disconnect = true;
}
else if ((p != null) && (p.t == Event.EType.GETSERVER))
try {
put(new Event(Event.EType.SERVER).setString(host).setInteger(port));
} catch (Exception e) {
debug("Could not pong server in Comms.run() -- quitting.");
e.printStackTrace();
disconnect = true;
}
else if (p != null) {
if (!parent.handle(p))
doNotify(p);
}
if (timeout) { // If we timed out:
p = ping(); // send a ping.
if (p == null) { // if ping unsuccessful: disconnect
disconnect = true;
} else {
if (p.t == EType.PONG) { // if it is successful and we received PONG: delete p
p = null; // OTHERWISE: keep p for next round! This solves a race condition
}
}
} else {
p = null;
}
if (disconnect) {
p = new Event(Event.EType.DISCONNECT);
if (!parent.handle(p))
doNotify(p);
setDoneTrue();
}
}
disconnect();
debug("End of Comms.run()");
}
}
public boolean stop() {
networkThread.setDoneTrue();
return networkThread.disconnect();
}
public void injectNullReceived() {
networkThread.injectNullReceived();
}
}