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.
Leave a Reply