Netty全家宴—带你实现第一个Netty Demo

微信公众号:大黄奔跑

写在之前

之前介绍了 Netty 开胃菜的三道小菜,分别为Nio buffer、Nio Channel、Nio Selector。

前面说了这么多,还在 Netty 门外徘徊,今天会给大家展示一个 Netty 真面目。但是我不打算Netty开篇就扎根于某个菜品中。

本篇会带着大家介绍第一个 Netty demo,后续文章的分析都是基于这一个 Demo 展开,第一次看这个 demo 的同学可能会很多地方看不懂,本篇文章先按下不表,后续问问会一一介绍,避免刚开始开宴就沉迷于某一道下酒菜中,忽略了整个口味俱佳的美味。

封面来源于李安饮食男女的全家宴,每次看这部电影口水直流。

主要目的

主要利用 Netty 来展示一个客户端与服务器连接的应用程序,程序目的很简单,客户端将消息发送给服务器、服务器再将消息返回给客户端。

虽然目的简单,但是这个demo意义重点,后续我们会沿着这个demo一步步走下去,试图拆解netty的各个细节。

客户端和服务器端模型

服务器端建立

Netty 巧妙地将数据处理和具体的服务器启动等连接过程分离开来,使用者可以自定义数据处理工具,而每个程序的启动类大同小异,可以做到真正的共用。

所以要实现服务器端配置主要有两部分:

  1. 服务器的启动代码——主要将服务器绑定到需要监听的连接请求的端口上
  2. 具体的业务代码——上一步说的数据处理逻辑,在 Netty 中用一系列handler实现

1. 启动代码实现

为了避免代码过长,省略掉了暂时不考虑的代码,尽可能把核心内容展示给大家。

public class NettyServer {

  public static void main(String[] args) {

    // 1. 首先创建 两个线程组 BossGroup和WorkerGroup
    EventLoopGroup bossGroup = new NioEventLoopGroup();
    EventLoopGroup workGroup = new NioEventLoopGroup();

    // 2. 创建服务端启动类,配置启动参数
    ServerBootstrap serverBootstrap = new ServerBootstrap();

    // 3. 配置具体的参数,配置具体参数
    /**
     * 3.1 配置group
     * 3.2 使用 NioServerSocketChannel 作为服务器的通道实现
     * 3.3 设置具体的Handler
     */
    serverBootstrap.group(bossGroup, workGroup)
      .channel(NioServerSocketChannel.class)
      .childHandler(new ChannelInitializer < SocketChannel > () {
        @Override
        protected void initChannel(SocketChannel socketChannel) throws Exception {
          //4. 给 pipeline 添加处理器,每当有连接accept时,就会运行到此处。
          socketChannel.pipeline().addLast(new NettyServerHandler());
        }
      });

    System.out.println("server is ready……");

    // 5. 绑定端口并且同步,生成了一个ChannelFuture 对象
    ChannelFuture channelFuture = serverBootstrap.bind(8887).sync();

    // 6. 对channel进行关闭,注意这里全部都是异步操作
    channelFuture.channel().closeFuture().sync();
  }
}

注意上面的示例代码中,最后添加了一个 childHandler,其中加入了一个new NettyServerHandler(),前面说过,大家姑且可以将其看作是具体业务逻辑代码处理器,该 Handler 需要用户自定义。

2. 业务逻辑——Handler

服务器Handler默认都是处理服务器响应传入的消息,自定义的Handler可以通过继承Netty预置 ChannelInboundHandlerAdapter,至于为何需要继承该类,此处 按下不表 + 1,后续会写文章详细介绍。

public class NettyServerHandler extends ChannelInboundHandlerAdapter {

  /**
   * channelRead()——对于每个传入的消息都需要调用
   *
   * @param ctx
   * @param msg
   */
  @Override
  public void channelRead(ChannelHandlerContext ctx, Object msg) {
    // 压缩
    ByteBuf in = (ByteBuf) msg;
    //将消息记录到控制台
    System.out.println("Server received: " + in .toString(CharsetUtil.UTF_8));
    //将接收到的消息写给发送者
    ctx.write( in );
  }

  /**
   * 通知ChannelInboundHandler最后一次对channelRead()的调用是当前批量读取中的最后一条消息
   *
   * @param ctx
   * @throws Exception
   */
  @Override
  public void channelReadComplete(ChannelHandlerContext ctx) throws Exception {
    //将未决消息冲刷到远程节点,并且关闭该 Channel
    ctx.writeAndFlush(Unpooled.EMPTY_BUFFER)
      .addListener(ChannelFutureListener.CLOSE);
  }

  /**
   * 在读取期间,有异常的时候会调用
   *
   * @param ctx
   * @param cause
   */
  @Override
  public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) {
    //打印异常栈跟踪
    cause.printStackTrace();
    //关闭该Channel
    ctx.close();
  }
}

上来劈头盖脸写了两段代码,并且不加任何介绍,多多少少有一些耍流氓,因此,让我们回顾一下刚才两段代码完成服务器实现的主要步骤,主要可以划分为如下六步,这里的步骤可以好好理解一下,后续所有的文章的分析其实都是来源于此六步,这六步可以说是 Netty的六脉神剑。

服务器端启动过程

(1)NettyServerHandler 实现了业务逻辑

(2)NettyServer 主要是启动类,用于引导服务器,引导过程可以细化为六小步。

  • 创建两个用于处理连接和业务的线程组。EventLoopGroup bossGroup = new NioEventLoopGroup();
  • 创建服务端启动类,以引导和绑定服务器。ServerBootstrap serverBootstrap = new ServerBootstrap();
  • 指定所使用的NIO传输Channel。.channel(NioServerSocketChannel.class)
  • 使用Handler实例,处理具体业务逻辑。
