How to Build Your Own Distributed RPC Framework from Scratch
This article walks through the motivation, core components, technology choices, architecture, and implementation details—including service registration, provider and consumer modules, custom protocol design, serialization, load balancing, and Netty-based I/O—of a self‑written distributed RPC framework, and presents performance test results.
Preface
Why write your own RPC framework? From a personal growth perspective, understanding the essential elements of an RPC framework—service registration and discovery, load balancing, serialization protocol, RPC communication protocol, socket communication, asynchronous calls, circuit breaking, etc.—helps a developer comprehensively improve core skills. Reading source code alone can be superficial; building it yourself is the optimal path to mastery.
What is RPC
Remote Procedure Call (RPC) allows calling remote services as if they were local methods. Common implementations include gRPC, Dubbo, and Spring Cloud.
Distributed RPC Framework Elements
A distributed RPC framework relies on three basic components:
Service Provider
Service Consumer
Registry
These can be further extended to include service routing, load balancing, circuit breaking, serialization, and communication protocols.
1. Registry
The registry handles service registration and discovery. In dynamic cluster deployments, service addresses cannot be predetermined, so a unified registry is needed to discover services.
2. Service Provider (RPC Server)
The provider publishes service interfaces, registers metadata with the registry at startup, supports service deregistration, and starts a socket server to listen for client requests.
3. Service Consumer (RPC Client)
The consumer obtains service metadata from the registry, creates proxy objects, caches service addresses, selects a target address via a load‑balancing strategy, serializes request data, and communicates over sockets.
Technology Selection
Registry
Mature registries include Zookeeper, Nacos, Consul, and Eureka. This implementation supports both Nacos and Zookeeper, selectable via configuration.
IO Communication Framework
Netty is used as the underlying high‑performance, event‑driven, non‑blocking I/O framework.
Communication Protocol
TCP can cause packet fragmentation and aggregation (sticky packets). Three common solutions exist; this implementation adopts the third: a header containing magic number, version, type, and length, followed by the payload.
+--------+--------+--------+--------+--------+--------+--------+--------+--------+--------+--------+--------+--------+--------+--------+--------+
| BYTE | | | | | ........
+--------------------------------------------+--------+-----------------+--------+--------+--------+--------+--------+--------+-----------------+
| magic | version| type | content length | content byte[] |
+--------+-----------------------------------------------------------------------------------------+--------------------------------------------+The first byte is a magic number, e.g., 0x35.
The second byte denotes the protocol version.
The third byte indicates request type (0 for request, 1 for response).
The fourth byte specifies the length of the following content.
Serialization Protocol
The framework supports JavaSerializer, Protobuf, and Hessian. Protobuf is recommended for its compact binary format and high performance.
Load Balancing
Two main strategies are provided: random and round‑robin, each supporting weighted variants.
Overall Architecture
Implementation Details
Project Structure
Service Registration & Discovery
Zookeeper
Zookeeper uses a hierarchical node model similar to a file system. Persistent nodes store service names; temporary nodes under them hold IP, port, and serialization info.
Nacos
Nacos, an Alibaba open‑source microservice management middleware, provides registration and discovery via the NamingService interface (registerInstance, getAllInstances, subscribe).
Service Provider
OrcRpcAutoConfiguration initializes the registry and RPC boot starter. The server startup flow includes loading service metadata, starting Netty listeners, and handling Spring container events.
Service Consumer
The consumer creates proxy objects before the application fully starts, using either a Spring Context initialization listener or a BeanFactoryPostProcessor. This implementation uses the latter.
@Override
public void postProcessBeanFactory(ConfigurableListableBeanFactory beanFactory) throws BeansException {
this.beanFactory = beanFactory;
postProcessRpcConsumerBeanFactory(beanFactory, (BeanDefinitionRegistry) beanFactory);
}
private void postProcessRpcConsumerBeanFactory(ConfigurableListableBeanFactory beanFactory, BeanDefinitionRegistry beanDefinitionRegistry) {
String[] beanDefinitionNames = beanFactory.getBeanDefinitionNames();
int len = beanDefinitionNames.length;
for (int i = 0; i < len; i++) {
String beanDefinitionName = beanDefinitionNames[i];
BeanDefinition beanDefinition = beanFactory.getBeanDefinition(beanDefinitionName);
String beanClassName = beanDefinition.getBeanClassName();
if (beanClassName != null) {
Class<?> clazz = ClassUtils.resolveClassName(beanClassName, classLoader);
ReflectionUtils.doWithFields(clazz, new FieldCallback() {
@Override
public void doWith(Field field) throws IllegalArgumentException, IllegalAccessException {
parseField(field);
}
});
}
}
// registration logic omitted for brevity
}
private void parseField(Field field) {
OrcRpcConsumer orcRpcConsumer = field.getAnnotation(OrcRpcConsumer.class);
if (orcRpcConsumer != null) {
OrcRpcConsumerBeanDefinitionBuilder beanDefinitionBuilder = new OrcRpcConsumerBeanDefinitionBuilder(field.getType(), orcRpcConsumer);
BeanDefinition beanDefinition = beanDefinitionBuilder.build();
beanDefinitions.put(field.getName(), beanDefinition);
}
}Proxy Factory
public class JdkProxyFactory implements ProxyFactory {
@Override
public Object getProxy(ServiceMetadata serviceMetadata) {
return Proxy.newProxyInstance(serviceMetadata.getClazz().getClassLoader(), new Class[] {serviceMetadata.getClazz()},
new ClientInvocationHandler(serviceMetadata));
}
private class ClientInvocationHandler implements InvocationHandler {
private ServiceMetadata serviceMetadata;
public ClientInvocationHandler(ServiceMetadata serviceMetadata) {
this.serviceMetadata = serviceMetadata;
}
@Override
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
String serviceId = ServiceUtils.getServiceId(serviceMetadata);
ServiceURL service = InvocationServiceSelector.select(serviceMetadata);
OrcRpcRequest request = new OrcRpcRequest();
request.setMethod(method.getName());
request.setParameterTypes(method.getParameterTypes());
request.setParameters(args);
request.setRequestId(UUID.randomUUID().toString());
request.setServiceId(serviceId);
OrcRpcResponse response = InvocationClientContainer.getInvocationClient(service.getServerNet()).invoke(request, service);
if (response.getStatus() == RpcStatusEnum.SUCCESS) {
return response.getData();
} else if (response.getException() != null) {
throw new OrcRpcException(response.getException().getMessage());
} else {
throw new OrcRpcException(response.getStatus().name());
}
}
}
}IO Module (Netty)
The Netty IO service module handles client and server communication. Key classes include NettyNetClient, NettyNetServer, NettyClientChannelRequestHandler, and NettyServerChannelRequestHandler.
Encoder
@Override
protected void encode(ChannelHandlerContext ctx, ProtocolMsg msg, ByteBuf out) throws Exception {
// write magic number
out.writeByte(ProtocolConstant.MAGIC);
// write version
out.writeByte(ProtocolConstant.DEFAULT_VERSION);
// write message type
out.writeByte(msg.getMsgType());
// write content length
out.writeInt(msg.getContent().length);
// write content bytes
out.writeBytes(msg.getContent());
}Decoder
@Override
protected void decode(ChannelHandlerContext ctx, ByteBuf in, List<Object> out) throws Exception {
if (in.readableBytes() < BASE_LENGTH) {
return;
}
int beginIndex;
while (true) {
beginIndex = in.readerIndex();
in.markReaderIndex();
if (in.readByte() == ProtocolConstant.MAGIC) {
break;
}
in.resetReaderIndex();
in.readByte();
if (in.readableBytes() < BASE_LENGTH) {
return;
}
}
byte version = in.readByte();
byte type = in.readByte();
int length = in.readInt();
if (in.readableBytes() < length) {
in.readerIndex(beginIndex);
return;
}
byte[] data = new byte[length];
in.readBytes(data);
ProtocolMsg msg = new ProtocolMsg();
msg.setMsgType(type);
msg.setContent(data);
out.add(msg);
}Testing
On a MacBook Pro 13" (4‑core i5, 16 GB RAM) using Nacos as the registry, a single server and client with round‑robin load balancing were benchmarked using Apache ab.
8 threads, 10 000 requests: 18 s total, QPS ≈ 550.
100 threads, 10 000 requests: 13.8 s total, QPS ≈ 724.
Conclusion
Building this RPC framework reinforced knowledge of communication protocols, I/O frameworks, and exposed me to modern solutions like gRPC. Future work includes adding circuit‑breaking and other resilience mechanisms to continue learning and improving the project.
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.
Alibaba Cloud Developer
Alibaba's official tech channel, featuring all of its technology innovations.
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.
