Netty 学习笔记(五)、Netty 简单示例

概述

关于学习 Netty 的基本前置知识已全部整理完成,从本篇我们正式进入 Netty 框架的学习。学习任何框架最快的途径就是使用它,因此本篇我打算使用 Netty 实现时间服务器,通过代码的方式初步认识 Netty。


Netty 示例

下面我们直接看代码,其中每一行代码的作用我通过注释的方式给出:

Netty 服务端源码

public class TimeServer {

    public static void main(String[] args) {
        // 启动服务端,调用初始化方法
        new TimeServer().init();
    }

    private static final int PORT = 8888;

    private void init() {
        // 创建两个 NIO 线程组,这两个线程组就类似多 Reactor 模式中的 Reactor
        // group 线程组主要用于处理客户端连接
        EventLoopGroup group = new NioEventLoopGroup();
        // workGroup 线程组主要用于管道的读写操作
        EventLoopGroup workGroup = new NioEventLoopGroup();
        // bootstrap 是 Netty 用于启动 NIO 服务端的辅助类,主要用来简化代码
        ServerBootstrap bootstrap = new ServerBootstrap();
        // 将两个线程组作为参数传递,初始化 NIO 启动辅助类
        bootstrap.group(group, workGroup)
                // 设置 Channel 类型为 NioServerSocketChannel
                .channel(NioServerSocketChannel.class)
                // 设置最大连接数为 1024
                .option(ChannelOption.SO_BACKLOG, 1024)
                // 绑定 I/O 事件处理类
                .childHandler(new ChildChannelHandler());
        try {
            // 调用 bind(PORT) 方法绑定监听端口
            // 调用 sync() 方法等待绑定操作完成,操作完成后返回 ChannelFuture 对象
            // ChannelFuture 对象就类似 Future,主要用于异步操作的回调通知
            ChannelFuture f = bootstrap.bind(PORT).sync();
            // 调用 f.channel().closeFuture().sync() 阻塞等待服务端数据链路中断
            f.channel().closeFuture().sync();
        } catch (InterruptedException e) {
            e.printStackTrace();
        } finally {
            // 回收资源
            group.shutdownGracefully();
            workGroup.shutdownGracefully();
        }

    }

    private class ChildChannelHandler extends ChannelInitializer<SocketChannel> {

        @Override
        protected void initChannel(SocketChannel socketChannel) throws Exception {
            // 创建 Channel 成功后,在初始化时创建 TimeServerHandler 处理器,并让它绑定 pipeline,处理网络 IO 请求
            socketChannel.pipeline().addLast(new TimeServerHandler());
        }

    }

    private class TimeServerHandler extends ChannelHandlerAdapter {

        @Override
        public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception {
            // ByteBuf 相当于 ByteBuffer 的增强版,提供更多的功能
            ByteBuf buf = (ByteBuf) msg;
            // readableBytes() 方法可以返回缓冲区客户的字节长度
            byte[] bytes = new byte[buf.readableBytes()];
            buf.readBytes(bytes);
            String message = new String(bytes, "UTF-8");
            System.out.println("The time server receive message:" + message);
            String result = message.equalsIgnoreCase("QUERY TIME ORDER")
                    ? new Date(System.currentTimeMillis()).toString() : "I don't know";
            // 将处理结果转换为 ByteBuf 对象
            ByteBuf response = Unpooled.copiedBuffer(result.getBytes());
            // 结果返回给客户端
            ctx.write(response);
        }


        @Override
        public void channelReadComplete(ChannelHandlerContext ctx) throws Exception {
            // 读取数据时,数据首先被读取到缓冲区,上层应用从缓冲区读取
            // 调用 flush() 方法强制清空缓冲区,提高效率
            ctx.flush();
        }

        @Override
        public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) throws Exception {
            // 如果出现异常,关闭 ChannelHandlerContext ,释放相关资源
            ctx.close();
        }
    }
}

Netty 客户端源码

public class TimeClient {

    public static void main(String[] args) {
        // 启动客户端,调用初始化方法
        new TimeClient().connect();
    }

    private static final String ADDRESS = "127.0.0.1";

    private static final int PORT = 8888;

