【专栏】RPC系列(实战)-Hello Netty

106 阅读10分钟

关注公众号:离心计划,一起离开地球表面呀

【RPC系列合集】

【专栏】RPC系列(理论)-夜的第一章

【专栏】RPC系列(理论)-协议与序列化

【专栏】RPC系列(理论)-动态代理

【专栏】RPC系列(实战)-摸清RPC骨架

【专栏】RPC系列(实战)-优雅的序列化

【专栏】RPC系列(番外)-IO模型与线程模型

【专栏】RPC系列(番外)-“土气”的IO实现

【专栏】RPC系列(实战)-Hello Netty

【专栏】RPC系列(实战)-低配版NameServer

【专栏】RPC系列(实战)-负重前行的“动态代理”

作者:离心计划
链接:juejin.cn/post/715165…
来源:稀土掘金
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。

| 前言

    上一节【专栏】RPC系列(番外)-“土气”的IO实现 中我们用了Java中最朴素的方式实现了在番外篇【专栏】RPC系列(番外)-IO模型与线程模型 中的各种IO模型与线程模型,大家应该有了一个基本的印象,计算机世界的产物本质上都是封装,如果有一个框架帮我们做掉了复杂的连接管理、IO模型以及线程模型等等,甚至提供了一系列友好的API,那就万事大吉了,Netty就是这样的一个网络框架,它强大到各种你熟悉的分布式框架都会使用它作为网络层工具,比如Dubbo、RocketMq等等,那么在我们Sparrow-Rpc中也会使用Netty帮我们处理请求/响应这一基本通信方式,首先我们介绍一下Netty中的各种组件。

    本节代码:github.com/JAYqq/my-sp…

| Netty组件

Channel

    Channel 是NIO中的概念,在前一节代码的代码中也有体现,Channel是对底层操作系统Socket的封装抽象,提供了java层操作Socket的Api,Channel 的生命周期包括:

  1. ChannelUnregisteredChannel 已经被创建,但还未注册到EventLoop

  2. ChannelRegisteredChannel 已经被注册到了EventLoop

  3. ChannelActiveChannel 处于活跃状态,允许进行收发数据

  4. ChannelInactiveChannel 还没连接成功

ChannelFuture

    Netty中对于Channel的IO操作都是异步的,都会返回ChannelFuture对象并可以通过addListener方法给这个IO操作注册一个ChannelFutureListener用于IO事件回调

ChannelPipline

    一个channel对应一个pipline,pipline顾名思义就是管道,按顺序将channel收发的数据流向一个个ChannelHandler,所以ChannelHandler是组成ChannelPipline的基本单位。

    ChannelPipline还有一个重要的功能就是区分ChannelHandler,因为input的数据和output的数据可能只需要按顺序经过pipline上的某几个handler就行,ChannelPipline会确保数据只会在具有相同定向类型的两个ChannelHandler 之间传递

ChannelHandler

    用于处理对应Channel上发生的任何事件,事件类型主要有两种:Channel状态变化和Channel读写数据变化。

    这也是开发者重点关注的组件,消息的前后置处理是留给开发者处理的。

ChannelHandlerContext

    可以看到ChannelHandler的方法参数都有ChannelHandlerContext,这是作为ChannelPipline整个传递过程的上下文,这种策略在业务代码中也经常出现,将整个请求周期内的有效数据放到一个Context中流转,这种做法面对复杂的业务场景需要繁多的上下文信息很有效,当然也会出现BigObject的问题

EventLoop和Event Loop Group

    我们在【专栏】RPC系列(番外)-“土气”的IO实现 中发现处理IO的线程都会处在一个循环中,循环中的内容取决于我们对这个线程的定义,如果是监听连接的线程那么我们只要accept后有连接就分配给另一个线程去监听后续的读写事件,监听读写事件也是处在一个循环中等待读写事件发生,这个循环就是Loop,读写事件就是Event,合起来就是EventLoop。而在Netty中,我们不用自己定义线程,只要创建EventLoopGroup就可以,里面的线程数量也就是EventLoop的数量由开发者自己决定。

| Server端实现

    首先引入Netty的依赖:

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

    基于面向接口的初衷,我们定义一个TransportServer接口类表示实现网络传输的Server

public interface TransportServer extends Closeable {
    void start() throws Exception;
    @Override
    void close();
}

    然后创建一个NettyServer实现类,根据上面Netty的组件我们需要定义一个Channel以及两个EventGroup,为了使用在我们序列化小节提到的SPI机制创建一个配置文件(com.sparrow.rpc.core.TransportServer)指向NettyServer

com.sparrow.rpc.core.netty.server.NettyServer

