diff --git a/build.gradle b/build.gradle index 4a2a3f4..cf4b97e 100644 --- a/build.gradle +++ b/build.gradle @@ -15,6 +15,12 @@ dependencies { // https://mvnrepository.com/artifact/org.slf4j/slf4j-api implementation group: 'org.slf4j', name: 'slf4j-api', version: '1.7.35' + // https://mvnrepository.com/artifact/commons-io/commons-io + implementation group: 'commons-io', name: 'commons-io', version: '2.11.0' + + // https://mvnrepository.com/artifact/org.apache.commons/commons-lang3 + implementation group: 'org.apache.commons', name: 'commons-lang3', version: '3.12.0' + testImplementation 'ch.qos.logback:logback-classic:1.2.10' testImplementation 'org.junit.jupiter:junit-jupiter-api:5.8.1' diff --git a/src/main/java/me/bvn13/sewy/AbstractClientListener.java b/src/main/java/me/bvn13/sewy/AbstractClientListener.java index 5be796d..ece07a0 100644 --- a/src/main/java/me/bvn13/sewy/AbstractClientListener.java +++ b/src/main/java/me/bvn13/sewy/AbstractClientListener.java @@ -1,5 +1,5 @@ /* - Copyright 2020 Vyacheslav Boyko + Copyright 2022 Vyacheslav Boyko Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. @@ -18,49 +18,114 @@ package me.bvn13.sewy; import org.slf4j.Logger; import org.slf4j.LoggerFactory; -import java.io.BufferedReader; import java.io.IOException; -import java.io.InputStreamReader; -import java.io.PrintWriter; +import java.io.InputStream; +import java.io.OutputStream; import java.net.Socket; +import java.util.ArrayList; +import java.util.List; +import static me.bvn13.sewy.Sewy.SEPARATOR; + +/** + * TCP Client listener. + * This class provides methods to manage communications between {@link Server} and {@link Client}. + * Inherit this class to implement your own communication type. + */ public abstract class AbstractClientListener implements Runnable { - protected static final Logger log = LoggerFactory.getLogger(AbstractClientListener.class); + protected final Logger log = LoggerFactory.getLogger(this.getClass()); protected final Socket socket; - protected PrintWriter out; - protected BufferedReader in; + protected OutputStream out; + protected InputStream in; protected AbstractClientListener(Socket socket) { + log.debug("Initializing client listener"); this.socket = socket; try { - in = new BufferedReader(new InputStreamReader(socket.getInputStream())); - } catch (IOException e) { - throw new RuntimeException(e); - } - try { - out = new PrintWriter(socket.getOutputStream(), true); + this.in = socket.getInputStream(); + log.debug("BufferedReader successfully created"); + log.debug("PrintWriter successfully created"); + out = socket.getOutputStream(); + log.debug("OutputStream successfully created"); } catch (IOException e) { throw new RuntimeException(e); } } + /** + * Thread runner. + * Override it according the needs + */ + @Override + public abstract void run(); + + /** + * Reads line (separated with '\n') from socket + * @return the line read from socket + */ public String readLine() { + final byte[] bytes = readBytes(SEPARATOR); + final StringBuilder sb = new StringBuilder(); + for (byte aByte : bytes) { + sb.append((char) aByte); + } + final String string = sb.toString(); + if (log.isTraceEnabled()) log.trace("Received: " + string); + return string; + } + + /** + * Reads data from socket until {@code separator} is encountered + * @param separator byte to separate data portions + * @return array of bytes read from socket + */ + public byte[] readBytes(byte separator) { + final List data = new ArrayList<>(256); try { - return in.readLine(); + while (socket.isConnected()) { + byte[] portion = in.readNBytes(1); + if (portion == null || portion.length == 0 || portion[0] == separator) { + break; + } + data.add(portion[0]); + } + final byte[] bytes = new byte[data.size()]; + int i = 0; + for (Byte aByte : data) { + bytes[i++] = aByte; + } + if (log.isTraceEnabled()) log.trace("Received: " + new String(bytes)); + return bytes; } catch (IOException e) { throw new RuntimeException(e); } } + /** + * Writes line into socket ending with default separator '\n'. + * Flushes after writing. + * @param data data to be sent into socket + */ public void writeLine(String data) { - out.println(data); + if (log.isTraceEnabled()) log.trace("Sending: " + data); + try { + out.write(data.getBytes()); + out.write(SEPARATOR); + out.flush(); + } catch (IOException e) { + throw new RuntimeException(e); + } } - void stop() { - out.close(); + /** + * Stops client listener gracefully + */ + public void stop() { + log.debug("Stopping"); try { + out.close(); in.close(); } catch (IOException e) { log.warn("Unable to close IN client buffer"); diff --git a/src/main/java/me/bvn13/sewy/AbstractCommandExecutor.java b/src/main/java/me/bvn13/sewy/AbstractCommandExecutor.java new file mode 100644 index 0000000..d80daad --- /dev/null +++ b/src/main/java/me/bvn13/sewy/AbstractCommandExecutor.java @@ -0,0 +1,22 @@ +package me.bvn13.sewy; + +import me.bvn13.sewy.command.AbstractCommand; + +/** + * Interface to describe command executor. + * Every command encountered by {@link CommandClientListener} + * will be sent into such executor provided + * in {@link Client#Client(java.lang.String, int, java.lang.Class)} + * or {@link Server#Server(java.lang.String, int, java.lang.Class)} + * while instantiating Client and Server + * @param + */ +@FunctionalInterface +public interface AbstractCommandExecutor { + /** + * Command handler + * @param command incoming command + * @return response on incoming command is another command for corresponding side + */ + AbstractCommand onCommand(T command); +} diff --git a/src/main/java/me/bvn13/sewy/Client.java b/src/main/java/me/bvn13/sewy/Client.java index 0e37423..cc219b5 100644 --- a/src/main/java/me/bvn13/sewy/Client.java +++ b/src/main/java/me/bvn13/sewy/Client.java @@ -1,5 +1,5 @@ /* - Copyright 2020 Vyacheslav Boyko + Copyright 2022 Vyacheslav Boyko Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. @@ -18,81 +18,111 @@ package me.bvn13.sewy; import org.slf4j.Logger; import org.slf4j.LoggerFactory; -import java.io.BufferedReader; import java.io.IOException; -import java.io.InputStreamReader; -import java.io.PrintWriter; import java.net.Socket; import java.util.concurrent.ExecutorService; import java.util.concurrent.Executors; +import java.util.function.Function; import static java.lang.String.format; +import static me.bvn13.sewy.ClientListenerFactory.createClientListenerConstructor; -public class Client { - - private static final Logger log = LoggerFactory.getLogger(Client.class); +/** + * TCP Client. + * Create the instance of this class to connect to {@link Server} + */ +public class Client { + protected final Logger log = LoggerFactory.getLogger(this.getClass()); private final ExecutorService executor = Executors.newCachedThreadPool(); + protected T client; - private Socket socket; - private PrintWriter out; - private BufferedReader in; + protected Socket socket; + /** + * Default constructor is to delay connecting to server + */ public Client() { } - public Client(String host, int port) { - connect(host, port); + /** + * Connects to server immediately + * @param host host to connect to + * @param port port to be used while connecting + * @param clientListenerClass client listener class describing protocol of communications + */ + public Client(String host, int port, Class clientListenerClass) { + this(host, port, createClientListenerConstructor(clientListenerClass)); } - public void stop() { - out.close(); - try { - in.close(); - } catch (IOException e) { - log.warn("Unable to close IN client buffer"); - } - try { - socket.close(); - } catch (IOException e) { - throw new RuntimeException(e); - } + /** + * Connects to server immediately + * @param host host to connect to + * @param port port to be used while connecting + * @param clientListenerConstructor to provide constructor for client listener (see {@link me.bvn13.sewy.Client#Client(java.lang.String, int, java.lang.Class)}) + */ + public Client(String host, int port, Function clientListenerConstructor) { + log.debug("Creating client"); + connect(host, port, clientListenerConstructor); } - public void connect(String host, int port) { + /** + * Connects to {@link Server} + * @param host host to connect to + * @param port port to be used while connecting + * @param clientListenerConstructor to provide constructor for client listener (see {@link me.bvn13.sewy.Client#Client(java.lang.String, int, java.lang.Class)}) + */ + public void connect(String host, int port, Function clientListenerConstructor) { try { + log.debug(format("Connecting to %s:%d", host, port)); socket = new Socket(host, port); + client = clientListenerConstructor.apply(socket); + executor.execute(client); } catch (IOException e) { log.error(format("Error while conversation with %s:%d", host, port), e); stop(); - return; - } - try { - in = new BufferedReader(new InputStreamReader(socket.getInputStream())); - } catch (IOException e) { - throw new RuntimeException(e); - } - try { - out = new PrintWriter(socket.getOutputStream(), true); - } catch (IOException e) { - throw new RuntimeException(e); } } + /** + * Stops client gracefully + */ + public void stop() { + log.debug("Stopping client"); + client.stop(); + executor.shutdown(); + } + + /** + * To check whether socket is online + * @return + */ + public boolean isConnected() { + return socket != null && socket.isConnected(); + } + + /** + * Reads one line from socket + * @return the line read from socket + */ public String readLine() { - try { - return in.readLine(); - } catch (IOException e) { - throw new RuntimeException(e); - } + return client.readLine(); } + /** + * Reads data from socket until {@code separator} is encountered + * @param separator + * @return + */ + public byte[] readBytes(byte separator) { + return client.readBytes(separator); + } + + /** + * Writes line into socket ending with default separator '\n'. + * @param data data to be sent into socket + */ public void writeLine(String data) { - out.println(data); + client.writeLine(data); } - - boolean isConnected() { - return socket.isConnected(); - } - } diff --git a/src/main/java/me/bvn13/sewy/ClientListenerFactory.java b/src/main/java/me/bvn13/sewy/ClientListenerFactory.java new file mode 100644 index 0000000..7ddf448 --- /dev/null +++ b/src/main/java/me/bvn13/sewy/ClientListenerFactory.java @@ -0,0 +1,52 @@ +/* + Copyright 2022 Vyacheslav Boyko + + 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 me.bvn13.sewy; + +import java.lang.reflect.Constructor; +import java.net.Socket; +import java.util.function.Function; + +/** + * Factory constructing client listeners + */ +class ClientListenerFactory { + + /** + * Creates client listener constructor + * @param clientListenerClass class to be used as client listener + * @param generic type + * @return lambda method to create client listener + */ + @SuppressWarnings("unchecked") + static Function createClientListenerConstructor(Class clientListenerClass) { + + if (clientListenerClass.getGenericSuperclass() == null + /*|| !clientListenerClass.getGenericSuperclass().equals(T.class)*/) { + throw new IllegalArgumentException("Wrong client listener of type: "+clientListenerClass.getName()); + } + + return (client) -> { + try { + final Constructor constructor = clientListenerClass.getDeclaredConstructor(Socket.class); + constructor.setAccessible(true); + return (T) constructor.newInstance(client); + } catch (Exception e) { + throw new RuntimeException(e); + } + }; + } + +} diff --git a/src/main/java/me/bvn13/sewy/CommandClient.java b/src/main/java/me/bvn13/sewy/CommandClient.java new file mode 100644 index 0000000..cfc57e4 --- /dev/null +++ b/src/main/java/me/bvn13/sewy/CommandClient.java @@ -0,0 +1,81 @@ +/* + Copyright 2022 Vyacheslav Boyko + + 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 me.bvn13.sewy; + +import me.bvn13.sewy.command.AbstractCommand; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.net.Socket; +import java.util.function.Function; + +import static java.lang.String.format; +import static me.bvn13.sewy.ClientListenerFactory.createClientListenerConstructor; + +/** + * TCP Client. + * Works with command protocol. + * Create the instance of this class to connect to {@link Server} + */ +public class CommandClient extends Client { + private final Logger log = LoggerFactory.getLogger(this.getClass()); + + /** + * Default constructor is to delay connecting to Server + */ + public CommandClient() { + } + + /** + * Starts to connect to server immediately + * @param host host to connect to + * @param port port to be used while connecting + */ + public CommandClient(String host, int port) { + this(host, port, CommandClientListener.class); + } + + /** + * Starts to connect to server immediately + * @param host host to connect to + * @param port port to be used while connecting + * @param clientListenerClass client listener class describing protocol of communications + */ + public CommandClient(String host, int port, Class clientListenerClass) { + this(host, port, createClientListenerConstructor(clientListenerClass)); + } + + /** + * Connects to server immediately + * @param host host to connect to + * @param port port to be used while connecting + * @param clientListenerConstructor to provide constructor for client listener (see {@link me.bvn13.sewy.Client#Client(java.lang.String, int, java.lang.Class)}) + */ + public CommandClient(String host, int port, Function clientListenerConstructor) { + log.debug("Creating client"); + connect(host, port, clientListenerConstructor); + } + + /** + * Sends command to server + * @param command command to be sent + * @param generic type + */ + public void send(T command) { + log.debug("Start to send command: " + command); + client.send(command); + } +} diff --git a/src/main/java/me/bvn13/sewy/CommandClientListener.java b/src/main/java/me/bvn13/sewy/CommandClientListener.java new file mode 100644 index 0000000..9eb4c53 --- /dev/null +++ b/src/main/java/me/bvn13/sewy/CommandClientListener.java @@ -0,0 +1,102 @@ +/* + Copyright 2022 Vyacheslav Boyko + + 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 me.bvn13.sewy; + +import me.bvn13.sewy.command.AbstractCommand; +import org.apache.commons.lang3.SerializationUtils; + +import java.io.IOException; +import java.io.Serializable; +import java.net.Socket; + +import static java.lang.String.format; +import static me.bvn13.sewy.Sewy.SEPARATOR; + +/** + * Client listener describing protocol-oriented communication + */ +public class CommandClientListener extends AbstractClientListener implements AbstractCommandExecutor { + public CommandClientListener(Socket socket) { + super(socket); + } + + /** + * Thread runner + */ + @Override + public void run() { + for (Thread.yield(); !socket.isConnected() && !socket.isClosed(); Thread.yield()) { + } + while (socket.isConnected()) { + Thread.yield(); + byte[] line = readBytes(SEPARATOR); + if (line == null || line.length == 0) { + continue; + } + final Object command = SerializationUtils.deserialize(line); + if (command == null) { + continue; + } + if (!Sewy.getRegisteredDataTypes().contains(command.getClass())) { + log.error("Unexpected command received"); + continue; + } + log.debug("Command received: " + command); + if (!(command instanceof AbstractCommand)) { + log.warn("Incorrect command received: " + command); + continue; + } + final Serializable response = onCommand((AbstractCommand) command); + log.debug(format("Response for %s is: %s", command, response)); + try { + out.write(SerializationUtils.serialize(response)); + out.write(SEPARATOR); + } catch (IOException e) { + throw new RuntimeException(e); + } + } + } + + + /** + * Method to receive the data command-by-command incoming from clients + * You need to override it + * + * @param command serialized command to be checked by using + *

{@code instanceof ConcreteCommandClass}

+ * @return server answer on client command + */ + public AbstractCommand onCommand(AbstractCommand command) { + return null; + } + + /** + * Sends command to opposite side + * @param command command to be sent + * @param generic type + */ + public void send(T command) { + log.debug("Start to send command: " + command); + try { + out.write(SerializationUtils.serialize(command)); + out.write(SEPARATOR); + out.flush(); + } catch (IOException e) { + throw new RuntimeException(e); + } + } + +} diff --git a/src/main/java/me/bvn13/sewy/DefaultClientListener.java b/src/main/java/me/bvn13/sewy/DefaultClientListener.java deleted file mode 100644 index cbcbd50..0000000 --- a/src/main/java/me/bvn13/sewy/DefaultClientListener.java +++ /dev/null @@ -1,14 +0,0 @@ -package me.bvn13.sewy; - -import java.net.Socket; - -public class DefaultClientListener extends AbstractClientListener { - public DefaultClientListener(Socket socket) { - super(socket); - } - - @Override - public void run() { - - } -} diff --git a/src/main/java/me/bvn13/sewy/Server.java b/src/main/java/me/bvn13/sewy/Server.java index f80019f..c4a50bb 100644 --- a/src/main/java/me/bvn13/sewy/Server.java +++ b/src/main/java/me/bvn13/sewy/Server.java @@ -1,5 +1,5 @@ /* - Copyright 2020 Vyacheslav Boyko + Copyright 2022 Vyacheslav Boyko Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. @@ -19,7 +19,6 @@ import org.slf4j.Logger; import org.slf4j.LoggerFactory; import java.io.IOException; -import java.lang.reflect.Constructor; import java.net.InetAddress; import java.net.ServerSocket; import java.net.Socket; @@ -32,28 +31,47 @@ import java.util.concurrent.Executors; import java.util.function.Function; import static java.lang.String.format; +import static me.bvn13.sewy.ClientListenerFactory.createClientListenerConstructor; +/** + * TCP Server. + * Create the instance of this class to connect to {@link Client} + */ public class Server { - private static final Logger log = LoggerFactory.getLogger(Server.class); + private final Logger log = LoggerFactory.getLogger(this.getClass()); private final ExecutorService executor = Executors.newCachedThreadPool(); - final List clients = Collections.synchronizedList(new ArrayList<>()); + private final List clients = Collections.synchronizedList(new ArrayList<>()); private ServerSocket socket; + /** + * @param host host to bind in order to start listen to clients + * @param port port to start listen to + */ public Server(String host, int port) { - this(host, port, DefaultClientListener.class); + this(host, port, CommandClientListener.class); } - @SuppressWarnings("unchecked") + /** + * @param host host to bind in order to start listen to clients + * @param port port to start listen to + * @param clientListenerClass client listen class to be used for communication + */ public Server(String host, int port, Class clientListenerClass) { - this(host, port, defaultClientListenerConstructor(clientListenerClass)); + this(host, port, createClientListenerConstructor(clientListenerClass)); } + /** + * + * @param host host to bind in order to start listen to clients + * @param port port to start listen to + * @param clientListenerConstructor to provide constructor for client listener (see {@link me.bvn13.sewy.Server#Server(java.lang.String, int, java.lang.Class)}) + */ @SuppressWarnings("unchecked") - public Server(String host, int port, Function clientListenerConstructor) { - + public Server(String host, int port, Function clientListenerConstructor) { + log.debug("Starting server"); executor.execute(() -> { try (final ServerSocket server = new ServerSocket(port, 0, InetAddress.getByName(host))) { @@ -73,7 +91,12 @@ public class Server { } + /** + * Stops server gracefully + * Disconnects from every client + */ public void stop() { + log.debug("Stopping server"); final Iterator iterator = clients.iterator(); while (iterator.hasNext()) { final AbstractClientListener client = iterator.next(); @@ -83,28 +106,12 @@ public class Server { executor.shutdown(); } - boolean isListening() { + /** + * To check whether the server is ready for new connections + * @return + */ + public boolean isListening() { return socket != null && socket.isBound(); } - @SuppressWarnings("unchecked") - private static Function defaultClientListenerConstructor(Class clientListenerClass) { - - if (clientListenerClass.getGenericSuperclass() == null - || !clientListenerClass.getGenericSuperclass().equals(AbstractClientListener.class)) { - throw new IllegalArgumentException("Wrong client listener of type: "+clientListenerClass.getName()); - } - - return (client) -> { - try { - final Constructor constructor = clientListenerClass.getDeclaredConstructor(Socket.class); - constructor.setAccessible(true); - return constructor.newInstance(client); - } catch (Exception e) { - throw new RuntimeException(e); - } - }; - - } - } diff --git a/src/main/java/me/bvn13/sewy/Sewy.java b/src/main/java/me/bvn13/sewy/Sewy.java index 2ca28fc..c351a35 100644 --- a/src/main/java/me/bvn13/sewy/Sewy.java +++ b/src/main/java/me/bvn13/sewy/Sewy.java @@ -1,5 +1,5 @@ /* - Copyright 2020 Vyacheslav Boyko + Copyright 2022 Vyacheslav Boyko Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. @@ -15,23 +15,56 @@ */ package me.bvn13.sewy; -import java.io.Serializable; +import me.bvn13.sewy.command.AbstractCommand; + +import java.util.ArrayList; import java.util.List; import java.util.concurrent.CopyOnWriteArrayList; import java.util.concurrent.locks.ReentrantLock; +/** + * Supporting class providing protocol restrictions + */ public final class Sewy { + static final byte SEPARATOR = '\n'; + private static Sewy INSTANCE; private static final ReentrantLock LOCK = new ReentrantLock(); - private final List registeredDataTypes = new CopyOnWriteArrayList<>(); + private final List> registeredDataTypes = new CopyOnWriteArrayList<>(); - public static void register(Serializable clazz) { + /** + * Registers command in white list for further communications + * @param clazz command class + * @param generic type + */ + public static void register(Class clazz) { getInstance().registeredDataTypes.add(clazz); } - private Sewy() {} + /** + * Registers commands in white list for further communications + * @param classes array of command classes + * @param generic type + */ + public static void register(Class[] classes) { + for (Class clazz: classes) { + register(clazz); + } + } + + private Sewy() { + } + + @SuppressWarnings("unchecked") + static List> getRegisteredDataTypes() { + final List> dataTypes = new ArrayList<>(); + for (Class registeredDataType : INSTANCE.registeredDataTypes) { + dataTypes.add((Class) registeredDataType); + } + return dataTypes; + } private static Sewy getInstance() { try { diff --git a/src/main/java/me/bvn13/sewy/SimpleClientListener.java b/src/main/java/me/bvn13/sewy/SimpleClientListener.java new file mode 100644 index 0000000..a99bf44 --- /dev/null +++ b/src/main/java/me/bvn13/sewy/SimpleClientListener.java @@ -0,0 +1,36 @@ +/* + Copyright 2022 Vyacheslav Boyko + + 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 me.bvn13.sewy; + +import java.net.Socket; + +/** + * Simple client listener is to disable automatic receiving and sending data. + * Override {@link SimpleClientListener#run()} to implement business logic of communicating + */ +public class SimpleClientListener extends AbstractClientListener { + protected SimpleClientListener(Socket socket) { + super(socket); + } + + /** + * Thread runner + */ + @Override + public void run() { + + } +} diff --git a/src/main/java/me/bvn13/sewy/command/AbstractCommand.java b/src/main/java/me/bvn13/sewy/command/AbstractCommand.java new file mode 100644 index 0000000..c447eb7 --- /dev/null +++ b/src/main/java/me/bvn13/sewy/command/AbstractCommand.java @@ -0,0 +1,25 @@ +/* + Copyright 2022 Vyacheslav Boyko + + 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 me.bvn13.sewy.command; + +import java.io.Serializable; + +/** + * The very parent class every command should be inherited from + */ +public class AbstractCommand implements Serializable { + +} diff --git a/src/main/java/me/bvn13/sewy/command/PingCommand.java b/src/main/java/me/bvn13/sewy/command/PingCommand.java new file mode 100644 index 0000000..9a90853 --- /dev/null +++ b/src/main/java/me/bvn13/sewy/command/PingCommand.java @@ -0,0 +1,22 @@ +package me.bvn13.sewy.command; + +import java.time.Instant; + +public class PingCommand extends AbstractCommand { + private final long time; + + public PingCommand() { + this.time = Instant.now().toEpochMilli(); + } + + public long getTime() { + return time; + } + + @Override + public String toString() { + return "PingCommand{" + + "time=" + time + + '}'; + } +} diff --git a/src/main/java/me/bvn13/sewy/command/PongCommand.java b/src/main/java/me/bvn13/sewy/command/PongCommand.java new file mode 100644 index 0000000..7ccfd8c --- /dev/null +++ b/src/main/java/me/bvn13/sewy/command/PongCommand.java @@ -0,0 +1,38 @@ +package me.bvn13.sewy.command; + +import java.time.Instant; + +public class PongCommand extends AbstractCommand { + private final long time; + private long pingTime; + + public PongCommand(PingCommand ping) { + this.pingTime = ping.getTime(); + this.time = Instant.now().toEpochMilli(); + } + + public long getTime() { + return time; + } + + public long getPingTime() { + return pingTime; + } + + public void setPingTime(long pingTime) { + this.pingTime = pingTime; + } + + public long getLatency() { + return time - pingTime; + } + + @Override + public String toString() { + return "PongCommand{" + + "time=" + time + + ", pingTime=" + pingTime + + ", latency=" + getLatency() + + '}'; + } +} diff --git a/src/test/java/me/bvn13/sewy/EchoClientListener.java b/src/test/java/me/bvn13/sewy/EchoClientListener.java index e3909e4..0d29080 100644 --- a/src/test/java/me/bvn13/sewy/EchoClientListener.java +++ b/src/test/java/me/bvn13/sewy/EchoClientListener.java @@ -1,16 +1,40 @@ +/* + Copyright 2022 Vyacheslav Boyko + + 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 me.bvn13.sewy; import java.net.Socket; +/** + * Simple ECHOed client listener + * Writes into socket all the data received before + */ public class EchoClientListener extends AbstractClientListener { public EchoClientListener(Socket socket) { super(socket); } + /** + * Thread runner + */ @Override public void run() { while (socket.isConnected()) { - writeLine(readLine()); + Thread.yield(); + final String data = readLine(); + writeLine(data); } } } diff --git a/src/test/java/me/bvn13/sewy/ServerTest.java b/src/test/java/me/bvn13/sewy/ServerTest.java index 097151d..9354af1 100644 --- a/src/test/java/me/bvn13/sewy/ServerTest.java +++ b/src/test/java/me/bvn13/sewy/ServerTest.java @@ -1,9 +1,29 @@ +/* + Copyright 2022 Vyacheslav Boyko + + 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 me.bvn13.sewy; +import me.bvn13.sewy.command.AbstractCommand; +import me.bvn13.sewy.command.PingCommand; +import me.bvn13.sewy.command.PongCommand; import org.junit.jupiter.api.Assertions; import org.junit.jupiter.params.ParameterizedTest; import org.junit.jupiter.params.provider.ValueSource; +import java.util.concurrent.atomic.AtomicLong; + public class ServerTest { private static final int START_PORT = 12345; @@ -21,7 +41,7 @@ public class ServerTest { @ValueSource(ints = START_PORT + 2) void givenServerRunning_whenClientConnects_thenServerCanStopClientListener(int port) throws InterruptedException { Server server = new Server("localhost", port); - Client client = new Client("localhost", port); + Client client = new Client<>("localhost", port, SimpleClientListener.class); Thread.sleep(1000); Assertions.assertTrue(server.isListening()); Assertions.assertTrue(client.isConnected()); @@ -39,13 +59,8 @@ public class ServerTest { @ParameterizedTest @ValueSource(ints = START_PORT + 4) - void serverStartedWithLambdaProvidedClientListener(int port) throws InterruptedException { - Server server = new Server("localhost", port, (socket) -> new AbstractClientListener(socket) { - @Override - public void run() { - - } - }); + void serverIsAbleToStartWithLambdaProvidedClientListener(int port) throws InterruptedException { + Server server = new Server("localhost", port, SimpleClientListener.class); Thread.sleep(1000); Assertions.assertTrue(server.isListening()); server.stop(); @@ -54,8 +69,8 @@ public class ServerTest { @ParameterizedTest @ValueSource(ints = START_PORT + 5) void simpleEchoClientServer(int port) { - new Server("localhost", port, EchoClientListener.class); - Client client = new Client("localhost", port); + new Server("192.168.0.153", port, EchoClientListener.class); + Client client = new Client<>("192.168.0.153", port, SimpleClientListener.class); client.writeLine("hello"); String response1 = client.readLine(); Assertions.assertEquals("hello", response1); @@ -64,4 +79,37 @@ public class ServerTest { Assertions.assertEquals("olleh", response2); } + @ParameterizedTest + @ValueSource(ints = START_PORT + 6) + void serverIsAbleToPingPong(int port) throws InterruptedException { + Sewy.register(PingCommand.class); + Sewy.register(PongCommand.class); + + Server server = new Server("localhost", port, (socket) -> new CommandClientListener(socket) { + @Override + public AbstractCommand onCommand(AbstractCommand command) { + if (command instanceof PingCommand) { + return new PongCommand((PingCommand) command); + } + throw new IllegalArgumentException(command.toString()); + } + }); + + AtomicLong latency = new AtomicLong(0); + CommandClient client = new CommandClient("localhost", port, (socket) -> new CommandClientListener(socket) { + @Override + public AbstractCommand onCommand(AbstractCommand command) { + if (command instanceof PongCommand) { + latency.set(((PongCommand)command).getLatency()); + return null; + } else { + throw new IllegalArgumentException(command.toString()); + } + } + }); + client.send(new PingCommand()); + Thread.sleep(1000); + Assertions.assertTrue(latency.get() > 0); + } + } diff --git a/src/test/resources/logback.xml b/src/test/resources/logback.xml new file mode 100644 index 0000000..e13e20a --- /dev/null +++ b/src/test/resources/logback.xml @@ -0,0 +1,11 @@ + + + + %d{HH:mm:ss.SSS} [%thread] %-5level %logger{36} \(%class{0}.java:%line\) - %msg%n + + + + + + + \ No newline at end of file