All Downloads are FREE. Search and download functionalities are using the official Maven repository.

org.cp.elements.tools.net.EchoServer Maven / Gradle / Ivy

/*
 * Copyright 2011-Present Author or Authors.
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *     http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */
package org.cp.elements.tools.net;

import static org.cp.elements.lang.LangExtensions.assertThat;
import static org.cp.elements.lang.RuntimeExceptionsFactory.newIllegalArgumentException;
import static org.cp.elements.net.NetworkUtils.close;
import static org.cp.elements.net.NetworkUtils.lenientParsePort;

import java.io.IOException;
import java.net.ServerSocket;
import java.net.Socket;
import java.util.Optional;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.TimeUnit;

import org.cp.elements.lang.Integers;
import org.cp.elements.lang.ThrowableUtils;
import org.cp.elements.lang.concurrent.ThreadUtils;
import org.cp.elements.net.ServicePort;
import org.cp.elements.tools.net.support.AbstractClientServerSupport;
import org.cp.elements.util.ArrayUtils;

/**
 * The {@link EchoServer} class is used to echo messages back to a echo client.
 *
 * @author John Blum
 * @see java.lang.Runnable
 * @see java.net.ServerSocket
 * @see java.net.Socket
 * @see java.util.concurrent.ExecutorService
 * @see java.util.concurrent.Executors
 * @see java.util.concurrent.TimeUnit
 * @see org.cp.elements.net.ServicePort
 * @see org.cp.elements.tools.net.support.AbstractClientServerSupport
 * @since 1.0.0
 */
@SuppressWarnings("unused")
public class EchoServer extends AbstractClientServerSupport implements Runnable {

  protected static final int EXECUTOR_THREAD_POOL_SIZE = 10;

  protected static final long DEFAULT_DURATION_MILLISECONDS = TimeUnit.SECONDS.toMillis(15);

  private static final int THIRTY = 30;

  /**
   * Main method used to run the {@link EchoServer} program.
   *
   * @param args array of {@link String arguments} passed into this program from the command-line.
   * @see #validateArguments(String[])
   * @see #newEchoServer(int)
   */
  public static void main(String[] args) {

    validateArguments(args);
    newEchoServer(lenientParsePort(args[0])).run();
  }

  /**
   * Validates the array of {@link String arguments} passed into the {@link EchoServer} program from the command-line.
   *
   * @param args array of {@link String arguments} passed into this program from the command-line.
   */
  private static void validateArguments(String[] args) {

    if (ArrayUtils.isEmpty(args)) {
      System.err.printf("$ java -server ... %s %n", EchoServer.class.getName());
      System.exit(1);
    }
  }

  /**
   * Factory method used to construct a new instance of the {@link EchoServer} initialized with the given {@code port}.
   *
   * @param port {@link Integer} value indicating the port number on which the {@link EchoServer} will listen.
   * @return a new instance of the {@link EchoServer} initialized on the given {@code port}.
   * @throws IllegalArgumentException if the given {@code port} number is not valid.
   * @see #EchoServer(int)
   */
  public static EchoServer newEchoServer(int port) {
    return new EchoServer(port);
  }

  private final int port;

  private ExecutorService echoService;

  private final ServerSocket serverSocket;

  /**
   * Constructs a new instance of the {@link EchoServer} listening on the given {@code port}.
   *
   * @param port {@link Integer} value indicating the port number on which the {@link EchoServer} will listen.
   * @throws IllegalArgumentException if the given {@code port} number is not valid.
   * @see #newServerSocket(int)
   */
  public EchoServer(int port) {

    assertThat(port)
      .throwing(newIllegalArgumentException("Port [%d] must be greater than 1024 and less than equal to 65535", port))
      .isGreaterThanAndLessThanEqualTo(ServicePort.MAX_RESERVED_PORT, ServicePort.MAX_PORT);

    this.port = port;
    this.serverSocket = newServerSocket(port);

    Runtime.getRuntime().addShutdownHook(new Thread(this::shutdown));
  }

  /**
   * Determine whether this {@link EchoServer} is running.
   *
   * @return a boolean value indicating whether this {@link EchoServer} is running.
   * @see #isRunning()
   */
  public boolean isNotRunning() {
    return !isRunning();
  }

