Nice To Meet You Netty!

Java网络编程 专栏收录该内容
4 篇文章 0 订阅


Nice To Meet You Netty!

1. Netty是啥?

​ Netty是由JBOSS提供的一个java开源框架,现为 Github上的独立项目。Netty提供异步的、事件驱动的网络应用程序框架和工具,用以快速开发高性能、高可靠性的网络服务器和客户端程序。

​ 也就是说,Netty 是一个基于NIO的客户、服务器端编程框架,使用Netty 可以确保你快速和简单的开发出一个网络应用,例如实现了某种协议的客户、服务端应用。Netty相当于简化和流线化了网络应用的编程开发过程,例如:基于TCP和UDP的socket服务开发。

​ “快速”和“简单”并不用产生维护性或性能上的问题。Netty 是一个吸收了多种协议(包括FTP、SMTP、HTTP等各种二进制文本协议)的实现经验,并经过相当精心设计的项目。最终,Netty 成功的找到了一种方式,在保证易于开发的同时还保证了其应用的性能,稳定性和伸缩性

​ 以上来源于百度百科对Netty 的解释,简单的说Netty 就是对java NIO封装的一个框架,可以更简单,更方便的使用NIO

2. 为啥使用Netty?

​ 当然如果对NIO编程精通的大神完全可以不用Netty,自己编写相应的代码。为什么使用Netty,当然是因为学习Netty的成本比学习底层NIO开发的成本相对来说较低,又能 很 方便 的 实现很牛逼的功能,所以采用喽。

​ 哈哈,道理是这么个道理,但是需要换个说法,Netty 能屏蔽底层API的复杂性,使程序员操作起来更方便,这里首先复习一下Java原生API对网络编程这一块的内容。

2.1 BIO(同步、阻塞)

​ 经典的IO模型也就是传统的服务器端同步阻塞 I/O处理(也就是BIO,Blocking I/O)的经典编程模型,也就是Socket(套接字编程),主要原因在于socket.accept()、socket.read()、socket.write()三个主要函数都是同步阻塞的,所以,传统的BIO也是同步阻塞的,当服务器每得到一个新的连接时,就会开启一个线程来处理这个连接的任务。之所以使用多线程,当一个连接在处理I/O的时候,系统是阻塞的,如果是单线程的话必然就挂死在那里;但CPU是被释放出来的,开启多线程,就可以让CPU去处理更多的事情。

​ 这里思考一个问题,平时我们编写接口的时候,并没有进行套接字,多线程这些内容的编写,但是工作总要有某个东西完成,到底是谁做的呢?如果对Tomcat 或者Jetty 这类容器,了解的比较多的就会知道,其实这些事情都是容器在处理的。这些项目容器,负责与请求打交道,而今天使用的Netty 也是可以完成类似的操作的。

2.1.1 同步/异步

​ 同步,异步这两个概念最直观的表现,就是Ajax的页面和非Ajax的页面,局部渲染就是异步的最佳诠释

​ 我个人对这两个概念的理解是,生活中的一个实际例子 是打电话 与 发短信。

2.1.1.1模拟场景1 — 同步 (打电话)

​ Miss A: 帅哥,我需要你帮我看一下,哪款手机更适合我这种高贵,典雅,有气质的美女。

​ Little Brother B:好的,我帮您看看,我店里的最新旗舰手机Huwei Mate30 比较适合您。(在专心帮Miss A 处理问题,此时可能 有 Miss C打电话过来,但是并没有被 Little Brother B 处理)

​ Miss A: 麻烦您帮我看看还有其他的款式。(此时Little Brother B并没有去处理 Miss C 打过来的电话)

​ 通过这个例子,我想表达的是同步 指 的是 专一的做某件事情,有种两耳不闻窗外事,一心只读圣贤书的感觉

2.1.1.2模拟场景2 — 异步 (发短信)

