Problem Statement:
Write a server (“Application”) in Java that opens a socket and restricts input to at most 5 concurrent clients. Clients will connect to the Application and write any number of 9 digit numbers, and then close the connection. The Application must write a de-duplicated list of these numbers to a log file in no particular order.
Primary Considerations
- The Application should work correctly as defined below in Requirements.
- The overall structure of the Application should be simple.
- The code of the Application should be descriptive and easy to read, and the build method and runtime parameters must be well-described and work.
- The design should be resilient with regard to data loss.
- The Application should be optimized for maximum throughput, weighed along with the other Primary Considerations and Requirements below.
Requirements
- The Application must accept input from at most 5 concurrent clients on TCP/IP port 4000.
- Input lines presented to the Application via its socket must either be composed of exactly nine decimal digits (e.g.: 314159265 or 007007009) immediately followed by a server-native newline sequence; or a termination sequence as detailed in #9, below.
- Numbers presented to the Application must include leading zeros as necessary to ensure they are each 9 decimal digits.
- The log file, to be named "numbers.log”, must be created anew and/or cleared when the Application starts.
- Only numbers may be written to the log file. Each number must be followed by a server-native newline sequence.
- No duplicate numbers may be written to the log file.
- Any data that does not conform to a valid line of input should be discarded and the client connection terminated immediately and without comment.
- Every 10 seconds, the Application must print a report to standard output:
- The difference since the last report of the count of new unique numbers that have been received.
- The difference since the last report of the count of new duplicate numbers that have been received.
- The total number of unique numbers received for this run of the Application.
- Example text for #8: Received 50 unique numbers, 2 duplicates. Unique total: 567231
- If any connected client writes a single line with only the word "terminate" followed by a server-native newline sequence, the Application must disconnect all clients and perform a clean shutdown as quickly as possible.
- Clearly state all of the assumptions you made in completing the Application along with any instructions on how to set up and run it in a README file.
Notes
- You may write tests at your own discretion. Tests are useful to ensure your Application passes Primary Consideration A.
- You may use common libraries in your project such as Apache Commons and Google Guava, particularly if their use helps improve Application simplicity and readability. However the use of large frameworks, such as Akka, is prohibited.
- Your Application may not for any part of its operation use or require the use of external systems, for example Apache Kafka or Redis.
- At your discretion, leading zeroes present in the input may be stripped—or not used—when writing output to the log or console.
- Robust implementations of the Application typically handle more than 2M numbers per 10-second reporting period on a modern MacBook Pro laptop (e.g.: 16 GiB of RAM and a 2.5 GHz Intel i7 processor).
My implementation:
Application.java:
public class Application {
static final int PORT = 4000;
public static void main(String[] args) {
Server server = new Server();
System.out.println("Starting server");
server.start(PORT);
}
}
Server.java:
import java.io.*;
import java.net.ServerSocket;
import java.net.Socket;
import java.net.SocketException;
import java.util.concurrent.BlockingQueue;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.LinkedBlockingQueue;
import java.util.regex.Pattern;
public class Server {
private static final int MAX_CLIENTS = 5;
private ServerSocket serverSocket;
private ExecutorService executorService = Executors.newFixedThreadPool(MAX_CLIENTS);
private FileWriter fw;
private BufferedWriter bw;
BlockingQueue blockingQueue = new LinkedBlockingQueue();
public void start(int port) {
try {
serverSocket = new ServerSocket(port);
} catch (IOException e) {
e.printStackTrace();
}
try {
fw = new FileWriter("numbers.log", false);
} catch (IOException e) {
e.printStackTrace();
}
bw = new BufferedWriter(fw);
LogWriterTask logWriterTask = new LogWriterTask(blockingQueue, bw);
logWriterTask.start();
while (true) {
try {
ClientHandler clientHandler = new ClientHandler(serverSocket.accept(), blockingQueue);
executorService.submit(clientHandler);
} catch (IOException e) {
e.printStackTrace();
}
}
}
private class ClientHandler extends Thread {
private Socket clientSocket;
private BufferedReader in;
BlockingQueue<Integer> blockingQueue;
private Pattern numberPattern = Pattern.compile("\\d{9}");
public ClientHandler(Socket socket, BlockingQueue<Integer> blockingQueue) {
this.clientSocket = socket;
this.blockingQueue = blockingQueue;
}
@Override
public void run() {
try {
in = new BufferedReader(
new InputStreamReader(clientSocket.getInputStream()));
String inputLine = "";
System.out.println("Client connection started");
while (true) {
try {
inputLine = in.readLine();
if (inputLine == null) {
break;
}
if (inputLine.equals("terminate")) { //Disconnect all clients
System.exit(0);
};
} catch (SocketException e) {
e.printStackTrace();
stopClient();
break;
}
int num = processNumber(inputLine);
if (num == -1) {
stopClient();
break;
}
blockingQueue.add(num);
}
} catch (IOException e) {
e.printStackTrace();
} finally {
stopClient();
}
}
public void stopClient() {
try {
in.close();
clientSocket.close();
System.out.println("Client connection closed");
} catch (IOException e) {
e.printStackTrace();
}
}
private int processNumber(String inputLine) {
if (!numberPattern.matcher(inputLine).matches()) return -1; //Invalid format, terminate client
int num;
try {
num = Integer.parseInt(inputLine);
} catch(NumberFormatException e) { //Invalid format, terminate client
num = -1;
}
return num;
}
}
}
LogWriterTask.java:
import java.io.BufferedWriter;
import java.io.IOException;
import java.util.Timer;
import java.util.TimerTask;
import java.util.concurrent.BlockingQueue;
public class LogWriterTask extends Thread {
private final static int MAX_NINE_DIGIT_RANGE = 1000000000;
private final static int SUMMARY_WAIT_PERIOD = 10000;
private int uniqueCount = 0;
private int duplicateCount = 0;
private int uniqueTotal = 0;
private BlockingQueue<Integer> blockingQueue;
private BufferedWriter bw;
private int[] uniqueNums = new int[MAX_NINE_DIGIT_RANGE];
public LogWriterTask(BlockingQueue<Integer> blockingQueue, BufferedWriter bw) {
this.bw = bw;
this.blockingQueue = blockingQueue;
Timer timer = new Timer();
timer.schedule(new SummaryTask(), 0, SUMMARY_WAIT_PERIOD);
}
@Override
public void run() {
while (true) {
while (!blockingQueue.isEmpty()) {
int num = blockingQueue.poll();
if (uniqueNums[num] == 0) {
try {
uniqueCount++;
uniqueTotal++;
bw.write(String.format("%09d", num));
bw.newLine();
bw.flush();
} catch (IOException e) {
e.printStackTrace();
}
} else {
duplicateCount++;
}
uniqueNums[num]++;
}
}
}
class SummaryTask extends TimerTask {
@Override
public void run() {
System.out.printf("Received %d unique numbers, %d duplicates. Unique total: %d\n", uniqueCount, duplicateCount, uniqueTotal);
uniqueCount = 0;
duplicateCount = 0;
}
}
}
Client.java:
import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStreamReader;
import java.io.PrintWriter;
import java.net.Socket;
public class Client {
private Socket clientSocket;
private PrintWriter out;
private BufferedReader in;
public void startConnection(String ip, int port) {
try {
clientSocket = new Socket(ip, port);
out = new PrintWriter(clientSocket.getOutputStream(), true);
in = new BufferedReader(new InputStreamReader(clientSocket.getInputStream()));
} catch (IOException e) {
System.err.println("Error when initializing connection");
}
}
public void sendMessage(String msg) {
try {
out.println(msg);
return;
} catch (Exception e) {
return;
}
}
public void stopConnection() {
try {
in.close();
out.close();
clientSocket.close();
} catch (IOException e) {
System.err.println("error when closing");
}
}
Testing the solution:
Client.java:
import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStreamReader;
import java.io.PrintWriter;
import java.net.Socket;
public class Client {
private Socket clientSocket;
private PrintWriter out;
private BufferedReader in;
public void startConnection(String ip, int port) {
try {
clientSocket = new Socket(ip, port);
out = new PrintWriter(clientSocket.getOutputStream(), true);
in = new BufferedReader(new InputStreamReader(clientSocket.getInputStream()));
} catch (IOException e) {
System.err.println("Error when initializing connection");
}
}
public void sendMessage(String msg) {
try {
out.println(msg);
return; //in.readLine();
} catch (Exception e) {
return;
}
}
public void stopConnection() {
try {
in.close();
out.close();
clientSocket.close();
} catch (IOException e) {
System.err.println("error when closing");
}
}
ApplicationTest.java:
import org.junit.jupiter.api.Test;
public class ApplicationTest {
private static final String LOCAL_HOST = "127.0.0.1";
private static final int PORT = 4000;
@Test
public void testThroughput() {
Client client = new Client();
client.startConnection(LOCAL_HOST, PORT);
Client client2 = new Client();
client2.startConnection(LOCAL_HOST, PORT);
for (int i = 0; i < 50000; i++) {
int minNum = 0;
int maxNum = 999999999;
int random = (int) ((Math.random() * (maxNum - minNum)) + minNum);
String randomStr = String.format("%09d", random);
client.sendMessage(randomStr);
int random2 = (int) ((Math.random() * (maxNum - minNum)) + minNum);
String randomStr2 = String.format("%09d", random2);
client.sendMessage(randomStr2);
}
}
@Test
public void testTerminateSequence() {
Client client = new Client();
client.startConnection(LOCAL_HOST, 4000);
client.sendMessage("123456789");
client.sendMessage("terminate");
}
@Test
public void testInvalidInput() {
Client client = new Client();
client.startConnection(LOCAL_HOST, 4000);
client.sendMessage("123456789");
client.sendMessage("123");
client.sendMessage("123456789");
}
@Test
public void testDuplicates() {
Client client = new Client();
client.startConnection(LOCAL_HOST, 4000);
client.sendMessage("123456789");
client.sendMessage("987654321");
client.sendMessage("123456789");
client.sendMessage("987654321");
}
}