自定义RPC框架基础-来点Netty通讯的知识吧

39 阅读13分钟

一:基本构件

A.NioEventLoopGroup

NioEventLoopGroup 是 Netty 中的一个核心组件,负责管理 I/O 操作的线程池,它是 Netty 在高并发、低延迟的网络通信中提供的一个重要抽象。EventLoopGroupEventLoop 是 Netty 的事件驱动机制的基础,而 NioEventLoopGroup 则是一个基于 Java NIO(Non-blocking I/O)的线程池实现,专门用于处理 I/O 事件。

1. 什么是 NioEventLoopGroup

NioEventLoopGroup 是 Netty 中实现 EventLoopGroup 接口的一个类。它为 NIO(非阻塞 I/O)操作提供了多线程处理支持,并且可以调度 I/O 操作和事件的处理。EventLoopGroup 管理多个 EventLoop,每个 EventLoop 负责处理某个特定 I/O 操作(例如接收连接、读取数据、写入数据等)。

关键点:
  • 线程池管理NioEventLoopGroup 管理一组 EventLoop,每个 EventLoop 都会绑定到一个特定的线程,用于处理 I/O 操作。
  • 非阻塞 I/O:每个 EventLoop 在其负责的线程中使用非阻塞 I/O 操作,即通过 select 方法来轮询 I/O 操作,而不是阻塞线程等待数据。
  • 事件驱动:Netty 的 I/O 操作和事件的处理是基于事件驱动模型的,NioEventLoopGroup 会根据网络事件调度 EventLoop 执行特定的回调函数。

2. NioEventLoopGroup 是如何工作的

在 Netty 中,网络事件的处理是基于 EventLoop 的事件驱动机制。每个 EventLoop 绑定到一个线程,这个线程负责监听和处理与 I/O 相关的事件,比如接收数据、发送数据、连接请求等。

EventLoopGroupEventLoop 的关系:
  • EventLoopGroup 是线程池,管理多个 EventLoop,负责 I/O 线程的调度。
  • EventLoop 是一个专门用于执行 I/O 事件的执行单元。每个 EventLoop 会负责处理多个通道(Channel)上的事件,并且与其线程一一对应。

3. NioEventLoopGroup 的常见用途

在 Netty 中,你通常会创建一个 NioEventLoopGroup 来处理网络通信的事件。它用于:

  • 服务器端NioEventLoopGroup 会创建一个用于接收连接的 ServerBootstrap,并为每个客户端连接分配一个独立的 EventLoop 进行处理。
  • 客户端:客户端使用 NioEventLoopGroup 来管理连接的 I/O 操作,并监听连接、读取和写入事件。

4. NioEventLoopGroup 如何调度任务

NioEventLoopGroup 通过以下几个关键步骤调度和处理任务:

  1. 事件循环NioEventLoopGroup 内部会有多个 EventLoop,每个 EventLoop 在其线程中不断循环,检查是否有 I/O 事件需要处理。
  2. 选择器(Selector) :每个 EventLoop 内部会有一个 Selector,它负责监听通道(Channel)上的 I/O 事件。例如,监听客户端连接的建立、数据的读取和写入等。
  3. 任务调度EventLoop 负责从 Selector 中获取 I/O 事件,然后调度对应的事件处理器(通常是用户提供的 ChannelHandler)来处理这些事件。
  4. I/O 操作:Netty 使用 Selector 实现非阻塞 I/O,因此不会在 I/O 操作时阻塞线程。当 Selector 检测到通道有事件(如可读或可写)时,EventLoop 会处理这些事件。