public class NettyServer implements TransportServer {
    private final int port = 8888;
    private EventLoopGroup acceptEventGroup;
    private EventLoopGroup workerEventGroup;
    private Channel channel;
}

    然后我们主要在start方法中启动我们的NettyServer,整个start方法如下,我们一个个讲

@Override
public void start() throws Exception {
    //专门处理连接的group,相当于多路复用的监听线程
    acceptEventGroup = buildEventGroup();
    //处理io事件的worker
    workerEventGroup = buildEventGroup();
    ChannelHandler channelHandler = buildChannelHandler();
    ServerBootstrap serverBootstrap = buildBoostrap(acceptEventGroup, workerEventGroup, channelHandler);
    //绑定host和端口
    bindChannel(serverBootstrap);
}

    首先是创建EventLoopGroup,方法如下,由于Epoll并不是在所有操作系统上都支持,所以我们动态决定使用select还是epoll作为selector

private EventLoopGroup buildEventGroup() {
    if (Epoll.isAvailable()) {
        return new EpollEventLoopGroup();
    } else {
        return new NioEventLoopGroup();
    }
}

    然后是构建ChannelHandler处理channel上的每一个IO事件,对应的是ChannelPipline将重要的几个组件串在一起,我们这里包括了四个部分

  1. IdleStateHandler 用于心跳检测,前三个参数分别表示读、写、读写三个超时时间

  2. RpcCommandDecoder 由于Server端是接收请求,所以需要一个对RpcCommand的反序列化步骤

  3. RpcResponseEncoder 对返回的RpcResponse进行序列化

  4. RpcRequestHandler 对每个接收到的请求进行处理的步骤

    private ChannelHandler buildChannelHandler() { return new ChannelInitializer() { @Override protected void initChannel(Channel channel) throws Exception { channel.pipeline() .addLast(new IdleStateHandler(10, 0, 0, TimeUnit.SECONDS)) .addLast(new RpcCommandDecoder()) .addLast(new RpcResponseEncoder()) .addLast(new RpcRequestHandler()); } }; }

    先贴一下到目前为止我们的项目代码结构是这样的,大家可以对照文章开头的项目地址查看此节的代码,上面涉及到的四个部分我们稍后会讲,不急

                     \

    接着NettyServer的构建,下一步是BootStrap的构建,BootStrap可以理解为Netty的控制中心,它把在此之前创建的EventLoopGroup、ChannelHandler都绑定到一起。这里涉及到Netty可对连接的配置,包括对TCP的配置,通过childOption进行配置,这里就不展开了

private ServerBootstrap buildBoostrap(EventLoopGroup acceptLoopGroup, EventLoopGroup workerEventGroup, ChannelHandler channelHandler) {
    ServerBootstrap serverBootstrap = new ServerBootstrap();
    serverBootstrap.channel(Epoll.isAvailable() ? EpollServerSocketChannel.class : NioServerSocketChannel.class)
            .group(acceptLoopGroup, workerEventGroup)
            .childHandler(channelHandler)
            //是否开启Nagle算法,该算法会缓存网络数据包,相当于批处理(适合高流量,不适合实时性高场景)
            .childOption(ChannelOption.TCP_NODELAY, true)
            //是否连接保活,可以复用连接,减少建立连接开销
            .childOption(ChannelOption.SO_KEEPALIVE, true);
    return serverBootstrap;
}

    bootstrap创建后相当于IO模型、线程模型都已经搭建完成,最后只需要把我们监听的端口和bootstrap绑定到一起生成对外的这个channel就可以进行对外连接了

private void bindChannel(ServerBootstrap serverBootstrap) throws InterruptedException {
    this.channel = serverBootstrap.bind("localhost", port)
            .sync()
            .channel();
}

ChannelHandler的实现

    刚才在实现ChannelHandler的时候提到了Encoder、Decoder还有两个handler,这里我们先讲序列化与反序列化。

    在章节【专栏】RPC系列(实战)-优雅的序列化 中我们了解过了序列化,也实现了几种不同的序列化方式,其实这里的实现原理和之前对RpcRequest的序列化是一样的,把一个个的字段通过put、putInt等方法放到一个ByteBuffer中就行了,只不过我们因为和Netty相关这里用的不是ByteBuffer而是Netty自定义的ByteBuf,篇幅原因我们贴一下RpcResponseEncoder的方法,这里我们直接用了序列化小结准备的Hessian方式

