Netty 入门案例

1. 前言

本节主要是使用 Netty 来开发服务端和客户端,Netty 的开发模式基本上都是主启动类 + 自定义业务 Handler,Netty 是基于责任链的模式来管理自定义部分的 Handler,本节带大家感受一下 Netty 的开发。

需求: 本节主要通过 Netty 来实现我们的第一个 Demo,主要功能是分别建立两个项目(客户端和服务端),客户端向服务端发送 Hello World,服务端接受到数据之后打印到控制台,并且给客户端响应。

2. 环境搭建

第一步: 使用 Maven 构建工程,项目结构如下:图片描述

第二步: netty-demo-clientnetty-demo-server 两个工程的 pom.xml 导入 Netty 坐标,Netty 主流有三个版本,分别是 3.x、4.x、5.x,一般主流都是使用 4.x 版本。

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

第三步: netty-demo-client 工程相关类

建立两个类,分别是客户端启动类 NettyClient.java 和业务处理类 NettyClientHandler.java

图片描述

第四步: netty-demo-server 工程相关类

建立两个类,分别是服务端启动类 NettyServer.java 和业务处理类 NettyServerHandler.java
图片描述

3. 核心流程

客户端和服务端通信流程图如下图所示:
图片描述
核心步骤说明:

  1. 在 NettyClientHandler 的 channelActive () 方法往服务端发送消息;
  2. 在 NettyServerHandler 的 channelRead () 方法接受消息,并且响应消息给客户端;
  3. 在 NettyClientHandler 的 channelRead () 方法接受服务端的响应消息。

4. 如何自定义 Handler

在 Netty 的开发当中,最核心就是自定义 Handler,通常根据不同的业务定义不同的 Handler。自定义 Handler 一般分为三个核心步骤:

  1. 需要继承 ChannelInboundHandlerAdapter 类;

实例:

public class TestHandler extends ChannelInboundHandlerAdapter {
    @Override
    public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception {
     	//业务处理   
    }
}

  1. 重写几个核心的方法,其中 channelRead () 是业务逻辑编写,使用最多;
方法名称 触发时机 常见业务场景
channelActive 连接就绪时触发 连接时进行登录认证
channelRead 通道有数据可读取时触发 读取数据并且做处理,这个是用的最多的方法
channelInactive 连接断开时触发 连接断开,删除服务端对于的 Session 关系;也可以在这里实现断开重新
exceptionCaught 发生异常时触发 发生日常时,做日志记录
  1. 把 Handler 添加到 Pipeline 管道里面

实例:

bootstrap.handler(new ChannelInitializer<SocketChannel>() {
    @Override
    public void initChannel(SocketChannel ch) {
        //自定义业务 Handler
        ch.pipeline().addLast(new TestHandler());
    }
});

5. 服务端实现

5.1 服务端启动类

public class NettyServer {
    public static void main(String[] args) {
        //线程组-主要是监听客户端请求
        NioEventLoopGroup bossGroup = new NioEventLoopGroup();
        //线程组-主要是处理具体业务
        NioEventLoopGroup workerGroup = new NioEventLoopGroup();
		//启动类
        ServerBootstrap serverBootstrap = new ServerBootstrap();
        serverBootstrap
            	//指定线程组
                .group(bossGroup, workerGroup)
            	//指定 NIO 模式
                .channel(NioServerSocketChannel.class)
            	//双向链表管理
                .childHandler(new ChannelInitializer<NioSocketChannel>() {
                    protected void initChannel(NioSocketChannel ch) {
                        //责任链,指定自定义处理业务的 Handler
                        ch.pipeline().addLast(new NettyServerHandler());
                    }
                });
		//绑定端口号
        serverBootstrap.bind(80);
    }
}

代码说明:

  1. 以上都是模板代码,需要变动的是根据不同的业务自定义对应的 Handler,并且在 initChannel () 添加逻辑处理器;
  2. 根据实际情况指定 bind () 方法的端口号,注意的是端口号不能和其它端口号冲突;
  3. 这里大家先熟练掌握模板代码的编写,后面章节会讲解 NioEventLoopGroup、Pipeline 等核心组件的原理。

5.2 自定义 Handler

