七的博客

手写RPC框架系列(七) - Netty 快速入门

RPC手写系列

手写RPC框架系列(七) - Netty 快速入门

前一章节我们介绍了基本的通信相关的基础知识,有了基础知识的铺垫有利于本章节的学习。

本章节包含的大致内容如下:

  • 理解 Netty 是什么以及核心组件
  • 为什么要选用 Netty
  • 快速上手 Netty
  • 总结

1. Netty 是什么

引用一段维基对 Netty 的介绍:

Netty 是一个非阻塞I/O客户端-服务器框架,主要用于开发 Java 网络应用程序,如协议服务器和客户端。异步事件驱动的网络应用程序框架和工具用于简化网络编程,例如 TCP 和 UDP 套接字服务器。Netty 包括了反应器编程模式的实现。Netty 最初由JBoss开发,现在由 Netty 项目社区开发和维护。

从这段介绍中我们可以总结出几点:

  • 非阻塞I/O客户端-服务器框架
  • 用于开发 Java 语言的网络应用程序
  • 支持 TCP和UDP套接字服务器
  • 基于反应器编程模式的实现
  • Netty 最初由 JBoss 开发,现在由 Netty 项目社区开发和维护。

2. 为什么要选用 Netty

上个章节中,我们也对比过 Java 领域内比较常用的网络通信框架。 这三个框架分别是:

2.1 Java 原生 NIO

早先我接触过几个生产的项目就是用的原生的 Java NIO 编写,项目开发的比较早,稳定成熟的 NIO 框架选择不多,故项目组是使用原生的 Java NIO 编写。 在维护以及阅读代码过程中,可以发现明显的几个缺点:

  • 代码很复杂,特别是各种状态的判断,如果处理不当的话就会出现阻塞的问题,在生产环境中碰到过几次。
  • 开发效率不高,比较网络连接、粘包处理、编解码等大量的重复代码。
  • 在 JDK 的前几个版本中,知名的 Epoll Bug 导致 CPU 使用率持续 100%,这也是一直被人吐槽的一个点。

2.2 Apache Mina

