Building a Mini RPC in Java 17: From Zero to Understanding Remote Calls
This article walks through creating a minimal Java 17 RPC framework—from defining a simple binary protocol and using JDK serialization to implementing server loops, dynamic proxy‑based clients, and a runnable demo—while also highlighting the gaps that production‑grade solutions like Dubbo or gRPC must fill.
What RPC Actually Does
When a method runs locally, arguments and return values stay in memory; a remote call must serialize arguments into a byte stream, send them over the network, deserialize on the server, invoke the real method, then send the result back. RPC hides this whole chain behind a stub so the caller sees only a normal interface.
RPC adds a layer of encapsulation so the caller cannot perceive the network; the stub on both client and server sides shields all communication details.
Designing a Simple Binary Protocol
The protocol consists of three parts for a request: method name length (int), method name bytes (UTF‑8), argument count (int), and for each argument its length (int) followed by serialized data. The response contains a status byte (0 = success, 1 = error) and an int length plus the serialized payload.
Serialization
For this prototype the built‑in JDK serialization is used. Objects are turned into byte arrays with ObjectOutputStream and restored with ObjectInputStream. Serialized objects must implement Serializable.
public static byte[] serialize(Object obj) throws IOException {
try (ByteArrayOutputStream bos = new ByteArrayOutputStream();
ObjectOutputStream oos = new ObjectOutputStream(bos)) {
oos.writeObject(obj);
oos.flush();
return bos.toByteArray();
}
}Deserialization reads the bytes back into an object using ObjectInputStream.
Server Side
The server opens a ServerSocket, accepts connections in a blocking loop, and processes each request:
try (ServerSocket serverSocket = new ServerSocket(port)) {
while (true) {
Socket socket = serverSocket.accept();
handleRequest(socket);
}
} handleRequestreads the request with DataInputStream, extracts the method name and arguments according to the protocol, finds the matching method by name and argument count via reflection, invokes it, serializes the result, and writes the response back.
Method lookup is simplified to name‑plus‑parameter‑count matching (no overload support). The JDK reflection automatically unboxes wrapper types to primitives, so Integer arguments work for int parameters.
Client Side and Dynamic Proxy
The client creates a proxy for the service interface using JDK dynamic proxies. Calls on the proxy are intercepted and turned into network requests.
MathService mathService = client.createProxy(MathService.class);
boolean result = mathService.isPrime(17);The createProxy method delegates to Proxy.newProxyInstance:
public <T> T createProxy(Class<T> serviceInterface) {
return (T) Proxy.newProxyInstance(
serviceInterface.getClassLoader(),
new Class[]{serviceInterface},
new RpcInvocationHandler()
);
}The RpcInvocationHandler.invoke method opens a new Socket, writes the request using RpcProtocol.writeRequest, reads the response with RpcProtocol.readResponse, checks the status byte, and returns the deserialized data. Short‑lived sockets are used for simplicity; production frameworks employ connection pools and long‑lived connections.
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
try (Socket socket = new Socket(host, port);
DataOutputStream out = new DataOutputStream(socket.getOutputStream());
DataInputStream in = new DataInputStream(socket.getInputStream())) {
RpcProtocol.writeRequest(out, method.getName(), args);
Object[] response = RpcProtocol.readResponse(in);
byte status = (Byte) response[0];
Object data = response[1];
if (status == RpcProtocol.STATUS_ERROR) {
throw new RuntimeException("Remote call failed: " + data);
}
return data;
}
}Running the Demo
Both server and client are started in the same JVM; the server listens on port 5005. The client invokes isPrime(17), isPrime(20), and add(3,5). Sample output shows server logs for each request and the client‑side results.
[RPC Server] Service started, listening on port 5005
[RPC Server] Received request: isPrime, args: 1
[RPC Server] Returned: true
isPrime(17) = true
[RPC Server] Received request: isPrime, args: 1
[RPC Server] Returned: false
isPrime(20) = false
[RPC Server] Received request: add, args: 2
[RPC Server] Returned: 8
add(3, 5) = 8What a Production‑Grade RPC Still Needs
Service discovery : replace the hard‑coded localhost address with a registry (e.g., Nacos, ZooKeeper).
Long connections and pooling : avoid the overhead of creating a socket per call.
High‑performance serialization : switch from JDK serialization to Protobuf, Hessian, etc., for smaller payloads and faster processing.
Timeouts and retries : handle network failures with configurable timeouts and retry policies.
Method routing with overload support : include parameter type information in the protocol.
Concurrent request handling : use a thread pool or asynchronous I/O (e.g., Netty) instead of a single‑threaded loop.
Observability : add tracing, metrics, and structured logging for debugging distributed calls.
Adding these capabilities turns the prototype into a framework comparable to Dubbo or gRPC.
Conclusion
RPC abstracts away network transport, serialization, method dispatch, and proxying so that distributed calls look like local ones. By assembling sockets, JDK serialization, reflection, and dynamic proxies, the core skeleton is straightforward. Understanding this skeleton makes it easier to read and troubleshoot mature frameworks, which simply refine each of these layers.
Signed-in readers can open the original source through BestHub's protected redirect.
This article has been distilled and summarized from source material, then republished for learning and reference. If you believe it infringes your rights, please contactand we will review it promptly.
samdeepthink
Knowledge Planet: Old Dock's Tech Chronicles Zhihu: SamDeepThinking A technical manager who still codes heavily on the front line. From junior developer to tech lead, then tech manager, now leading the whole front‑ and back‑end development team—leveling up along the way. I have some insights on programming, career development, and tech management.
How this landed with the community
Was this worth your time?
0 Comments
Thoughtful readers leave field notes, pushback, and hard-won operational detail here.