  /**
   * Determine whether this {@link EchoServer} is running.
   *
   * @return a boolean value indicating whether this {@link EchoServer} is running.
   * @see #isRunning(ServerSocket)
   * @see #getServerSocket()
   */
  public boolean isRunning() {
    return isRunning(getServerSocket());
  }

  /**
   * Returns a reference to the Echo Service.
   *
   * @return a reference to the Echo Service.
   * @see java.util.concurrent.ExecutorService
   */
  protected ExecutorService getEchoService() {
    return this.echoService;
  }

  /**
   * Returns the port on which this {@link EchoServer} is listening for {@link EchoClient} connections.
   *
   * @return a {@link Integer} value indicating the port number the {@link EchoServer} is using to listen
   * for {@link EchoClient} connections.
   */
  public int getPort() {
    return this.port;
  }

  /**
   * Gets the {@link ServerSocket} used by this {@link EchoServer} to accept {@link EchoClient} connections.
   *
   * @return the {@link ServerSocket} used by this {@link EchoServer} to accept {@link EchoClient} connections.
   * @see java.net.ServerSocket
   */
  protected ServerSocket getServerSocket() {
    return this.serverSocket;
  }

  /**
   * Runs the {@link EchoServer}.
   *
   * @see #runEchoService(ServerSocket)
   * @see #getServerSocket()
   */
  @Override
  public void run() {
    getLogger().info(() -> String.format("Starting EchoServer on port [%d]...", getPort()));
    runEchoService(getServerSocket());
  }

  /**
   * Starts this {@link EchoServer} and waits up to {@literal 15 seconds} for the {@link EchoServer} to start.
   *
   * @return this {@link EchoServer}.
   * @see #runAndWaitFor(long)
   */
  public EchoServer runAndWaitFor() {
    return runAndWaitFor(DEFAULT_DURATION_MILLISECONDS);
  }

  /**
   * Starts this {@link EchoServer} and waits up to the given {@code duration} for the {@link EchoServer} to start.
   *
   * @param duration length of time in milliseconds to wait for this {@link EchoServer} to start.
   * @return this {@link EchoServer}.
   * @see #run()
   * @see #waitFor(long)
   */
  public EchoServer runAndWaitFor(long duration) {

    run();
    waitFor(duration);

    return this;
  }

  /**
   * Starts the 'echo service'.
   *
   * @param serverSocket {@link ServerSocket} used by this {@link EchoServer} to accept {@link EchoClient} connections.
   * @see java.net.ServerSocket
   */
  protected void runEchoService(ServerSocket serverSocket) {

    if (isRunning(serverSocket)) {

      this.echoService = newExecutorService();

      this.echoService.submit(() -> {
        try {
          while (isRunning(serverSocket)) {
            Socket echoClient = serverSocket.accept();

            getLogger().info(() -> String.format("EchoClient connected from [%s]",
              echoClient.getRemoteSocketAddress()));

            this.echoService.submit(() -> {
              sendResponse(echoClient, receiveMessage(echoClient));
              close(echoClient);
            });
          }
        }
        catch (IOException cause) {
          if (isRunning(serverSocket)) {
            getLogger().warning(() -> String.format("An IO error occurred while listening for EchoClients:%n%s",
              ThrowableUtils.getStackTrace(cause)));
          }
        }
      });

      getLogger().info(() -> String.format("EchoServer running on port [%d]", getPort()));
    }
  }

  /**
   * Constructs a new instance of an {@link ExecutorService} to run the Echo Service.
   *
   * @return a new instance of {@link ExecutorService} used to run the Echo Service.
   * @see java.util.concurrent.Executors#newFixedThreadPool(int)
   * @see java.util.concurrent.ExecutorService
   * @see #runEchoService(ServerSocket)
   */
  protected ExecutorService newExecutorService() {
    return Executors.newFixedThreadPool(EXECUTOR_THREAD_POOL_SIZE);
  }

