Netty 入门案例
1. 前言
本节主要是使用 Netty 来开发服务端和客户端,Netty 的开发模式基本上都是 主启动类 + 自定义业务 Handler ,Netty 是基于责任链的模式来管理自定义部分的 Handler,本节带大家感受一下 Netty 的开发。
需求: 本节主要通过 Netty 来实现我们的第一个 Demo,主要功能是分别建立两个项目(客户端和服务端),客户端向服务端发送 Hello World,服务端接受到数据之后打印到控制台,并且给客户端响应。
2. 环境搭建
第一步: 使用 Maven 构建工程,项目结构如下:
第二步: netty-demo-client 和 netty-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. 核心流程
客户端和服务端通信流程图如下图所示:

核心步骤说明:
- 在 NettyClientHandler 的 channelActive () 方法往服务端发送消息;
 - 在 NettyServerHandler 的 channelRead () 方法接受消息,并且响应消息给客户端;
 - 在 NettyClientHandler 的 channelRead () 方法接受服务端的响应消息。
 
4. 如何自定义 Handler
在 Netty 的开发当中,最核心就是自定义 Handler,通常根据不同的业务定义不同的 Handler。自定义 Handler 一般分为三个核心步骤:
- 需要继承 
ChannelInboundHandlerAdapter类; 
实例:
public class TestHandler extends ChannelInboundHandlerAdapter {
    @Override
    public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception {
        //业务处理   
    }
}
- 重写几个核心的方法,其中 channelRead () 是业务逻辑编写,使用最多;
 
| 方法名称 | 触发时机 | 常见业务场景 | 
|---|---|---|
| channelActive | 连接就绪时触发 | 连接时进行登录认证 | 
| channelRead | 通道有数据可读取时触发 | 读取数据并且做处理,这个是用的最多的方法 | 
| channelInactive | 连接断开时触发 | 连接断开,删除服务端对于的 Session 关系;也可以在这里实现断开重新 | 
| exceptionCaught | 发生异常时触发 | 发生日常时,做日志记录 | 
- 把 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);
    }
}
代码说明:
- 以上都是模板代码,需要变动的是根据不同的业务自定义对应的 Handler,并且在 initChannel () 添加逻辑处理器;
 - 根据实际情况指定 bind () 方法的端口号,注意的是端口号不能和其它端口号冲突;
 - 这里大家先熟练掌握模板代码的编写,后面章节会讲解 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;
    }
}
代码说明:
- 
这个逻辑处理器继承自 ChannelInboundHandlerAdapter,然后覆盖了 channelRead () 方法;
 - 
channelRead () 方法,接受客户端请求数据时会触发该方法,一般是用来处理具体的业务;
 - 
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();
    }
}
代码说明:
- 以上都是模板代码,需要变动的是根据不同的业务自定义对应的 Handler,并且在 initChannel () 添加逻辑处理器;
 - 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);
    }
}
代码说明:
- 这个逻辑处理器继承自 ChannelInboundHandlerAdapter,然后覆盖了 channelActive () 和 channelRead () 方法;
 - channelActive (),这个方法会在客户端连接建立成功之后被调用,可以在这个方法里面,做一些初始化的工作,该方法仅被调用一次;
 - channelRead 方法,在接受客户端响应时触发,会触发多次。
 
7. 测试效果
服务端打印:

客户端打印:

8. 视频演示
9. 小结
通过以上的代码,我们主要实现了客户端和服务端之间的通讯,需要掌握以下关键点:
- 客户端和服务端的启动类代码,这个基本上是固定写法;
 - 掌握 Handler 的作用以及如何自定义,几个核心方法的触发时机以及常见的应用场景;
 - 和传统的 socket 编程不同的是,Netty 里面数据是以 ByteBuf 为单位的。
 
访问者可将本网站提供的内容或服务用于个人学习、研究或欣赏,以及其他非商业性或非盈利性用途,但同时应遵守著作权法及其他相关法律的规定,不得侵犯本网站及相关权利人的合法权利。
本网站内容原作者如不愿意在本网站刊登内容,请及时通知本站,邮箱:80764001@qq.com,予以删除。
