Java Netty通信编程

前些天分享了关于Socket与NIO通信编程的文章,没看过的朋友可以阅读一下

Java Socket通信编程     Java NIO通信编程

基于传统NIO进行通信编程是很让人头秃的一件事,幸好有大佬已经提供了完整的解决方案:Netty。Netty的API很简洁,屏蔽了NIO中复杂的细节,使用起来非常方便顺手。下面来看一下Netty的来龙去脉并用它实现一个简单又不失全面的通信demo。

Netty的历史

Netty是由Trustin Lee(韩国)于2004年开发,各版本的时间线如下

  • 2004年6月 Netty2 发布

    声称是Java社区中第一个基于事件驱动的应用网络框架

  • 2008年10月 Netty3 发布

  • 2013年7月 Netty4 发布

  • 2013年12 发布Netty5.0.0.Alpha 1

  • 2015年11月废弃5.0.0

Trustin Lee在一开始编写Netty时,任职于JBOSS公司,所以Netty3.x及之前的版本包名都是 org.jboss.netty。而编写Netty4.x 时,他自己搞了Netty社区出来,用来研发及维护,相应的包名也改为了 io.netty。

另外,MINA这个框架也是作者参与研发的。有个人叫Alex,他为 Apache Directory 开发网络框架,但是觉得不好用,看到 Netty2 后找到作者邀请合作开发,结合两种框架,随后有了MINA。 

如今Netty和MINA都在更新维护中,但是Netty作为Trustin Lee的亲儿子,当然会更受他推崇,根据现在的使用情况来看,使用Netty也确实是主流。

Netty5.0版本为什么做出来后又被废弃了呢,主要是因为

  • 过于复杂,并且性能上没有优势
  • 精力有限,维护不过来了

所以干脆不要它了。

相比原生NIO,Netty的优点

netty做的更多:

  • 支持常用的应用层协议        
  • 解决传输问题:粘包、半包现象
  • 支持流量整形, 流量控制,黑白名单等
  • 完善的断连、idle等异常处理

netty做的更好:

  • 规避了JDK NIO bug

               经典epoll bug:异常唤醒导致cpu 100%

  • api更友好、更强大
  • 隔离变化,屏蔽细节,oio、nio的模式切换代码改动量很小

很多项目都已经用上了Netty

  • 数据库:Cassandra
  • 大数据:Spark、Hadoop
  • Message Queue:Rocketmq
  • 检索:Elasticsearch
  • 框架:gRPC、Aparche Dubbo、Spring5
  • 分布式协调器:Zookeeper
  • 工具类:async-http-client

Netty的关键组件

  1. Channel(通道):

    Channel是Netty中数据的载体,代表了一个开放的连接,可以进行读取、写入、绑定等操作。在Netty中,Channel是与底层I/O操作相关联的。
  2. EventLoop(事件循环):

    EventLoop是Netty中处理所有事件的核心抽象。它负责处理连接的创建、数据的读写、定时任务等。Netty应用程序通常包含一个或多个EventLoop,每个EventLoop都可以被多个Channel注册,一个Channel的所有事件都归该EventLoop处理。
  3. ChannelHandler(通道处理器):

    ChannelHandler是用于处理Channel中各种事件的接口。它允许开发者自定义业务逻辑,例如编解码、数据处理、异常处理等。ChannelHandler可以被添加到ChannelPipeline中,组成一个处理输入、输出数据的流水线链条。
  4. ChannelPipeline(通道流水线):

    ChannelPipelineChannelHandler链的容器。每个Channel都有一个关联的ChannelPipeline,用于维护和执行ChannelHandler链。数据通过ChannelPipeline时,经过一系列的ChannelHandler,形成一条处理链。
  5. Bootstrap和ServerBootstrap:

    BootstrapServerBootstrap是Netty用于启动和配置Channel的辅助类。Bootstrap用于客户端,而ServerBootstrap用于服务器端。它们简化了启动过程,提供了一些配置选项。
  6. ChannelFuture(通道异步操作结果):

    ChannelFuture表示一个异步操作的结果,例如Channel的连接、数据的写入等。Netty中的很多操作都是异步的,ChannelFuture用于监听和处理这些异步操作。
  7. ByteBuf(字节缓冲区):

    ByteBuf是Netty中用于处理字节数据的缓冲区。与java.nio.ByteBuffer相比,ByteBuf提供了更加灵活、强大的API,支持动态扩容和零拷贝等特性。
  8. Codec(编解码器):

    Codec是编解码器的抽象,用于在ChannelPipeline中进行数据的编解码。Netty提供了许多内置的编解码器,例如StringEncoderStringDecoder等。
  9. ChannelOption(通道选项):

    ChannelOption用于配置Channel的各种参数,例如TCP参数、连接超时时间等。通过ChannelOption可以灵活配置Channel的行为。