public class RpcResponseEncoder extends MessageToByteEncoder<RpcResponse> {
    @Override
    protected void encode(ChannelHandlerContext channelHandlerContext, RpcResponse rpcResponse, ByteBuf byteBuf) throws Exception {
        byte[] bytes = SerializeSupport.serialize(rpcResponse, SerializerType.HESSIAN.getType());
        byteBuf.writeInt(bytes.length);
        byteBuf.writeBytes(bytes);
    }
}

    而关于handler我们先介绍IdleStateHandler。心跳检测是为了保活,在这里也就是保证连接的正常。IdleStateHandler的机制是通过传入的三个参数,分别对读事件、写事件或者读写事件进行检测,如果配置了readerIdleTime是3秒,那么当前channel如果读事件没出现超过三秒,就会调用 userEventTrigger 方法,所以IdleHandler配置完了后就要在我们自定义的Handler中重写userEventTrigger方法

public IdleStateHandler(
            long readerIdleTime, long writerIdleTime, long allIdleTime,
            TimeUnit unit)

    RpcRequestHandler是核心组件,主要处理接收到请求后要做的事情,在写代码之前想清楚,我们拿到请求后要做的事情主要分为三步

  1. 根据服务签名找到对应的服务

  2. 反序列化请求参数,调用服务对应方法

  3. 拿到结果组装成RpcResponse返回给客户端

    这三步逻辑都在invokeService方法中,这里还区别了一下心跳的请求。代码中涉及到一个ServiceHub 这个在之后介绍NameServer与服务注册的时候会讲,可以直接拷贝这个类到目录下就行

private RpcResponse invokeService(RpcCommand command) {
    RpcResponse response = new RpcResponse();
    response.setHeader(command.getHeader());
    //心跳检测请求返回Pong
    if (CommandTypes.RPC_HEARTBEAT_REQUEST.getType().equals(command.getHeader().getType())) {
        response.getHeader().setType(CommandTypes.RPC_HEARTBEAT_RESPONSE.getType());
        response.setData("Pong");
    } else {
        response.getHeader().setType(CommandTypes.RPC_RESPONSE.getType());
        RpcRequest rpcRequest = SerializeSupport.parse(command.getData());
        //找到ServiceHub中注册的服务
        Object service = ServiceHub.getInstance().getService(rpcRequest.buildServiceSign());
        if (Objects.isNull(service)) {
            response.setCode(RspCode.UNKNOWN_SERVICE.getCode());
            response.setErrorMsg(RspCode.UNKNOWN_SERVICE.getMessage());
            log.warn("Service named {} not found!", rpcRequest.buildServiceSign());
            return response;
        }
        //反序列化参数
        Object[] args = SerializeSupport.parse(rpcRequest.getParameters());
        try {
            Class[] argClass = new Class[args.length];
            for (int i = 0; i < args.length; i++) {
                argClass[i] = args[i].getClass();
            }
            //调用服务对应方法
            Method method = service.getClass().getMethod(rpcRequest.getMethodName(), argClass);
            Object ans = method.invoke(service, args);
            //组装response返回
            response.setData(ans);
            response.setCode(RspCode.SUCCESS.getCode());
        } catch (Exception e) {
            log.warn("invoke error", e);
            response.setCode(RspCode.UNKNOWN_SERVICE.getCode());
            response.setErrorMsg(RspCode.UNKNOWN_SERVICE.getMessage());
        }
    }
    return response;
}

    对于心跳检测中对应空闲事件发生后出发的方法,我们简单粗暴的处理,发生后就直接断开连接

@Override
public void userEventTriggered(ChannelHandlerContext ctx, Object evt) throws Exception {
    if (evt instanceof IdleStateEvent) {
        IdleState state = ((IdleStateEvent) evt).state();
        //如果是读空闲
        if (state == IdleState.READER_IDLE) {
            log.info("Idle state error,prepare to close");
                ctx.close();
            }
        } else {
            super.userEventTriggered(ctx, evt);
        }
    }

    至此我们的NettyServer完成的差不多了,文章中只讲解了重要代码片段,具体代码参考文章开头链接此节的代码。

| Client实现

    Client的Netty部分实现方式和Server是差不多的,涉及到的也是相关的一些组件,主要讲解一下项目设计上,为了对客户端调用方便,我们设计了两个接口TransportClient 和 RpcTransport ,RpcTransport 主要负责提供发送请求的功能,TransportClient 负责创建RpcTransport 目的也是为了利用SPI可拔插,客户端可以通过TransportClient 创建想要的 RpcTransport ,所以我们的NettyClient是实现了TransportClient ,记得也要对这两个接口配置SPI的配置文件。

    Client的ChannelPipline上同样和Server一样要串上四个部分:IdleStateHandler、一对Decoder和Encoder以及自定义Handler。IdleStateHandler在客户端是对写事件的超时控制,长时间没有发送请求就会触发 userEventTrigger 方法,逻辑也简单就是判断是写空闲后往channel中发送心跳上报的请求就行了,通过头部中的type区别正常请求和心跳请求

