refactoring, describing with JavaDocs
This commit is contained in:
parent
ac945faef8
commit
380dd5f8b0
@ -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'
|
||||
|
@ -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<Byte> 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");
|
||||
|
22
src/main/java/me/bvn13/sewy/AbstractCommandExecutor.java
Normal file
22
src/main/java/me/bvn13/sewy/AbstractCommandExecutor.java
Normal file
@ -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 <T>
|
||||
*/
|
||||
@FunctionalInterface
|
||||
public interface AbstractCommandExecutor<T extends AbstractCommand> {
|
||||
/**
|
||||
* Command handler
|
||||
* @param command incoming command
|
||||
* @return response on incoming command is another command for corresponding side
|
||||
*/
|
||||
AbstractCommand onCommand(T command);
|
||||
}
|
@ -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<T extends AbstractClientListener> {
|
||||
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<Socket, T> 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<Socket, T> 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();
|
||||
}
|
||||
|
||||
}
|
||||
|
52
src/main/java/me/bvn13/sewy/ClientListenerFactory.java
Normal file
52
src/main/java/me/bvn13/sewy/ClientListenerFactory.java
Normal file
@ -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 <T> generic type
|
||||
* @return lambda method to create client listener
|
||||
*/
|
||||
@SuppressWarnings("unchecked")
|
||||
static <T extends AbstractClientListener> Function<Socket, T> 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<CommandClientListener> constructor = clientListenerClass.getDeclaredConstructor(Socket.class);
|
||||
constructor.setAccessible(true);
|
||||
return (T) constructor.newInstance(client);
|
||||
} catch (Exception e) {
|
||||
throw new RuntimeException(e);
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
}
|
81
src/main/java/me/bvn13/sewy/CommandClient.java
Normal file
81
src/main/java/me/bvn13/sewy/CommandClient.java
Normal file
@ -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<CommandClientListener> {
|
||||
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<Socket, CommandClientListener> clientListenerConstructor) {
|
||||
log.debug("Creating client");
|
||||
connect(host, port, clientListenerConstructor);
|
||||
}
|
||||
|
||||
/**
|
||||
* Sends command to server
|
||||
* @param command command to be sent
|
||||
* @param <T> generic type
|
||||
*/
|
||||
public <T extends AbstractCommand> void send(T command) {
|
||||
log.debug("Start to send command: " + command);
|
||||
client.send(command);
|
||||
}
|
||||
}
|
102
src/main/java/me/bvn13/sewy/CommandClientListener.java
Normal file
102
src/main/java/me/bvn13/sewy/CommandClientListener.java
Normal file
@ -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
|
||||
* <p>{@code instanceof ConcreteCommandClass}</p>
|
||||
* @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 <T> generic type
|
||||
*/
|
||||
public <T extends AbstractCommand> 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);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
@ -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() {
|
||||
|
||||
}
|
||||
}
|
@ -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<AbstractClientListener> clients = Collections.synchronizedList(new ArrayList<>());
|
||||
private final List<AbstractClientListener> 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<Socket, AbstractClientListener> clientListenerConstructor) {
|
||||
|
||||
public Server(String host, int port, Function<Socket, CommandClientListener> 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<AbstractClientListener> 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<Socket, AbstractClientListener> 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<AbstractClientListener> constructor = clientListenerClass.getDeclaredConstructor(Socket.class);
|
||||
constructor.setAccessible(true);
|
||||
return constructor.newInstance(client);
|
||||
} catch (Exception e) {
|
||||
throw new RuntimeException(e);
|
||||
}
|
||||
};
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
@ -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<Serializable> registeredDataTypes = new CopyOnWriteArrayList<>();
|
||||
private final List<Class<?>> registeredDataTypes = new CopyOnWriteArrayList<>();
|
||||
|
||||
public static void register(Serializable clazz) {
|
||||
/**
|
||||
* Registers command in white list for further communications
|
||||
* @param clazz command class
|
||||
* @param <T> generic type
|
||||
*/
|
||||
public static <T extends AbstractCommand> void register(Class<T> clazz) {
|
||||
getInstance().registeredDataTypes.add(clazz);
|
||||
}
|
||||
|
||||
private Sewy() {}
|
||||
/**
|
||||
* Registers commands in white list for further communications
|
||||
* @param classes array of command classes
|
||||
* @param <T> generic type
|
||||
*/
|
||||
public static <T extends AbstractCommand> void register(Class<T>[] classes) {
|
||||
for (Class<T> clazz: classes) {
|
||||
register(clazz);
|
||||
}
|
||||
}
|
||||
|
||||
private Sewy() {
|
||||
}
|
||||
|
||||
@SuppressWarnings("unchecked")
|
||||
static List<Class<AbstractCommand>> getRegisteredDataTypes() {
|
||||
final List<Class<AbstractCommand>> dataTypes = new ArrayList<>();
|
||||
for (Class<?> registeredDataType : INSTANCE.registeredDataTypes) {
|
||||
dataTypes.add((Class<AbstractCommand>) registeredDataType);
|
||||
}
|
||||
return dataTypes;
|
||||
}
|
||||
|
||||
private static Sewy getInstance() {
|
||||
try {
|
||||
|
36
src/main/java/me/bvn13/sewy/SimpleClientListener.java
Normal file
36
src/main/java/me/bvn13/sewy/SimpleClientListener.java
Normal file
@ -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() {
|
||||
|
||||
}
|
||||
}
|
25
src/main/java/me/bvn13/sewy/command/AbstractCommand.java
Normal file
25
src/main/java/me/bvn13/sewy/command/AbstractCommand.java
Normal file
@ -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 {
|
||||
|
||||
}
|
22
src/main/java/me/bvn13/sewy/command/PingCommand.java
Normal file
22
src/main/java/me/bvn13/sewy/command/PingCommand.java
Normal file
@ -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 +
|
||||
'}';
|
||||
}
|
||||
}
|
38
src/main/java/me/bvn13/sewy/command/PongCommand.java
Normal file
38
src/main/java/me/bvn13/sewy/command/PongCommand.java
Normal file
@ -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() +
|
||||
'}';
|
||||
}
|
||||
}
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -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<SimpleClientListener> 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<SimpleClientListener> 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);
|
||||
}
|
||||
|
||||
}
|
||||
|
11
src/test/resources/logback.xml
Normal file
11
src/test/resources/logback.xml
Normal file
@ -0,0 +1,11 @@
|
||||
<configuration>
|
||||
<appender name="STDOUT" class="ch.qos.logback.core.ConsoleAppender">
|
||||
<encoder>
|
||||
<pattern>%d{HH:mm:ss.SSS} [%thread] %-5level %logger{36} \(%class{0}.java:%line\) - %msg%n</pattern>
|
||||
</encoder>
|
||||
</appender>
|
||||
|
||||
<root level="trace">
|
||||
<appender-ref ref="STDOUT" />
|
||||
</root>
|
||||
</configuration>
|
Loading…
Reference in New Issue
Block a user