对于各个组件的实现细节,推荐去看一个【Netty实战】这本书,书中对Netty介绍的很详细,这里仅介绍下编程时接触最多的ChannelHandler

最重要的两个ChannelHandler是ChannelInboundHandlerChannelOutboundHandler。

在一个channel有读事件触发后,channel会顺着ChannelPipeline顺序走完所有ChannelInboundHandler。而当写事件触发后,channel会顺着ChannelPipeline倒序走完所有ChannelOutboundHandler。ChannelInboundHandler与ChannelOutboundHandler在ChannelPipeline中的顺序与其使用ChannelInitializer进行初始化时的添加顺序一致,所以每一个Handler都要放在正确的位置,代码才能正常工作。

假设伪代码如下

ServerBootstrap serverBootstrap = new ServerBootstrap();
serverBootstrap.group(bossGroup, workerGroup)
        .channel(NioServerSocketChannel.class)
        .childHandler(new ChannelInitializer<SocketChannel>() {
            @Override
            protected void initChannel(SocketChannel socketChannel) throws Exception {
                //给pipeline管道设置Handler处理链,这里要特别注意Handler的顺序
                socketChannel.pipeline()
                        .addLast( new ChannelInboundHandler1() )
                        .addLast( new ChannelOutboundHandler1() )
                        .addLast( new ChannelInboundHandler2() )
                        .addLast( new ChannelOutboundHandler2() )
                        .addLast( new ChannelInboundHandler3() )
                        .addLast( new ChannelOutboundHandler3() );
            }
        });

那么读写数据时Handler的处理先后顺序如下

下面通过一个demo将上面提到的组件串联使用起来,看看它们是怎么打配合的,废话不多说

上代码

代码分为服务端和客户端两块内容,文章结尾有源码地址,感兴趣的同学可以pull下来自己调试一下。由于涉及的类比较多,我也推荐你对着源码看文章,这样会比较清晰。

服务端代码

服务启动类

使用Netty进行网络通信编程的话,一般使用nio的形式,所以本次代码的通信模式也是nio模式

public class NettyServer {

    public static void run() throws InterruptedException {
        // 创建两个线程组 boosGroup、workerGroup。  因为服务端只用到一个端口来监听接入请求,所以boosGroup的线程数设为1(不设置的话实际也不会多启无用的线程,只是多new了几个无用的EventLoop对象)
        EventLoopGroup bossGroup = new NioEventLoopGroup(1);
        EventLoopGroup workerGroup = new NioEventLoopGroup();
        // 因为LoggingHandler是可以共享使用的,所以这里先将它new出来,后面所有的ChannelPipeline中都用它
        LoggingHandler logHandler = new LoggingHandler();
        try {
            ServerBootstrap serverBootstrap = new ServerBootstrap();
            // 设置两个线程组boosGroup和workerGroup
            // boosGroup用来处理socket的连接请求,由ServerSocketChannel生成SocketChannel,然后从workerGroup中挑选一个eventLoop,将SocketChannel交给它进行接下来的通信工作
            serverBootstrap.group(bossGroup, workerGroup)
                    // 设置服务端通道实现类型
                    .channel(NioServerSocketChannel.class)
                    // 设置最大的等待连接数量
                    .option(ChannelOption.SO_BACKLOG, 1024)
                    // 如果TCP_NODELAY没有设置为true,那么底层的TCP为了能减少交互次数,会将网络数据积累到一定的数量后,
                    // 服务器端才发送出去,会造成一定的延迟。在互联网应用中,通常希望服务是低延迟的,建议将TCP_NODELAY设置为true。
                    .childOption(NioChannelOption.TCP_NODELAY, true)
                    // 为ServerSocketChannel设置handler
                    .handler(logHandler)
                    // 为SocketChannel设置pipeline
                    .childHandler(new ChannelInitializer<SocketChannel>() {
                        @Override
                        protected void initChannel(SocketChannel socketChannel) throws Exception {
                            //给pipeline管道设置Handler处理链,这里要特别注意Handler的顺序
                            socketChannel.pipeline()
                                    // netty日志
                                    .addLast("logHandler", logHandler)
                                    // 空闲检测
                                    .addLast("serviceIdleCheckHandler", new ServiceIdleCheckHandler())
                                    // tcp粘包、半包处理
                                    .addLast("tcpPacketDecoder", new TcpPacketDecoder())
                                    .addLast("tcpPacketEncoder", new TcpPacketEncoder())
                                    // 将ByteBuf解析为双端约定好的通信协议对象
                                    .addLast("protocolDecoder", new ProtocolDecoder())
                                    .addLast("protocolEncoder", new ProtocolEncoder())
                                    // 具体业务分发处理
                                    .addLast("serviceProcessHandler", new ServiceProcessHandler());
                        }
                    });
            // 绑定端口号,启动服务端
            ChannelFuture channelFuture = serverBootstrap.bind(9001).sync();
            // 等待channel关闭并且阻塞当期线程直到它完成
            channelFuture.channel().closeFuture().sync();
        } finally {
            // 优雅的关闭线程组
            bossGroup.shutdownGracefully();
            workerGroup.shutdownGracefully();
        }
    }

