package envoy.client.net; import java.io.*; import java.net.Socket; import java.util.concurrent.TimeoutException; import java.util.logging.*; import dev.kske.eventbus.core.*; import dev.kske.eventbus.core.Event; import envoy.data.*; import envoy.event.*; import envoy.util.*; import envoy.client.data.ClientConfig; import envoy.client.event.EnvoyCloseEvent; /** * Establishes a connection to the server, performs a handshake and delivers certain objects to the * server. * * @author Kai S. K. Engelbart * @author Maximilian Käfer * @author Leon Hofmeister * @since Envoy Client v0.1-alpha */ public final class Client implements Closeable { // Connection handling private Socket socket; private Receiver receiver; private boolean online; // Asynchronously initialized during handshake private volatile User sender; private volatile boolean rejected; // Configuration, logging and event management private static final ClientConfig config = ClientConfig.getInstance(); private static final Logger logger = EnvoyLog.getLogger(Client.class); private static final EventBus eventBus = EventBus.getInstance(); /** * Constructs a client and registers it as an event listener. * * @since Envoy Client v0.2-beta */ public Client() { eventBus.registerListener(this); } /** * Enters the online mode by acquiring a user ID from the server. As a connection has to be * established and a handshake has to be made, this method will block for up to 5 seconds. If * the handshake does exceed this time limit, an exception is thrown. * * @param credentials the login credentials of the user * @throws TimeoutException if the server could not be reached * @throws IOException if the login credentials could not be written * @throws InterruptedException if the current thread is interrupted while waiting for the * handshake response */ public void performHandshake(LoginCredentials credentials) throws TimeoutException, IOException, InterruptedException { if (online) throw new IllegalStateException("Handshake has already been performed successfully"); rejected = false; // Establish TCP connection logger.log(Level.FINER, String.format("Attempting connection to server %s:%d...", config.getServer(), config.getPort())); socket = new Socket(config.getServer(), config.getPort()); logger.log(Level.FINE, "Successfully established TCP connection to server"); // Create object receiver receiver = new Receiver(socket.getInputStream()); // Register user creation processor, contact list processor, message cache and // authentication token receiver.registerProcessor(User.class, sender -> this.sender = sender); // Start receiver receiver.start(); // Write login credentials SerializationUtils.writeBytesWithLength(credentials, socket.getOutputStream()); // Wait for a maximum of five seconds to acquire the sender object final long start = System.currentTimeMillis(); while (sender == null) { // Quit immediately after handshake rejection // This method can then be called again if (rejected) { socket.close(); receiver.removeAllProcessors(); return; } if (System.currentTimeMillis() - start > 5000) { rejected = true; socket.close(); receiver.removeAllProcessors(); throw new TimeoutException("Did not log in after 5 seconds"); } Thread.sleep(500); } // Remove handshake specific processors receiver.removeAllProcessors(); online = true; logger.log(Level.INFO, "Handshake completed."); } /** * Sends an object to the server. * * @param obj the object to send * @throws IllegalStateException if the client is not online * @throws RuntimeException if the object serialization failed * @since Envoy Client v0.2-beta */ public void send(Serializable obj) throws IllegalStateException, RuntimeException { checkOnline(); logger.log(Level.FINE, "Sending " + obj); try { SerializationUtils.writeBytesWithLength(obj, socket.getOutputStream()); } catch (final IOException e) { throw new RuntimeException(e); } } /** * Sends a message to the server. The message's status will be incremented once it was delivered * successfully. * * @param message the message to send * @since Envoy Client v0.3-alpha */ public void sendMessage(Message message) { send(message); message.nextStatus(); } /** * Requests a new {@link IDGenerator} from the server. * * @since Envoy Client v0.3-alpha */ public void requestIDGenerator() { logger.log(Level.INFO, "Requesting new id generator..."); send(new IDGeneratorRequest()); } @Event(HandshakeRejection.class) @Priority(1000) private void onHandshakeRejection() { rejected = true; } @Override @Event(EnvoyCloseEvent.class) @Priority(50) public void close() { if (online) { logger.log(Level.INFO, "Closing connection..."); try { // The sender must be reset as otherwise the handshake is immediately closed sender = null; online = false; socket.close(); } catch (final IOException e) { logger.log(Level.WARNING, "Failed to close socket: ", e); } } } /** * Ensured that the client is online. * * @throws IllegalStateException if the client is not online * @since Envoy Client v0.3-alpha */ private void checkOnline() throws IllegalStateException { if (!online) throw new IllegalStateException("Client is not online"); } /** * @return the {@link User} as which this client is logged in * @since Envoy Client v0.1-alpha */ public User getSender() { return sender; } /** * Sets the client user which is used to send messages. * * @param clientUser the client user to set * @since Envoy Client v0.2-alpha */ public void setSender(User clientUser) { sender = clientUser; } /** * @return the {@link Receiver} used by this {@link Client} * @since v0.2-alpha */ public Receiver getReceiver() { return receiver; } /** * @return {@code true} if a connection to the server could be established * @since Envoy Client v0.2-alpha */ public boolean isOnline() { return online; } }