Understanding RPC Implementation Principles and a Basic Java RPC Framework Example

This article explains the fundamentals of Remote Procedure Call (RPC), detailing the end‑to‑end workflow from defining service interfaces and implementations, registering services in a Zookeeper registry, handling network communication with Netty, and generating client‑side proxies using Spring and JDK dynamic proxies, all illustrated with complete Java code examples.

Architecture Digest
Architecture Digest
Architecture Digest
Understanding RPC Implementation Principles and a Basic Java RPC Framework Example

Introduction

The article discusses the principles of Remote Procedure Call (RPC), defining RPC as a protocol that allows a program on one machine to invoke a sub‑program on another machine without extra programming effort.

What a Basic RPC Call Involves

Popular RPC frameworks such as Dubbo rely on interface‑based remote method invocation. The client only needs the interface definition, and Java generates a dynamic proxy via Proxy and InvocationHandler. The proxy’s #invoke method performs the remote call and obtains the result.

RPC fundamentally requires network communication, serialization/deserialization, and codec handling. In clustered deployments, services register themselves with a registry (e.g., Zookeeper) to enable service discovery.

Basic Implementation

Service Side (Producer)

Service Interface

/**
 * @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, a custom XML namespace registers the service implementation and publishes it to the registry. Example XSD fragment:

<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 Spring configuration registers the service bean and the custom storm:service element with attributes such as interface, ref, groupName, weight, appKey, workerThreads, serverPort, and timeout.

Network Communication

The producer uses Netty as a high‑performance NIO framework. The Netty server bootstrap configures boss and worker groups, channel options, and adds handlers for decoding, encoding, and business logic.

public void start(final int port) {
    synchronized (NettyServer.class) {
        if (bossGroup != null || workerGroup != null) {
            return;
        }
        bossGroup = new NioEventLoopGroup();
        workerGroup = new NioEventLoopGroup();
        ServerBootstrap serverBootstrap = new ServerBootstrap();
        serverBootstrap
            .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) throws Exception {
                    ch.pipeline().addLast(new NettyDecoderHandler(StormRequest.class, serializeType));
                    ch.pipeline().addLast(new NettyEncoderHandler(serializeType));
                    ch.pipeline().addLast(new NettyServerInvokeHandler());
                }
            });
        try {
            channel = serverBootstrap.bind(port).sync().channel();
        } catch (InterruptedException e) {
            throw new RuntimeException(e);
        }
    }
}

The core server handler NettyServerInvokeHandler extends SimpleChannelInboundHandler and processes incoming StormRequest objects, performs service lookup, reflection‑based method invocation, and writes back a StormResponse.

@Override
protected void channelRead0(ChannelHandlerContext ctx, StormRequest request) throws Exception {
    if (ctx.channel().isWritable()) {
        ProviderService metaDataModel = request.getProviderService();
        long consumeTimeOut = request.getInvokeTimeout();
        String methodName = request.getInvokedMethodName();
        // Service lookup and semaphore‑based rate limiting omitted for brevity
        Method method = localProviderCache.getServiceMethod();
        Object result = method.invoke(serviceObject, request.getArgs());
        StormResponse response = new StormResponse();
        response.setInvokeTimeout(consumeTimeOut);
        response.setUniqueKey(request.getUniqueKey());
        response.setResult(result);
        ctx.writeAndFlush(response);
    } else {
        logger.error("------------channel closed!---------------");
    }
}

Custom encoders/decoders inherit from Netty’s MessageToByteEncoder and ByteToMessageDecoder, allowing plug‑in of serialization frameworks such as Hessian or Protobuf.

Request and Response Wrappers

public class StormRequest implements Serializable {
    private static final long serialVersionUID = -5196465012408804755L;
    private String uniqueKey;
    private ProviderService providerService;
    private String invokedMethodName;
    private Object[] args;
    private String appName;
    private long invokeTimeout;
    // getters and setters omitted
}
public class StormResponse implements Serializable {
    private static final long serialVersionUID = 5785265307118147202L;
    private String uniqueKey;
    private long invokeTimeout;
    private Object result;
    // getters and setters omitted
}

Client Side (Consumer)

The client generates a dynamic proxy for the service interface, discovers service instances from the registry, selects one using a load‑balancing strategy, and sends the request via Netty.

public Object getProxy() {
    return Proxy.newProxyInstance(Thread.currentThread().getContextClassLoader(),
        new Class<?>[]{targetInterface}, this);
}
String serviceKey = targetInterface.getName();
List<ProviderService> providerServices = registerCenter4Consumer.getServiceMetaDataMap4Consume().get(serviceKey);
ClusterStrategy clusterStrategyService = ClusterEngine.queryClusterStrategy(clusterStrategy);
ProviderService providerService = clusterStrategyService.select(providerServices);

The invocation handler builds a StormRequest, submits it to a fixed‑size thread pool, and waits synchronously for the StormResponse returned by Netty.

Future<StormResponse> responseFuture = fixedThreadPool.submit(RevokerServiceCallable.of(inetSocketAddress, request));
StormResponse response = responseFuture.get(request.getInvokeTimeout(), TimeUnit.MILLISECONDS);
if (response != null) {
    return response.getResult();
}

Netty’s client handler stores asynchronous responses into a blocking queue so the invoking thread can retrieve them synchronously.

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

Running Example

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

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

Conclusion

The article provides a concise end‑to‑end walkthrough of RPC, covering service interface definition, registration, network transport with Netty, server‑side handling, client‑side proxy generation, service discovery, and load balancing, enabling readers to build a simple yet functional RPC framework in Java.

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.

JavaRPCserializationspringZooKeeper
Architecture Digest
Written by

Architecture Digest

Focusing on Java backend development, covering application architecture from top-tier internet companies (high availability, high performance, high stability), big data, machine learning, Java architecture, and other popular fields.

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.