    public static void main(String[] args) throws InterruptedException {
        run();
    }

}

主要流程没啥说的,这里主要讲一下我设置的这几个Handler。

LoggingHandler:这是Netty内置的一个Handler用于打印Netty内部的日志,使用它我们可以直观的看到Netty正在干什么事,或者说当前这个进程正在发生哪些网络事件。

17:09:04.671 [nioEventLoopGroup-3-1] DEBUG io.netty.handler.logging.LoggingHandler - [id: 0xf838db11, L:/127.0.0.1:9001 - R:/127.0.0.1:55369] READ COMPLETE
17:09:09.534 [nioEventLoopGroup-3-1] DEBUG io.netty.handler.logging.LoggingHandler - [id: 0xf838db11, L:/127.0.0.1:9001 - R:/127.0.0.1:55369] READ: 66B
         +-------------------------------------------------+
         |  0  1  2  3  4  5  6  7  8  9  a  b  c  d  e  f |
+--------+-------------------------------------------------+----------------+
|00000000| 00 40 00 00 00 01 00 00 00 01 66 31 63 65 64 65 |.@........f1cede|
|00000010| 39 39 61 30 34 34 34 65 63 66 39 63 63 39 65 38 |99a0444ecf9cc9e8|
|00000020| 65 62 35 39 37 31 31 32 31 34 7b 22 74 69 6d 65 |eb59711214{"time|
|00000030| 22 3a 34 34 33 38 33 36 36 35 30 30 34 35 31 36 |":44383665004516|
|00000040| 36 7d                                           |6}              |
+--------+-------------------------------------------------+----------------+
17:09:09.537 [nioEventLoopGroup-3-1] INFO com.example.rpcdemo.nio.netty.handler.ServiceProcessHandler - 消息到达ServiceProcessHandler,开始执行请求处理的具体逻辑,{"messageBody":{"time":443836650045166},"messageHeader":{"opCode":1,"streamId":"f1cede99a0444ecf9cc9e8eb59711214","version":1}}
17:09:09.537 [nioEventLoopGroup-3-1] DEBUG io.netty.handler.logging.LoggingHandler - [id: 0xf838db11, L:/127.0.0.1:9001 - R:/127.0.0.1:55369] WRITE: 2B
         +-------------------------------------------------+
         |  0  1  2  3  4  5  6  7  8  9  a  b  c  d  e  f |
+--------+-------------------------------------------------+----------------+
|00000000| 00 40                                           |.@              |
+--------+-------------------------------------------------+----------------+
17:09:09.538 [nioEventLoopGroup-3-1] DEBUG io.netty.handler.logging.LoggingHandler - [id: 0xf838db11, L:/127.0.0.1:9001 - R:/127.0.0.1:55369] WRITE: 64B
         +-------------------------------------------------+
         |  0  1  2  3  4  5  6  7  8  9  a  b  c  d  e  f |
