基于Netty的简单RPC框架的实现

2,741 阅读11分钟

这是我参与8月更文挑战的第20天,活动详情查看:8月更文挑战

前言

现在网上有很多使用Netty构建RPC框架的例子。为什么我在这里写一篇文章来讨论它?我很清楚,我可能没有把它们写得那么好。我们写它有两个原因:

  • 首先,因为在学习了Netty之后,我们需要不断的练习才能更好的掌握Netty的使用。显然,基于 Netty 实现 RPC 框架是一个很好的实践。
  • 其次,因为市面上有很多RPC框架,比如Dubbo,都是框架通信底层的Netty,所以通过这个例子我们也可以更好的体验RPC的设计。

接下来,我将从以下几点来讲解如何基于Netty实现一个简单的RPC框架:

  • 什么是RPC?
  • 实现RPC框架需要注意哪些方面?
  • 如何使用 Netty?

什么是RPC?

RPC,Remote Procedure Call,可以像本地调用一样调用远程服务。它是进程间通信的一种方式。每个人都必须清楚这个概念。你可以看RPC文章,后来发现其实我们可以换一种方式来思考RPC,即从本地调用开始,再派生出RPC调用。

1.本地函数调用

局部函数对我们来说很常见,比如下面的例子:

public String sayHello(String name) {
    return "hello, " + name;
}

我们只需要传入一个参数,调用sayHello方法就可以得到一个输出,即输入参数->方法体->输出。输入参数、输出参数和方法体都在同一个进程空间中。这是本地函数调用。

2. 套接字通信

有没有办法在不同的进程之间进行通信?调用者在进程A中,需要调用方法A,而方法A在进程B中。

最容易想到的方法是使用Socket通信,使用Socket可以完成跨进程调用,我们需要约定一个进程通信协议来进行参数、调用函数、参数。写Socket应该知道Socket是一种比较原始的方式,我们需要多注意一些细节,比如参数和函数需要转换成字节流进行网络传输,也就是序列化操作,然后反序列化;使用socket进行底层通信,代码编程更容易出错。

如果一个调用者需要关注这么多问题,那无疑是一场灾难。那么有没有什么简单的方法可以让我们的调用者不需要关注细节,让调用者可以调用本地函数,也可以只调用参数,调用方法,等待返回结果呢?

3.RPC框架

RPC框架就是用来解决上述问题的。它使调用者能够调用远程服务,例如调用本地函数。底层通信细节对调用者是透明的。它屏蔽了各种复杂性,并为呼叫者提供了终极体验。

需要解决 RPC 调用的哪些方面

如前所述,RPC 框架允许调用者像调用本地函数一样调用远程服务。那你怎么做呢?

在使用时,调用者直接调用本地函数并传入相应的参数。它不需要处理其他细节。至于通信细节,交给RPC框架来实现。实际上,RPC 框架采用代理类的方式,特别是动态代理。它在运行时动态创建新的类,即代理类,并在该类中实现通信细节,如参数序列化等。

当然,不仅仅是序列化,我们还需要约定一个双方通信的协议格式,指定协议格式,比如请求参数的数据类型,请求的参数,方法名等请求等,这样就可以按照格式序列化后进行网络传输,然后服务器接收到请求对象,按照指定的格式进行解码,让服务器知道具体的音调。通过哪种方法,会传递哪些参数?

刚才提到网络传输,RPC框架的一个重要部分就是网络传输。服务部署在不同的主机上。如何高效地进行网络传输,尽可能不丢包,保证数据完整准确的快速传输?事实上,它使用了我们今天的主角 Netty 作为一个高性能的网络通信框架,足以完成我们的任务。

前面说了这么多,接下来的RPC框架应该重点关注哪些点呢?

  • 代理(动态代理)
  • 通讯协议
  • 序列化
  • 网络传输

当然,一个好的RPC框架需要注意的不止以上几点,但本文旨在做一个简单的RPC框架,了解以上要点就足够了。

基于Netty的RPC框架的实现

最后,本文的重点是根据需要注意的几个关键点(代理、序列化、协议、编解码)使用Netty来一一实现RPC。

1. 协议

首先,我们需要确定通信双方的协议格式、请求对象和响应对象。

