/* * Copyright (c) 2014 tabletoptool.com team. * All rights reserved. This program and the accompanying materials * are made available under the terms of the GNU Public License v3.0 * which accompanies this distribution, and is available at * http://www.gnu.org/licenses/gpl.html * * Contributors: * rptools.com team - initial implementation * tabletoptool.com team - further development */ package com.t3.clientserver.connection; import java.io.IOException; import java.net.ServerSocket; import java.net.Socket; import java.util.ArrayList; import java.util.Collections; import java.util.HashMap; import java.util.HashSet; import java.util.Iterator; import java.util.List; import java.util.Map; import java.util.Set; import org.apache.log4j.Logger; import com.t3.clientserver.Command; import com.t3.clientserver.NetworkSerializer; import com.t3.clientserver.handler.DisconnectHandler; import com.t3.clientserver.handler.MessageHandler; /** * @author drice */ public class ServerConnection extends AbstractConnection implements MessageHandler, DisconnectHandler { private static final Logger log = Logger.getLogger(ServerConnection.class); private final ServerSocket socket; private final ListeningThread listeningThread; private final DispatchThread dispatchThread; // private final ReaperThread reaperThread; private Map<String, ClientConnection> clients = Collections.synchronizedMap(new HashMap<String, ClientConnection>()); private List<ServerObserver> observerList = Collections.synchronizedList(new ArrayList<ServerObserver>()); public ServerConnection(int port) throws IOException { socket = new ServerSocket(port); dispatchThread = new DispatchThread(this); dispatchThread.start(); listeningThread = new ListeningThread(this, socket); listeningThread.start(); } public void addObserver(ServerObserver observer) { observerList.add(observer); } public void removeObserver(ServerObserver observer) { observerList.remove(observer); } @Override public void handleMessage(String id, byte[] message) { dispatchMessage(id, message); } public void broadcastMessage(byte[] message) { synchronized (clients) { for (ClientConnection conn : clients.values()) { conn.sendMessage(message); } } } public void broadcastMessage(String[] exclude, byte[] message) { Set<String> excludeSet = new HashSet<String>(); for (String e : exclude) { excludeSet.add(e); } synchronized (clients) { for (Map.Entry<String, ClientConnection> entry : clients.entrySet()) { if (!excludeSet.contains(entry.getKey())) { entry.getValue().sendMessage(message); } } } } public void sendMessage(String id, byte[] message) { sendMessage(id, null, message); } public void sendMessage(String id, Object channel, byte[] message) { ClientConnection client = clients.get(id); client.sendMessage(channel, message); } /** * Server subclasses may override this method to perform serial handshaking * before the connection is accepted into its pool. By default, this just * returns true. * @param conn * @return true if the connection should be added to the pool */ public boolean handleConnectionHandshake(String id, Socket socket) { return true; } public void close() throws IOException { listeningThread.suppressErrors(); log.debug("Server closing down"); socket.close(); synchronized (clients) { for (ClientConnection conn : clients.values()) { conn.close(); } } listeningThread.requestStop(); log.debug("Server stopping listening thread"); try { listeningThread.join(); } catch (InterruptedException e) { log.error(e.getMessage(), e); } // reaperThread.requestStop(); // try { // reaperThread.join(); // } catch (InterruptedException e) { // log.error(e.getMessage(), e); // } } private void reapClients() { log.debug("About to reap clients"); synchronized (clients) { log.debug("Reaping clients"); for (Iterator<Map.Entry<String, ClientConnection>> i = clients.entrySet().iterator(); i.hasNext(); ) { Map.Entry<String, ClientConnection> entry = i.next(); ClientConnection conn = entry.getValue(); if (!conn.isAlive()) { log.debug("\tReaping: " + conn.getId()); try { i.remove(); fireClientDisconnect(conn); conn.close(); } catch (IOException e) { log.error(e.getMessage(), e); } } } } } private void fireClientConnect(ClientConnection conn) { log.debug("Firing: clientConnect: " + conn.getId()); for (ServerObserver observer : observerList) { observer.connectionAdded(conn); } } private void fireClientDisconnect(ClientConnection conn) { log.debug("Firing: clientDisconnect: " + conn.getId()); for (ServerObserver observer : observerList) { observer.connectionRemoved(conn); } } //// // DISCONNECT HANDLER @Override public void handleDisconnect(AbstractConnection conn) { if (conn instanceof ClientConnection) { log.debug("HandleDisconnect: " + ((ClientConnection)conn).getId()); fireClientDisconnect((ClientConnection) conn); } } //// // Threads private static class ListeningThread extends Thread { private final ServerConnection server; private final ServerSocket socket; private boolean stopRequested = false; private boolean suppressErrors = false; private int nextConnectionId = 0; private synchronized String nextClientId(Socket socket) { return socket.getInetAddress().getHostAddress() + "-" + (nextConnectionId++); } public ListeningThread(ServerConnection server, ServerSocket socket) { this.server = server; this.socket = socket; } public void requestStop() { stopRequested = true; } public void suppressErrors() { suppressErrors = true; } @Override public void run() { while (!stopRequested) { try { Socket s = socket.accept(); log.debug("Client connecting ..."); String id = nextClientId(s); // Make sure the client is allowed if (!server.handleConnectionHandshake(id, s)) { log.debug("Client closing: bad handshake"); s.close(); continue; } ClientConnection conn = new ClientConnection(s, id); conn.addMessageHandler(server); conn.addDisconnectHandler(server); conn.start(); log.debug("About to add new client"); synchronized (server.clients) { server.reapClients(); log.debug("Adding new client"); server.clients.put(conn.getId(), conn); server.fireClientConnect(conn); //System.out.println("new client " + conn.getId() + " added, " + server.clients.size() + " total"); } } catch (IOException e) { if (!suppressErrors) { log.error(e.getMessage(), e); } } } } } private static class DispatchThread extends Thread implements MessageHandler { private final ServerConnection server; private List<Message> queue = Collections.synchronizedList(new ArrayList<Message>()); private boolean stopRequested = false; public DispatchThread(ServerConnection server) { super("Dispatcher Thread"); this.server = server; } public void requestStop() { stopRequested = true; } @Override public void handleMessage(String id, byte[] message) { queue.add(new Message(id, message)); synchronized (this) { this.notify(); } } @Override public void run() { while (!stopRequested) { while (queue.size() > 0) { Message msg = queue.remove(0); try { if (log.isDebugEnabled()) { log.debug("Server handling: " + msg.id); } server.handleMessage(msg.id, msg.message); } catch (Throwable t) { // Don't let anything kill this thread log.error(t.getMessage(), t); } } synchronized (this) { try { this.wait(); } catch (InterruptedException e) { log.error(e.getMessage(), e); } } } } } private static class Message { final String id; final byte[] message; public Message(String id, byte[] message) { this.id = id; this.message = message; } } public void broadcastCallMethod(Enum<? extends Command> method, Object... parameters) { broadcastMessage(NetworkSerializer.serialize(method, parameters)); } public void broadcastCallMethod(String[] exclude, Enum<? extends Command> method, Object... parameters) { byte[] data = NetworkSerializer.serialize(method, parameters); broadcastMessage(exclude, data); } public void callMethod(String id, Enum<? extends Command> method, Object... parameters) { byte[] data = NetworkSerializer.serialize(method, parameters); sendMessage(id, null, data); } public void callMethod(String id, Object channel, Enum<? extends Command> method, Object... parameters) { byte[] data = NetworkSerializer.serialize(method, parameters); sendMessage(id, channel, data); } }