Skip to main content

Hello World Socket Server

Build your first SSL socket server with Sproogy in under 15 minutes.

What We'll Build

A complete SSL socket server that:

  • Accepts secure SSL connections
  • Routes requests to controllers
  • Returns JSON responses
  • Demonstrates the complete request/response flow

We'll also build a simple client to test it.

Prerequisites

Before starting, you need:

Step 1: Create SSL Certificate

For development, create a self-signed certificate:

# Navigate to your project's src/main/resources directory
cd src/main/resources

# Generate keystore with self-signed certificate
keytool -genkeypair -alias sproogyserver \
-keyalg RSA -keysize 2048 \
-validity 365 \
-keystore keystore.jks \
-storepass changeit \
-keypass changeit \
-dname "CN=localhost, OU=Dev, O=Sproogy, L=City, ST=State, C=US"

# Generate truststore (for client)
keytool -exportcert -alias sproogyserver \
-keystore keystore.jks \
-storepass changeit \
-file server.cer

keytool -importcert -alias sproogyserver \
-file server.cer \
-keystore truststore.jks \
-storepass changeit \
-noprompt
Production Use

Self-signed certificates are for development only. In production, use certificates from a trusted CA.

Step 2: Project Setup

build.gradle:

plugins {
id 'java'
id 'application'
}

group = 'com.example'
version = '1.0.0'

sourceCompatibility = '11'

repositories {
mavenCentral()
mavenLocal()
}

dependencies {
// Sproogy modules
implementation 'com.sproogy:core:1.0.0'
implementation 'com.sproogy:socket-common:1.0.0'
implementation 'com.sproogy:socket-server:1.0.0'

// Required dependencies
implementation 'com.fasterxml.jackson.core:jackson-databind:2.15.2'
implementation 'io.github.cdimascio:dotenv-java:3.0.0'
implementation 'org.slf4j:slf4j-simple:2.0.7'
}

application {
mainClass = 'com.example.ServerApp'
}

Step 3: Configuration

.env:

SOCKET_PORT=8443
KEYSTORE_PATH=src/main/resources/keystore.jks
KEYSTORE_PASSWORD=changeit

src/main/resources/application.yml:

application:
socket:
format:
idKey: "id"
headerKey: "headers"
endpointKey: "endpoint"
payloadKey: "data"
statusKey: "status"

Step 4: Create SSL Configuration

src/main/java/com/example/config/SSLConfiguration.java:

package com.example.config;

import com.sproogy.annotations.Component;
import com.sproogy.annotations.Env;
import com.sproogy.socket.server.security.SSLFactoryLoader;

import javax.net.ssl.*;
import java.io.FileInputStream;
import java.security.KeyStore;

@Component
public class SSLConfiguration implements SSLFactoryLoader {

@Env("KEYSTORE_PATH")
private String keystorePath;

@Env("KEYSTORE_PASSWORD")
private String keystorePassword;

@Override
public SSLServerSocketFactory getSSLServerSocketFactory() throws Exception {
// Load keystore
KeyStore keyStore = KeyStore.getInstance("JKS");
try (FileInputStream fis = new FileInputStream(keystorePath)) {
keyStore.load(fis, keystorePassword.toCharArray());
}

// Initialize KeyManagerFactory
KeyManagerFactory kmf = KeyManagerFactory.getInstance(
KeyManagerFactory.getDefaultAlgorithm()
);
kmf.init(keyStore, keystorePassword.toCharArray());

// Initialize SSLContext
SSLContext sslContext = SSLContext.getInstance("TLS");
sslContext.init(kmf.getKeyManagers(), null, null);

return sslContext.getServerSocketFactory();
}
}

Step 5: Create Request/Response DTOs

src/main/java/com/example/dto/GreetingRequest.java:

package com.example.dto;

public class GreetingRequest {
private String name;
private String language;

public GreetingRequest() {}

public String getName() { return name; }
public void setName(String name) { this.name = name; }

public String getLanguage() { return language; }
public void setLanguage(String language) { this.language = language; }
}

src/main/java/com/example/dto/GreetingResponse.java:

package com.example.dto;

public class GreetingResponse {
private String message;
private long timestamp;

public GreetingResponse() {}

public GreetingResponse(String message) {
this.message = message;
this.timestamp = System.currentTimeMillis();
}

public String getMessage() { return message; }
public void setMessage(String message) { this.message = message; }

public long getTimestamp() { return timestamp; }
public void setTimestamp(long timestamp) { this.timestamp = timestamp; }
}

Step 6: Create Controller

src/main/java/com/example/controller/GreetingController.java:

package com.example.controller;