.childHandler(new ChannelInitializer < SocketChannel > () {
  @Override
  protected void initChannel(SocketChannel socketChannel) throws Exception {
    //4. 给 pipeline 添加处理器,每当有连接accept时,就会运行到此处。
    socketChannel.pipeline().addLast(new NettyServerHandler());
  }
});
  • 异步绑定服务器,阻塞等待直到绑定完成。ChannelFuture channelFuture = serverBootstrap.bind(8887).sync();
  • 关闭Channel。channelFuture.channel().closeFuture().sync();

至此服务器端代码,已经实现完成。这里忽略了部分异常处理逻辑,主要是避免被太多无关紧要内容打乱,想要实验的同学可以私信我提供完整 demo。

客户端实现

客户端主要处理的逻辑同样划分为两部分,业务逻辑和引导类,整体思路与服务器端类似。

整体步骤大概 分为四步:

  1. 连接到服务器
  2. 发送消息给服务器
  3. 对于每个消息,等待并接收从服务器发回的消息
  4. 关闭与服务器连接

1. 客户端启动实现

整体处理思路与服务器端类似,不同的是,客户端是使用服务器ip和端口连接到远程地址,而不是绑定到一个一直被监听的端口。

public class NettyClient {

  public static void main(String[] args) {

    // 1. 客户端需要一个事件循环组
    EventLoopGroup clientGroup = new NioEventLoopGroup();

    try {
      // 2. 创建客户端启动对象
      Bootstrap bootstrap = new Bootstrap();

      // 3. 设置启动器的相关参数
      /**
       * 3.1 设置线程组
       * 3.2 设置客户端通道的实现类(使用反射)
       * 3.3 设置具体的处理handler
       */
      bootstrap.group(clientGroup)
        .channel(NioSocketChannel.class)
        .handler(new ChannelInitializer < SocketChannel > () {
          @Override
          protected void initChannel(SocketChannel socketChannel) throws Exception {
            // 添加客户端处理逻辑Handler
            socketChannel.pipeline().addLast(new NettyClientHandler());

          }
        });
      System.out.println("客户端 OK...");

      // 5. 连接服务器,注意这里全部都是异步的
      ChannelFuture channelFuture = bootstrap.connect("127.0.0.1", 8887).sync();

      // 6. 关闭通道连接监听
      channelFuture.channel().closeFuture().sync();
    } catch (InterruptedException e) {
      e.printStackTrace();
    } finally {
      clientGroup.shutdownGracefully();
    }
  }
}

整体流程可以参见服务器启动流程,说说不同之处。

  1. 为了初始化客户端启动类,创建一个bootstrap,这是专门用户处理客户端启动的类。
  2. 服务器端创建了两个NioEventLoopGroup线程组,而客户端这里只创建了一个线程组,具体原因此处按下不表(挖坑+1),后续会给大家补上。
  3. 连接服务器远程连接时,同时使用了 ip + host信息,而服务器只是绑定了端口号。

2. 业务逻辑——Handler

与服务器类似,客户端同样需要继承ChannelInboundHandlerAdapter用于客户端处理数据逻辑,不过需要实现的方法却大相同。

  1. 重写channelActive()方法——用于与服务器建立连接之后被调用,一般用于建立之后发送消息。
  2. 重写channelRead()方法——从服务器接收到一条消息后被调用
  3. 重写exceptionCaught()方法——用于发生异常时被调用
public class NettyClientHandler extends ChannelInboundHandlerAdapter {

  /**
   * 用于与服务器建立连接之后被调用,一般用于建立之后发送消息。
   *
   * @param ctx
   * @throws Exception
   */
  @Override
  public void channelActive(ChannelHandlerContext ctx) throws Exception {
    System.out.println("client: " + ctx);
    // 给服务器发送消息
    ctx.writeAndFlush(Unpooled.copiedBuffer("Hello ,服务器", CharsetUtil.UTF_8));
  }

  /**
   * 从服务器接收到一条消息后被调用
   *
   * @param ctx
   * @param msg
   * @throws Exception
   */
  @Override
  public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception {
    ByteBuf byteBuf = (ByteBuf) msg;
    System.out.println("服务器说: " + byteBuf.toString(CharsetUtil.UTF_8));
    System.out.println("服务器地址为: " + ctx.channel().remoteAddress());
  }

  /**
   * 处理异常信息
   *
   * @param ctx
   * @param cause
   * @throws Exception
   */
  @Override
  public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) throws Exception {
    cause.printStackTrace();
    ctx.close();
  }
}

到这里我们已经实现了客户端连接、启动与业务处理逻辑全部过程,虽然很多地方没有介绍,但是不妨碍我们从整体来看Netty执行过程,这里忽略的很多点,后续会一并分析。

总结

本篇主要想给大家展示Netty全貌是何样,只全貌再究细节是我一直比较推崇的学习思路,这样可以避免一开始局限与某一个细点。

当然本篇埋下了几个坑等待后续的挖掘,比如

(1)为啥服务器端需要需要创建两个NioEventLoopGroup线程组,而客户端只需要创建一个呢?

(2)为何 handler 都需要继承 Netty 预先设置的 Handler 呢?

后续文章会一并带领大家一起探索Netty的奥妙。

#学习路径##Java#
全部评论
大黄哥写的不错,支持一下
点赞 回复 分享
发布于 2021-04-01 15:28

相关推荐

点赞 评论 收藏
分享
10-11 17:45
门头沟学院 Java
点赞 评论 收藏
分享
点赞 12 评论
分享
牛客网
牛客企业服务