/* * 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.networking; import java.io.IOException; import java.util.Collections; import java.util.HashMap; import java.util.Map; import java.util.Map.Entry; import java.util.Random; import org.apache.log4j.Logger; import com.t3.client.TabletopTool; import com.t3.clientserver.connection.ClientConnection; import com.t3.clientserver.connection.ServerObserver; import com.t3.common.T3Constants; import com.t3.model.campaign.Campaign; import com.t3.networking.registry.T3Registry; import com.t3.transfer.AssetChunk; import com.t3.transfer.AssetProducer; import com.t3.transfer.AssetTransferManager; /** * @author drice */ public class T3Server { private static final Logger log = Logger.getLogger(T3Server.class); private static final int ASSET_CHUNK_SIZE = 5 * 1024; private final T3ServerConnection conn; private final ServerMethodHandler handler; private final ServerConfig config; private final Map<String, AssetTransferManager> assetManagerMap = Collections.synchronizedMap(new HashMap<String, AssetTransferManager>()); private final Map<String, ClientConnection> connectionMap = Collections.synchronizedMap(new HashMap<String, ClientConnection>()); private final AssetProducerThread assetProducerThread; private Campaign campaign; private ServerPolicy policy; private HeartbeatThread heartbeatThread; public T3Server(ServerConfig config, ServerPolicy policy) throws IOException { handler = new ServerMethodHandler(this); conn = new T3ServerConnection(this, config.getPort()); conn.addMessageHandler(handler); campaign = new Campaign(); assetProducerThread = new AssetProducerThread(); assetProducerThread.start(); this.config = config; this.policy = policy; // Start a heartbeat if requested if (config.isServerRegistered()) { heartbeatThread = new HeartbeatThread(); heartbeatThread.start(); } } public void configureClientConnection(ClientConnection connection) { String id = connection.getId(); assetManagerMap.put(id, new AssetTransferManager()); connectionMap.put(id, connection); } public ClientConnection getClientConnection(String id) { return connectionMap.get(id); } public String getConnectionId(String playerId) { return conn.getConnectionId(playerId); } /** * Forceably disconnects a client and cleans up references to it * * @param id * the connection ID */ public void releaseClientConnection(String id) { ClientConnection connection = getClientConnection(id); if (connection != null) { try { connection.close(); } catch (IOException e) { log.error("Could not release connection: " + id, e); } } assetManagerMap.remove(id); connectionMap.remove(id); } public void addAssetProducer(String connectionId, AssetProducer producer) { AssetTransferManager manager = assetManagerMap.get(connectionId); manager.addProducer(producer); } public void addObserver(ServerObserver observer) { if (observer != null) { conn.addObserver(observer); } } public void removeObserver(ServerObserver observer) { conn.removeObserver(observer); } public boolean isHostId(String playerId) { return config.getHostPlayerId() != null && config.getHostPlayerId().equals(playerId); } public T3ServerConnection getConnection() { return conn; } public boolean isPlayerConnected(String id) { return conn.getPlayer(id) != null; } public void setCampaign(Campaign campaign) { // Don't allow null campaigns, but allow the campaign to be cleared out if (campaign == null) { campaign = new Campaign(); } this.campaign = campaign; } public Campaign getCampaign() { return campaign; } public ServerPolicy getPolicy() { return policy; } public void updateServerPolicy(ServerPolicy policy) { this.policy = policy; } public ServerMethodHandler getMethodHandler() { return handler; } public ServerConfig getConfig() { return config; } public void stop() { try { conn.close(); if (heartbeatThread != null) { heartbeatThread.shutdown(); } if (assetProducerThread != null) { assetProducerThread.shutdown(); } } catch (IOException e) { // Not too concerned about this e.printStackTrace(); } } private static final Random random = new Random(); private class HeartbeatThread extends Thread { private boolean stop = false; private static final int HEARTBEAT_DELAY = 80 * 1000; // 80-100 seconds (server expects at least 120s) private static final int HEARTBEAT_FLUX = 20 * 1000; // 20 seconds @Override public void run() { while (!stop) { try { Thread.sleep(HEARTBEAT_DELAY + random.nextInt(HEARTBEAT_FLUX)); } catch (InterruptedException ie) { // This means stop break; } // Pulse if(!T3Registry.heartBeat(config.getPort())) { //if server is no longer registered (temporary disconnected?) TabletopTool.showError("Your server disconnected from T³. It can no longer be found."); //TODO try to reconnect first! } } } public void shutdown() { stop = true; interrupt(); } } //// // CLASSES private class AssetProducerThread extends Thread { private boolean stop = false; @Override public void run() { while (!stop) { try { boolean lookForMore = false; for (Entry<String, AssetTransferManager> entry : assetManagerMap.entrySet()) { AssetChunk chunk = entry.getValue().nextChunk(ASSET_CHUNK_SIZE); if (chunk != null) { lookForMore = true; getConnection().callMethod(entry.getKey(), T3Constants.Channel.IMAGE, NetworkCommand.updateAssetTransfer, chunk); } } if (lookForMore) { continue; } // Sleep for a bit synchronized (this) { Thread.sleep(500); } } catch (Exception e) { e.printStackTrace(); // keep on going } } } public void shutdown() { stop = true; } } //// // STANDALONE SERVER public static void main(String[] args) throws IOException { // This starts the server thread. new T3Server(new ServerConfig(), new ServerPolicy()); } }