    private void connect() {
        EventLoopGroup group = new NioEventLoopGroup();
        Bootstrap bootstrap = new Bootstrap();
        bootstrap.group(group)
                // 绑定 Channel 类型为 NioSocketChannel
                .channel(NioSocketChannel.class)
                // 禁止 Nagle 算法,该算法为了减少 TCP 包的数量,会将较小的包组合成大包发送
                // 在部分场景下,由于 TCP 延迟,该算法可能导致连续发送两个请求包
                .option(ChannelOption.TCP_NODELAY, true)
                // 这里使用匿名内部类绑定处理器
                .handler(new ChannelInitializer<SocketChannel>() {
                    @Override
                    protected void initChannel(SocketChannel socketChannel) throws Exception {
                        // NioSocketChannel 创建成功初始化时,将 TimeClientHandler 绑定到 pipeline,用于处理网络 IO
                        socketChannel.pipeline().addLast(new TimeClientHandler());
                    }
                });
        // 发起异步连接操作
        ChannelFuture future = bootstrap.connect(ADDRESS, PORT);
        // 阻塞等待客户端关闭链路
        try {
            future.channel().closeFuture().sync();
        } catch (InterruptedException e) {
            e.printStackTrace();
        } finally {
            group.shutdownGracefully();
        }
    }

    private class TimeClientHandler extends ChannelHandlerAdapter {

        private final ByteBuf buf;

        private TimeClientHandler() {
            byte[] bytes = "QUERY TIME ORDER".getBytes();
            buf = Unpooled.buffer(bytes.length);
            buf.writeBytes(bytes);
        }

        @Override
        public void channelActive(ChannelHandlerContext ctx) throws Exception {
            ctx.writeAndFlush(buf);
        }

        @Override
        public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception {
            ByteBuf buf = (ByteBuf) msg;
            byte[] bytes = new byte[buf.readableBytes()];
            buf.readBytes(bytes);
            String message = new String(bytes, "UTF-8");
            System.out.println("Client receive message:" + message);
        }

        @Override
        public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) throws Exception {
            ctx.close();
        }
    }
}

运行结果

服务端:
The time server receive message:QUERY TIME ORDER

客户端:
Client receive message:Tue Oct 13 17:28:48 CST 2020

示例分析

这里我主要挑选部分 Netty 核心 API 做简单介绍,后面在具体学习源码时详细介绍:


EventLoopGroup

如上述示例代码所示,在启动 Netty 服务端前,首先需要初始化两个 NioEventLoopGroup 对象。这两个对象实际上就是两个独立的 Reactor 线程池。其中它的类图如下所示:
EventLoopGroup 类图
如上图所示,该类实现了 ExecutorService 线程池接口。其中 Netty 根据具体功能分以下两种 Group:

  • BossGroup:接受客户端 TCP 连接,初始化 Channel、将链路状态的变更事件通知给 ChannlePipeline;
  • WorkerGroup:异步读取数据,异步发送数据,执行系统调用及执行定时任务 Task

在具体使用过程中可以通过调整线程池的线程数、是否共享线程池等方式改变 Reactor 模型的状态。

Reactor 模型主要分为以下几种:单线程 Reactor、多线程 Reactor、多线程多Reactor。其中在多 Reactor 模型下,可以如上面示例所示,一个负责处理 Accept,一个负责处理 I/O,也可以像我上篇博客最后示例中那样,Accept 集中在某一个 Reactor,I/O 操作随机分配。

其中 Netty 通过两个 EventLoopGroup 逻辑隔离 NIO accept 和 I/O 线程操作,分离处理逻辑。


EventLoop

看类名我们就能猜到,EventLoop 就是 EventLoopGroup 中维护的线程对象。一个 EventLoop 绑定一个特定的线程,并且在它的整个生命周期内,绑定的线程都不会发生改变。其中该类的类图如下所示:
NioEventLoop
这里类结构比较复杂,我们暂时不做深入源码学习,只简单的提一下:抽象类 SingleThreadEventExecutor 中包含 Thread 类型线程对象,该线程实际上就是 EventLoop 内部维护的线程对象,最终该线程会调用到 NioEventLoop 的 run() 方法。

NioEventLoop 主要包含以下功能:

  • 作为 IO 线程,NioEventLoop 主要执行和 Channel 相关的 IO 操作,包含调用 select 等待就绪 I/O 事件,读写数据等
  • 手动添加任务执行和定时任务

关于 NioEventLoop 的介绍先写到这里,我们暂时只需要知道通过它关联某一个 Channel,并执行和该 Channel 相关的 I/O 操作即可。


ChannelPipeline

上述代码中我们主要通过 socketChannel.pipeline() 获取 ChannelPipeline 对象后调用 addLast() 方法添加具体 Handler,所以我们最后聊聊 ChannelPipeline 对象,其中该对象和 Channel,ChannelHandler,ChannelHandlerContext 关系密切,因此我放在一块介绍。

Channel 即管道的意思,一个 Channel 就表示一个客户端和服务端的连接通道,其中它是全双工的,并且支持异步处理。它主要和 Buffer 缓冲区实现 I/O 常见操作。

一般情况下我们不直接操作 Channel,而是通过 ChannelHandler 对象间接操作,根据具体功能的不同,ChannelHandler 分为 ChannelInboundHandler 和 ChannelOutboundHandler,这两种 Handler 分别处理输入数据和输出数据。根据不同的 I/O 事件,需要不用的 Handler 来处理。

