-
Notifications
You must be signed in to change notification settings - Fork 9
Expand file tree
/
Copy pathReverserver.java
More file actions
298 lines (249 loc) · 11.7 KB
/
Reverserver.java
File metadata and controls
298 lines (249 loc) · 11.7 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
import java.io.*;
import java.net.*;
import java.nio.charset.StandardCharsets;
import java.util.concurrent.*;
import java.util.logging.*;
/**
* Modern TCP echo server that reverses lines sent by clients.
*
* Key improvements over legacy version:
* - Proper resource management (no leaks)
* - Explicit UTF-8 encoding (cross-platform compatibility)
* - Virtual threads for scalability (Java 21+)
* - Comprehensive error handling and logging
* - Input validation (DoS protection)
* - Graceful shutdown
* - Explicit protocol with QUIT command
* - Testable design (AutoCloseable, port discovery)
*
* For educational use in computer networks course.
*/
public class Reverserver implements AutoCloseable {
private static final Logger LOGGER = Logger.getLogger(Reverserver.class.getName());
private static final int MAX_LINE_LENGTH = 8192; // Prevent DoS via huge lines
private final ServerSocket serverSocket;
private final ExecutorService acceptorExecutor;
private final ExecutorService clientExecutor;
private volatile boolean running = true;
/**
* Creates a reverse server on the specified port.
*
* @param port The port to listen on (use 0 for random available port)
* @throws IOException if the server socket cannot be created
*/
public Reverserver(int port) throws IOException {
// Create server socket - will throw IOException if port unavailable
this.serverSocket = new ServerSocket(port);
// Separate executor for accept loop (single thread)
this.acceptorExecutor = Executors.newSingleThreadExecutor(r -> {
Thread t = new Thread(r, "Reverserver-Acceptor");
t.setDaemon(true); // Don't prevent JVM shutdown
return t;
});
// Virtual threads for client handlers - scales to thousands of connections
// For Java < 21, use: Executors.newCachedThreadPool()
this.clientExecutor = Executors.newVirtualThreadPerTaskExecutor();
// Start accepting connections
acceptorExecutor.submit(this::acceptLoop);
LOGGER.info("Reverserver started on port " + getPort());
}
/**
* Returns the actual port the server is listening on.
* Useful when port 0 was specified (random port).
*/
public int getPort() {
return serverSocket.getLocalPort();
}
/**
* Main accept loop - runs in dedicated thread.
* Accepts incoming connections and spawns handler threads.
*/
private void acceptLoop() {
while (running) {
try {
// Blocks until client connects (or socket closed)
Socket clientSocket = serverSocket.accept();
// Log connection (useful for monitoring)
LOGGER.info("Client connected from " + clientSocket.getRemoteSocketAddress());
// Handle client in separate thread
clientExecutor.submit(() -> handleClient(clientSocket));
} catch (IOException e) {
// Only log if this is unexpected (not during shutdown)
if (running) {
LOGGER.log(Level.SEVERE, "Error accepting connection", e);
}
// If we're shutting down, this is expected - exit loop quietly
}
}
LOGGER.info("Accept loop terminated");
}
/**
* Handles a single client connection.
*
* Protocol:
* - Client sends lines of text
* - Server responds with reversed line
* - Client sends empty line or "QUIT" to disconnect
* - Client sends line > MAX_LINE_LENGTH: error and disconnect
*
* @param socket The client socket (will be closed when done)
*/
private void handleClient(Socket socket) {
// Use try-with-resources to ensure ALL resources are closed
// Order matters: socket must be outermost to ensure it's closed last
try (socket; // Java 9+ allows this syntax
BufferedReader input = new BufferedReader(
new InputStreamReader(socket.getInputStream(), StandardCharsets.UTF_8));
PrintWriter output = new PrintWriter(
new OutputStreamWriter(socket.getOutputStream(), StandardCharsets.UTF_8),
true) // auto-flush on println()
) {
// Set socket timeout to prevent hanging on slow/malicious clients
socket.setSoTimeout(30000); // 30 second timeout for reads
String line;
int linesProcessed = 0;
// Read lines until client disconnects or sends termination signal
while ((line = input.readLine()) != null) {
// Check for explicit termination command
if (line.equals("QUIT")) {
LOGGER.fine("Client sent QUIT command");
output.println("BYE");
break;
}
// Check for empty line (legacy termination method)
if (line.isEmpty()) {
LOGGER.fine("Client sent empty line, terminating");
break;
}
// Validate input length (DoS protection)
if (line.length() > MAX_LINE_LENGTH) {
LOGGER.warning("Client sent oversized line (" + line.length() + " chars)");
output.println("ERROR: Line too long (max " + MAX_LINE_LENGTH + " characters)");
break;
}
// Core functionality: reverse the line
String reversed = new StringBuilder(line).reverse().toString();
output.println(reversed);
linesProcessed++;
}
LOGGER.info("Client disconnected. Processed " + linesProcessed + " lines.");
} catch (SocketTimeoutException e) {
LOGGER.warning("Client timed out after inactivity");
} catch (IOException e) {
// Log unexpected I/O errors
LOGGER.log(Level.WARNING, "Error handling client", e);
} catch (Exception e) {
// Catch-all for unexpected errors (shouldn't happen, but defensive)
LOGGER.log(Level.SEVERE, "Unexpected error in client handler", e);
}
// Socket and streams automatically closed by try-with-resources
}
/**
* Gracefully shuts down the server.
*
* 1. Stops accepting new connections
* 2. Waits for existing clients to finish (up to 30 seconds)
* 3. Forces termination of remaining clients if necessary
*/
@Override
public void close() throws IOException {
LOGGER.info("Shutting down Reverserver...");
// Signal accept loop to stop
running = false;
// Close server socket - causes accept() to throw IOException
try {
serverSocket.close();
} catch (IOException e) {
LOGGER.log(Level.WARNING, "Error closing server socket", e);
}
// Shutdown accept loop executor
shutdownExecutor(acceptorExecutor, "acceptor", 5);
// Gracefully shutdown client handlers
shutdownExecutor(clientExecutor, "client handlers", 30);
LOGGER.info("Reverserver shutdown complete");
}
/**
* Helper method to gracefully shutdown an executor service.
* Attempts graceful shutdown, then forces if necessary.
*/
private void shutdownExecutor(ExecutorService executor, String name, int timeoutSeconds) {
executor.shutdown(); // Prevent new tasks, allow existing to complete
try {
// Wait for existing tasks to finish
if (!executor.awaitTermination(timeoutSeconds, TimeUnit.SECONDS)) {
LOGGER.warning(name + " did not terminate gracefully, forcing shutdown");
executor.shutdownNow(); // Force termination
// Wait a bit more for forced termination
if (!executor.awaitTermination(5, TimeUnit.SECONDS)) {
LOGGER.severe(name + " did not terminate even after forced shutdown");
}
} else {
LOGGER.info(name + " terminated gracefully");
}
} catch (InterruptedException e) {
LOGGER.warning(name + " shutdown interrupted");
executor.shutdownNow();
Thread.currentThread().interrupt(); // Restore interrupt status
}
}
/**
* Demonstration main method showing server usage.
*
* Creates server, connects client, sends some lines, disconnects.
*/
public static void main(String[] args) {
// Configure logging to show INFO and above
Logger.getLogger("").setLevel(Level.INFO);
try {
final int serverPort = 7777;
// Create server (try-with-resources ensures cleanup)
try (Reverserver server = new Reverserver(serverPort)) {
// Give server time to start (not necessary in production, but helps demo)
Thread.sleep(100);
// Create client connection
try (Socket clientSocket = new Socket(InetAddress.getLoopbackAddress(), serverPort);
PrintWriter output = new PrintWriter(
new OutputStreamWriter(clientSocket.getOutputStream(), StandardCharsets.UTF_8),
true); // auto-flush
BufferedReader input = new BufferedReader(
new InputStreamReader(clientSocket.getInputStream(), StandardCharsets.UTF_8))
) {
// Test 1: Basic reversal
System.out.println("=== Test 1: Basic reversal ===");
output.println("Hello, World!");
System.out.println("Sent: Hello, World!");
System.out.println("Received: " + input.readLine());
// Test 2: Unicode support
System.out.println("\n=== Test 2: Unicode support ===");
output.println("Héllo 世界! 🌍");
System.out.println("Sent: Héllo 世界! 🌍");
System.out.println("Received: " + input.readLine());
// Test 3: Multiple lines
System.out.println("\n=== Test 3: Multiple lines ===");
String[] testLines = {
"The quick brown fox",
"jumps over the lazy dog",
"TCP is reliable!"
};
for (String line : testLines) {
output.println(line);
System.out.println("Sent: " + line);
System.out.println("Received: " + input.readLine());
}
// Test 4: Graceful disconnect
System.out.println("\n=== Test 4: Graceful disconnect ===");
output.println("QUIT");
System.out.println("Sent: QUIT");
System.out.println("Received: " + input.readLine()); // Should get "BYE"
} // Client resources auto-closed
System.out.println("\n=== Client disconnected ===");
// Keep server running briefly to see logs
Thread.sleep(1000);
} // Server auto-closed via AutoCloseable
System.out.println("\n=== Server shutdown complete ===");
} catch (Exception e) {
LOGGER.log(Level.SEVERE, "Error in main", e);
System.exit(1);
}
}
}