+--------+-------------------------------------------------+----------------+
|00000000| 00 00 00 01 00 00 00 01 66 31 63 65 64 65 39 39 |........f1cede99|
|00000010| 61 30 34 34 34 65 63 66 39 63 63 39 65 38 65 62 |a0444ecf9cc9e8eb|
|00000020| 35 39 37 31 31 32 31 34 7b 22 74 69 6d 65 22 3a |59711214{"time":|
|00000030| 34 34 33 38 33 36 36 35 30 30 34 35 31 36 36 7d |443836650045166}|
+--------+-------------------------------------------------+----------------+
17:09:09.538 [nioEventLoopGroup-3-1] DEBUG io.netty.handler.logging.LoggingHandler - [id: 0xf838db11, L:/127.0.0.1:9001 - R:/127.0.0.1:55369] FLUSH

ServiceIdleCheckHandler:这是我自定义的空闲检测的类,继承自IdleStateHandler

public class ServiceIdleCheckHandler extends IdleStateHandler {

    /**
     * 空闲检测策略:如果10秒内该Channel都没有读事件发生时,则该Channel视为"空闲"
     */
    public ServiceIdleCheckHandler() {
        super(false, 10, 0, 0, TimeUnit.SECONDS);
    }

    /**
     * 当空闲检测检测到空闲后,会触发这个方法
     * (我这个服务器很傲娇,如果你客户端一直不跟我联系,我就主动跟你断绝关系)
     * IdleStateHandler中原方法是向pipeline中传递一个IdleStateEvent事件,这里我们改变一下处理方式,改为直接关闭这个"空闲"的channel
     */
    @Override
    protected void channelIdle(ChannelHandlerContext ctx, IdleStateEvent evt) throws Exception {
        if (evt == IdleStateEvent.FIRST_READER_IDLE_STATE_EVENT) {
            String channelStr = ctx.channel().toString();
            log.info("检测到空闲通道,即将关闭该通道,{}", channelStr);
            ctx.close();
            log.info("通道已关闭,{}", channelStr);
            return;
        }
        
        super.channelIdle(ctx, evt);
    }
}

TcpPacketDecoder,TcpPacketEncoder: 处理tcp粘包、半包问题的编解码器

这里先说下什么是粘包、半包现象,举个例子

发送方发送了两条消息:ABC     DEF

接收方收到消息:

  • 粘包现象发生时: ABCDEF
  • 半包现象发生时:AB   CD   EF

为什么会发生粘包现象?

  1. 发送方每次写入数据 < 套接字缓冲区大小,发送方多个小数据包被合并成一个TCP段传输。
  2. 接收方读取套接字缓冲区数据不够及时,导致多个数据包在缓冲区内合并。
为什么会发生半包现象?
  1. 发送方写入数据 > 套接字缓冲区大小,发送方一个数据包被拆成多个TCP段传输。
  2. 发送的数据大于协议的 MTU(Maximum Transmission Unit,最大传输单元),必须拆包

根本原因:TCP是流式协议,消息无边界。

解决办法:为消息设置边界。

为消息设置边界通常有三种方法:

  • 消息定长:规定每个消息的长度,接收方按照固定长度读取数据。
  • 消息分隔符:在消息中添加分隔符,接收方根据分隔符拆分消息。
  • 使用特定协议:定义协议头,包含消息的长度等信息。

Netty也支持这三种处理方式,并提供了具体的类来进行支持

解决方式解码编码
消息定长

FixedLengthFrameDecoder

不内置,几乎没人用
消息分隔符

DelimiterBasedFrameDecoder

不内置,太简单
协议头中存储消息长度

LengthFieldBasedFrameDecoder

LengthFieldPrepender

本次代码示例中使用的就是第三种方式,“在协议头中存储消息长度”,这也是业界比较推荐的处理方式。

看一下代码实现:

/**
 * 功能: 解决TCP传输的粘包,半包问题
 * <p>
 * 各参数说明(按参数顺序)
 * maxFrameLength:指定解码时,帧的最大长度。如果超过这个长度,LengthFieldBasedFrameDecoder 将抛出 TooLongFrameException 异常。
 * lengthFieldOffset:指定长度字段的偏移量,即长度字段在帧中的起始位置的偏移量。例如,如果长度字段位于帧的起始位置,则 lengthFieldOffset 为 0。
 * lengthFieldLength:指定长度字段的字节长度。常见的取值为 1、2、4 字节。
 * lengthAdjustment:长度字段的值表示的是整个帧的长度,如果帧包含其他固定长度的字段,需要进行长度的调整。lengthAdjustment 就是用来进行这种调整的。
 * initialBytesToStrip:指定从解码帧中跳过的字节数。在某些协议中,头部信息可能包含一些与长度字段无关的额外信息,这些信息需要跳过。
 * failFast:如果设置为 true,则表示在帧长度超过 maxFrameLength 时立即抛出异常。如果设置为 false,则表示只有在实际解码时发现帧长度超过 maxFrameLength 时才抛出异常。
 * </p>
 * 时间: 2024/2/26 18:38
 */
public class TcpPacketDecoder extends LengthFieldBasedFrameDecoder {

    public TcpPacketDecoder() {
        super(Integer.MAX_VALUE, 0, 2, 0, 2, true);
    }

}

/**
 * 功能: 解决TCP传输的粘包,半包问题
 * 各参数说明(按参数顺序)
 * lengthFieldLength: 表示长度字段的字节数。仅支持 1,2,3,4,8
 * lengthAdjustment: 表示长度字段值的调整值。长度字段的值表示的是整个帧的长度,如果帧包含其他固定长度的字段,需要进行长度的调整。lengthAdjustment 就是用来进行这种调整的。
 * lengthIncludesLengthFieldLength: 表示长度字段的值是否包含长度字段的长度。如果设置为 true,则表示长度字段的值包含长度字段自身的长度;如果设置为 false,则表示长度字段的值仅包含有效数据的长度,不包含长度字段本身的长度。
 * 时间: 2024/2/27 23:59
 */
public class TcpPacketEncoder extends LengthFieldPrepender {
    public TcpPacketEncoder() {
        super(2, 0, false);
    }
}

ProtocolDecoder,ProtocolEncoder: 客户端与服务端进行通信的自定义协议的编解码器,用于将字节流解析成Java协议对象,或将Java协议对象编码成字节流。

本次代码通信协议使用json形式,协议对象为Message,有两个属性:请求头MessageHeader和MessageBody。请求对象RequestMessage和返回对象ResponseMessage分别继承于Message,类图如下:详情请看源码(文末有分享链接)

对应的的部分代码如下:

/**
 * 功能: 将ByteBuf的内容解析为约定的协议对象
 */
public class ProtocolDecoder extends MessageToMessageDecoder<ByteBuf> {
    @Override
    protected void decode(ChannelHandlerContext ctx, ByteBuf byteBuf, List<Object> out) throws Exception {
        RequestMessage requestMessage = new RequestMessage();
        requestMessage.decode(byteBuf);
        out.add(requestMessage);
    }
}

/**
 * 功能: 将协议对象转为ByteBuf
 */
public class ProtocolEncoder extends MessageToMessageEncoder<ResponseMessage> {
    @Override
    protected void encode(ChannelHandlerContext ctx, ResponseMessage responseMessage, List<Object> out) throws Exception {
        ByteBuf buffer = ctx.alloc().buffer();
        responseMessage.encode(buffer);
        out.add(buffer);
    }
}

/**
 * 功能: netty传输的自定义协议的消息体
 */
@Data
public abstract class Message<T extends MessageBody> {

    MessageHeader messageHeader;

    T messageBody;


    /**
     * 将ByteBuf解析为Message对象
     *
     * @param byteBuf
     */
    public void decode(ByteBuf byteBuf) {
        // 先读取请求信息中的头部字段
        int version = byteBuf.readInt();
        int opCode = byteBuf.readInt();
        String streamId = byteBuf.readCharSequence(32, CharsetUtil.UTF_8).toString();
        this.messageHeader = new MessageHeader(version, opCode, streamId);

        // 剩余的内容都是消息体了
        String bodyStr = byteBuf.toString(CharsetUtil.UTF_8);
        Class<T> messageBodyClass = getMessageBodyClassFromOpCode(opCode);
        this.messageBody = JSONObject.parseObject(bodyStr, messageBodyClass, JSONReader.Feature.SupportClassForName);
    }