import com.example.dto.GreetingRequest;
import com.example.dto.GreetingResponse;
import com.sproogy.annotations.Controller;
import com.sproogy.socket.annotations.AppMapping;
import com.sproogy.socket.annotations.PathVariable;
import com.sproogy.socket.annotations.RequestParam;
import com.sproogy.socket.response.ResponseEntity;

import java.util.HashMap;
import java.util.Map;

@Controller
public class GreetingController {

private static final Map<String, String> GREETINGS = new HashMap<>();

static {
GREETINGS.put("en", "Hello");
GREETINGS.put("es", "Hola");
GREETINGS.put("fr", "Bonjour");
GREETINGS.put("de", "Guten Tag");
GREETINGS.put("it", "Ciao");
}

@AppMapping("/hello/{name}")
public ResponseEntity<GreetingResponse> hello(@PathVariable("name") String name) {
String message = "Hello, " + name + "!";
return ResponseEntity.ok(new GreetingResponse(message));
}

@AppMapping("/greet")
public ResponseEntity<GreetingResponse> greet(GreetingRequest request) {
String greeting = GREETINGS.getOrDefault(
request.getLanguage(),
"Hello"
);
String message = greeting + ", " + request.getName() + "!";
return ResponseEntity.ok(new GreetingResponse(message));
}

@AppMapping("/goodbye/{name}")
public ResponseEntity<GreetingResponse> goodbye(
@PathVariable("name") String name,
@RequestParam("reason") String reason
) {
String message = "Goodbye, " + name + "! Reason: " + reason;
return ResponseEntity.ok(new GreetingResponse(message));
}

@AppMapping("/ping")
public ResponseEntity<Map<String, Object>> ping() {
Map<String, Object> response = new HashMap<>();
response.put("status", "OK");
response.put("timestamp", System.currentTimeMillis());
response.put("server", "Sproogy v1.0");
return ResponseEntity.ok(response);
}
}

Step 7: Create Main Application

src/main/java/com/example/ServerApp.java:

package com.example;

import com.sproogy.SproogyApp;
import com.sproogy.annotations.Env;
import com.sproogy.annotations.SproogyApplication;

@SproogyApplication(basePackage = "com.example")
public class ServerApp {

public static void main(String[] args) {
SproogyApp.run(ServerApp.class, args);

// Server starts automatically
System.out.println("=".repeat(50));
System.out.println("Sproogy Socket Server is running!");
System.out.println("Port: " + System.getenv("SOCKET_PORT"));
System.out.println("Press Ctrl+C to stop");
System.out.println("=".repeat(50));

// Keep main thread alive
try {
Thread.currentThread().join();
} catch (InterruptedException e) {
System.out.println("Server stopped");
}
}
}

Step 8: Run the Server

./gradlew run

Expected Output:

==================================================
Sproogy Socket Server is running!
Port: 8443
Press Ctrl+C to stop
==================================================

The server is now listening on port 8443! 🚀

Step 9: Create a Test Client

Let's build a client to test our server.

src/main/java/com/example/ClientApp.java:

package com.example;

import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.sproogy.socket.client.SocketTemplate;
import com.sproogy.socket.loader.SSLSecuritySocketLoader;
import com.sproogy.socket.response.ResponseEntity;

import javax.net.ssl.*;
import java.io.FileInputStream;
import java.net.InetAddress;
import java.security.KeyStore;
import java.util.HashMap;
import java.util.Map;

public class ClientApp {

public static void main(String[] args) throws Exception {
// SSL Configuration
SSLSecuritySocketLoader sslLoader = () -> {
KeyStore trustStore = KeyStore.getInstance("JKS");
try (FileInputStream fis = new FileInputStream("src/main/resources/truststore.jks")) {
trustStore.load(fis, "changeit".toCharArray());
}

TrustManagerFactory tmf = TrustManagerFactory.getInstance(
TrustManagerFactory.getDefaultAlgorithm()
);
tmf.init(trustStore);

SSLContext sslContext = SSLContext.getInstance("TLS");
sslContext.init(null, tmf.getTrustManagers(), null);

return sslContext.getSocketFactory();
};

// Request/Response format configuration
ObjectMapper mapper = new ObjectMapper();
JsonNode config = mapper.createObjectNode()
.put("idKey", "id")
.put("headerKey", "headers")
.put("endpointKey", "endpoint")
.put("payloadKey", "data")
.put("statusKey", "status");

// Build SocketTemplate
SocketTemplate template = SocketTemplate.SocketTemplateBuilder.buildDefault(
InetAddress.getByName("localhost"),
8443,
sslLoader,
config, // response format
config // request format
);

System.out.println("Connected to server!");
System.out.println("=".repeat(50));

// Test 1: Simple path variable
System.out.println("\n[Test 1] GET /hello/World");
ResponseEntity<String> response1 = template.sendRequest("/hello/World", String.class);
System.out.println("Response: " + response1.getData());

// Test 2: Request body
System.out.println("\n[Test 2] POST /greet with request body");
Map<String, String> greetRequest = new HashMap<>();
greetRequest.put("name", "Alice");
greetRequest.put("language", "fr");
ResponseEntity<String> response2 = template.sendRequest("/greet", greetRequest, String.class);
System.out.println("Response: " + response2.getData());

// Test 3: Path variable + request params
System.out.println("\n[Test 3] GET /goodbye/Bob?reason=tired");
Map<String, String> params = new HashMap<>();
params.put("reason", "tired");
ResponseEntity<String> response3 = template.sendRequest("/goodbye/Bob", params, String.class);
System.out.println("Response: " + response3.getData());

// Test 4: Ping
System.out.println("\n[Test 4] GET /ping");
ResponseEntity<String> response4 = template.sendRequest("/ping", String.class);
System.out.println("Response: " + response4.getData());

System.out.println("\n" + "=".repeat(50));
System.out.println("All tests completed!");

System.exit(0);
}
}

