How Does RPC Work? A Deep Dive into Building a Custom Java RPC Framework

This article explains the principles of Remote Procedure Call (RPC), walks through a complete end‑to‑end RPC workflow—including interface definition, service registration, Netty‑based network communication, request/response handling, and client‑side dynamic proxy generation—while providing runnable Java code examples.

Java Interview Crash Guide
Java Interview Crash Guide
Java Interview Crash Guide
How Does RPC Work? A Deep Dive into Building a Custom Java RPC Framework

Introduction

Remote Procedure Call (RPC) is a communication protocol that allows a program running on one machine to invoke a method on a program running on another machine as if it were a local call. This article analyses what happens during a basic RPC invocation.

What a Basic RPC Call Involves

Modern RPC frameworks such as Dubbo use interface‑based remote method invocation: the client only needs the interface definition to call the remote service. In Java, an interface cannot be called directly; a dynamic proxy generated via Proxy and InvocationHandler creates a proxy object that forwards method calls to the remote side.

Behind the scenes, RPC is essentially network communication between two hosts, which brings in serialization, deserialization, and codec concerns. In clustered deployments, service instances register themselves with a registry so that clients can discover available providers.

The overall flow of a basic RPC call is illustrated below:

RPC flow diagram
RPC flow diagram

Basic Implementation

Service Interface (Provider)

/**
 * @author Sun Hao
 * @description Service interface
 */
public interface HelloService {
    String sayHello(String somebody);
}

Service Implementation

/**
 * @author Sun Hao
 * @description Service implementation
 */
public class HelloServiceImpl implements HelloService {
    @Override
    public String sayHello(String somebody) {
        return "hello " + somebody + "!";
    }
}

Service Registration

Using Spring XML, the service implementation is loaded as a bean and registered to a custom namespace. An XSD defines attributes such as interface , timeout , serverPort , appKey , groupName , weight , and workerThreads . The XSD and its handler are placed under META-INF on the classpath.

<xsd:element name="service">
    <xsd:complexType>
        <xsd:complexContent>
            <xsd:extension base="beans:identifiedType">
                <xsd:attribute name="interface" type="xsd:string" use="required"/>
                <xsd:attribute name="timeout" type="xsd:int" use="required"/>
                <xsd:attribute name="serverPort" type="xsd:int" use="required"/>
                <xsd:attribute name="ref" type="xsd:string" use="required"/>
                <xsd:attribute name="weight" type="xsd:int" use="optional"/>
                <xsd:attribute name="workerThreads" type="xsd:int" use="optional"/>
                <xsd:attribute name="appKey" type="xsd:string" use="required"/>
                <xsd:attribute name="groupName" type="xsd:string" use="optional"/>
            </xsd:extension>
        </xsd:complexContent>
    </xsd:complexType>
</xsd:element>

The corresponding NamespaceHandler registers a ProviderFactoryBeanDefinitionParser that creates a ProviderFactoryBean bean definition.

ProviderFactoryBean

public class ProviderFactoryBean implements FactoryBean, InitializingBean {
    private Class<?> serviceItf;
    private Object serviceObject;
    private String serverPort;
    private long timeout;
    private String appKey;
    private String groupName = "default";
    private int weight = 1;
    private int workerThreads = 10;
    // ... getters/setters ...
    @Override
    public Object getObject() throws Exception {
        return serviceProxyObject; // proxy created later
    }
    @Override
    public Class<?> getObjectType() { return serviceItf; }
    @Override
    public void afterPropertiesSet() throws Exception {
        NettyServer.singleton().start(Integer.parseInt(serverPort));
        List<ProviderService> list = buildProviderServiceInfos();
        RegisterCenter.singleton().registerProvider(list);
    }
}

Network Communication (Server)

The provider uses Netty as a high‑performance NIO server. The server bootstrap configures boss and worker groups, channel options, and a pipeline that includes a decoder, an encoder, and a business handler NettyServerInvokeHandler.

public void start(final int port) {
    synchronized (NettyServer.class) {
        if (bossGroup != null || workerGroup != null) return;
        bossGroup = new NioEventLoopGroup();
        workerGroup = new NioEventLoopGroup();
        ServerBootstrap b = new ServerBootstrap();
        b.group(bossGroup, workerGroup)
         .channel(NioServerSocketChannel.class)
         .option(ChannelOption.SO_BACKLOG, 1024)
         .childOption(ChannelOption.SO_KEEPALIVE, true)
         .childOption(ChannelOption.TCP_NODELAY, true)
         .handler(new LoggingHandler(LogLevel.INFO))
         .childHandler(new ChannelInitializer<SocketChannel>() {
             @Override
             protected void initChannel(SocketChannel ch) {
                 ch.pipeline().addLast(new NettyDecoderHandler(StormRequest.class, serializeType));
                 ch.pipeline().addLast(new NettyEncoderHandler(serializeType));
                 ch.pipeline().addLast(new NettyServerInvokeHandler());
             }
         });
        try {
            channel = b.bind(port).sync().channel();
        } catch (InterruptedException e) {
            throw new RuntimeException(e);
        }
    }
}