    public abstract Class<T> getMessageBodyClassFromOpCode(int opCode);

    public void encode(ByteBuf buffer) {
        // 先将返回信息中的头部信息写入buffer
        buffer.writeInt(this.messageHeader.getVersion());
        buffer.writeInt(this.messageHeader.getOpCode());
        buffer.writeCharSequence(this.messageHeader.getStreamId(), CharsetUtil.UTF_8);

        // 再将body体转为json写入buffer
        buffer.writeCharSequence(JSON.toJSONString(this.messageBody), CharsetUtil.UTF_8);
    }
}

public class RequestMessage extends Message<Operation> {

    public RequestMessage(String streamId, Operation operation) {
        MessageHeader messageHeader = new MessageHeader();
        messageHeader.setStreamId(streamId);
        messageHeader.setOpCode(OperationEnum.fromOperationClazz(operation.getClass()).getOpCode());
        this.messageHeader = messageHeader;
        this.messageBody = operation;
    }

    @Override
    public Class getMessageBodyClassFromOpCode(int opCode) {
        return OperationEnum.fromOpCode(opCode).getOperationClazz();
    }
}

public class ResponseMessage extends Message<OperationResult> {

    @Override
    public Class getMessageBodyClassFromOpCode(int opCode) {
        return OperationEnum.fromOpCode(opCode).getOperationResultClazz();
    }

}

这里提一下上面的两层编解码的关系

关于ByteToMessageDecoder与MessageToMessageDecoder

一次解码器:ByteToMessageDecoder

io.netty.buffer.ByteBuf (原始数据流)-> io.netty.buffer.ByteBuf (用户数据)

解决tcp的粘包、半包问题就属于一次解码。

二次解码器:MessageToMessageDecoder<I>

io.netty.buffer.ByteBuf (用户数据)-> Java Object

二次解码后ChannelPipeline中的流转的就是解析好的Java协议对象了。

Netty的设计中将两次解码分层处理了,这样每层的任务比较清晰,也能达到解耦的效果。

常见的“二次”编解码方式

  • Java 序列化
  • Marshaling
  • XML
  • JSON
  • MessagePack
  • Protobuf
  • 其他

继续回到Handler的介绍上

ServiceProcessHandler:进行具体业务处理的Handler

本次代码中服务端接收的请求分为两类:探活请求与业务请求,具体的处理逻辑封装在了各自的Operation对象中。

public class ServiceProcessHandler extends SimpleChannelInboundHandler<RequestMessage> {
    @Override
    protected void channelRead0(ChannelHandlerContext ctx, RequestMessage requestMessage) throws Exception {
        log.info("消息到达ServiceProcessHandler,开始执行请求处理的具体逻辑,{}", JSON.toJSONString(requestMessage));
        OperationResult operationResult = requestMessage.getMessageBody().execute();

        ResponseMessage responseMessage = new ResponseMessage();
        responseMessage.setMessageHeader(requestMessage.getMessageHeader());
        responseMessage.setMessageBody(operationResult);

        if (ctx.channel().isActive() && ctx.channel().isWritable()) {
            ctx.writeAndFlush(responseMessage);
        } else {
            log.error("channel当前不可写,丢弃此消息:" + JSON.toJSONString(responseMessage));
        }

    }
}

public class BusinessOperation extends Operation {

    private long userId;

    @Override
    public OperationResult execute() {
        log.info("收到BusinessOperation消息");
        BusinessOperationResult operationResult = new BusinessOperationResult(userId, 18, "张三");
        log.info("BusinessOperation消息处理完成");
        return operationResult;
    }
}

public class KeepaliveOperation extends Operation {

    private long time;

    public KeepaliveOperation() {
        this.time = System.nanoTime();
    }

    @Override
    public OperationResult execute() {
        return new KeepaliveOperationResult(time);
    }
}

ServiceProcessHandler处理完请求后就封装好ResponseMessage,经过两层编码后返回给客户端了。

客户端代码

客户端启动类

public class NettyClient {

