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:
- Completed Installation
- Basic understanding of Dependency Injection
- An SSL certificate (we'll create a self-signed one for testing)
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
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
| Component | Role |
|---|---|
SSLConfiguration | Provides SSL certificate for secure connections |
GreetingController | Handles requests at /hello/{name}, /greet, etc. |
DefaultServer | Accepts SSL connections, delegates to handlers |
ControllerHandler | Routes requests to appropriate controller methods |
RequestDeserializer | Converts JSON → Request object |
ResponseSerializer | Converts 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@AppMappingpatterns{name}in endpoint → Extracted with@PathVariable("name")datafields → Extracted with@RequestParamor 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
@AppMappingpattern - 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: