Java-68 深入浅出 分布式服务 Netty实现自定义RPC 附详细代码

点一下关注吧!!!非常感谢!!持续更新!!!

🚀 AI篇持续更新中!(长期更新)

AI炼丹日志-29 - 字节跳动 DeerFlow 深度研究框斜体样式架 私有部署 测试上手 架构研究,持续打造实用AI工具指南!📐🤖

💻 Java篇正式开启!(300篇)

目前2025年07月07日更新到:
Java-65 深入浅出 分布式服务 网络通信 BIO NIO AIO 详解 附代码
MyBatis 已完结,Spring 已完结,Nginx已完结,Tomcat已完结,分布式服务正在更新!深入浅出助你打牢基础!

📊 大数据板块已完成多项干货更新(300篇):

包括 Hadoop、Hive、Kafka、Flink、ClickHouse、Elasticsearch 等二十余项核心组件,覆盖离线+实时数仓全栈!
大数据-278 Spark MLib - 基础介绍 机器学习算法 梯度提升树 GBDT案例 详解

请添加图片描述

Netty

Netty 是一个 面向 JVM 的异步事件驱动网络开发框架,目标是让高并发、高吞吐的网络程序开发像写普通业务代码一样简单。它在内部采用 Reactor/Proactor + 线程池 组合,将 I/O 多路复用、缓冲区管理、协议编解码等复杂细节彻底封装,从而让开发者只需关注 ChannelHandler 里的业务逻辑。

在这里插入图片描述

结构速览

  • Channel:套接字抽象,支持 NIO、Epoll、KQueue、io_uring、UDT、QUIC 等多种传输
  • EventLoop/Group:事件循环线程,单线程串行化 + 任务队列,天然避免锁竞争
  • ChannelPipeline:责任链,入站/出站双向流,动态编解码、聚合、加密
  • ByteBuf / Buffer:内存模型,直接内存池、零拷贝、引用计数,5.0 起改为更安全的 Buffer API

基于Netty自定义RPC

基本介绍

RPC(Remote Procedure Call)又称为远程过程调用,是分布式系统中服务间通信的重要方式。远程调用主要分为两种类型,现代微服务架构中服务间通信基本以这两种方式为主:

  1. 基于HTTP的RESTful形式的广义远程调用

    • 代表框架:Spring Cloud的Feign和RestTemplate
    • 通信协议:HTTP/HTTPS协议
    • 特点:
      • 遵循RESTful设计风格
      • 一般采用短连接方式
      • 协议参数和响应序列化以JSON格式为主,也支持XML格式
      • 典型的应用场景包括Web服务、跨语言系统集成等
  2. 基于TCP协议的RPC远程调用

    • 代表框架:阿里的Dubbo、gRPC等
    • 通信协议:自定义二进制协议
    • 特点:
      • 主要通过Netty实现4层网络协议
      • 采用NIO进行异步高效传输
      • 支持多种序列化方式:JSON、hessian2、protobuf、Java自带序列化等
      • 可配置性强,性能更高
      • 典型的应用场景包括高并发微服务、内部服务调用等

实现目标

我们将模仿Dubbo框架实现一个简单的RPC调用演示,包含以下核心功能:

  1. 消费者和生产者约定统一的接口和协议
  2. 消费者远程调用生产者的方法
    3.生产者返回一个字符串响应
  3. 消费者接收并打印返回数据

详细实现步骤

第一步:创建公共接口项目
  1. 新建Maven项目rpc-api
  2. 定义接口规范:
public interface DemoService {
    String sayHello(String name);
}
  1. 打包发布到本地仓库或私服,供生产者和消费者项目依赖
第二步:创建生产者服务
  1. 新建Maven项目rpc-provider,依赖rpc-api
  2. 实现接口:
public class DemoServiceImpl implements DemoService {
    @Override
    public String sayHello(String name) {
        return "Hello " + name + "!";
    }
}
  1. 配置Netty服务端:
    • 监听指定端口(如8080)
    • 使用自定义协议编解码器
    • 实现请求处理逻辑
  2. 启动时注册服务到注册中心(可选)
第三步:创建消费者服务
  1. 新建Maven项目rpc-consumer,依赖rpc-api
  2. 实现透明调用:
    • 创建动态代理类
    • 代理类内部使用Netty客户端发起远程调用
  3. 配置Netty客户端:
    • 连接生产者服务端
    • 序列化请求参数
    • 处理响应结果
  4. 测试调用:
public class ConsumerApp {
    public static void main(String[] args) {
        DemoService service = ProxyFactory.create(DemoService.class);
        String result = service.sayHello("World");
        System.out.println("Received: " + result);
    }
}
技术实现要点
  1. 序列化方案选择:建议使用hessian2或protobuf以提高性能
  2. 连接管理:需要实现连接池机制
  3. 超时处理:设置合理的调用超时时间
  4. 负载均衡:多服务提供者时需实现负载均衡策略
  5. 容错机制:失败重试、熔断降级等

通过以上步骤,可以完成一个基本的RPC调用演示,展现了RPC框架的核心工作原理。在实际生产环境中,还需要考虑服务发现、监控、链路追踪等更多功能。

公共模块

首先,在公共模块中添加 Netty 的依赖

<dependency>
<groupId>io.netty</groupId>
<artifactId>netty-all</artifactId>
<version>4.1.16.Final</version>
</dependency>

生产者和消费者都需要依赖这个模块,这样提供者来实现接口并提供网络调用,消费者直接根据接口来进行 TCP 通信及一定协议定制获取提供者的实现返回值。

接口定义

package icu.wzk.nettyrpc.service;

public interface WzkUserService {
    String sayHello(String str);
}

生产者

package icu.wzk.nettyrpc.service.impl;

import icu.wzk.nettyrpc.service.WzkUserService;
import icu.wzk.nettyrpc.hanlder.WzkUserServerHandler;
import io.netty.bootstrap.ServerBootstrap;
import io.netty.channel.ChannelInitializer;
import io.netty.channel.ChannelPipeline;
import io.netty.channel.nio.NioEventLoopGroup;
import io.netty.channel.socket.SocketChannel;
import io.netty.channel.socket.nio.NioServerSocketChannel;
import io.netty.handler.codec.string.StringDecoder;
import io.netty.handler.codec.string.StringEncoder;

public class WzkUserServiceImpl implements WzkUserService {

    @Override
    public String sayHello(String str) {
        System.out.println("调用函数: " + str);
        return "调用成功: " + str;
    }

    public static void startServer(String hostName, int port) throws Exception {
        NioEventLoopGroup eventLoopGroup = new NioEventLoopGroup();
        ServerBootstrap bootstrap = new ServerBootstrap();
        bootstrap
                .group(eventLoopGroup)
                .channel(NioServerSocketChannel.class)
                .childHandler(new ChannelInitializer<SocketChannel>() {
                    @Override
                    protected void initChannel(SocketChannel socketChannel) throws Exception {
                        ChannelPipeline pipeline = socketChannel.pipeline();
                        pipeline
                                .addLast(new StringDecoder())
                                .addLast(new StringEncoder())
                                .addLast(new WzkUserServerHandler());
                    }
                });
        bootstrap.bind(hostName, port);
    }
}

在实现中加入了 Netty 的服务启动程序,上面的代码添加了 String 类型的 Handler:

package icu.wzk.nettyrpc.hanlder;

import icu.wzk.nettyrpc.service.impl.WzkUserServiceImpl;
import io.netty.channel.ChannelHandlerContext;
import io.netty.channel.ChannelInboundHandlerAdapter;

public class WzkUserServerHandler extends ChannelInboundHandlerAdapter {
    @Override
    public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception {
        if (msg.toString().startsWith("WzkUserService")) {
            String result = new WzkUserServiceImpl()
                    .sayHello(msg.toString().substring(msg.toString().lastIndexOf("#") + 1));
            ctx.writeAndFlush(result);
        }
    }
}

我们通过这样的方式直接启动:

package icu.wzk.nettyrpc;

import icu.wzk.nettyrpc.service.impl.WzkUserServiceImpl;

public class WzkNettyRPCServer {
    public static void main(String[] args) throws Exception {
        WzkUserServiceImpl.startServer("localhost", 9999);
    }
}

消费者

消费者有一个需要注意的地方,就是调用需要透明,也就是说,框架使用者不用关心底层的网络实现。这里可以使用JDK的动态带来实现。
思路:客户端调用代理方法,返回一个实现了 HelloSerivce 接口的代理对象,调用代理对象的方法,返回结果。
我们需要在代理中做手脚,当调用代理方法的时候,我们需要初始化 Netty 客户端,还需要向服务端请求数据,并返回数据。
首先创建代理相关的类:

package icu.wzk.nettyrpc;

import icu.wzk.nettyrpc.hanlder.WzkUserClientHandler;
import icu.wzk.nettyrpc.service.WzkUserService;
import io.netty.bootstrap.Bootstrap;
import io.netty.channel.ChannelInitializer;
import io.netty.channel.ChannelOption;
import io.netty.channel.ChannelPipeline;
import io.netty.channel.EventLoopGroup;
import io.netty.channel.nio.NioEventLoopGroup;
import io.netty.channel.socket.SocketChannel;
import io.netty.channel.socket.nio.NioSocketChannel;
import io.netty.handler.codec.string.StringDecoder;
import io.netty.handler.codec.string.StringEncoder;

import java.lang.reflect.Proxy;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;

public class WzkNettyRPCClient {

    private static ExecutorService executor =
            Executors.newFixedThreadPool(Runtime.getRuntime().availableProcessors());
    private static WzkUserClientHandler client;

    public Object createProxy(final Class<?> serviceClass, final String providerName) {
        return Proxy.newProxyInstance(
                Thread.currentThread().getContextClassLoader(),
                new Class<?>[]{serviceClass}, (proxy, method, args) -> {
                    if (null == client) {
                        initClient();
                    }
                    client.setParam(providerName + args[0]);
                    return executor.submit(client).get();
                }
        );
    }

    private static void initClient() throws Exception {
        client = new WzkUserClientHandler();
        EventLoopGroup group = new NioEventLoopGroup();
        Bootstrap bootstrap = new Bootstrap();
        bootstrap
                .group(group)
                .channel(NioSocketChannel.class)
                .option(ChannelOption.TCP_NODELAY, true)
                .handler(new ChannelInitializer<SocketChannel>() {
                    @Override
                    protected void initChannel(SocketChannel socketChannel) throws Exception {
                        ChannelPipeline pipeline = socketChannel.pipeline();
                        pipeline
                                .addLast(new StringDecoder())
                                .addLast(new StringEncoder())
                                .addLast(client);
                    }
                });
        bootstrap.connect("localhost", 9999).sync();
    }

    public static void main(String[] args) throws Exception {
        String providerName = "WzkUserService#sayHello#";
        WzkNettyRPCClient wzkNettyRPCClient = new WzkNettyRPCClient();
        WzkUserService wzkUserService =
                (WzkUserService) wzkNettyRPCClient.createProxy(WzkUserService.class, providerName);
        while (true) {
            Thread.sleep(1000);
            System.out.println(wzkUserService.sayHello("hello???"));
        }
    }
}

创建代理的逻辑:使用 JDK 的动态代理技术,代理对象中的 invoke 方法实现如下:如果 Client 没有初始化,则初始化 Client。将参数设置到 Client,使用线程池调用 Client 的 call 方法并阻塞等待数据返回。

初始化客户端逻辑:创建一个 Netty 的客户端,并连接提供者,并设置一个自定义 Handler,和一些 String 类型的序列化方式。

Handler

package icu.wzk.nettyrpc.hanlder;

import io.netty.channel.ChannelHandlerContext;
import io.netty.channel.ChannelInboundHandlerAdapter;

import java.util.concurrent.Callable;

public class WzkUserClientHandler extends ChannelInboundHandlerAdapter implements Callable {

    private ChannelHandlerContext context;
    private String result;
    private String param;

    public void setParam(String param) {
        this.param = param;
    }

    @Override
    public void channelActive(ChannelHandlerContext ctx) throws Exception {
        context = ctx;
    }

    @Override
    public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception {
        synchronized (this) {
            result = msg.toString();
            notify();
        }
    }

    @Override
    public Object call() throws Exception {
        synchronized (this) {
            context.writeAndFlush(param);
            wait();
            return result;
        }
    }
}

该类缓存了 ChannelHandlerContext,用于下次使用,有两个属性:返回结果和请求参数。
当成功连接后,缓存 ChannelHandlerContext,当调用 call 方法的时候,将请求参数发送到服务端。当服务端收到并返回数据后,调用 ChannelRead 方法,将返回值赋值给 result,并唤醒等待在 call 方法上的线程。

测试结果

我们启动两个程序,Server 端结果如下所示:
在这里插入图片描述

客户端如下所示:
在这里插入图片描述

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

武子康

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值