服务器端示例:
import io.netty.bootstrap.ServerBootstrap;
import io.netty.channel.Channel;
import io.netty.channel.ChannelInitializer;
import io.netty.channel.nio.NioEventLoopGroup;
import io.netty.channel.socket.nio.NioServerSocketChannel;
import io.netty.handler.logging.LogLevel;
import io.netty.handler.logging.LoggingHandler;
​
public class NettyServer {
    public static void main(String[] args) throws InterruptedException {
        // 创建两个 NioEventLoopGroup,分别用于接收连接和处理数据
        NioEventLoopGroup bossGroup = new NioEventLoopGroup(1);  // 接收连接的线程池
        NioEventLoopGroup workerGroup = new NioEventLoopGroup(); // 处理I/O事件的线程池
​
        try {
            // 创建 ServerBootstrap 用于启动服务端
            ServerBootstrap bootstrap = new ServerBootstrap();
            bootstrap.group(bossGroup, workerGroup)
                    .channel(NioServerSocketChannel.class)  // 使用 NIO 的 ServerSocketChannel
                    .handler(new LoggingHandler(LogLevel.INFO)) // 日志处理器
                    .childHandler(new ChannelInitializer<Channel>() {
                        @Override
                        public void initChannel(Channel ch) {
                            // 初始化子通道的处理器
                            ch.pipeline().addLast(new MyServerHandler());
                        }
                    });
​
            // 绑定端口并启动服务
            bootstrap.bind(8080).sync().channel().closeFuture().sync();
        } finally {
            // 优雅地关闭线程池
            bossGroup.shutdownGracefully();
            workerGroup.shutdownGracefully();
        }
    }
}
客户端示例:
import io.netty.bootstrap.Bootstrap;
import io.netty.channel.Channel;
import io.netty.channel.ChannelInitializer;
import io.netty.channel.nio.NioEventLoopGroup;
import io.netty.channel.socket.nio.NioSocketChannel;
import io.netty.handler.logging.LogLevel;
import io.netty.handler.logging.LoggingHandler;
​
public class NettyClient {
    public static void main(String[] args) throws InterruptedException {
        NioEventLoopGroup group = new NioEventLoopGroup();
​
        try {
            Bootstrap bootstrap = new Bootstrap();
            bootstrap.group(group)
                     .channel(NioSocketChannel.class)
                     .handler(new LoggingHandler(LogLevel.INFO))
                     .handler(new ChannelInitializer<Channel>() {
                         @Override
                         public void initChannel(Channel ch) {
                             // 客户端的处理器
                             ch.pipeline().addLast(new MyClientHandler());
                         }
                     });
​
            // 连接到服务器
            bootstrap.connect("localhost", 8080).sync().channel().closeFuture().sync();
        } finally {
            group.shutdownGracefully();
        }
    }
}

6. 总结

  • NioEventLoopGroup 是 Netty 中用于管理非阻塞 I/O 操作的线程池,它为服务器和客户端提供了高效的事件处理模型。
  • 它通过 EventLoop 对每个通道的事件进行调度处理,确保 I/O 操作能够在高并发环境下高效执行。
  • 通过多线程和事件驱动机制,NioEventLoopGroup 能够优化资源使用并减少阻塞,适合高性能网络应用的需求。

B.Bootstrap

在 Netty 中,Bootstrap 是一个非常核心的概念,它是用来配置和启动网络应用的工具类。通过 Bootstrap,你可以轻松地设置服务器端或客户端的各种参数,并启动相应的网络通信。

Netty 提供了两种类型的 BootstrapServerBootstrapBootstrap,分别用于服务器端和客户端的配置和启动。尽管它们在使用上有一些不同,但它们都遵循类似的配置方式,主要的区别在于它们的 Channel 类型和 EventLoopGroup 配置。

1. Bootstrap 简介

Bootstrap 是一个用于启动 Netty 应用程序的辅助类,提供了多种设置方法来配置网络通信的各个方面(如 I/O 线程、通道类型、处理器等)。它有两种主要的用途:

  • Bootstrap:客户端启动的配置类,用于启动客户端应用程序。
  • ServerBootstrap:服务器端启动的配置类,用于启动服务器端应用程序。

2. ServerBootstrapBootstrap 的使用

(1) ServerBootstrap — 用于服务器端启动

ServerBootstrap 是服务器端启动的配置类,主要用于设置服务器端的监听端口、线程池、通道和通道处理器等。它的核心目的是创建一个可以接受客户端连接的服务器。