请求对象:

@Data
@ToString
public class RpcRequest {
    /**
     * ID of Request Object
     */
    private String requestId;
    /**
     * Class name
     */
    private String className;
    /**
     * Method name
     */
    private String methodName;
    /**
     * Parameter type
     */
    private Class<?>[] parameterTypes;
    /**
     * Participation
     */
    private Object[] parameters;
}
  • 客户端使用请求对象的 ID 来验证服务器请求和响应是否匹配。

响应对象:

@Data
public class RpcResponse {
    /**
     * Response ID
     */
    private String requestId;
    /**
     * error message
     */
    private String error;
    /**
     * Return results
     */
    private Object result;
}

2.序列化

市面上的序列化协议很多,比如jdk、protobuf、kyro、Google的Hessian等,只要不选择JDK的序列化方式(因为它的性能太差,序列化产生的流太大),也可以使用其他方法。为方便起见,选择JSON作为序列化协议,使用fast JSON作为JSON框架。

为方便后续扩展,定义了序列化接口:

public interface Serializer {
    /**
     * java Object to binary
     *
     * @param object
     * @return
     */
    byte[] serialize(Object object) throws IOException;

    /**
     * Binary conversion to java objects
     *
     * @param clazz
     * @param bytes
     * @param <T>
     * @return
     */
    <T> T deserialize(Class<T> clazz, byte[] bytes) throws IOException;
}

因为我们采用的是JSON方式,所以定义了JSONSerializer的实现类:

public class JSONSerializer implements Serializer{

    @Override
    public byte[] serialize(Object object) {
        return JSON.toJSONBytes(object);
    }

    @Override
    public <T> T deserialize(Class<T> clazz, byte[] bytes) {
        return JSON.parseObject(bytes, clazz);
    }
}

如果以后要使用其他序列化方法,可以自行实现序列化接口。

3. 编解码器

协议格式和序列化方式约定好后,我们还需要编解码器。编码器将请求对象转换为适合传输的格式(通常为字节流),相应的解码器则是将网络字节流转换为应用程序的消息格式。

编码器实现:

public class RpcEncoder extends MessageToByteEncoder {
    private Class<?> clazz;
    private Serializer serializer;

    public RpcEncoder(Class<?> clazz, Serializer serializer) {
        this.clazz = clazz;
        this.serializer = serializer;
    }


    @Override
    protected void encode(ChannelHandlerContext channelHandlerContext, Object msg, ByteBuf byteBuf) throws Exception {
        if (clazz != null && clazz.isInstance(msg)) {
            byte[] bytes = serializer.serialize(msg);
            byteBuf.writeInt(bytes.length);
            byteBuf.writeBytes(bytes);
        }
    }
}

解码器实现:

public class RpcDecoder extends ByteToMessageDecoder {
    private Class<?> clazz;
    private Serializer serializer;

    public RpcDecoder(Class<?> clazz, Serializer serializer) {
        this.clazz = clazz;
        this.serializer = serializer;
    }
    @Override
    protected void decode(ChannelHandlerContext channelHandlerContext, ByteBuf byteBuf, List<Object> list) throws Exception {
        //Because before encoding, write a Int type, 4 bytes to indicate the length.
        if (byteBuf.readableBytes() < 4) {
            return;
        }
        //Mark the current reading position
        byteBuf.markReaderIndex();
        int dataLength = byteBuf.readInt();
        if (byteBuf.readableBytes() < dataLength) {
            byteBuf.resetReaderIndex();
            return;
        }
        byte[] data = new byte[dataLength];
        //Read the data in byteBuf into the data byte array
        byteBuf.readBytes(data);
        Object obj = serializer.deserialize(clazz, data);
        list.add(obj);
    }
}

4.Netty客户端

下面我们来看看Netty客户端是如何实现的,也就是如何使用Netty打开客户端。

其实熟悉Netty的朋友应该都知道,我们需要注意以下几点:

  • 写一个启动方法指定传输使用Channel
  • 指定ChannelHandler在网络传输中读写数据
  • 添加编解码器
  • 添加失败重试机制
  • 添加发送请求消息的方法

我们来看一下具体的实现代码:

@Slf4j
public class NettyClient {
    private EventLoopGroup eventLoopGroup;
    private Channel channel;
    private ClientHandler clientHandler;
    private String host;
    private Integer port;
    private static final int MAX_RETRY = 5;
    public NettyClient(String host, Integer port) {
        this.host = host;
        this.port = port;
    }
    public void connect() {
        clientHandler = new ClientHandler();
        eventLoopGroup = new NioEventLoopGroup();
        //Startup class
        Bootstrap bootstrap = new Bootstrap();
        bootstrap.group(eventLoopGroup)
                //Channel for specified transmission
                .channel(NioSocketChannel.class)
                .option(ChannelOption.SO_KEEPALIVE, true)
                .option(ChannelOption.TCP_NODELAY, true)
                .option(ChannelOption.CONNECT_TIMEOUT_MILLIS,5000)
                .handler(new ChannelInitializer<SocketChannel>() {
                    @Override
                    protected void initChannel(SocketChannel ch) throws Exception {
                        ChannelPipeline pipeline = ch.pipeline();
                        //Add encoder
                        pipeline.addLast(new RpcEncoder(RpcRequest.class, new JSONSerializer()));
                        //Add Decoder
                        pipeline.addLast(new RpcDecoder(RpcResponse.class, new JSONSerializer()));
                        //Request Processing Class
                        pipeline.addLast(clientHandler);
                    }
                });
        connect(bootstrap, host, port, MAX_RETRY);
    }

    /**
     * Failure reconnection mechanism, refer to Netty's entry actual gold digging Brochure
     *
     * @param bootstrap
     * @param host
     * @param port
     * @param retry
     */
    private void connect(Bootstrap bootstrap, String host, int port, int retry) {
        ChannelFuture channelFuture = bootstrap.connect(host, port).addListener(future -> {
            if (future.isSuccess()) {
                log.info("Successful connection to server");
            } else if (retry == 0) {
                log.error("The number of retries has been exhausted and the connection has been abandoned");
            } else {
                //The number of reconnections:
                int order = (MAX_RETRY - retry) + 1;
                //The interval between reconnections
                int delay = 1 << order;
                log.error("{} : Connection failed, para. {} Reconnect....", new Date(), order);
                bootstrap.config().group().schedule(() -> connect(bootstrap, host, port, retry - 1), delay, TimeUnit.SECONDS);
            }
        });
        channel = channelFuture.channel();
    }

    /**
     * send message
     *
     * @param request
     * @return
     */
    public RpcResponse send(final RpcRequest request) {
        try {
            channel.writeAndFlush(request).await();
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        return clientHandler.getRpcResponse(request.getRequestId());
    }
    @PreDestroy
    public void close() {
        eventLoopGroup.shutdownGracefully();
        channel.closeFuture().syncUninterruptibly();
    }
}

我们对数据处理的重点是ClientHandler类,它继承了ChannelDuplexHandler类,可以处理出站和入站数据。

public class ClientHandler extends ChannelDuplexHandler {
    /**
     * Maintaining mapping relationship between request object ID and response result Future using Map
     */
    private final Map<String, DefaultFuture> futureMap = new ConcurrentHashMap<>();
    @Override
    public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception {
        if (msg instanceof RpcResponse) {
            //Get the response object
            RpcResponse response = (RpcResponse) msg;
            DefaultFuture defaultFuture =
            futureMap.get(response.getRequestId());
            //Write the result to DefaultFuture
            defaultFuture.setResponse(response);
        }
        super.channelRead(ctx,msg);
    }

    @Override
    public void write(ChannelHandlerContext ctx, Object msg, ChannelPromise promise) throws Exception {
        if (msg instanceof RpcRequest) {
            RpcRequest request = (RpcRequest) msg;
            //Before sending the request object, save the request ID and construct a mapping relationship with the response Future.
            futureMap.putIfAbsent(request.getRequestId(), new DefaultFuture());
        }
        super.write(ctx, msg, promise);
    }

