Java Simple Remote Call Using HTTP/REST — Simple Patterns

Java Simple Remote Call: Example, Code, and Best PracticesRemote procedure calls let one program invoke behavior in another program, potentially running on a different machine. In Java, there are many approaches to make a “simple remote call” — from raw sockets and HTTP to higher-level RPC frameworks. This article explains core concepts, shows practical examples (socket-based and HTTP-based), gives production-minded best practices, and compares approaches so you can choose the right tool for your use case.


When to consider a simple remote call

A “simple remote call” is appropriate when:

  • You need straightforward inter-process communication between services with low complexity.
  • Latency and throughput requirements are modest.
  • You control both client and server implementations.
  • You prefer minimal dependencies and fast iteration.

If you require advanced features (service discovery, retries, observability, binary serialization for performance, streaming, authentication at scale), consider gRPC, Thrift, or a full service mesh.


Concepts and trade-offs

A simple remote call system needs to address:

  • Transport: TCP sockets, HTTP/1.1, or HTTP/2.
  • Serialization: text (JSON, XML) or binary (Java serialization, Protobuf).
  • Concurrency: handling multiple requests concurrently on the server.
  • Error handling and timeouts.
  • Security: authentication, authorization, and encryption (TLS).
  • Observability: logging, metrics, and tracing.

Trade-offs:

  • Simplicity vs. features: raw sockets are minimal but require more glue (framing, error handling). HTTP+JSON is easy and interoperable but less efficient than binary protocols.
  • Performance vs. readability: binary formats and persistent connections are faster; text-based formats are easier to debug.
  • Control vs. ecosystem: frameworks (Spring, gRPC) provide features but add complexity and dependencies.

Example 1 — Simple socket-based RPC (minimal dependencies)

This approach demonstrates building a tiny request/response RPC over TCP sockets using JSON messages. It’s educational and useful when you want minimal dependencies and full control.

Server (simple command dispatcher):

// Server.java import java.io.*; import java.net.*; import java.util.concurrent.*; import com.fasterxml.jackson.databind.*; public class Server {     private final int port;     private final ExecutorService pool = Executors.newFixedThreadPool(8);     private final ObjectMapper mapper = new ObjectMapper();     public Server(int port) { this.port = port; }     public void start() throws IOException {         try (ServerSocket serverSocket = new ServerSocket(port)) {             System.out.println("Server listening on " + port);             while (true) {                 Socket client = serverSocket.accept();                 pool.submit(() -> handleClient(client));             }         }     }     private void handleClient(Socket client) {         try (client;              BufferedReader in = new BufferedReader(new InputStreamReader(client.getInputStream()));              BufferedWriter out = new BufferedWriter(new OutputStreamWriter(client.getOutputStream()))) {             String line;             while ((line = in.readLine()) != null) {                 // Expect each request as a single JSON line                 Request req = mapper.readValue(line, Request.class);                 Response resp = dispatch(req);                 String json = mapper.writeValueAsString(resp);                 out.write(json);                 out.write(" ");                 out.flush();             }         } catch (IOException e) {             System.err.println("Client connection error: " + e.getMessage());         }     }     private Response dispatch(Request req) {         try {             if ("echo".equals(req.method)) {                 return new Response("ok", req.params);             } else if ("add".equals(req.method) && req.params instanceof java.util.Map) {                 java.util.Map<?,?> map = (java.util.Map<?,?>) req.params;                 int a = (int) map.getOrDefault("a", 0);                 int b = (int) map.getOrDefault("b", 0);                 return new Response("ok", a + b);             } else {                 return new Response("error", "unknown method");             }         } catch (Exception e) {             return new Response("error", e.getMessage());         }     }     public static void main(String[] args) throws IOException {         new Server(5555).start();     } } class Request {     public String method;     public Object params; } class Response {     public String status;     public Object result;     public Response() {}     public Response(String status, Object result) { this.status = status; this.result = result; } } 

Client:

// Client.java import java.io.*; import java.net.*; import com.fasterxml.jackson.databind.*; public class Client implements Closeable {     private final Socket socket;     private final BufferedReader in;     private final BufferedWriter out;     private final ObjectMapper mapper = new ObjectMapper();     public Client(String host, int port) throws IOException {         this.socket = new Socket(host, port);         this.in = new BufferedReader(new InputStreamReader(socket.getInputStream()));         this.out = new BufferedWriter(new OutputStreamWriter(socket.getOutputStream()));     }     public Response call(Request req) throws IOException {         String json = mapper.writeValueAsString(req);         out.write(json);         out.write(" ");         out.flush();         String line = in.readLine();         return mapper.readValue(line, Response.class);     }     @Override     public void close() throws IOException {         socket.close();     }     public static void main(String[] args) throws Exception {         try (Client c = new Client("localhost", 5555)) {             Request r = new Request();             r.method = "add";             java.util.Map<String,Integer> params = new java.util.HashMap<>();             params.put("a", 5);             params.put("b", 7);             r.params = params;             Response resp = c.call(r);             System.out.println("Response: " + resp.status + " -> " + resp.result);         }     } } 

Notes:

  • This example uses Jackson (add dependency com.fasterxml.jackson.core:jackson-databind).
  • The framing is line-delimited JSON; other framing schemes (length-prefix) are common for robustness.
  • No TLS, no authentication; not production-ready without those.

Example 2 — HTTP/REST-based simple remote call

HTTP/JSON is often the easiest path for remote calls because of universality and tooling. Here’s a minimal example using Java’s built-in HttpServer and the HttpClient in the JDK (no external web framework required).

Server:

// HttpServerApp.java import com.sun.net.httpserver.*; import java.io.*; import java.net.*; import java.nio.charset.StandardCharsets; import com.fasterxml.jackson.databind.*; public class HttpServerApp {     public static void main(String[] args) throws IOException {         ObjectMapper mapper = new ObjectMapper();         HttpServer server = HttpServer.create(new InetSocketAddress(8080), 0);         server.createContext("/rpc", exchange -> {             if (!"POST".equals(exchange.getRequestMethod())) {                 exchange.sendResponseHeaders(405, -1);                 return;             }             try (InputStream is = exchange.getRequestBody()) {                 Request req = mapper.readValue(is, Request.class);                 Response resp = handle(req);                 byte[] respBytes = mapper.writeValueAsBytes(resp);                 exchange.getResponseHeaders().set("Content-Type", "application/json");                 exchange.sendResponseHeaders(200, respBytes.length);                 try (OutputStream os = exchange.getResponseBody()) {                     os.write(respBytes);                 }             } catch (Exception e) {                 byte[] err = mapper.writeValueAsBytes(new Response("error", e.getMessage()));                 exchange.sendResponseHeaders(500, err.length);                 try (OutputStream os = exchange.getResponseBody()) { os.write(err); }             }         });         server.start();         System.out.println("HTTP RPC server started on :8080");     }     private static Response handle(Request req) {         if ("echo".equals(req.method)) return new Response("ok", req.params);         if ("add".equals(req.method) && req.params instanceof java.util.Map) {             java.util.Map<?,?> m = (java.util.Map<?,?>) req.params;             int a = (int) m.getOrDefault("a", 0);             int b = (int) m.getOrDefault("b", 0);             return new Response("ok", a + b);         }         return new Response("error", "unknown method");     }     static class Request { public String method; public Object params; }     static class Response { public String status; public Object result; public Response() {} public Response(String s, Object r){status=s;result=r;} } } 

Client:

// HttpClientApp.java import java.net.http.*; import java.net.*; import java.nio.charset.StandardCharsets; import com.fasterxml.jackson.databind.*; public class HttpClientApp {     public static void main(String[] args) throws Exception {         ObjectMapper mapper = new ObjectMapper();         HttpClient client = HttpClient.newHttpClient();         var reqObj = new HttpServerApp.Request();         reqObj.method = "add";         java.util.Map<String,Integer> params = new java.util.HashMap<>();         params.put("a", 3);         params.put("b", 4);         reqObj.params = params;         String body = mapper.writeValueAsString(reqObj);         HttpRequest req = HttpRequest.newBuilder()             .uri(new URI("http://localhost:8080/rpc"))             .header("Content-Type", "application/json")             .POST(HttpRequest.BodyPublishers.ofString(body, StandardCharsets.UTF_8))             .build();         HttpResponse<String> resp = client.send(req, HttpResponse.BodyHandlers.ofString());         System.out.println("Status: " + resp.statusCode());         System.out.println("Body: " + resp.body());     } } 

Notes:

  • Easy to test with curl or browser tools.
  • Use TLS (HTTPS) and authentication headers for real deployments.
  • Consider Spring Boot or other frameworks if you need routing, dependency injection, or more features.

Comparison of approaches

Approach Pros Cons
Raw sockets + JSON Very lightweight; full control over protocol You must implement framing, retries, timeouts, security; less interoperable
HTTP/JSON Universal, debuggable, lots of tooling More overhead per request; less efficient than binary protocols
gRPC (Protobuf) High performance, streaming, strong typing More boilerplate and build tooling; harder to debug raw wire traffic
Java RMI Built into Java, easy for Java-only environments Tied to Java, fragile across versions, less common in microservices
Message queue (Kafka/RabbitMQ) Asynchronous, durable delivery Complexity, eventual consistency, not direct request/response

Best practices

  • Design the API contract: method names, parameter schema, and error model. Use DTOs and clear status codes/messages.
  • Use timeouts on client calls; never block indefinitely.
  • Validate inputs server-side and fail fast with meaningful errors.
  • Make your protocol robust: use length-prefix frames or line-delimited JSON; handle partial reads.
  • Secure transport: use TLS and authenticate clients (mTLS, JWTs, API keys).
  • Add retries with exponential backoff for idempotent operations; avoid retrying non-idempotent writes unless you have an idempotency key.
  • Instrument: add logs, structured request/response IDs, and distributed tracing headers.
  • Limit concurrency and apply backpressure on servers (thread pools, queue limits).
  • Version your API and make changes backward-compatible where possible (additive fields, optional params).
  • Prefer well-supported libraries (Jackson/Gson for JSON, Protobuf for binary, gRPC for high-performance RPC) rather than custom binary formats.

Testing and debugging tips

  • Start with unit tests for your request handling logic.
  • Add integration tests that spin up server instances on ephemeral ports.
  • Use tools: curl, httpie for HTTP; wireshark/tcpdump for raw socket inspection.
  • Add health endpoints and readiness checks.
  • Simulate network faults (latency, disconnects) with tools like tc (Linux) or network emulators.
  • Load test with tools such as Apache JMeter, Gatling, or k6.

When to move beyond “simple”

Consider adopting a more feature-rich solution when:

  • You need high throughput and low latency across many services (gRPC).
  • You need language-agnostic contracts and schema evolution (Protobuf/Thrift).
  • You require built-in service discovery, retries, circuit breakers, and observability at scale (service meshes, API gateways).
  • You need asynchronous, durable messaging guarantees (Kafka, RabbitMQ).

Quick checklist before going to production

  • [ ] TLS for all network traffic
  • [ ] Authentication and authorization
  • [ ] Timeouts on client and server
  • [ ] Retry/backoff policy for clients
  • [ ] Proper input validation and error handling
  • [ ] Monitoring, logging, and tracing
  • [ ] Rate limiting and resource quotas
  • [ ] Automated tests (unit, integration, load)

This covers practical examples, trade-offs, and concrete best practices for implementing a Java simple remote call. If you want, I can:

  • Convert the socket example to use length-prefixed framing.
  • Show a gRPC example with Protobuf.
  • Add JWT-based authentication to the HTTP example.

Comments

Leave a Reply

Your email address will not be published. Required fields are marked *