Skip to content

ConnectionServer

Luca_Previ0o edited this page Dec 10, 2025 · 2 revisions

ConnectionServer: TCP Transport Layer

ConnectionServer is the Transport Layer (Layer 2) of JSI's architecture. It provides TCP socket management and thread-per-client concurrency on top of the abstract Server foundation.

Position in Architecture

┌─────────────────────────────────────┐
│  HttpServer / DatabaseServer        │  ← Protocol implementations
│  (Your custom protocol)             │
├─────────────────────────────────────┤
│  ConnectionServer                   │  ← YOU ARE HERE
│  (TCP socket handling)              │
├─────────────────────────────────────┤
│  Server (abstract base)             │  ← Foundation layer
└─────────────────────────────────────┘

File: connection/ConnectionServer.java


Class Structure

Complete Implementation

package jsi.connection;

import java.io.*;
import java.net.ServerSocket;
import java.nio.file.*;
import java.nio.charset.StandardCharsets;
import jsi.Request;
import jsi.Server;

public abstract class ConnectionServer extends Server {

    private final int port;

    public ConnectionServer(int port) { 
        this.port = port; 
    }

    @Override
    public void start() {
        System.out.println("Server is starting...");
        onBeforeStart();

        try (ServerSocket socket = new ServerSocket(port)) {
            System.out.println("Server started on port " + port);
            onServerStarted();
            
            while (true) {
                Socket clientSocket = socket.accept();
                
                // Spawn new thread for each client
                new Thread(() -> {
                    try (
                        BufferedReader in = new BufferedReader(
                            new InputStreamReader(clientSocket.getInputStream(), 
                                StandardCharsets.UTF_8));
                        PrintWriter out = new PrintWriter(clientSocket.getOutputStream())
                    ) {
                        String request = in.readLine();
                        Response response = handleRequest(parseRequest(request));
                        out.println(response.serialize());
                        out.flush();
                    } catch (IOException e) { 
                        e.printStackTrace(); 
                    } finally {
                        try { 
                            clientSocket.close(); 
                        } catch (IOException e) { 
                            e.printStackTrace(); 
                        }
                    }
                }).start();
            }
        } catch (IOException e) { 
            e.printStackTrace(); 
        }
    }

    /**
     * Parse the incoming request string into a Request object.
     * Subclasses implement protocol-specific parsing.
     */
    protected abstract Request parseRequest(String input);

    /**
     * Read a file from the filesystem.
     * Utility method available to all ConnectionServer subclasses.
     */
    protected String readFile(String filePath) throws IOException {
        Path path = Paths.get(filePath);
        return Files.readString(path, StandardCharsets.UTF_8);
    }
}

Key Responsibilities

1. Port Management

private final int port;

public ConnectionServer(int port) { 
    this.port = port; 
}

Unlike the base Server class, ConnectionServer assumes TCP networking, so it requires a port number.

2. TCP Socket Lifecycle

try (ServerSocket socket = new ServerSocket(port)) {
    while (true) {
        Socket clientSocket = socket.accept();  // Block until client connects
        // ... handle client ...
    }
}

Key operations:

  • Bind: new ServerSocket(port) binds to the specified port
  • Listen: Automatically begins listening for connections
  • Accept: socket.accept() blocks until a client connects, then returns a Socket

3. Thread-Per-Client Concurrency

Socket clientSocket = socket.accept();

new Thread(() -> {
    // This code runs in a separate thread for this client
    handleClientRequest();
}).start();

// Main thread continues, ready to accept next client

Threading model:

Main Thread              Client Thread 1        Client Thread 2
    │                           │                     │
    ├──accept()────────────────►│                     │
    │   (spawns thread)         ├─ parse request      │
    │                           ├─ handle request     │
    ├──accept()──────────────────────────────────────►│
    │   (spawns thread)         ├─ send response      ├─ parse request
    │                           └─ close socket       ├─ handle request
    ├──accept()                                       ├─ send response
    .   (blocked)                                     └─ close socket
    .                                                 .