    /**
     * Get response results
     *
     * @param requsetId
     * @return
     */
    public RpcResponse getRpcResponse(String requsetId) {
        try {
            DefaultFuture future = futureMap.get(requsetId);
            return future.getRpcResponse(5000);
        } finally {
            //After success, remove from map
            futureMap.remove(requsetId);
        }
    }
}

参考文章:xilidou.com/2018/09/2..…

从上面的实现可以看出,我们定义了一个Map来维护请求ID和响应结果的映射关系。目的是验证服务器响应是否与请求匹配。因为 Netty 通道可能被多个线程使用,当结果返回时,你不知道从哪个线程返回,所以需要一个映射关系。

我们的结果封装在 DefaultFuture 中,因为 Netty 是一个异步框架,所有的返回都是基于 Future 和 Callback 机制。这里我们自定义Future来实现客户端“异步调用”

public class DefaultFuture {
    private RpcResponse rpcResponse;
    private volatile boolean isSucceed = false;
    private final Object object = new Object();

    public RpcResponse getRpcResponse(int timeout) {
        synchronized (object) {
            while (!isSucceed) {
                try {
                    object.wait(timeout);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
            return rpcResponse;
        }
    }

    public void setResponse(RpcResponse response) {
        if (isSucceed) {
            return;
        }
        synchronized (object) {
            this.rpcResponse = response;
            this.isSucceed = true;
            object.notify();
        }
    }
}
  • 实际上,使用了等待和通知机制,并由布尔变量辅助。

5. Netty 服务器

Netty server 的实现与client 类似,但需要注意的是,在对请求进行解码时,需要通过proxy 调用本地函数。以下是服务器端代码:

public class NettyServer implements InitializingBean {
    private EventLoopGroup boss = null;
    private EventLoopGroup worker = null;
    @Autowired
    private ServerHandler serverHandler;
    @Override
    public void afterPropertiesSet() throws Exception {
        //zookeeper is used here as the registry, which is not covered in this article and can be ignored.
        ServiceRegistry registry = new ZkServiceRegistry("127.0.0.1:2181");
        start(registry);
    }

    public void start(ServiceRegistry registry) throws Exception {
        //Thread pool responsible for handling client connections
        boss = new NioEventLoopGroup();
        //Thread pool for read and write operations
        worker = new NioEventLoopGroup();
        ServerBootstrap serverBootstrap = new ServerBootstrap();
        serverBootstrap.group(boss, worker)
                .channel(NioServerSocketChannel.class)
                .option(ChannelOption.SO_BACKLOG, 1024)
                .childHandler(new ChannelInitializer<SocketChannel>() {
                    @Override
                    protected void initChannel(SocketChannel ch) throws Exception {
                        ChannelPipeline pipeline = ch.pipeline();
                        //Add Decoder
                        pipeline.addLast(new RpcEncoder(RpcResponse.class, new JSONSerializer()));
                        //Add encoder
                        pipeline.addLast(new RpcDecoder(RpcRequest.class, new JSONSerializer()));
                        //Adding Request Processor
                        pipeline.addLast(serverHandler);

                    }
                });
        bind(serverBootstrap, 8888);
    }

    /**
     * If port binding fails, port number + 1, rebind
     *
     * @param serverBootstrap
     * @param port
     */
    public void bind(final ServerBootstrap serverBootstrap,int port) {
        serverBootstrap.bind(port).addListener(future -> {
            if (future.isSuccess()) {
                log.info("port[ {} ] Binding success",port);
            } else {
                log.error("port[ {} ] Binding failed", port);
                bind(serverBootstrap, port + 1);
            }
        });
    }

    @PreDestroy
    public void destory() throws InterruptedException {
        boss.shutdownGracefully().sync();
        worker.shutdownGracefully().sync();
        log.info("Close Netty");
    }
}

下面是处理读写操作的Handler类:

@Component
@Slf4j
@ChannelHandler.Sharable
public class ServerHandler extends SimpleChannelInboundHandler<RpcRequest> implements ApplicationContextAware {
    private ApplicationContext applicationContext;
    @Override
    protected void channelRead0(ChannelHandlerContext ctx, RpcRequest msg) {
        RpcResponse rpcResponse = new RpcResponse();
        rpcResponse.setRequestId(msg.getRequestId());
        try {
            Object handler = handler(msg);
            log.info("Get the return result: {} ", handler);
            rpcResponse.setResult(handler);
        } catch (Throwable throwable) {
            rpcResponse.setError(throwable.toString());
            throwable.printStackTrace();
        }
        ctx.writeAndFlush(rpcResponse);
    }

    /**
     * Server uses proxy to process requests
     *
     * @param request
     * @return
     */
    private Object handler(RpcRequest request) throws ClassNotFoundException, InvocationTargetException {
        //Use Class.forName to load Class files
        Class<?> clazz = Class.forName(request.getClassName());
        Object serviceBean = applicationContext.getBean(clazz);
        log.info("serviceBean: {}",serviceBean);
        Class<?> serviceClass = serviceBean.getClass();
        log.info("serverClass:{}",serviceClass);
        String methodName = request.getMethodName();

        Class<?>[] parameterTypes = request.getParameterTypes();
        Object[] parameters = request.getParameters();

        //Using CGLIB Reflect
        FastClass fastClass = FastClass.create(serviceClass);
        FastMethod fastMethod = fastClass.getMethod(methodName, parameterTypes);
        log.info("Start calling CGLIB Dynamic Proxy Execution Server-side Method...");
        return fastMethod.invoke(serviceBean, parameters);
    }

    @Override
    public void setApplicationContext(ApplicationContext applicationContext) throws BeansException {
        this.applicationContext = applicationContext;
    }
}

6. 客户端代理

客户端使用 Java 动态代理来实现代理类中的通信细节。众所周知,Java动态代理需要实现InvocationHandler接口。

@Slf4j
public class RpcClientDynamicProxy<T> implements InvocationHandler {
    private Class<T> clazz;
    public RpcClientDynamicProxy(Class<T> clazz) throws Exception {
        this.clazz = clazz;
    }

    @Override
    public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
        RpcRequest request = new RpcRequest();
        String requestId = UUID.randomUUID().toString();

        String className = method.getDeclaringClass().getName();
        String methodName = method.getName();

        Class<?>[] parameterTypes = method.getParameterTypes();

        request.setRequestId(requestId);
        request.setClassName(className);
        request.setMethodName(methodName);
        request.setParameterTypes(parameterTypes);
        request.setParameters(args);
        log.info("Request content: {}",request);
        
        //Open Netty client and connect directly
        NettyClient nettyClient = new NettyClient("127.0.0.1", 8888);
        log.info("Start connecting to the server:{}",new Date());
        nettyClient.connect();
        RpcResponse send = nettyClient.send(request);
        log.info("The request call returns the result:{}", send.getResult());
        return send.getResult();
    }
}
  • 在invoke方法中封装请求对象,构造NettyClient对象,打开客户端并发送请求消息

代理工厂如下:

public class ProxyFactory {
    public static <T> T create(Class<T> interfaceClass) throws Exception {
        return (T) Proxy.newProxyInstance(interfaceClass.getClassLoader(),new Class<?>[] {interfaceClass}, new RpcClientDynamicProxy<T>(interfaceClass));
    }
}
  • 通过 Proxy.newProxyInstance 为接口创建代理类

7.RPC远程调用测试

应用程序接口:

public interface HelloService {
    String hello(String name);
}
  • 准备测试 API 接口

客户:

@SpringBootApplication
@Slf4j
public class ClientApplication {
    public static void main(String[] args) throws Exception {
        SpringApplication.run(ClientApplication.class, args);
        HelloService helloService = ProxyFactory.create(HelloService.class);
        log.info("Response results“: {}",helloService.hello("pjmike"));
    }
}
  • 客户端调用接口的方法

服务器:

//Server-side implementation
@Service
public class HelloServiceImpl implements HelloService {
    @Override
    public String hello(String name) {
        return "hello, " + name;
    }
}

运行结果:

概括

最重要的是,我们基于Netty实现了一个非非常简单的RPC框架,离成熟的RPC框架还很远,甚至基本的registry都没有实现。但是通过这次的实践,我可以说对RPC有了更深入的了解,明白了一个RPC框架需要注意哪些方面。当我们以后使用成熟的RPC框架,比如Dubbo的时候,要能够了解它的底层,就是使用Netty作为基础的通信框架。未来,深入挖掘开源 RPC 框架源代码将相对容易。