  /**
   * Receives a {@link String message} from a {@link EchoClient} over the given {@link Socket}.
   *
   * @param socket {@link Socket} used to receive the {@link EchoClient EchoClient's} {@link String message}.
   * @return a {@link String} containing the message sent by the {@link EchoClient}.
   */
  @Override
  protected String receiveMessage(Socket socket) {

    try {

      String message = super.receiveMessage(socket);

      getLogger().fine(() -> String.format("Received message [%1$s] from EchoClient [%2$s]",
        message, socket.getRemoteSocketAddress()));

      return message;
    }
    catch (IOException cause) {
      getLogger().warning(() -> String.format("Failed to receive message from EchoClient [%s]",
        socket.getRemoteSocketAddress()));

      getLogger().fine(() -> ThrowableUtils.getStackTrace(cause));

      return "What?";
    }
  }

  /**
   * Sends the {@link String message} received from the Echo Client back to the Echo Client on the given {@link Socket}.
   *
   * @param socket {@link Socket} used to send the Echo Client's {@link String message} back to the Echo Client.
   * @param message {@link String} containing the message to send the Echo Client.  This is the same message
   * sent by the Echo Client and received by the Echo Server.
   * @see AbstractClientServerSupport#sendMessage(Socket, String)
   */
  protected void sendResponse(Socket socket, String message) {

    try {
      getLogger().info(() -> String.format("Sending response [%1$s] to EchoClient [%2$s]",
        message, socket.getRemoteSocketAddress()));

      sendMessage(socket, message);
    }
    catch (IOException cause) {
      getLogger().warning(() -> String.format("Failed to send response [%1$s] to EchoClient [%2$s]",
        message, socket.getRemoteSocketAddress()));

      getLogger().fine(() -> ThrowableUtils.getStackTrace(cause));
    }
  }

  /**
   * Stops this {@link EchoServer} taking it offline and out-of-service.
   */
  public void shutdown() {

    getLogger().info("Stopping EchoServer...");

    closeServerSocket();
    stopEchoService();

    getLogger().info("EchoServer stopped");
  }

  /**
   * Closes the {@link ServerSocket} used by the {@link EchoServer} to receive {@link EchoClient} connections.
   *
   * @see #getServerSocket()
   */
  protected void closeServerSocket() {

    ServerSocket serverSocket = getServerSocket();

    if (!close(serverSocket)) {
      getLogger().warning(() -> String.format(
        "Failed to close ServerSocket bound to address [%s], listening on port [%d]",
          serverSocket.getInetAddress(), serverSocket.getLocalPort()));
    }
  }

  /**
   * Stops the Echo Service, taking it offline and out-of-service.
   *
   * @return a boolean value indicating whether the Echo Service has been shutdown successfully.
   * @see #getEchoService()
   */
  protected boolean stopEchoService() {

    return Optional.ofNullable(getEchoService())
      .map(localEchoService -> {

        localEchoService.shutdown();

        try {
          if (!localEchoService.awaitTermination(THIRTY, TimeUnit.SECONDS)) {
            localEchoService.shutdownNow();

            if (!localEchoService.awaitTermination(THIRTY, TimeUnit.SECONDS)) {
              getLogger().warning("Failed to shutdown EchoService");
            }
          }
        }
        catch (InterruptedException ignore) {
          Thread.currentThread().interrupt();
        }

        return localEchoService.isShutdown();

      }).orElse(false);
  }

  /**
   * Waits up to {@literal 15 seconds} for this {@link EchoServer} to start.
   *
   * @return a boolean value indicating whether this {@link EchoServer} started successfully.
   * @see #waitFor(long)
   */
  public boolean waitFor() {
    return waitFor(DEFAULT_DURATION_MILLISECONDS);
  }

  /**
   * Waits for the given {@code duration} for this {@link EchoServer} to start.
   *
   * @param duration length of time to wait for this {@link EchoServer} to start.
   * @return a boolean value indicating whether this {@link EchoServer} started successfully.
   * @see ThreadUtils#waitFor(long)
   */
  public boolean waitFor(long duration) {

    return ThreadUtils.waitFor(duration)
      .checkEvery(Integers.FIVE_HUNDRED)
      .on(this::isRunning);
  }
}




© 2015 - 2025 Weber Informatics LLC | Privacy Policy