Add client configuration to build.gradle:

task runClient(type: JavaExec) {
classpath = sourceSets.main.runtimeClasspath
mainClass = 'com.example.ClientApp'
}

Step 10: Test the Server

Terminal 1 (Server):

./gradlew run

Terminal 2 (Client):

./gradlew runClient

Expected Client Output:

Connected to server!
==================================================

[Test 1] GET /hello/World
Response: {"message":"Hello, World!","timestamp":1234567890}

[Test 2] POST /greet with request body
Response: {"message":"Bonjour, Alice!","timestamp":1234567891}

[Test 3] GET /goodbye/Bob?reason=tired
Response: {"message":"Goodbye, Bob! Reason: tired","timestamp":1234567892}

[Test 4] GET /ping
Response: {"status":"OK","timestamp":1234567893,"server":"Sproogy v1.0"}

==================================================
All tests completed!

What Just Happened?

Let's break down the complete flow:

1. Server Startup

2. Request Processing

When the client sends /hello/World:

3. Key Components

ComponentRole
SSLConfigurationProvides SSL certificate for secure connections
GreetingControllerHandles requests at /hello/{name}, /greet, etc.
DefaultServerAccepts SSL connections, delegates to handlers
ControllerHandlerRoutes requests to appropriate controller methods
RequestDeserializerConverts JSON → Request object
ResponseSerializerConverts Response object → JSON

Experiment: Add a New Endpoint

Add this method to GreetingController:

@AppMapping("/echo")
public ResponseEntity<Map<String, Object>> echo(Map<String, Object> data) {
Map<String, Object> response = new HashMap<>();
response.put("echo", data);
response.put("receivedAt", System.currentTimeMillis());
return ResponseEntity.ok(response);
}

Restart the server and test:

# In client code or manual test
curl -k https://localhost:8443 -d '{"endpoint":"/echo","data":{"foo":"bar"}}'

Request Format Details

Sproogy expects this JSON structure:

{
"id": "unique-request-id",
"endpoint": "/path/to/endpoint",
"headers": {
"Authorization": "Bearer token",
"Custom-Header": "value"
},
"data": {
"key": "value"
}
}

Mapping:

  • endpoint → Matched against @AppMapping patterns
  • {name} in endpoint → Extracted with @PathVariable("name")
  • data fields → Extracted with @RequestParam or deserialized to method parameter

Response Format

Sproogy sends this JSON structure:

{
"id": "same-as-request-id",
"status": 200,
"headers": {},
"data": {
"message": "Hello, World!",
"timestamp": 1234567890
}
}

Key Takeaways

🎯 Zero Boilerplate: No manual socket code, SSL setup is a single class

🎯 Convention-Based Routing: @AppMapping("/hello/{name}") automatically extracts name

🎯 Automatic Serialization: Request/response bodies are automatically JSON-converted

🎯 Type-Safe: Path variables and request params are type-converted automatically

🎯 Dependency Injection: Controllers, services, and repositories all wired automatically

Common Issues

"Connection refused"

  • Server not running
  • Wrong port number
  • Firewall blocking connections

"SSL Handshake failed"

  • Client truststore doesn't trust server certificate
  • Certificate expired
  • Wrong SSL protocol version

"Endpoint not found"

  • Typo in @AppMapping pattern
  • Controller not scanned (check package)
  • Server not restarted after code changes

Next Steps

Congratulations! You've built a complete SSL socket server with Sproogy.

Explore more advanced topics:

👉 Socket Programming Deep Dive

👉 Filters for Authentication

👉 Advanced SSL Configuration