ServerBootstrap 配置步骤:
  1. 创建 EventLoopGroup: 服务器端需要两个 EventLoopGroup

    • bossGroup:专门负责接受客户端的连接请求,并为每个连接分配一个处理线程。
    • workerGroup:专门处理已连接客户端的数据读写。
  2. 配置 ServerBootstrap

    • 设置 Channel 类型,通常使用 NioServerSocketChannel 来表示 NIO 模式下的服务端。
    • 设置 handler,这是一个配置父级通道的处理器,通常用于设置全局的日志记录、监控等。
    • 设置 childHandler,这是一个配置子通道的处理器,用于处理具体的网络业务逻辑。
  3. 启动服务器: 使用 bind() 方法启动服务器,绑定指定端口并开始监听。

示例代码:
import io.netty.bootstrap.ServerBootstrap;
import io.netty.channel.Channel;
import io.netty.channel.ChannelInitializer;
import io.netty.channel.EventLoopGroup;
import io.netty.channel.nio.NioEventLoopGroup;
import io.netty.channel.socket.nio.NioServerSocketChannel;
import io.netty.handler.logging.LogLevel;
import io.netty.handler.logging.LoggingHandler;
​
public class NettyServer {
    public static void main(String[] args) throws InterruptedException {
        // 创建 EventLoopGroup
        EventLoopGroup bossGroup = new NioEventLoopGroup(1);  // 用于接收客户端连接的线程池
        EventLoopGroup workerGroup = new NioEventLoopGroup(); // 用于处理 I/O 操作的线程池
​
        try {
            ServerBootstrap bootstrap = new ServerBootstrap();
            bootstrap.group(bossGroup, workerGroup)
                     .channel(NioServerSocketChannel.class) // 设置 NIO 服务器通道
                     .handler(new LoggingHandler(LogLevel.INFO)) // 设置全局日志处理器
                     .childHandler(new ChannelInitializer<Channel>() {
                         @Override
                         public void initChannel(Channel ch) {
                             // 设置每个客户端连接的处理器
                             ch.pipeline().addLast(new MyServerHandler());
                         }
                     });
​
            // 绑定端口并启动服务器
            bootstrap.bind(8080).sync().channel().closeFuture().sync();
        } finally {
            bossGroup.shutdownGracefully();
            workerGroup.shutdownGracefully();
        }
    }
}
(2) Bootstrap — 用于客户端启动

Bootstrap 用于客户端的启动配置。与 ServerBootstrap 类似,Bootstrap 也会设置 EventLoopGroupChannel,不过它只需要一个线程池(group),并且配置的 ChannelNioSocketChannel(表示客户端的 NIO 通道)。

Bootstrap 配置步骤:
  1. 创建 EventLoopGroup: 客户端只需要一个 EventLoopGroup,用于处理所有的 I/O 操作。

  2. 配置 Bootstrap

    • 设置 Channel 类型,通常使用 NioSocketChannel 来表示 NIO 客户端。
    • 设置 handler,通常是用于客户端网络逻辑的处理器。
  3. 启动客户端: 使用 connect() 方法连接到远程服务器,建立网络连接。

示例代码:
import io.netty.bootstrap.Bootstrap;
import io.netty.channel.Channel;
import io.netty.channel.ChannelInitializer;
import io.netty.channel.EventLoopGroup;
import io.netty.channel.nio.NioEventLoopGroup;
import io.netty.channel.socket.nio.NioSocketChannel;
import io.netty.handler.logging.LogLevel;
import io.netty.handler.logging.LoggingHandler;
​
public class NettyClient {
    public static void main(String[] args) throws InterruptedException {
        EventLoopGroup group = new NioEventLoopGroup();
​
        try {
            Bootstrap bootstrap = new Bootstrap();
            bootstrap.group(group)
                     .channel(NioSocketChannel.class) // 设置 NIO 客户端通道
                     .handler(new LoggingHandler(LogLevel.INFO)) // 设置日志处理器
                     .handler(new ChannelInitializer<Channel>() {
                         @Override
                         public void initChannel(Channel ch) {
                             // 设置客户端的处理器
                             ch.pipeline().addLast(new MyClientHandler());
                         }
                     });
​
            // 连接到服务器
            bootstrap.connect("localhost", 8080).sync().channel().closeFuture().sync();
        } finally {
            group.shutdownGracefully();
        }
    }
}

3. Bootstrap 的核心方法和配置