@Override
public void userEventTriggered(ChannelHandlerContext ctx, Object evt) throws Exception {
    if (evt instanceof IdleStateEvent) {
        IdleStateEvent event = (IdleStateEvent) evt;
        if (event.state() == IdleState.WRITER_IDLE) {
            log.info("Idle send to [{}]", ctx.channel().remoteAddress());
            RpcCommand rpcCommand = new RpcCommand();
            RpcHeader header = new RpcHeader();
            header.setType(CommandTypes.RPC_HEARTBEAT_REQUEST.getType());
            header.setVersion("v1.0");
            header.setTraceId(UUID.randomUUID().toString());
            rpcCommand.setHeader(header);
            rpcCommand.setData("Ping".getBytes(StandardCharsets.UTF_8));
            ctx.channel().writeAndFlush(rpcCommand).addListener(channelListenFuture -> {
                if (!channelListenFuture.isSuccess()) {
                    log.warn("Send request error");
                }
            });
        }
    } else {
        super.userEventTriggered(ctx, evt);
    }
}

    客户端与服务端的Pipline的传递方式就像是这样:

    在 createChannel 方法中用到一个 ChannelRegister ,它用来避免重复创建连接,可以连接复用。

    我们重点讲解一下NettyTransport请求,它实现了唯一的方法 send ,我们在实现了 TransportClient 的 NettyClient 中创建它,传入了一个channel告诉它需要往哪里send数据

public CompletableFuture<RpcResponse> send(RpcCommand request) {
    if (!channel.isActive()) {
        throw new IllegalStateException("Unhealthy channel state");
    }
    CompletableFuture<RpcResponse> future = new CompletableFuture<>();
    try {
        pendingRequests.put(request.getHeader().getTraceId(), future);
        channel.writeAndFlush(request).addListener(channelListenFuture -> {
            if (!channelListenFuture.isSuccess()) {
                log.warn("Send request error");
                future.completeExceptionally(channelListenFuture.cause());
                channel.close();
            }
        });
    } catch (Exception e) {
        log.warn("Send message error");
        future.completeExceptionally(e);
        pendingRequests.remove(request.getHeader().getTraceId());
    }
    return future;
}

    用到了PendingRequest 用来装载在途请求,里面存在的请求是还没拿到响应的请求,有了这个在途请求我们才能在RpcResponseHandler中当拿到响应后,根据头部的traceId找到这个响应对应的请求并通过future.complete方法通知被这个future.get 阻塞的请求线程 (com.sparrow.mason.core.netty.handler.RpcResponseHandler)

@Override
protected void channelRead0(ChannelHandlerContext channelHandlerContext, RpcResponse rpcResponse) throws Exception {
    String type = rpcResponse.getHeader().getType();
    if (CommandTypes.RPC_HEARTBEAT_RESPONSE.getType().equals(type)) {
        log.info("HeartBeat [{}]", rpcResponse.getData());
    } else if (CommandTypes.RPC_RESPONSE.getType().equals(type)) {
        CompletableFuture<RpcResponse> rspFuture = PendingRequests.getInstance().remove(rpcResponse.getHeader().getTraceId());
        //通过complete将reponse返回给对应future的get阻塞线程
        rspFuture.complete(rpcResponse);
    }
}

    最后我们梳理一下一个正常的RpcCommand通过刚才写的这么多代码发送的一个路径

  1. 客户端调用NettyClient的createTransport方法创建NettyTransport

  2. 组装好RpcCommand后调用NettyTransport的send方法,send方法将请求写入建立好连接的channel中返回一个future,并将future暂存到PendingRequest中

  3. 客户端拿到future后调用future.get方法阻塞等待数据到来

  4. RpcCommand来到NettyServer的ChannelPipline流转,经过反序列化以及RpcRequestHandler的处理后,拿到远端服务对应方法的结果,组装成RpcResponse,写到channel中返回给客户端

  5. 客户端拿到Response后经过反序列化后从PendingRequest拿到对应的future,调用complete方法完成这个future

  6. 被future阻塞的线程拿到结果,做自定义处理

| 小结

    这一节代码量有点大,非常建议先把代码clone下来对照着看会清楚很多,最后的流程梳理应该对大家也有帮助。至此我们通过两篇番外篇+一篇实战篇熟悉了Netty所涉及的知识,也完成了Sparrow-Rpc的Netty部分开发,下一节我们会讲解服务注册的流程,NameServer我们会以很简单的方式实现,有一说一不规范,但是你懂了它是做什么的,那么怎么做只是一种选择,下周见~