package be.nikiroo.utils.serial.server; import java.io.IOException; import java.net.ServerSocket; import java.net.Socket; import java.net.UnknownHostException; import be.nikiroo.utils.TraceHandler; /** * This class implements a simple server that can listen for connections and * send/receive objects. *

* Note: this {@link Server} has to be discarded after use (cannot be started * twice). * * @author niki */ abstract class Server implements Runnable { protected final String key; protected long id = 0; private final String name; private final Object lock = new Object(); private final Object counterLock = new Object(); private ServerSocket ss; private int port; private boolean started; private boolean exiting = false; private int counter; private long bytesReceived; private long bytesSent; private TraceHandler tracer = new TraceHandler(); /** * Create a new {@link ConnectActionServer} to handle a request. * * @param s * the socket to service * * @return the action */ abstract ConnectActionServer createConnectActionServer(Socket s); /** * Create a new server that will start listening on the network when * {@link Server#start()} is called. * * @param port * the port to listen on, or 0 to assign any unallocated port * found (which can later on be queried via * {@link Server#getPort()} * @param key * an optional key to encrypt all the communications (if NULL, * everything will be sent in clear text) * * @throws IOException * in case of I/O error * @throws UnknownHostException * if the IP address of the host could not be determined * @throws IllegalArgumentException * if the port parameter is outside the specified range of valid * port values, which is between 0 and 65535, inclusive */ public Server(int port, String key) throws IOException { this((String) null, port, key); } /** * Create a new server that will start listening on the network when * {@link Server#start()} is called. *

* All the communications will happen in plain text. * * @param name * the server name (only used for debug info and traces) * @param port * the port to listen on * * @throws IOException * in case of I/O error * @throws UnknownHostException * if the IP address of the host could not be determined * @throws IllegalArgumentException * if the port parameter is outside the specified range of valid * port values, which is between 0 and 65535, inclusive */ public Server(String name, int port) throws IOException { this(name, port, null); } /** * Create a new server that will start listening on the network when * {@link Server#start()} is called. * * @param name * the server name (only used for debug info and traces) * @param port * the port to listen on * @param key * an optional key to encrypt all the communications (if NULL, * everything will be sent in clear text) * * @throws IOException * in case of I/O error * @throws UnknownHostException * if the IP address of the host could not be determined * @throws IllegalArgumentException * if the port parameter is outside the specified range of valid * port values, which is between 0 and 65535, inclusive */ public Server(String name, int port, String key) throws IOException { this.name = name; this.port = port; this.key = key; this.ss = new ServerSocket(port); if (this.port == 0) { this.port = this.ss.getLocalPort(); } } /** * The traces handler for this {@link Server}. * * @return the traces handler */ public TraceHandler getTraceHandler() { return tracer; } /** * The traces handler for this {@link Server}. * * @param tracer * the new traces handler */ public void setTraceHandler(TraceHandler tracer) { if (tracer == null) { tracer = new TraceHandler(false, false, false); } this.tracer = tracer; } /** * The name of this {@link Server} if any. *

* Used for traces and debug purposes only. * * @return the name or NULL */ public String getName() { return name; } /** * Return the assigned port. * * @return the assigned port */ public int getPort() { return port; } /** * The total amount of bytes received. * * @return the amount of bytes received */ public long getBytesReceived() { return bytesReceived; } /** * The total amount of bytes sent. * * @return the amount of bytes sent */ public long getBytesSent() { return bytesSent; } /** * Start the server (listen on the network for new connections). *

* Can only be called once. *

* This call is asynchronous, and will just start a new {@link Thread} on * itself (see {@link Server#run()}). */ public void start() { new Thread(this).start(); } /** * Start the server (listen on the network for new connections). *

* Can only be called once. *

* You may call it via {@link Server#start()} for an asynchronous call, too. */ @Override public void run() { ServerSocket ss = null; boolean alreadyStarted = false; synchronized (lock) { ss = this.ss; if (!started && ss != null) { started = true; } else { alreadyStarted = started; } } if (alreadyStarted) { tracer.error(name + ": cannot start server on port " + port + ", it is already started"); return; } if (ss == null) { tracer.error(name + ": cannot start server on port " + port + ", it has already been used"); return; } try { tracer.trace(name + ": server starting on port " + port + " (" + (key != null ? "encrypted" : "plain text") + ")"); while (started && !exiting) { count(1); final Socket s = ss.accept(); new Thread(new Runnable() { @Override public void run() { ConnectActionServer action = null; try { action = createConnectActionServer(s); action.connect(); } finally { count(-1); if (action != null) { bytesReceived += action.getBytesReceived(); bytesSent += action.getBytesSent(); } } } }).start(); } // Will be covered by @link{Server#stop(long)} for timeouts while (counter > 0) { Thread.sleep(10); } } catch (Exception e) { if (counter > 0) { onError(e); } } finally { try { ss.close(); } catch (Exception e) { onError(e); } this.ss = null; started = false; exiting = false; counter = 0; tracer.trace(name + ": client terminated on port " + port); } } /** * Will stop the server, synchronously and without a timeout. */ public void stop() { tracer.trace(name + ": stopping server"); stop(0, true); } /** * Stop the server. * * @param timeout * the maximum timeout to wait for existing actions to complete, * or 0 for "no timeout" * @param wait * wait for the server to be stopped before returning * (synchronous) or not (asynchronous) */ public void stop(final long timeout, final boolean wait) { if (wait) { stop(timeout); } else { new Thread(new Runnable() { @Override public void run() { stop(timeout); } }).start(); } } /** * Stop the server (synchronous). * * @param timeout * the maximum timeout to wait for existing actions to complete, * or 0 for "no timeout" */ private void stop(long timeout) { tracer.trace(name + ": server stopping on port " + port); synchronized (lock) { if (started && !exiting) { exiting = true; try { getConnectionToMe().connect(); long time = 0; while (ss != null && timeout > 0 && timeout > time) { Thread.sleep(10); time += 10; } } catch (Exception e) { if (ss != null) { counter = 0; // will stop the main thread onError(e); } } } } // return only when stopped while (started || exiting) { try { Thread.sleep(10); } catch (InterruptedException e) { } } } /** * Return a connection to this server (used by the Exit code to send an exit * message). * * @return the connection * * @throws UnknownHostException * the host should always be NULL (localhost) * @throws IOException * in case of I/O error */ abstract protected ConnectActionClient getConnectionToMe() throws UnknownHostException, IOException; /** * Change the number of currently serviced actions. * * @param change * the number to increase or decrease * * @return the current number after this operation */ private int count(int change) { synchronized (counterLock) { counter += change; return counter; } } /** * This method will be called on errors. *

* By default, it will only call the trace handler (so you may want to call * super {@link Server#onError} if you override it). * * @param e * the error */ protected void onError(Exception e) { tracer.error(e); } /** * Return the next ID to use. * * @return the next ID */ protected synchronized long getNextId() { return id++; } /** * Method called when * {@link ServerObject#onRequest(ConnectActionServerObject, Object, long)} * has successfully finished. *

* Can be used to know how much data was transmitted. * * @param id * the ID used to identify the request * @param bytesReceived * the bytes received during the request * @param bytesSent * the bytes sent during the request */ @SuppressWarnings("unused") protected void onRequestDone(long id, long bytesReceived, long bytesSent) { } }