Bootstrap 提供了几个非常重要的方法来配置网络应用:

  • group(EventLoopGroup group) : 设置 EventLoopGroup,这是所有 I/O 操作的线程池。通常,服务器端需要两个线程池,客户端只需要一个。

  • channel(Class<? extends Channel> channelClass) : 设置通道类型。通常,服务器端使用 NioServerSocketChannel,客户端使用 NioSocketChannel

  • handler(ChannelHandler handler) : 设置父级通道的处理器。这个处理器用于全局处理,如日志记录、异常处理等。

  • childHandler(ChannelHandler handler) : 设置子通道的处理器。每个接入的客户端连接都会创建一个新的 Channel,并且该 Channel 会绑定处理器来处理该连接的业务逻辑。

  • bind(int port)connect(String host, int port)

    • bind() 用于服务器端绑定端口并开始监听。
    • connect() 用于客户端连接到服务器。

4. 总结

  • ServerBootstrapBootstrap 都是启动 Netty 应用的工具类,前者用于服务器端,后者用于客户端。
  • 它们都通过 EventLoopGroup 管理 I/O 线程池,通过 Channel 管理网络通信通道。
  • Bootstrap 提供了灵活的配置方法,允许开发者根据需要调整线程池、通道、事件处理器等,从而实现高效的异步 I/O 操作。

Bootstrap 使得 Netty 的使用变得非常简洁、灵活,通过高度的抽象让开发者能够专注于网络通信的业务逻辑,而不必处理底层的 I/O 细节。

C.Handler相关

Netty 中,Handler 是一个非常重要的组件,它负责处理通道上的事件或数据。Netty 使用 ChannelPipeline 来组织处理逻辑,Handler 就是其中的一个处理器。每个 Handler 是一个功能单元,它会处理通过 ChannelPipeline 传递的事件或消息,通常用于对数据进行编码、解码、处理业务逻辑、出站数据的编码和入站数据的解码等。

1. 什么是 Handler?

Handler 是 Netty 中一个用于处理消息或事件的类。它可以作为 ChannelPipeline 的一个处理节点,完成网络 I/O 操作后的数据处理工作。具体来说,Handler 会被安排到管道中,并负责处理来自远程端的数据(例如解码),或者将数据发送给远程端(例如编码)。Handler 本身是一个接口,通常会有一些具体实现类,比如 ChannelInboundHandlerChannelOutboundHandler

2. ChannelPipelineHandler 的关系

ChannelPipeline 是 Netty 中非常核心的概念,它是一个处理事件和消息的 链条。每个 Channel 都有一个 ChannelPipeline,它包含了一系列的 Handler,这些 Handler 会按照顺序依次处理通过通道的数据。

一个 ChannelPipeline 中可以有多个 Handler,每个 Handler 负责处理不同类型的事件或消息。例如,某些 Handler 负责对入站消息进行解码,另一些负责对出站消息进行编码,或者执行业务逻辑。

3. ChannelHandler 接口

ChannelHandler 是 Netty 中处理事件和消息的基础接口,所有的事件处理器类都需要实现 ChannelHandler 接口。这个接口本身没有定义很多方法,而是通过其子接口来区分不同的操作类型。

重要的子接口:
  • ChannelInboundHandler:专门用于处理入站(从远程端接收到的数据)事件。入站事件包括读取数据、异常处理、通道激活等。
  • ChannelOutboundHandler:专门用于处理出站(要发送到远程端的数据)事件。出站事件包括数据的写入、flush 等。

4. 常见的 Handler 实现类

1. ChannelInboundHandlerAdapter

ChannelInboundHandlerAdapterChannelInboundHandler 接口的一个默认实现,它提供了许多事件处理的空实现,你只需要继承它并实现你感兴趣的事件处理方法即可。

常用的方法:

  • channelRead() :每当从通道读取到数据时,该方法会被调用。
  • exceptionCaught() :当出现异常时,Netty 会调用这个方法。
  • channelActive() :当通道连接上远程服务器时调用。
  • channelInactive() :当通道断开连接时调用。

例如,ChannelInboundHandlerAdapter 可能用于实现一个简单的回声服务器(Echo Server):

public class EchoServerHandler extends ChannelInboundHandlerAdapter {
    @Override
    public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception {
        // 将接收到的数据发送回客户端
        ctx.writeAndFlush(msg);
    }
​
    @Override
    public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) {
        // 处理异常
        cause.printStackTrace();
        ctx.close();
    }
}
2. ChannelOutboundHandlerAdapter

