So I've just written a framework that is supposed to make it easier to create a network-based application (client-server) using the native java.net package. I was wondering if the structure of my code is well understandable and if there are ways to improve my code (in both, readability and performance). I provided a UML-diagram to make it easier to gain a fast overview of the code I wrote:
And here is the source of the classes:
AbstractClient.java
package com.joshuafeld.net;
import java.io.IOException;
import java.net.Socket;
/**
* A client can connect to a running server using native Java sockets. It can perform requests
* and perform actions based on potential requests it can get from the server.
*/
public abstract class AbstractClient extends AbstractSocketWrapper {
/**
* Creates a new client with a socket that is connected to the specified port number on the
* named host.
*
* <p>If the specified host is {@code null} it is the equivalent of specifying the address as
* {@link java.net.InetAddress#getByName(String) InetAddress.getByName}{@code (null)}. In
* other words, it is equivalent to specifying an address of the loopback interface.
*
* @param address The host name, or {@code null} for the loopback address.
* @param port The port number.
*
* @throws IOException If an I/O error occurs when creating the underlying socket.
*/
public AbstractClient(final String address, final int port) throws IOException {
super(new Socket(address, port));
new Thread(this).start();
}
/**
* This is just used to guarantee consistency in the client-server system. It calls the
* method {@link #handleMessage(String) handleMessage} which then can be extended by a subclass.
* This is created in the hope of making the system more understandable for a new user.
*
* @param message The message to handle.
*/
@Override
/* default */ void handle(final String message) {
handleMessage(message);
}
/**
* Handles an incoming message from the server.
*
* @param message The incoming message.
*/
public abstract void handleMessage(String message);
}
AbstractServer.java
package com.joshuafeld.net;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.io.IOException;
import java.net.ServerSocket;
import java.net.Socket;
import java.util.LinkedList;
import java.util.List;
/**
* A server that is based on the native Java sever socket implementation. A server waits for
* requests to come in over the network. It performs some operation based on that request, and then
* possibly returns a result to the requester.
*/
public abstract class AbstractServer {
/**
* Logs messages to the console.
*/
private static final Logger logger = LoggerFactory.getLogger(AbstractServer.class);
/**
* Waits for requests to come in over the network.
*/
private ServerSocket serverSocket;
/**
* The currently open socket connections.
*/
private final List<ConnectionSocket> sockets;
/**
* Creates a server with a server socket, bound to the specified port. A port number of
* {@code 0} means that the port number is automatically allocated, typically from an ephemeral
* port range. This port number can then be retrieved by calling {@link #getPort() getPort}.
*
* @param port The port number, or {@code 0} to use a port number that is
* automatically allocated.
*
* @throws IllegalArgumentException If the port parameter is outside the specified range of
* valid port values which is between 0 and 65535, inclusive.
*/
public AbstractServer(final int port) {
if (port < 0 || port > 0xFFFF) {
throw new IllegalArgumentException("Port value out of range: " + port);
}
sockets = new LinkedList<>();
try {
serverSocket = new ServerSocket(port);
} catch (final IOException exception) {
logger.error("An I/O error occurred while opening the server socket", exception);
}
new Thread(new ConnectionAcceptor()).start();
}
/**
* Closes the server socket as well as all connected sockets.
*/
public void close() {
try {
serverSocket.close();
} catch (final IOException exception) {
logger.error("An I/O error occurred while closing the server socket", exception);
}
sockets.forEach(this::close);
}
/**
* Closes the specified socket and removes it from the list of currently connected sockets.
*
* @param socket The socket to close.
*/
public void close(final ConnectionSocket socket) {
handleDisconnect(socket);
sockets.remove(socket);
socket.close();
}
/**
* Broadcasts a message to all connected sockets/clients.
*
* @param message The message to broadcast.
*/
public void broadcast(final String message) {
sockets.forEach(socket -> socket.send(message));
}
/**
* Handles an incoming message from the specified socket.
*
* @param socket The socket at which the message arrived.
* @param message The message.
*/
public abstract void handleMessage(ConnectionSocket socket, String message);
/**
* Handles a newly connected socket.
*
* @param socket The newly connected socket.
*/
public abstract void handleConnect(ConnectionSocket socket);
/**
* Handles a disconnected/closed socket.
*
* @param socket The closed socket.
*/
public abstract void handleDisconnect(ConnectionSocket socket);
/**
* Returns the IP address to which the server socket is bound.
*
* <p>If the server socket was bound prior to being {@link #close() closed}, then this method
* will continue to return the IP address after the server socket is closed.
*
* @return The IP address to which the server socket is bound.
*/
public String getAddress() {
return serverSocket.getInetAddress().getHostAddress();
}
/**
* Returns the port number on which the server socket is listening.
*
* <p>If the server socket was bound prior to being {@link #close() closed}, then this method
* will continue to return the port number after the server socket is closed.
*
* @return The port number on which the server socket is listening or -1 if the socket is not
* bound yet.
*/
public int getPort() {
return serverSocket.getLocalPort();
}
/**
* Listens for a connection to be made to the server socket and accepts it.
*/
private class ConnectionAcceptor implements Runnable {
/**
* Creates a new connection acceptor.
*/
private ConnectionAcceptor() {
// empty on purpose
}
/**
* Listens for a connection to be made to the server socket and accepts it. The method
* blocks until a connection is made or the server is {@link #close() closed}.
*/
@Override
public void run() {
while (!serverSocket.isClosed()) {
try {
final ConnectionSocket socket = new ConnectionSocket(serverSocket.accept());
handleConnect(socket);
new Thread(socket).start();
sockets.add(socket);
} catch (final IOException exception) {
logger.error("An I/O error occurred while waiting for a connection", exception);
}
}
}
}
/**
* The socket that represents the connection to a specific client.
*/
public class ConnectionSocket extends AbstractSocketWrapper {
/**
* Creates a new wrapped socket. The socket has to be connected already.
*
* @param socket The socket to wrap.
*/
public ConnectionSocket(final Socket socket) {
super(socket);
}
/**
* Passes the handling of the message on to the method
* {@link #handleMessage(ConnectionSocket, String) handleMessage}.
*
* @param message The message to handle.
*/
@Override
void handle(final String message) {
handleMessage(this, message);
}
}
}
AbstractSocketWrapper.java
package com.joshuafeld.net;
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.UncheckedIOException;
import java.net.Socket;
/**
* A simple wrapper for the native Java {@link Socket Socket} that automatically handles messages
* as soon as they are received by the underlying socket.
*/
public abstract class AbstractSocketWrapper implements Runnable {
/**
* Logs messages to the console.
*/
private static final Logger logger = LoggerFactory.getLogger(AbstractSocketWrapper.class);
/**
* The underlying socket/communication endpoint.
*/
private final Socket socket;
/**
* Reads incoming messages from the input stream.
*/
private BufferedReader input;
/**
* Writes messages to the output stream.
*/
private PrintWriter output;
/**
* Creates a new wrapped socket. The socket has to be connected already.
*
* @param socket The socket to wrap.
*/
public AbstractSocketWrapper(final Socket socket) {
this.socket = socket;
try {
input = new BufferedReader(new InputStreamReader(socket.getInputStream()));
output = new PrintWriter(socket.getOutputStream(), true);
} catch (final IOException exception) {
logger.error("An I/O error occurred while creating the input/output stream or the "
+ "socket is not connected", exception);
}
}
/**
* Listens for new messages on the input stream and passes them on to the
* {@link #handle(String) handle} method for further handling.
*/
@Override
public void run() {
while (!socket.isClosed()) {
try {
input.lines().forEach(this::handle);
} catch (final UncheckedIOException exception) {
// This will happen if the input stream is closed. Because this is natural behavior
// and not really an error, we will just ignore this and not log it into the
// console.
break;
}
}
}
/**
* Sends a message to the connected socket. The message is terminated by a newline.
*
* @param message The message to send.
*/
public void send(final String message) {
if (!socket.isClosed()) {
output.println(message);
}
}
/**
* Closes the socket as well as the input reader and output writer.
*
* <p>Once a socket has been closed, it is not available for further networking use (i.e. can't
* be reconnected or rebound). A new socket needs to be created.
*/
public void close() {
try {
socket.close();
input.close();
} catch (final IOException exception) {
logger.error("An I/O error occurred while closing the socket/input", exception);
}
output.close();
}
/**
* Handles a received message.
*
* @param message The message to handle.
*/
/* default */ abstract void handle(final String message);
}
Example of a simple daytime server/client using this code:
DaytimeServer.java
package com.joshuafeld.net.daytime;
import com.joshuafeld.net.AbstractServer;
import java.text.SimpleDateFormat;
import java.util.Date;
public class DaytimeServer extends AbstractServer {
private static final SimpleDateFormat DATE_FORMAT = new SimpleDateFormat(
"yyyy/MM/dd HH:mm:ss");
public DaytimeServer() {
super(13);
}
@Override
public void handleConnect(final ConnectionSocket socket) {
socket.send(DATE_FORMAT.format(new Date()));
socket.close();
}
@Override
public void handleMessage(final ConnectionSocket socket, final String message) {
// nothing to do here
}
@Override
public void handleDisconnect(final ConnectionSocket socket) {
// nothing to do here
}
public static void main(String[] args) {
new DaytimeServer();
}
}
DaytimeClient.java
package com.joshuafeld.net.daytime;
import com.joshuafeld.net.AbstractClient;
import java.io.IOException;
public class DaytimeClient extends AbstractClient {
public DaytimeClient(String address) throws IOException {
super(address, 13);
}
@Override
public void handleMessage(final String message) {
System.out.println(message);
close();
}
public static void main(String[] args) throws IOException {
new DaytimeClient("localhost");
}
}