package apps.frontend; import java.io.IOException; import java.net.ServerSocket; import java.net.Socket; import java.util.HashMap; import java.util.HashSet; import java.util.Map; import java.util.Set; import player.request.factory.RequestFactory; import player.request.grammar.AbortRequest; import player.request.grammar.PingRequest; import player.request.grammar.Request; import player.request.grammar.StartRequest; import player.request.grammar.StopRequest; import util.http.HttpReader; import util.http.HttpWriter; import util.logging.GamerLogger; import util.match.Match; /** * Frontend is an application that allows a number of gamer backends * to all play games from a single "frontend" address. Since each match * has a single unique ID associated with it, this frontend server can * route requests from the game server to the backend server associated * with that particular match. * * TODO: This is a very naive implementation. It assumes that the backend * servers will never be busy with anything other than games served by this * frontend, and that they'll never crash or go into bad states. Furthermore, * it assumes that games always end with a STOP request, which won't be the * case in a web-based system if the user just navigates away from a page that * is serving a game. * * @author Sam Schreiber */ public final class Frontend extends Thread { /** * Backend encapsulates all of the information that we need to * uniquely identify a backend server, and determine whether we've * already allocated a game to it. * * @author Sam */ class Backend { public int port; public String ip; public boolean active; public Backend(String ip, int port) { this.ip = ip; this.port = port; this.active = false; } } /** * FrontendStatusReporter is a thread that periodically reports * how many frontends are connected to the backend. This information * is written to the standard output, and also logged in a logfile. * * @author Sam */ class FrontendStatusReporter extends Thread { @Override public void run() { while (!isInterrupted()) { try { sleep(10000); int nBusy = 0; for(Backend b : theBackends) if(b.active) nBusy++; GamerLogger.log("Frontend", "Current status: " + nBusy + "/" + theBackends.size() + " backends busy."); } catch(Exception e) { GamerLogger.logStackTrace("Frontend", e); } } } } /** * BackendRegistrationThread is a thread that waits for backend servers * to register themselves with the frontend. Backend servers that want * this frontend to serve them games should register with this frontend * by sending a port number in an HTTP request to the registration port * on the frontend. The frontend will reply with "REGISTERED", and will * add that backend to the list of active backends. * * @author Sam */ class BackendRegistrationThread extends Thread { @Override public void run() { while (!isInterrupted()) { try { Socket connection = backendRegistration.accept(); String in = HttpReader.readAsServer(connection); String ip = connection.getInetAddress().getHostAddress(); GamerLogger.log("Frontend", "[Received at " + System.currentTimeMillis() + "] Registration: " + ip + " : " + in); try { int p = Integer.parseInt(in); theBackends.add(new Backend(ip, p)); writeResponse(connection, "REGISTERED"); } catch(NumberFormatException e) { GamerLogger.logStackTrace("Frontend", e); writeResponse(connection, "FAILURE"); } } catch (Exception e) { GamerLogger.logStackTrace("Frontend", e); } } } } // All of the information for tracking backends and match // backends to the games that they're playing. private Set<Backend> theBackends = new HashSet<Backend>(); private Map<String, Backend> matchToBackendMap = new HashMap<String, Backend>(); // Server socket for listening for connections from the real // game server, so they can be sent to backends. private ServerSocket backendRegistration; private ServerSocket listener; public Frontend(int port) { listener = null; while(listener == null) { try { listener = new ServerSocket(port); } catch (IOException ex) { listener = null; port++; System.err.println("Failed to start frontend on port: " + (port-1) + " trying port " + port); } } // We absolutely require that the registration port be REGISTRATION_PORT, // because the backend servers will depend on that. try { backendRegistration = new ServerSocket(REGISTRATION_PORT); } catch (IOException ex) { backendRegistration = null; System.err.println("Failed to start backend registration on port: " + REGISTRATION_PORT); System.exit(1); } } private Backend findAvailableBackend() { for(Backend b : theBackends) { if(!b.active) return b; } return null; } @Override public void run() { new FrontendStatusReporter().start(); new BackendRegistrationThread().start(); while (!isInterrupted()) { try { Socket connection = listener.accept(); String in = HttpReader.readAsServer(connection); GamerLogger.log("Frontend", "[Received at " + System.currentTimeMillis() + "] " + in, GamerLogger.LOG_LEVEL_DATA_DUMP); Request request = new RequestFactory().create(null, in); String matchId = request.getMatchId(); if (request instanceof StartRequest) { Backend b = findAvailableBackend(); if (b == null) { // If there are no available backends, // respond with "busy". writeResponse(connection, "busy"); continue; } // Start this match on a new backend, assuming that // we've found one that is available. b.active = true; matchToBackendMap.put(matchId, b); } if (request instanceof PingRequest) { if(findAvailableBackend() == null) { writeResponse(connection, "busy"); } else { writeResponse(connection, "available"); } continue; } Backend b = matchToBackendMap.get(matchId); if(b == null) { writeResponse(connection, "busy"); continue; } // If we've reached this point, we know that we have a request // "in" that needs to be served to the backend "b", which has // been marked active (so it's now dedicated to this match). // Start a separate thread to open a connection to the backend // and wait for the response, and pass it back to the actual // server as soon as the backend responds. RequestHandler handler = new RequestHandler(connection, in, b, request instanceof StopRequest || request instanceof AbortRequest); handler.start(); } catch (Exception e) { GamerLogger.logStackTrace("Frontend", e); } } } private static void writeResponse(Socket connection, String out) throws Exception { HttpWriter.writeAsServer(connection, out); connection.close(); GamerLogger.log("Frontend", "[Sent at " + System.currentTimeMillis() + "] " + out, GamerLogger.LOG_LEVEL_DATA_DUMP); } public static void main(String[] args) { Match fakeMatch = new Match("Frontend." + System.currentTimeMillis(), 0, 0, null); GamerLogger.startFileLogging(fakeMatch, ""); GamerLogger.setFileToDisplay("Frontend"); Frontend theFrontend = new Frontend(9147); theFrontend.run(); } // Calling this method will register yourself with the frontend at // "frontendAddress", indicating that you have a gamer available on // the port "myPort". public static final int REGISTRATION_PORT = 11111; public static void registerWithFrontend(String frontendAddress, int myPort, boolean orDie) { try { Socket connection = new Socket(frontendAddress, REGISTRATION_PORT); HttpWriter.writeAsClient(connection, "", "" + myPort, "Backend"); String status = HttpReader.readAsClient(connection); if (status.equals("REGISTERED")) { GamerLogger.log("GamePlayer", "Registered successfully with frontend server."); } else { GamerLogger.log("GamePlayer", "Failure to register with frontend server: " + status); if(orDie) System.exit(1); } connection.close(); } catch(Exception e) { GamerLogger.logStackTrace("GamePlayer", e); if(orDie) System.exit(1); } } }