    //此客户端对象拥有的channel
    private Channel channel;

    // 定义一个静态的EVENT_LOOP_GROUP,客户端所有channel共用这一个EVENT_LOOP_GROUP
    public static final EventLoopGroup EVENT_LOOP_GROUP = new NioEventLoopGroup();

    // 请求等待中心,存放等待请求结果返回的Future对象
    public static final RequestPendingCenter REQUEST_PENDING_CENTER = new RequestPendingCenter();

    public synchronized void initClient() {
        try {
            // 双重检查,防止并发访问时多次初始化
            if (this.channel != null) {
                return;
            }
            log.info("开始初始化客户端并建立连接");
            Bootstrap bootstrap = new Bootstrap();
            bootstrap.group(EVENT_LOOP_GROUP)
                    .option(ChannelOption.TCP_NODELAY, true)
                    .channel(NioSocketChannel.class)
                    .handler(new ChannelInitializer<SocketChannel>() {
                        @Override
                        protected void initChannel(SocketChannel socketChannel) throws Exception {
                            socketChannel.pipeline()
                                    // 空闲检测
                                    .addLast("idleCheck", new ClientIdleCheckHandler())
                                    // netty日志
                                    .addLast("logHandler", new LoggingHandler())
                                    // tcp粘包、半包处理
                                    .addLast("tcpPacketDecoder", new TcpPacketDecoder())
                                    .addLast("tcpPacketEncoder", new TcpPacketEncoder())
                                    // 将ByteBuf解析为双端约定好的通信协议对象
                                    .addLast("protocolDecoder", new ProtocolDecoder())
                                    .addLast("protocolEncoder", new ProtocolEncoder())
                                    // 空闲检测具体处理逻辑
                                    .addLast("keepaliveHandler", new KeepaliveHandler())
                                    // 返回结果分发处理
                                    .addLast("responseDispatcherHandler", new ResponseDispatcherHandler(REQUEST_PENDING_CENTER));
                        }
                    });
            //连接服务端
            ChannelFuture channelFuture = bootstrap.connect("127.0.0.1", 9001).sync();
            //将channel赋给实例属性,方便对channel进行复用
            this.channel = channelFuture.channel();
            log.info("客户端初始化成功");
            //对通道关闭进行监听,这里不能一直等待,否则会阻塞线程
//            channelFuture.channel().closeFuture().sync();

        } catch (InterruptedException e) {
            throw new RuntimeException(e);
        } finally {
            // 假设 EVENT_LOOP_GROUP 被用于了多个channel, 此channel关闭时,不能将 EVENT_LOOP_GROUP 关掉,确实需要关闭时通过 closeAll 方法关闭
//            EVENT_LOOP_GROUP.shutdownGracefully();
        }
    }

    /**
     * 发送消息
     */
    public OperationResult send(Operation operation) throws ExecutionException, InterruptedException {
        if (this.channel == null) {
            initClient();
        }
        String streamId = IdUtil.fastSimpleUUID();
        // 创建一个Future对象,并放入"请求等待中心"中,等待结果返回
        OperationRequestFuture requestFuture = new OperationRequestFuture();
        REQUEST_PENDING_CENTER.add(streamId, requestFuture);

        // 发送消息给对端
        RequestMessage requestMessage = new RequestMessage(streamId, operation);
        this.channel.writeAndFlush(requestMessage);

        // 阻塞等待返回结果,这是Future的特性
        // 当ResponseDispatcherHandler将返回结果set进Future对象后,requestFuture.get()就能拿到结果并结束阻塞了
        OperationResult operationResult = requestFuture.get();
        return operationResult;
    }

    /**
     * 关闭eventLoop线程池
     */
    public static void closeAll() {
        EVENT_LOOP_GROUP.shutdownGracefully();
    }

    /**
     * 通过main方法启动客户端
     */
    public static void main(String[] args) throws InterruptedException, ExecutionException {
        try {
            NettyClient nettyClient = new NettyClient();
            for (int i = 1; i <= 10; i++) {
                BusinessOperation businessOperation = new BusinessOperation();
                businessOperation.setUserId(i);
                OperationResult result = nettyClient.send(businessOperation);
                log.info("future服务端返回结果:{}", JSON.toJSONString(result));
                ThreadUtil.sleep(10, TimeUnit.SECONDS);
            }
            log.info("调用结束");
        } finally {
            NettyClient.closeAll();
        }
    }

}