本篇示例代码采用 Netty 5.0.0 版本,这里 API 介绍采用 Netty 4.x 版本。Netty 5.0.0 版本不存在 ChannelInboundHandler 接口,只有 ChannelInboundHandlerAdapter 类,并且该类目前不建议使用。

ChannelPipeline 实际上就是以链表的形式维护了一组 ChannelHandler ,当存在具体的 I/O 事件需要处理时,它负责调用具体的 Handler 对事件进行处理。其中 Channel 和 ChannelPipeline一一对应,两者之间可以通过 pipeline() 、channel() 方法互相获取对象。

ChannelPipeline 也不是直接管理 ChannelHandler 的,而是通过 ChannelHandlerContext 间接管理。ChannelHandlerContext 对象可以获取与之相关的 Channel 和 ChannelHandler,其中它和 ChannelHandler 一一对应。四者间的关系如下图所示:
Channel 关系图
如上图所示,一个 Channel 对应一个 ChannelPipeline,每一个 ChannelPipeline 包含多个 ChannelHandler,其中 ChannelPipeline 通过 ChannelHandlerContext 间接调用 ChannelHandler。

ChannelPipeline 内部通过双向链表的形式维护 ChannelHandlerContext 链表,并且链表头和表尾两部分创建两个特殊 Handler,我们手动添加的 Handler 都包含在这两个 Handler 之间,具体效果如下:
ChannelPipeline
这里需要特别的注意的一点是:Inbound 是顺序执行的,Outbound 是逆序执行的。举个例子,假设此时我们调用 write() 方法处理输出流 I/O 事件,下面我列举出具体处理过程,这里暂时省略源码,直接给出结果:

  1. 首先会调用 tail 的 write() 方法,即从链表尾部开始执行
  2. tail write() 会沿着 context 链向前寻找,直至找到一个 OutBound 类型的 Context
  3. 调用该 Context 对应 Handler 的 write() 方法
  4. 默认实现的 write() 方法不做任何处理,继续向前传播

其中输入流的处理正好相反,这里我们省略其过程。由此可见,Pipeline 、Context,Handler 三者相互配合实现 pipeline 的事件传播。


Netty 和 Reactor 模式

最后我们来简单聊聊 Netty 是如何使用 Reactor 模式的,这里我们和官方 Reactor 模型结构进行类比。

从结构层面:

  • Handle:事件资源对应 SelectionKey
  • Synchronous Event Demultiplexer:同步事件分离器即 NIO 中的 Selector,Netty 中 Selector 的细节维护在 LoopGroup中,因此也可以把事件分离器看做 EventLoopGroup。
  • EventHandler:事件处理器即 ChannelHandler
  • Concrete Event Handler:开发者主动实现的 ChannelHandler
  • Initiation Dispatcher:分发器即 EventLoop

至此,Netty 的初步认识基本结束!


参考
《Netty 权威指南》
https://www.cnblogs.com/duanxz/p/3724395.html
https://blog.csdn.net/qq_21033663/article/details/105674261
https://www.cnblogs.com/qdhxhz/p/10234908.html
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
       在Java界,Netty无疑是开发网络应用的拿手菜。你不需要太多关注复杂的NIO模型和底层网络的细节,使用其丰富的接口,可以很容易的实现复杂的通讯功能。 本课程准备的十二个实例,按照由简单到复杂的学习路线,让你能够快速学习如何利用Netty来开发网络通信应用。                每个实例简洁、清爽、实用,重点在“用”上,即培训大家如何熟练的使用Netty解决实际问题,抛弃以往边讲应用边分析源码的培训模式所带来的“高不成低不就”情况,在已经能够熟练使用、并且清楚开发流程的基础上再去分析源码就会思路清晰、事半功倍。        本套课程的十二个实例,各自独立,同时又层层递进,每个实例都是针对典型的实际应用场景,学了马上就能应用到实际项目中去。 学习Netty 总有一个理由给你惊喜!! 一、应用场景        Netty已经众多领域得到大规模应用,这些领域包括:物联网领域、互联网领域、电信领域、大数据领域、游戏行业、企业应用、银行证券金融领域、。。。  二、技术深度        多款开源框架中应用了Netty,如阿里分布式服务框架 Dubbo 的 RPC 框架、淘宝的消息中间件 R0cketMQ、Hadoop 的高性能通信和序列化组件 Avro 的 RPC 框架、开源集群运算框架 Spark、分布式计算框架 Storm、并发应用和分布式应用 Akka、。。。  三、就业前景         很多大厂在招聘高级/资深Java工程师时要求熟练学习、或熟悉Netty,下面只是简要列出,这个名单可以很长。。。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值