Benefits:

  • Simple to understand and implement
  • Each client is isolated (crash doesn't affect others)
  • Natural request/response pairing

Limitations:

  • One thread per connection (doesn't scale to thousands of connections)
  • Thread creation overhead
  • No connection pooling or reuse

4. Request/Response Pipeline

// 1. Read raw data from socket
String requestString = in.readLine();

// 2. Parse into Request object (protocol-specific)
Request request = parseRequest(requestString);

// 3. Process request (business logic)
Response response = handleRequest(request);

// 4. Serialize response
String responseString = response.serialize();

// 5. Write to socket
out.println(responseString);
out.flush();

This pipeline abstracts the wire protocol from the application protocol.

5. Resource Management

try (
    BufferedReader in = new BufferedReader(...);
    PrintWriter out = new PrintWriter(...)
) {
    // Use streams
} finally {
    clientSocket.close();  // Always close socket
}

Try-with-resources ensures streams are closed even if exceptions occur.


Using ConnectionServer

Example 1: Echo Server

Simplest possible protocol - echo back what client sends:

import jsi.connection.ConnectionServer;
import jsi.Request;
import jsi.Response;

public class EchoServer extends ConnectionServer {
    
    public EchoServer(int port) {
        super(port);
    }
    
    @Override
    protected Request parseRequest(String input) {
        // Wrap string in a Request object
        return new SimpleRequest(input);
    }
    
    @Override
    public Response handleRequest(Request request) {
        // Echo back the same content
        String message = ((SimpleRequest) request).getMessage();
        return new SimpleResponse("ECHO: " + message);
    }
    
    public static void main(String[] args) {
        new EchoServer(9000).start();
    }
}

// Simple request/response implementations
class SimpleRequest implements Request {
    private String message;
    
    public SimpleRequest(String message) { this.message = message; }
    public String getMessage() { return message; }
    
    @Override
    public String serialize() { return message; }
}

class SimpleResponse implements Response {
    private String message;
    
    public SimpleResponse(String message) { this.message = message; }
    
    @Override
    public String serialize() { return message; }
}

Test with telnet:

$ telnet localhost 9000
> Hello, server!
< ECHO: Hello, server!

Example 2: Line-Based Protocol Server

Custom protocol where each line is a command:

public class CommandServer extends ConnectionServer {
    
    public CommandServer(int port) {
        super(port);
    }
    
    @Override
    protected Request parseRequest(String input) {
        // Parse: "COMMAND arg1 arg2"
        String[] parts = input.split(" ", 2);
        String command = parts[0];
        String args = parts.length > 1 ? parts[1] : "";
        
        return new CommandRequest(command, args);
    }
    
    @Override
    public Response handleRequest(Request request) {
        CommandRequest cmd = (CommandRequest) request;
        
        switch (cmd.getCommand()) {
            case "PING":
                return new CommandResponse("PONG");
            case "TIME":
                return new CommandResponse(LocalDateTime.now().toString());
            case "ECHO":
                return new CommandResponse(cmd.getArgs());
            default:
                return new CommandResponse("ERROR: Unknown command");
        }
    }
}

Test:

$ telnet localhost 9000
> PING
< PONG
> TIME
< 2025-12-10T14:30:00
> ECHO Hello World
< Hello World

Java Socket API Deep Dive

ServerSocket

ServerSocket is Java's TCP server socket implementation.

Creation:

ServerSocket serverSocket = new ServerSocket(port);

Behind the scenes:

  1. Allocates a socket file descriptor
  2. Binds to 0.0.0.0:port (all network interfaces)
  3. Calls OS listen() syscall with default backlog (50)

Accepting connections:

Socket clientSocket = serverSocket.accept();  // Blocks

This method blocks until a client connects, then returns a new Socket representing that connection.

Socket (Client Connection)

Each accepted connection gets its own Socket object.

Reading data:

InputStream in = clientSocket.getInputStream();
BufferedReader reader = new BufferedReader(
    new InputStreamReader(in, StandardCharsets.UTF_8)
);

String line = reader.readLine();  // Blocks until newline

Writing data:

OutputStream out = clientSocket.getOutputStream();
PrintWriter writer = new PrintWriter(out);

writer.println("Hello, client!");
writer.flush();  // Important! Forces data to be sent immediately

Character Encoding

JSI consistently uses UTF-8:

new InputStreamReader(clientSocket.getInputStream(), StandardCharsets.UTF_8);

This ensures international characters are handled correctly.


Threading Model Details

Thread Creation

new Thread(() -> {
    // Client handling code
}).start();

Each Thread object:

  • Creates a new OS-level thread
  • Has its own stack (typically 1 MB on 64-bit JVM)
  • Scheduled by OS thread scheduler

Cost analysis:

  • Memory: ~1 MB per thread (stack space)
  • Creation time: ~0.2-0.5 ms
  • Context switching: Adds overhead when switching between threads

Practical limits:

  • Thousands of threads: Possible but slow
  • Tens of thousands: System will struggle
  • Hundreds of thousands: Not feasible

Alternative: Thread Pools

For production systems, use thread pools:

private ExecutorService threadPool = Executors.newFixedThreadPool(100);

@Override
public void start() {
    try (ServerSocket socket = new ServerSocket(port)) {
        while (true) {
            Socket clientSocket = socket.accept();
            
            // Submit to thread pool instead of creating new thread
            threadPool.submit(() -> handleClient(clientSocket));
        }
    }
}

Benefits:

  • Limits concurrent threads
  • Reuses threads (no creation overhead)
  • Bounded resource usage

Connection Lifecycle

Client connects
    ↓
Main thread: accept() returns
    ↓
New thread spawned
    ↓
Thread reads request
    ↓
Thread processes request
    ↓
Thread writes response
    ↓
Thread closes socket
    ↓
Thread terminates

Note: Each connection is completely independent. No state is shared between threads (unless you explicitly share it).


File I/O Utility

ConnectionServer provides a utility method for reading files:

protected String readFile(String filePath) throws IOException {
    Path path = Paths.get(filePath);
    return Files.readString(path, StandardCharsets.UTF_8);
}

Usage Example

public class FileServer extends ConnectionServer {
    
    @Override
    public Response handleRequest(Request request) {
        String filename = ((FileRequest) request).getFilename();
        
        try {
            String content = readFile("files/" + filename);
            return new FileResponse(200, content);
        } catch (IOException e) {
            return new FileResponse(404, "File not found");
        }
    }
}

Path Resolution

Paths.get("static/index.html")

This resolves relative to the current working directory (where the JVM was started).

Best practice: Use absolute paths or configure a base directory:

private String baseDir = "/var/www/html";

protected String readFile(String relativePath) throws IOException {
    Path fullPath = Paths.get(baseDir, relativePath);
    return Files.readString(fullPath, StandardCharsets.UTF_8);
}

Extension Points

1. Custom Parsing

Implement parseRequest() for your protocol:

@Override
protected Request parseRequest(String input) {
    // JSON-RPC parsing
    JsonObject json = JsonParser.parse(input);
    String method = json.get("method").getAsString();
    JsonArray params = json.get("params").getAsJsonArray();
    
    return new JsonRpcRequest(method, params);
}

2. Connection Hooks

Override lifecycle methods:

@Override
protected void onBeforeStart() {
    System.out.println("Loading configuration...");
    loadConfig();
    connectToDatabase();
}

@Override
protected void onServerStarted() {
    System.out.println("Server ready!");
    registerWithServiceDiscovery();
}

3. Custom Threading

Override start() for complete control:

@Override
public void start() {
    ExecutorService pool = Executors.newFixedThreadPool(50);
    
    onBeforeStart();
    
    try (ServerSocket socket = new ServerSocket(port)) {
        onServerStarted();
        
        while (true) {
            Socket client = socket.accept();
            pool.submit(() -> handleClient(client));
        }
    } catch (IOException e) {
        e.printStackTrace();
    }
}

4. Middleware Pattern

Add request/response interceptors:

public abstract class MiddlewareConnectionServer extends ConnectionServer {
    
    private List<Middleware> middlewares = new ArrayList<>();
    
    public void use(Middleware middleware) {
        middlewares.add(middleware);
    }
    
    @Override
    public Response handleRequest(Request request) {
        // Pre-processing
        for (Middleware m : middlewares) {
            request = m.before(request);
        }
        
        // Actual handling
        Response response = doHandleRequest(request);
        
        // Post-processing
        for (Middleware m : middlewares) {
            response = m.after(response);
        }
        
        return response;
    }
    
    protected abstract Response doHandleRequest(Request request);
}

Performance Characteristics

Throughput

With thread-per-client model:

  • Short-lived connections: 1000-5000 req/sec (depends on request processing time)
  • Long-lived connections: Limited by thread count (typically < 10,000 concurrent)

Latency

  • Thread creation: 0.2-0.5 ms per connection
  • Context switching: Adds 1-10 µs per switch
  • Socket I/O: Depends on network and data size

Scalability Limits

Connections     Memory (1 MB/thread)    Context Switching
─────────────────────────────────────────────────────────
100             100 MB                  Negligible
1,000           1 GB                    Noticeable
10,000          10 GB                   Significant
100,000         100 GB                  Unusable

For high-concurrency scenarios, consider:

  • Thread pools (limit concurrent threads)
  • Async I/O (NIO, Netty)
  • Connection multiplexing (HTTP/2)

Related Documentation


Next: Explore HTTP Server to see how HTTP protocol is built on ConnectionServer, or Database Server for database query handling.

Clone this wiki locally