客户端与服务端的“两次”编解码方式基本相同,主要的不同在于空闲检测的处理与响应结果的处理上。

ClientIdleCheckHandler,KeepaliveHandler:空闲检测与相应处理。

本次代码中,客户端检测5秒内是否有写事件发生,如果没有的话,就向服务端发送一个探活包,以保活它与服务端之间的连接。(毕竟我们这个服务端比较傲娇,10秒内没收到客户端消息的话就主动断开连接了)

public class ClientIdleCheckHandler extends IdleStateHandler {
    public ClientIdleCheckHandler() {
        super(0, 5, 0, TimeUnit.SECONDS);
    }
}

public class KeepaliveHandler extends ChannelInboundHandlerAdapter {
    
    @Override
    public void userEventTriggered(ChannelHandlerContext ctx, Object evt) throws Exception {
        if (evt == IdleStateEvent.FIRST_WRITER_IDLE_STATE_EVENT) {
            log.info("检测到写空闲,即将发送探活包");
            KeepaliveOperation keepaliveOperation = new KeepaliveOperation();
            String streamId = IdUtil.fastSimpleUUID();
            RequestMessage requestMessage = new RequestMessage(streamId, keepaliveOperation);
            ctx.writeAndFlush(requestMessage);
            log.info("探活包发送完成");
        }

        super.userEventTriggered(ctx, evt);
    }
}

ResponseDispatcherHandler:接收到服务端的返回结果,并赋值给对应Future。

客户端发消息的设计里涉及到对Future的使用,因为客户端也是使用的Netty的nio模式,所以请求发出后,代码并不会阻塞等待结果返回,那怎么实现阻塞等待的效果呢?那就使用Future。这里我使用的Future是Netty中内置的DefaultPromise,这个Future支持我们手动给Future赋值以取消阻塞。google guava包中的SettableFuture也能实现类似的效果。

使用Future进行阻塞等待的流程如下图:

代码如下:

public class OperationRequestFuture extends DefaultPromise<OperationResult> {

}

/**
 * 功能: 请求等待中心,存放等待返回结果的Future
 */
@Slf4j
public class RequestPendingCenter {

    private Map<String, OperationRequestFuture> requestFutureMap = new ConcurrentHashMap<>();

    public void add(String streamId, OperationRequestFuture operationRequestFuture) {
        requestFutureMap.put(streamId, operationRequestFuture);
    }

    public void set(String streamId, OperationResult operationResult) {
        if (requestFutureMap.containsKey(streamId)) {
            requestFutureMap.get(streamId).setSuccess(operationResult);
            requestFutureMap.remove(streamId);
            return;
        }
        log.error("streamId:{} 在请求等待集合中不存在", streamId);
    }
    
}

public class ResponseDispatcherHandler extends SimpleChannelInboundHandler<ResponseMessage> {

    private RequestPendingCenter requestPendingCenter;

    public ResponseDispatcherHandler(RequestPendingCenter requestPendingCenter) {
        this.requestPendingCenter = requestPendingCenter;
    }

    /**
     * 这里处理服务端返回结果,并放入Future中
     */
    @Override
    protected void channelRead0(ChannelHandlerContext ctx, ResponseMessage responseMessage) throws Exception {
        String streamId = responseMessage.getMessageHeader().getStreamId();
        OperationResult operationResult = responseMessage.getMessageBody();
        log.info("收到服务端回复:{}", JSON.toJSONString(operationResult));
        // 通过"请求等待中心",将返回结果赋值给指定Future
        requestPendingCenter.set(streamId, operationResult);
    }
}

至此,客户端与服务端就编写完成了,后面我会继续基于这个代码实现一个spring+netty的rpc调用工程,敬请期待!

源码分享

为了方便调试,我将客户端与服务端分了两个项目

https://gitee.com/huo-ming-lu/RpcDemoServer
https://gitee.com/huo-ming-lu/RpcDemoClient

本篇文章涉及到的类均在 com.example.rpcdemo.nio.netty 包下,敬请查阅。

  • 17
    点赞
  • 29
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值