The NettyServerInvokeHandler extracts the target method via reflection, applies a semaphore for rate‑limiting, invokes the method on the service object, and writes a StormResponse back to the client.

@Override
protected void channelRead0(ChannelHandlerContext ctx, StormRequest request) throws Exception {
    if (!ctx.channel().isWritable()) { logger.error("channel closed"); return; }
    ProviderService meta = request.getProviderService();
    String methodName = request.getInvokedMethodName();
    Method method = meta.getServiceMethod();
    Object result = method.invoke(meta.getServiceObject(), request.getArgs());
    StormResponse resp = new StormResponse();
    resp.setUniqueKey(request.getUniqueKey());
    resp.setResult(result);
    ctx.writeAndFlush(resp);
}

Request / Response Beans

public class StormRequest implements Serializable {
    private String uniqueKey;
    private ProviderService providerService;
    private String invokedMethodName;
    private Object[] args;
    private String appName;
    private long invokeTimeout;
    // getters / setters
}
public class StormResponse implements Serializable {
    private String uniqueKey;
    private long invokeTimeout;
    private Object result;
    // getters / setters
}

Client (Consumer) Side

The consumer creates a JDK dynamic proxy for the service interface. When a method is invoked, the proxy builds a StormRequest, selects a provider from the registry using a load‑balancing strategy, and sends the request over Netty.

public Object getProxy() {
    return Proxy.newProxyInstance(Thread.currentThread().getContextClassLoader(),
                                 new Class<?>[]{targetInterface}, this);
}

Service discovery reads the list of providers from Zookeeper, applies a clustering strategy (e.g., round‑robin), and picks an address.

String serviceKey = targetInterface.getName();
List<ProviderService> providers = RegisterCenter.singleton()
        .getServiceMetaDataMap4Consume().get(serviceKey);
ClusterStrategy strategy = ClusterEngine.queryClusterStrategy(clusterStrategy);
ProviderService chosen = strategy.select(providers);

The chosen provider’s IP and port are used to create an InetSocketAddress. A thread pool submits a RevokerServiceCallable that performs the Netty request and waits for a StormResponse with a timeout.

Future<StormResponse> future = fixedThreadPool.submit(
        RevokerServiceCallable.of(inetSocketAddress, request));
StormResponse resp = future.get(request.getInvokeTimeout(), TimeUnit.MILLISECONDS);
return resp != null ? resp.getResult() : null;

Because Netty returns results asynchronously, the server handler stores the response in a blocking queue ( RevokerResponseHolder) so the client thread can retrieve it synchronously.

@Override
protected void channelRead0(ChannelHandlerContext ctx, StormResponse response) {
    RevokerResponseHolder.putResultValue(response);
}

Running Example

The MainServer class loads storm-server.xml to publish the service, while the Client class loads storm-client.xml to obtain a HelloService bean and invoke sayHello("World").

public class MainServer {
    public static void main(String[] args) throws Exception {
        ClassPathXmlApplicationContext ctx =
            new ClassPathXmlApplicationContext("storm-server.xml");
        System.out.println("Service published");
    }
}

public class Client {
    public static void main(String[] args) throws Exception {
        ClassPathXmlApplicationContext ctx =
            new ClassPathXmlApplicationContext("storm-client.xml");
        HelloService hello = (HelloService) ctx.getBean("helloService");
        System.out.println(hello.sayHello("World"));
    }
}

Result Diagrams

Producer diagram
Producer diagram
Consumer diagram
Consumer diagram
Registry diagram
Registry diagram

Conclusion

This article presented a simple yet complete RPC workflow, covering service interface definition, registration to Zookeeper, Netty‑based network handling, request/response serialization, dynamic proxy generation on the client, and basic load‑balancing. The full source code is available at https://github.com/fankongqiumu/storm.git . Readers are encouraged to explore the repository for a more comprehensive implementation.

Original Source

Signed-in readers can open the original source through BestHub's protected redirect.

Sign in to view source
Republication Notice

This article has been distilled and summarized from source material, then republished for learning and reference. If you believe it infringes your rights, please contactadmin@besthub.devand we will review it promptly.

JavaRPCZooKeeperNetty
Java Interview Crash Guide
Written by

Java Interview Crash Guide

Dedicated to sharing Java interview Q&A; follow and reply "java" to receive a free premium Java interview guide.

0 followers
Reader feedback

How this landed with the community

Sign in to like

Rate this article

Was this worth your time?

Sign in to rate
Discussion

0 Comments

Thoughtful readers leave field notes, pushback, and hard-won operational detail here.