ChannelOutboundHandlerAdapterChannelOutboundHandler 的默认实现,提供了出站事件的空实现。

常用的方法:

  • write() :向远程端写数据时调用该方法。
  • flush() :刷新缓冲区中的数据。
  • close() :关闭连接。

一个典型的 ChannelOutboundHandlerAdapter 示例可能用于对数据进行编码:

public class MyEncoder extends ChannelOutboundHandlerAdapter {
    @Override
    public void write(ChannelHandlerContext ctx, Object msg, ChannelPromise promise) throws Exception {
        // 对消息进行编码并写入
        byte[] encoded = encode(msg);
        ctx.write(encoded, promise);
    }
​
    private byte[] encode(Object msg) {
        // 编码逻辑
        return msg.toString().getBytes(StandardCharsets.UTF_8);
    }
}

5. ChannelHandlerContext

每个 Handler 都会接收到一个 ChannelHandlerContext 参数。ChannelHandlerContext 提供了访问 ChannelPipeline、管理事件流转、发送和接收消息等功能的方法。

例如,ChannelHandlerContext 提供的方法:

  • fireChannelRead() :将入站消息转发给下一个 Handler
  • writeAndFlush() :将数据写到网络通道,并立即发送。

6. 内置的 Handler 类

Netty 提供了许多常见的内置 Handler 类,用于实现一些常见的网络编程模式。以下是一些常用的内置 Handler 类:

  • StringDecoder:将入站的字节流解码为字符串。
  • StringEncoder:将出站的字符串编码为字节流。
  • ByteToMessageDecoder:处理字节流并将其解码为其他对象。
  • MessageToByteEncoder:将消息编码成字节流。
  • IdleStateHandler:用于检测连接是否空闲,并在空闲时触发相应事件(例如心跳机制)。

7. Handler 顺序与管道流转

ChannelPipeline 中的 Handler 会按照添加的顺序依次执行,所有的事件和消息都会经过这些 Handler。如果某个 Handler 完成了数据的处理(比如编码或解码),它会将处理结果传递给下一个 Handler

对于入站事件(如接收到的消息),它们会沿着管道从 第一个 Handler 传递到 最后一个 Handler。对于出站事件(如发送的数据),它们则是从 最后一个 Handler 向前传递到 第一个 Handler

8. 使用 Handler 的示例

假设我们想创建一个简单的客户端/服务器通信,包含了数据解码和编码的处理:

public class MyServerInitializer extends ChannelInitializer<SocketChannel> {
    @Override
    protected void initChannel(SocketChannel ch) {
        ChannelPipeline pipeline = ch.pipeline();
        // 添加解码器
        pipeline.addLast(new StringDecoder());
        // 添加编码器
        pipeline.addLast(new StringEncoder());
        // 添加自定义的业务逻辑处理器
        pipeline.addLast(new MyServerHandler());
    }
}
​
public class MyServerHandler extends ChannelInboundHandlerAdapter {
    @Override
    public void channelRead(ChannelHandlerContext ctx, Object msg) {
        String message = (String) msg;
        System.out.println("Received message: " + message);
        ctx.writeAndFlush("Echo: " + message);  // 将消息返回给客户端
    }
}

在上面的代码中,MyServerInitializer 配置了 ChannelPipeline,将字符串解码器(StringDecoder)和字符串编码器(StringEncoder)添加到管道中,然后再添加自定义的 MyServerHandler 来处理接收到的数据并做回显。

总结

  • Handler 是 Netty 中处理消息或事件的核心组件,按照不同的功能可以分为 ChannelInboundHandlerChannelOutboundHandler
  • ChannelPipeline 用来组织和管理这些 Handler,确保事件和数据按照顺序流转。
  • 内置 Handler 例如 StringDecoderStringEncoderIdleStateHandler 提供了常见的网络处理功能,方便开发者快速构建网络应用。
  • Handler 类和 ChannelHandlerContext 一起帮助开发者管理数据的处理流。

通过合理地配置 HandlerChannelPipeline,Netty 可以帮助开发者高效地实现各种网络通信逻辑。