Apache Mina 也是一个比较流行的 Java 网络编程框架,出现的时间也比较早,第一次发布出来的版本是 MINA 0.7.1,发布时间在 23/May/2005 ( 版本链接 https://mina.apache.org/mina-project/downloads_old.html)。

此后的数年时间,Mina 项目都一直在更新,但是后面版本迭代的速度越来越慢。 主要一个原因还是社区不太活跃,导致项目更新迭代的更慢。

Mina 比较明显的缺点如下:

  • 社区不活跃,更新迭代慢,最近几年几乎停止更新。
  • Mina 在某些情况下会有较高的内存消耗,特别是高负载情况下。
  • Mina 的 API 有些不太直观,上手起来比较复杂。
  • 添加新协议支持会更复杂。

2.3 Netty

Netty 也是一个网络编程框架,出现的时间比 Mina 还要早,并且作者都是同一个人。 目前 Netty 被广泛用于通讯类的产品上,包括一些知名的大数据组件基本都是采用 Netty 进行通信。

Netty 的缺点如下:

  • Netty 中错误处理因为是在异步编程模型中,所以会比较复杂。 正确地处理以及传播错误需要对框架有比较深刻的理解。
  • 需要谨慎管理资源,典型的就是 ButeBuf 的分配已经释放,如果处理不当会导致内存泄漏。
  • 配置选项太多,虽然说灵活,但是配置起来比较复杂。

Netty 的优点如下:

  • 高性能以及稳定性,这个已经在全世界海量的生产应用中得到了验证。
  • 简单易用, Netty 抽象出了一套高层的 API,隐藏了 Java NIO 的复杂性,可以快速实现复杂的网路通信。
  • 多协议支持,常用的通信协议基本都已经支持。
  • 社区活跃,文档丰富,这可以解决大部分开发中遇到的问题。

通过上面的介绍,我们可以选择基于 Netty 来开发 RPC 框架网络通信部分。

3. Netty 的基本用法以及快速入门

这里我们将不会详细深入的讲解 Netty ,毕竟我们的重点是侧重于使用 RPC 的网络通信。

通常开发一个 Netty 应用分为以下几个步骤:

  1. 引入Netty依赖:需要在 pom.xml 中添加如下内容:
    <dependency>
      <groupId>io.netty</groupId>
      <artifactId>netty-all</artifactId>
      <version>4.1.45.Final</version>
   </dependency>
  1. 创建 Bootstrap 实例: Netty中,服务端跟客户端创建的 Bootstrap 实例是不一样的,服务端创建的是 ServerBootstrap ,而客户端创建的是 Bootstrap 。

  2. 配置 Channel: 选择合适的 Channel 类型,例如 NioServerSocketChannel,并配置 ChnanelPipleline 。 ChannelPipeline 主要是管理数据处理的各个阶段,你可以添加自定义的处理器 ( ChannelHandler ) 来处理业务。

  3. 编写对应的业务逻辑处理器: 编写一个或者多个 ChannelHandler 来处理输入的数据,比方说在 channelRead 方法中对收到的数据进行处理。

  4. 启动 Bootstrap : 如果服务端的应用,则调用 Bootstrap.bind(端口) 进行服务端口的绑定,客户端的应用则是通过 Bootstrap.connect(目的地址 + 端口) 进行连接创建。

通过以上几个简单的步骤,你就可以创建对应的 Netty 服务端以及客户端的应用,比原生的 Java NIO 大幅度的降低了工作量以及学习成本。

下面我们快速通过一个 HelloWorld 例子来实际上手:

3.1 编写 HelloWorld 服务端

首先创建一个简单的服务端,它接收连接并返回 “Hello, world!“。

import io.netty.bootstrap.ServerBootstrap;
import io.netty.channel.*;
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 HelloWorldServer {

    private int port;

    public HelloWorldServer(int port) {
        this.port = port; // 服务器端口
    }

    public void start() {
        // 用于接受客户端连接的线程组
        EventLoopGroup bossGroup = new NioEventLoopGroup(); 
        // 用于处理已接受的连接的线程组
        EventLoopGroup workerGroup = new NioEventLoopGroup(); 
        try {
            ServerBootstrap b = new ServerBootstrap();
            b.group(bossGroup, workerGroup)
             .channel(NioServerSocketChannel.class) // 指定使用NIO的通信模式
             .childHandler(new ChannelInitializer<SocketChannel>() {
                 @Override
                 protected void initChannel(SocketChannel ch) {
                     // 为新连接的Channel注册处理器:先解码、编码,再处理业务逻辑
                     ch.pipeline().addLast(new StringDecoder(), new StringEncoder(), new SimpleChannelInboundHandler<String>() {
                         @Override
                         protected void channelRead0(ChannelHandlerContext ctx, String msg) {
                             System.out.println("Received: " + msg); // 打印接收到的消息
                             ctx.writeAndFlush("Hello, world!"); // 发送响应消息给客户端
                         }
                     });
                 }
             })
             .option(ChannelOption.SO_BACKLOG, 128) // 设置TCP连接的队列长度
             .childOption(ChannelOption.SO_KEEPALIVE, true); // 设置保持连接状态

            // 绑定端口并启动服务器
            ChannelFuture f = b.bind(port).sync(); 
            // 等待服务器socket关闭
            f.channel().closeFuture().sync();
        } catch (InterruptedException e) {
            e.printStackTrace();
        } finally {
            // 优雅停机
            workerGroup.shutdownGracefully();
            bossGroup.shutdownGracefully();
        }
    }

    public static void main(String[] args) {
        // 服务端的端口设置为 8080
        int port = 8080;
        new HelloWorldServer(port).start();
    }
}

3.2 编写 HelloWorld 客户端

这是一个简单的客户端,它连接到服务器,在连接成功后发送一个消息并接收响应。

import io.netty.bootstrap.Bootstrap;
import io.netty.channel.*;
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;

public class HelloWorldClient {

    private final String host;
    private final int port;

    public HelloWorldClient(String host, int port) {
        this.host = host; // 服务器的IP地址
        this.port = port; // 服务器的端口
    }

    public void start() throws InterruptedException {
        EventLoopGroup group = new NioEventLoopGroup(); // 创建客户端处理线程组
        try {
            Bootstrap b = new Bootstrap();
            b.group(group)
             .channel(NioSocketChannel.class) // 使用NIO通信模式
             .handler(new ChannelInitializer<SocketChannel>() {
                 @Override
                 public void initChannel(SocketChannel ch) {
                     // 设置pipeline,处理网络IO
                     ch.pipeline().addLast(new StringDecoder(), new StringEncoder(), new SimpleChannelInboundHandler<String>() {
                         @Override
                         protected void channelRead0(ChannelHandlerContext ctx, String msg) {
                             // 打印服务器的响应
                             System.out.println("Server replied: " + msg); 
                         }

                         @Override
                         public void channelActive(ChannelHandlerContext ctx) {
                             // 连接成功后,立马给服务端发送消息
                             ctx.writeAndFlush("Hello from client!"); 
                         }
                     });
                 }
             });

            // 连接到服务器
            ChannelFuture f = b.connect(host, port).sync(); 
            // 等待连接关闭
            f.channel().closeFuture().sync();
        } finally {
            // 关闭线程组
            group.shutdownGracefully();
        }
    }

    public static void main(String[] args) throws InterruptedException {
        // 连接到服务端 localhost:8080
        new HelloWorldClient("localhost", 8080).start();
    }
}

3.3 运行效果

3.3.1 服务端输出

  • 当客户端连接并发送消息 "Hello from client!" 时,服务端的 channelRead0 方法会被触发,从而接收并打印这条消息。因此,服务端会输出:

    Received: Hello from client!
    
  • 然后服务端会向客户端发送响应 “Hello, world!“。

3.3.2 客户端输出

  • 客户端启动并连接到服务端后,其 channelActive 方法被触发,客户端发送 “Hello from client!” 到服务端。

  • 当服务端响应 “Hello, world!” 到客户端时,客户端的 channelRead0 方法会被触发,从而接收并打印这条消息。因此,客户端会输出:

  Server replied: Hello, world!

这样的输出显示了一个基本的客户端-服务端通信,其中客户端发送了一个初始消息并接收了来自服务端的响应。服务端接收了客户端的消息并发送了一个响应。掌握上面的这种模式是接下来编写 RPC 通信部分的基础。

4. 总结

通过这个章节的学习,我们对基本的 Java 领域的网络通信框架进行对比以及选型,同时上手了一个 Netty 的 HelloWorld 例子。有了这部分基础后,下一章节我们将开始使用 Netty 开发 RPC 相关的通讯部分。

5. 参考链接