public class NettyServerHandler extends ChannelInboundHandlerAdapter {
    //接受客户端端响应时触发该事件
    @Override
    public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception {
        //转换为 ByteBuf 缓冲区(底层是 byte[] 数组)
        ByteBuf buffer=(ByteBuf)msg;
        //定义一个 byte[] 数组
        byte[] bytes=new byte[buffer.readableBytes()];
        //缓冲区把数据写到 byte[] 数组
        buffer.readBytes(bytes);
        //把 byte[] 转换字符串
        String req=new String(bytes,"UTF-8");
        System.out.println("客户端请求:"+req);
        
        //给客户端响应信息>>>>>>>>>>>>>>>>>>>>>>>>>>>>
        String res="Hello World>>>>Client";
        //把字符串转换 ByteBuf
        ByteBuf buf=getByteBuf(ctx,res);
        
		//把 ByteBuf 写到通道并且刷新
        ctx.writeAndFlush(buf);
    }
    
    private ByteBuf getByteBuf(ChannelHandlerContext ctx,String str) {
        // 1. 获取二进制抽象 ByteBuf
        ByteBuf buffer = ctx.alloc().buffer();
        // 2. 准备数据,指定字符串的字符集为 utf-8
        byte[] bytes = str.getBytes(Charset.forName("utf-8"));
        // 3. 填充数据到 ByteBuf
        buffer.writeBytes(bytes);
        return buffer;
    }
}

代码说明:

  1. 这个逻辑处理器继承自 ChannelInboundHandlerAdapter,然后覆盖了 channelRead () 方法;

  2. channelRead () 方法,接受客户端请求数据时会触发该方法,一般是用来处理具体的业务;

  3. Netty 是面向 ByteBuf 通讯的,接受数据和响应数据都需要转换 ByteBuf,ByteBuf 的 API 后面再详细讲解,这里我们需要知道的是常见创建 ByteBuf 有常见两种方式,①通过 Unpooled 非池化工具类来操作;②通过 ctx.alloc().buffer() 来获取。最后我们调用 ctx.channel().writeAndFlush() 把数据写到服务端。

6. 客户端实现

6.1 客户端启动类

public class NettyClient {
    public static void main(String[] args) throws InterruptedException {
        NioEventLoopGroup workerGroup = new NioEventLoopGroup();

        Bootstrap bootstrap = new Bootstrap();
        bootstrap
                // 1.指定线程模型
                .group(workerGroup)
                // 2.指定 IO 类型为 NIO
                .channel(NioSocketChannel.class)
                // 3.IO 处理逻辑
                .handler(new ChannelInitializer<SocketChannel>() {
                    @Override
                    public void initChannel(SocketChannel ch) {
                        //自定义业务 Handler
                        ch.pipeline().addLast(new NettyClientHandler());
                    }
                });
        // 4.建立连接
        ChannelFuture future=bootstrap.connect("127.0.0.1", 80).sync();
    }
}

代码说明:

  1. 以上都是模板代码,需要变动的是根据不同的业务自定义对应的 Handler,并且在 initChannel () 添加逻辑处理器;
  2. connect () 方法,指定对应服务端 ip 和 port。

6.2 自定义 Handler

public class NettyClientHandler extends ChannelInboundHandlerAdapter {
    //客户端连接成功之后触发该事件,只会触发一次
    @Override
    public void channelActive(ChannelHandlerContext ctx) throws Exception {
        ctx.channel().writeAndFlush(Unpooled.copiedBuffer("Hello World".getBytes()));
    }

    //接受服务端响应时触发该事件
    @Override
    public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception {
        ByteBuf buffer=(ByteBuf)msg;
        byte[] bytes=new byte[buffer.readableBytes()];
        buffer.readBytes(bytes);
        String res=new String(bytes,"UTF-8");
        System.out.println("服务端响应:"+res);
    }
}

代码说明:

  1. 这个逻辑处理器继承自 ChannelInboundHandlerAdapter,然后覆盖了 channelActive () 和 channelRead () 方法;
  2. channelActive (),这个方法会在客户端连接建立成功之后被调用,可以在这个方法里面,做一些初始化的工作,该方法仅被调用一次;
  3. channelRead 方法,在接受客户端响应时触发,会触发多次。

7. 测试效果

服务端打印:
图片描述

客户端打印:
图片描述

8. 视频演示

9. 小结

通过以上的代码,我们主要实现了客户端和服务端之间的通讯,需要掌握以下关键点:

  1. 客户端和服务端的启动类代码,这个基本上是固定写法;
  2. 掌握 Handler 的作用以及如何自定义,几个核心方法的触发时机以及常见的应用场景;
  3. 和传统的 socket 编程不同的是,Netty 里面数据是以 ByteBuf 为单位的。