​ Miss A 的快递到了,Little Brother B 快递小哥哥,给Miss A打电话,Miss A没有接到,这时候 Little Brother B 发了一个短信给Miss A,通知Miss A 您有新的快递到了, 我在XXX地点等您,最多半个小时,如果您现在不方便收快递,我下午再给您送过来。(重点)Little Brother B 发完短信后,继续给 Miss C 打电话,做同样的事情。

​ 而异步能则表现的比较混乱,更有种 家事国事天下事,事事关心的感觉

​ 对比上述两个场景,Little Brother B 对待事情的不同处理方法,就是同步 与 异步的最佳实践。

​ 总结:同步与异步 这里 要是结合 CPU 与 线程模型理解更容易些,CPU执行的时间片段 内,只能执行某一个线程里面的代码,而同步 就 是 只有一个线程的特例,所有的代码都要一行,一行执行。而 异步 则 恰巧对应多线程的模型,每条线程 都希望能争抢到 CPU 的执行时间 片段。

2.1.2阻塞/非阻塞

​ 还是模拟场景吧,阻塞与非阻塞 更像 排队等公交的 场景,早上我在生命科学园地铁站等公交到公司上班,途径该路线的公交 有417 和 871 两趟列车,供我选择。

2.1.2.1 模拟场景1 — 阻塞(我就要上417 , 871 来了 我也不上)

​ 阻塞指的目标明确,我就认准了417 ,其他的来了,我就不坐,爱咋咋地,迟不迟到,打不打卡无所谓,哥就是喜欢417,就是任性。

2.1.2.2 场景模拟2 — 非阻塞 (我要上班不迟到,那辆公交来了我都上)

​ 非阻塞的目标是 为了 尽快赶到公司,不能迟到,得 按时打卡, 只要能让我快速到达公司的任何 一列公交都可以,不一定要等 417 ,871 来了 我也坐。

​ 总结,阻塞与非阻塞 描述 的是 对获取资源时操作的 描述。上个例子 中 资源 就 相当于 公交车。

2.1.3 BIO 的缺点

​ 最重要的一点就是 一个 请求对应 一个线程,如果并发访问量比较大的情况下,并且不能及时得到响应的时候,会根据容器的执行策略进行响应的操作,这也就意味着,大量的用户请求无法命中。

2.2 NIO(同步、非阻塞)

​ 既然BIO 有缺陷,技术总是要解决遇到的问题,那么开发JDK的大神们就忍不住了,这不能忍,不就并发吗,那就搞个NIO解决一下痛点。

​ 非阻塞IO(NIO Blocking IO), NIO之所以是同步,是因为它的accept/read/write方法的内核I/O操作都会阻塞当前线程。

​ NIO的三个主要组成部分:Channel(通道)、Buffer(缓冲区)、Selector(选择器)

​ 这里的东西太多了,就不展开了,选择性跳过。

​ 有兴趣的可以参考 https://www.cnblogs.com/sxkgeek/p/9488703.html 博客 阅览一下,阅览就好,因为要是把这都弄得 明明 白白 的 也就没今天的 主角 Netty 什么事情了。

3.哪里使用Netty?

​ 各领域应用,例如大数据、游戏等,Netty 作为高性能的通信框架用于内部各模块的数据分发、传输和汇总等,实现模块之间高性能通信

​ 总结,Netty主要用于 数据传输

3.1 互联网行业

在分布式系统中,各个节点之间需要远程服务调用,高性能的RPC框架必不可少,Netty作为异步高新能的通信框架,往往作为基础通信组件被这些RPC框架使用。

​ 典型的应用有:阿里分布式服务框架Dubbo的RPC框架使用Dubbo协议进行节点间通信,Dubbo协议默认使用Netty作为基础通信组件,用于实现各进程节点之间的内部通信。

​ 除了 Dubbo 之外,淘宝的消息中间件 RocketMQ 的消息生产者和消息消费者之间,也采用 Netty 进行高性能、异步通信。

3.2 游戏行业

​ 无论是手游服务端还是大型的网络游戏,Java语言得到了越来越广泛的应用。Netty作为高性能的基础通信组件,它本身提供了TCP/UDP和HTTP协议栈。

​ 非常方便定制和开发私有协议栈,账号登录服务器,地图服务器之间可以方便的通过Netty进行高性能的通信

3.3 大数据领域

​ 经典的Hadoop的高性能通信和序列化组件Avro的RPC框架,默认采用Netty进行跨界点通信,它的Netty Service基于Netty框架二次封装实现。

4.Netty的核心组件

​ 这里先列举一下 Netty 的 官方 资料

  1. 官网 Netty:Home (https://netty.io/)

  2. Netty 本身 是一个开源项目,github 地址 (https://github.com/netty/netty) git上 有 Netty使用的基础Demo

  3. stackoverflow 英文 论坛 (https://stackoverflow.com/questions/tagged/netty)里面不仅包含Netty,还有各种各样技术问题

  4. Netty权威指南 第2版 (中文版,你懂得内部交流使用)

    我会结合 Netty 官方提供 的文档 介绍 一下 Netty 从 启动, 到自定义业务 的实现所涉及的组件,以及开发时所需的一些思想

4.1 启动引导类( ServerBootstrap 或者 Bootstrap )

4.1.1 ServerBootstrap( 服务端使用 )

​ 一个Netty应用通常由一个Bootstrap开始,它主要作用是配置整个Netty程序,串联起各个组件。Netty提供了一整套的 方法链 调用,用于初始化启动引导类。

​ 官方Demo ( 节选自 Snoop项目的实现)

        // Configure the server.
        EventLoopGroup bossGroup = new NioEventLoopGroup(1);
        EventLoopGroup workerGroup = new NioEventLoopGroup();
        try {
            ServerBootstrap b = new ServerBootstrap();
            b.group(bossGroup, workerGroup);
            b.channel(NioServerSocketChannel.class);
            b.handler(new LoggingHandler(LogLevel.INFO));
            b.childHandler(new HttpSnoopServerInitializer());
            
        } finally {
            bossGroup.shutdownGracefully();
            workerGroup.shutdownGracefully();
        }

​ EventLoopGroup 指的 是实现 轮询 组 (本质是一个 死循环 的 处理事件 线程)

​ 但是 由于 负责 的功能 不同, 被 人为 的分成了 请求接收线程 和 业务处理线程

	1. 从 上面 的 Demo 中 可以看 到 bossGroup 被分成了 请求接收 线程 (判断依据 调用 group(param1,param2) 方法时,在 param1 的 位置,使用构造 方法 创建 对象 传入 某个 数值 ,代表的是 线程池 的核心 线程 数量 ,例如EventLoopGroup bossGroup = new NioEventLoopGroup(1) 代表 的 是 创建 了 一个 核心 线程数 为 1 的 线程池,默认的 则 代表 0 ,这里涉及到线程 的相关 知识,在这里 就不展开 介绍了)

 	2. 同样的 原理 , 业务处理线程 的 判断依据 也是 看调用 group(param1,param2) 方法时,但 所在 位置 确实 param2
4.1.2 Bootstrap (客户端使用)

​ 相对于服务端来说,客户端启动都不会涉及很多内容,线程池只有调度线程池,没有工作线程池。

​ 官方Demo ( 节选自 Snoop项目的实现)

// Configure the client.
EventLoopGroup group = new NioEventLoopGroup();
try {
    Bootstrap b = new Bootstrap();
    b.group(group);
    b.channel(NioSocketChannel.class);
    b.handler(new HttpSnoopClientInitializer());

} finally {
    // Shut down executor threads to exit.
    group.shutdownGracefully();
}

​ 这里可以 看出 客户端(Client 端)使用的 启动 引导 是Bootstrap 而不是 ServerBootstrap,这里需要注意一下

4.1.3 ServerBootstrap / Bootstrap 常用API 介绍
4.1.3.1 ServerBootstrap 常用 API
  1. .group(EventLoopGroup param1, EventLoopGroup param2) 用于 设置 请求接收线程 和 业务处理线程

  2. .channel(Class XXXChannel.class) 用于 设置 channel 通道 类型(注意 这里 传入的 是Channel 通道 的 class文件)

  3. .handler(ChannelHandler handlerPipeline) 用于 设置 请求接收 线程 的 责任链

  4. .childHandler(ChannelHandler handlerPipeline) 用于 设置 业务处理 线程 的 责任链

  5. .localAddress(InetSocketAddress address)用于 设置 服务 启动 占用 的 ip 及 端口号

  6. .option(ChannelOption channelOption)用于 设置 请求接收线程 的 通道类型

  7. .childOption(ChannelOption channelOption)用于 设置 业务处理线程 的 通道类型

    总结: 这里 其实 与NIO 的Reactor 线程 模型 很相似

4.1.3.2 Bootstrap 常用 API

​ 相对于 ServerBootstrap 的 API 而言 BootStrap 的 API 更少些, 因为 客户端 只需要 维护 请求发送 线程即可,并不需要 业务 处理线程

  1. .group(EventLoopGroup param1)用于 设置 请求发送线程

​ 注意: 这里需要 注意 的是 BootStrap 的API 与 ServerBootstrap 的API 很相似,但是 BootStrap 只有 一个 请求 发送线程,所以 ServerBootstrap 中 所有 以 child 开头的 API 在 Client 端 均无法 使用

4.2 责任链管理类(ChannelInitializer )

​ 在官方 的Demo 项目 Snoop 中 针对 于 启动引导类 中 的 .handler() 或 .childHandler() 两个 方法 的 参数 单独封装了 一个 类, 再看其他 实现的 时候, 也有使用 匿名 实现 的方式 进行 责任链 管理的

4.2.1 封装 成类 的方式

​ 这里 以 服务端(Server) 为例, 客户端(client) 的例子 是相似的

public class HttpSnoopServerInitializer extends ChannelInitializer<SocketChannel> {

    @Override
    public void initChannel(SocketChannel ch) {
        
        ChannelPipeline p = ch.pipeline();
        p.addLast(new HttpRequestDecoder());
        p.addLast(new HttpResponseEncoder());
        p.addLast(new HttpSnoopServerHandler());
        
    }
}
  1. 自定义 的 ChannelInitializer 需要 继承 ChannelInitializer

  2. 重写 initChannel 方法

  3. 通过 ChannelPipeline 设置 Handler

    ChannelPipeline 是 责任链 的一种 实现

    我先谈下 自己 对责任链 的理解

    责任链 就 像 一串 杂果 味 的 冰糖葫芦 ,每一种 水果 负责 刺激 不同 味觉 的 味蕾。举个 例子, 我买了一串 杂果 味 的冰糖葫芦,水果 顺序 从上 倒下 依次 是 橘子 --> 苹果 --> 大枣 --> 芒果 --> 哈密瓜 --> … 这些水果 都是 已经 被预先 安排好了,只要 我 按 顺序 来 品尝 这美味 就 可以了。责任链 最大 的优势 就是 实现了 可插拔 式 的编程 ,面对 快速 的需求变更 可以 做到 在尽量小的 改动 原有 代码 的前提 下 满足新的 需求 变更,从而更适应 快速 的敏捷 开发。

    总结: 降低了代码 之间 的 耦合,提高了 代码 的 复用率。是一种值得尝试 借鉴 的编程 方式。

    同样 换成 代码 的实现 也是 如此,只不过 串 上 穿 的 不在 是 水果,而是 各种 Handler,这里 结合 demo 中代码 进行一下阐述, 首先 会 初始化 一个 ChannelPipeline ,调用 向链上 添加 Handler 的 API (addLast 将新的 Handler 添加在末尾),无论 是 Decoder 还是 Encoder ,亦或是 其他 的 ,本质上 都是 Handler

    提到 Handler 需要 提到一点, 就是 Netty 的 事件 机制, 有点 扯远了, 还是先 把 匿名 实现 的代码 也看下吧,等下 再来看事件 机制。

4.2.2 匿名 方式 实现

匿名方式 实现 指的 是在 初始化 bootstrap 的时候 ,使用 匿名 的 实现方式

bootstrap.childHandler(new ChannelInitializer<SocketChannel>() {
    @Override
    protected void initChannel(SocketChannel ch) throws Exception {
        ch.pipeline().addLast(new MyChannelHandler1());
        ch.pipeline().addLast(new MyChannelHandler2());
        ch.pipeline().addLast(new MyChannelHandler3());
    }
});
  1. 不需要 创建 新 的 类
  2. 通过 启动 引导类 ,调用 .childHandler(new ChannelInitializer() { })的方式 实现
  3. 重写 initChannel方法

4.3 任务处理器(XXXHandler)

提到 Handler 就要 说到 netty 的 事件 机制了。netty 代码 的 执行 ,都是 通过 事件 触发的,每一个 Handler 都会继承相应 的事件, 总的来说 netty 的 事件 分为 入站 事件(ChannelInboundHandlerAdapter), 出站 事件(ChannelOutboundHandlerAdapter), 还有 就是 入站 和 出站 的混合事件(ChannelDuplexHandler),因此 每个 在 ChannelPipeline上 的 Handler 都会 继承 其中 的某一个 事件。

​ 当继承 完 某一个 事件 后 可以 通过 重写 相应 的 事件 方法 ,控制 自己代码 的执行

​ 这里 用 入站 事件(ChannelInboundHandlerAdapter) 举例

先贴段 官方 的示例 代码

public class HttpSnoopServerHandler extends SimpleChannelInboundHandler<Object> {
    @Override
    public void channelReadComplete(ChannelHandlerContext ctx) {
        ctx.flush();
    }
    
    @Override
    protected void channelRead0(ChannelHandlerContext ctx, Object msg) {
    }
}

​ 分析 : HttpSnoopServerHandler 继承了 SimpleChannelInboundHandler 说明 该 Handler 会在 数据 入站 之前执行,里面 重写 两个 方法 ,一个是channelReadComplete()方法, 另一个 是 channelRead0()方法。

  1. channelReadComplete() 代表的 是 通道 读取 完成 时 触发 的方法

  2. channelRead0() 代表的 是 收到 通道消息 的 时候 触发 的方法

总结, Netty 的 EventLoopGroup 的作用 是 管理 通过 事件 的监听 触发相应的 代码 执行

	1. 因此 使用 Netty 的第一步要做的就是 继承 相应 的 事件 (入站 ,出站, 或者混合 事件 或 其 相关子类)

 	2. 通过 重写 相关的 事件 方法 ,将 自己 的代码 嵌入 到 需要执行 部分

4.3 通道(Channel)

Netty使用 channel 通道 作为 数据 传输 媒介,它 代表 了一个 Socket 链接,或者 其它和IO操作相关 的 组件,它和EventLoop一起用来参与IO处理。

4.4 Future

在Netty中所有的IO操作都是异步的,因此,你不能立刻得知消息是否被正确处理,但是我们可以过一会等它执行完成或者直接注册一个监听,具体的实现就是通过Future和ChannelFutures,他们可以注册一个监听,当操作执行成功或失败时监听会自动触发。

总之,所有的操作都会返回一个ChannelFuture。

5. 框架整合

5.1 服务端(Server端 )

5.1.1 服务端(Server端 )伴随 模块 启动 的同时 一起启动

​ 5.1.1 Spring boot CommandLineRunner接口

​ 可实现在应用初始化后,去执行一段代码块逻辑,这段初始化代码在整个应用生命周期内只会执行一次。

​ 关于 CommandLineRunner 的相关 内容 可以 参阅 https://www.cnblogs.com/chenpi/p/9696310.html

5.1.2 使用@Component 注解 将 NettyServer 类 委托 Spring 管理

​ 将NettyServer 作为 组件 注入到 委托给 Spring 管理, 这里需要 注意 一下, 当 把 Server 委托给 Spring管理后,那么ChannelInitializer 和 XXXHandler 也要 委托 给 Spring 管理,否则 需要 参考 下列文章 进行 相关处理

​ spring非IOC容器中的对象获取IOC容器中对象的方法(https://www.cnblogs.com/chenny3/p/10226207.html )

5.1.3 netty的@ChannelHandler.Sharable

​ 将Handler类 设置 为 单例模式,这里需要注意的是

​ 如果 不在 ChannelInitializer 私有化 Handler 那么 @ChannelHandler.Sharable 会 失效

​ eg .

@Component
@ChannelHandler.Sharable
public class NettyServerHandler extends ChannelInboundHandlerAdapter {
   
}
@Component
public class NettyServerInitializer extends ChannelInitializer<SocketChannel> {

    @Resource
    private NettyServerHandler nettyServerHandler;
    
   	@Override
    protected void initChannel(SocketChannel ch) throws Exception {
        ChannelPipeline channel = ch.pipeline();
    	channel.addLast("serviceHandler", nettyServerHandler);
        /**
         * 如果 channel.addLast("serviceHandler", new NettyServerHandler()); 的形式
         * 那么 @ChannelHandler.Sharable 将 失效
         */
    }
}

5.2 客户端(client端)

​ 整合总是 需要 伴随 着 业务 调整 而改变,根据 不同 的需求 做出 响应 的调整,灵活运用技术,才能不断的提升

​ 5.2.1 客户端(client端)

​ 总的思路如下:

	1. 需要通过 ChannelFutura 对 异步 获取的Server 端 结果 进行 响应的 处理


 		2. 如果想提高 Channel 的利用率, 需要解决Channel 的多路复用 问题

5.3 整合后代位置

​ http://gitlab.chngyx.com.cn/spot/hnit-nio-service.git 项目

​ hnit-nio-service/src/main/java/com/hnit/nio/test/nettyServer 包 中 的内容

6. Nice To Meet You Netty!

​ 在处理Netty 过程遇到的问题,及注意事项

6.1 编程 思维 误区

​ 对于 非阻塞 编程,一定 要摒弃 平时 编写 controller 时候 的 思维模式,因为平时编写 接口的 时候并不需要我们亲自 处理 网络 相关的 内容,而使用 netty 则 需要 协调 网络 相关 的 内容。

​ eg. 在 处理 server 端 返回 结果 的 时候,常规方式 操作 需要 格外 注意 一定 要等 获取到 返回值 后 在关闭 Channel ,换句话说 就是 需要 调用 如下 阻塞 的方法 后, 才能 显示 调用 channel 关闭 方法。

channel.closeFuture().sync();//一定要等待服务端关闭通道

6.2 client 端 获取 server 端 返回值 的 时候,不能 从 ChannelFutura 中 获取

​ ChannelFutura 确实 是 可以 获取 Server 的响应,但这个 响应 仅仅 是一个 状态, 而不是 服务端 传递回来的数据,如果 想要 获取 数据 ,则需要 使用 闭锁 方式 获取

​ 闭锁 ,本质上 是 等待 其他 线程 执行 完毕 后 在 执行 调用整者 线程

​ 在 netty 客户 端 中,我 在 客户端 初始化了 一个 CountDownLunch 锁, 然后 将 这个 锁 传递 到 ChannelInitializer 最后 到 Handler 的 channelRead0() 方法 中 处理 响应 内容

7. 有不足 或者 错误 的 地方,还请指正

参考文档:

  1. java IO、NIO、AIO详解(https://www.cnblogs.com/sxkgeek/p/9488703.html )
  2. Netty的实现原理、特点与优势、以及适用场景 ( http://www.360doc.com/content/18/1122/20/99071_796591489.shtml )
  3. netty的优势( https://blog.csdn.net/shijinghan1126/article/details/88088422 )
  4. Spring boot CommandLineRunner接口使用例子(https://www.cnblogs.com/chenpi/p/9696310.html )
  5. @Autowired 与@Resource的区别(详细)( https://blog.csdn.net/weixin_40423597/article/details/80643990 )
  • 0
    点赞
  • 0
    评论
  • 0
    收藏
  • 一键三连
    一键三连
  • 扫一扫,分享海报

©️2021 CSDN 皮肤主题: 书香水墨 设计师:CSDN官方博客 返回首页
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值