(未完待续)【Netty专题】Netty实战与核心组件详解

目录

前言

Netty啊,真的是大名鼎鼎!Netty之于Java网络编程,相当于Spring之于Java开发。两者都是Java生态的金字塔尖的【框架】!所以,非常推荐Java程序员学习这个框架。

Netty有多牛逼?据说,曾经在性能上把谷歌公司一个用C++写的网络通信框架都给人干碎了。后来,后者参照了Netty的设计方案,才完成了超越。

阅读对象

需要有一定的网络编程基础。如若没有,请务必要学习下【阅读导航】中提到的系列上一篇文章。

另外,如果你们了解【设计模式】中的【责任链模式】就更好了。因为在Netty的开发中,Handler使用了【责任链模式】的方式,将各个Handler链化起来。

而且很负责地告诉大家,好多优秀的Java源码,都有【责任链模式】的影子。所以,去学习吧,能帮助你阅读源码以及提升自己的编程技巧。传送门:《史上最全设计模式导学目录(完整版)

阅读导航

系列上一篇文章:《【Netty专题】【网络编程】从OSI、TCP/IP网络模型开始到BIO、NIO(Netty前置知识)

前置知识

Reactor模型(未完待续)

课程内容

一、Netty简介

1.1 Netty是什么

Netty是由 JBOSS 提供的一个Java开源网络通信框架。它是一个【异步事件驱动】的网络应用程序框架,用于快速开发可维护的高性能协议服务器和客户端。

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

上面有个2细节很重要:

  1. Netty是一个【异步事件驱动】的网络应用程序框架(异步,事件驱动,如何理解?)
  2. Netty 是一个基于NIO的客户、服务器端的编程通信框架。NIO,NIO,NIO,讲三遍
    (PS:有心的同学这个时候应该回忆以下,Java的NIO编程里面,有什么组件,或者细节来着?)

1.2 Netty有什么优势

相比传统Java Socket编程、Java NIO,Netty具有如下明显优势

  1. 提供了更高层次的封装,API使用简单,降低了网络编程开发门槛;

传统的NIO开发,你需要独自考虑、处理网络编程中遇到的一些常见问题。如:【断线重连】、【 网络闪断】、【心跳处理】、【粘包】、【半包读写】、【网络拥塞】和【异常流】

  1. 功能强大,预制了多种编解码功能, 支持多种主流的协议;

编解码:网络编程一定要处理的环节。因为数据在网络中传输是需要转换为二进制的,不可能是明文

支持的协议:传输层有TCP、UDP、本地传输;应用层有HTTP、WebSocket等

  1. 定制能力强,通过自定义的ChannelHandler实现更灵活的拓展
  2. 性能高,对比其他主流的NIO框架,Netty的综合性能更优
  3. 成熟、稳定。Netty目前基本没有大更新了,基本上已经修复了所有JDK NIO的BUG
  4. 社区活跃
  5. 已经经历了大规模商业应用的考验,质量有保证

二、第一个Netty程序

2.1 Netty简单使用示例

话不多说,我们先来简单使用一下,开始我们的第一个Netty程序,然后再一点一点推敲。

先导入pom:

xml 复制代码
<dependency>
	<groupId>io.netty</groupId>
	<artifactId>netty-all</artifactId>
	<version>4.1.42.Final </version>
	<scope>compile</scope>
</dependency>

然后引入服务端代码:

java 复制代码
/**
 * Netty服务端
 *
 * @author zhangshen
 * @date 2023/10/21 14:52
 * @slogan 编码即学习,注释断语义
 **/
public class NettyServer {
    static final int PORT = 9999;
    public static void main(String[] args) throws InterruptedException {

        EventLoopGroup bossEventLoopGroup = new NioEventLoopGroup();
        EventLoopGroup wokerEventLoopGroup = new NioEventLoopGroup();

        try {
            ServerBootstrap bootstrap = new ServerBootstrap();
            bootstrap.group(bossEventLoopGroup, wokerEventLoopGroup)
                    .channel(NioServerSocketChannel.class)
                    .localAddress(new InetSocketAddress(PORT))
                    .childHandler(new ChannelInitializer<SocketChannel>() {

                        @Override
                        protected void initChannel(SocketChannel ch) throws Exception {
                            ch.pipeline().addLast(new NettyServerHandler());
                        }
                    });
            System.out.println("Netty服务端正在启动...");

            // 异步绑定到服务器,sync()会阻塞到完成
            ChannelFuture channelFuture = bootstrap.bind().sync();

            // 对通道关闭进行监听,closeFuture是异步操作,监听通道关闭
            // 通过sync()同步等待通道关闭处理完毕,这里会阻塞等待通道关闭完成
            channelFuture.channel().closeFuture().sync();
        } finally {
            bossEventLoopGroup.shutdownGracefully().sync();
        }
    }
}


/**
 * Netty服务端,自定义handler
 *
 * @author zhangshen
 * @date 2023/10/21 15:01
 * @slogan 编码即学习,注释断语义
 **/
public class NettyServerHandler extends ChannelInboundHandlerAdapter {

    @Override
    public void channelActive(ChannelHandlerContext ctx) throws Exception {
        System.out.println("Netty服务器:客户端连接已建立");
        super.channelActive(ctx);
    }

    @Override
    public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception {

        ByteBuf in = (ByteBuf) msg;
        System.out.println("Netty服务器收到消息:" + in.toString(CharsetUtil.UTF_8));

        String responseMsg = "你好啊,Netty客户端";
        ByteBuf buf = Unpooled.copiedBuffer(responseMsg, CharsetUtil.UTF_8);
        ctx.writeAndFlush(buf);
    }

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

接着是Netty客户端代码示例:

java 复制代码
/**
 * Netty客户端代码示例
 *
 * @author zhangshen
 * @date 2023/10/21 15:05
 * @slogan 编码即学习,注释断语义
 **/
public class NettyClient {

    static final int NETTY_SERVER_PORT = 9999;

    public static void main(String[] args) throws InterruptedException {
        EventLoopGroup eventLoopGroup = new NioEventLoopGroup();
        try {
            Bootstrap bootstrap = new Bootstrap();
            bootstrap.group(eventLoopGroup)
                    .channel(NioSocketChannel.class)
                    .remoteAddress(new InetSocketAddress("127.0.0.1", NETTY_SERVER_PORT))
                    .handler(new ChannelInitializer<SocketChannel>() {
                        @Override
                        protected void initChannel(SocketChannel ch) throws Exception {
                            ch.pipeline().addLast(new NettyClientHandler());
                        }
                    });

            // 异步连接到服务器,sync()会阻塞到完成,和服务器的不同点
            ChannelFuture channelFuture = bootstrap.connect().sync();

            // 阻塞当前线程,直到客户端的Channel被关闭
            channelFuture.channel().closeFuture().sync();
        } finally {
            eventLoopGroup.shutdownGracefully().sync();
        }
    }
}

/**
 * Netty客户端代码,自定义handler
 *
 * @author zhangshen
 * @date 2023/10/21 15:05
 * @slogan 编码即学习,注释断语义
 **/
public class NettyClientHandler extends SimpleChannelInboundHandler<ByteBuf> {

    @Override
    protected void channelRead0(ChannelHandlerContext ctx, ByteBuf msg) throws Exception {
        System.out.println("客户端收到消息:" + msg.toString(CharsetUtil.UTF_8));
    }

    @Override
    public void channelActive(ChannelHandlerContext ctx) throws Exception {
        String msgAfterTCP = "你好啊,Netty服务器";
        ctx.writeAndFlush(Unpooled.copiedBuffer(msgAfterTCP, CharsetUtil.UTF_8));
        ctx.alloc().buffer();
    }
}

2.2 代码解读

我们先来简单总结下服务端NettyServer的流程:

  1. 先声明一个ServerBootstrap(Bootstrap的一种)、EventLoopGroup。服务端声明了2个EventLoopGroup,其实1个也可以。这个跟Reactor模式有关
  2. 初始化bootstrap。通过链式调用设置一些属性,比如:group()、localAddress()、childHandler()(其实这些方法,我们可以看成Java POJO中的setXxx() )。最核心的是新增了一个自定义的NettyClientHandler
  3. 然后bootstrap.bind()
  4. 监听通道关闭
  5. 在finally块内关闭EventLoopGroup

而客户端NettyClient呢,它的流程如下:

  1. 先声明一个Bootstrap、EventLoopGroup
  2. 初始化bootstrap。通过链式调用设置一些属性,比如:group()、channel()、remoteAddress()、handler()(其实这些方法,我们可以看成Java POJO中的setXxx() )。最核心的是新增了一个自定义的NettyServerHandler
  3. 然后bootstrap.connect()
  4. 监听通道关闭
  5. 在finally块内关闭EventLoopGroup

看看,从代码主流程来看,其实客户端跟服务端基本没什么很大的区别。当然这不是主要的东西。最重要的是,大家发现没有,这里出现了好几个陌生的API,这就是Netty提供给我们的核心组件!这些组件会在后面给大家详解介绍,也是本文的核心所在!这些组件,分别是:EventLoopGroupBootstra(ServerBootstrap)NioServerSocketChannel(NioSocketChannel)ChannelHandlerChannelPipelineByteBuf

这些API组件有什么特别吗?

同学们还记得我们说【Netty是什么吗】?【Netty 是一个基于NIO的客户、服务器端的编程通信框架】啊!那同学们还记得Java NIO 3个核心组件吗?Channel通道Selector多路复用器ByteBuffer缓冲区嘛(其实还要考虑一个多线程)。

好了,就算我不说你们通过英文翻译也稍微能一一对应上了,既然Netty是基于NIO的,那NIO的这些细节,肯定也会被包含在Netty的组件中!比如:

  • EventLoopGroup:直译【事件循环组】。NIO代码里面很多while(true),然后不断循环检测事件发生,像不像?对的,EventLoopGroup可以看成是一个【线程池】
  • Channel:通道嘛,这个大家最容易理解了。可能有些朋友还不明白Channel通道是什么,其实就是BIO演变到NIO之后,Socket被封装到了Channel里面

OK,更多详细介绍我会在【三、Netty核心组件详解】中讲到。

2.3 Netty的特性

我们在最开始介绍Netty的时候,有这么描述过:它是一个【异步事件驱动】的网络应用程序框架。并且向大家抛出了这么个问题:如何理解【异步事件驱动】?

如果你们看了我上一篇文章其实不难理解。【异步事件驱动】=【异步】+【事件驱动】。

  • 异步:跟同步相对。异步在编程中的体现就是,新开一条线程去处理任务
  • 事件驱动:其实就是Reactor模式在Netty中的体现。Netty是基于Reactor模式设计的

只不过稍微有些不同的是:Netty对于事件的定义。

2.3.1 Netty的事件

Netty 使用不同的事件来通知我们状态的改变或者是操作的状态。这使得我们能够基于已经发生的事件来触发适当的动作。Netty 事件是按照它们与入站或出站数据流的相关性进行分类的。

  1. 可能由入站数据或者相关的状态更改而触发的事件包括:
    • 连接已被激活或者连接失活;
    • 数据读取;
    • 用户事件;
    • 错误事件
  2. 出站事件是未来将会触发的某个动作的操作结果,这些动作包括:
    • 打开或者关闭到远程节点的连接;
    • 将数据写到或者冲刷到套接字

每个事件都可以被分发给 ChannelHandler 类中的某个用户实现的方法,既然事件分为入站和出站,用来处理事件的 ChannelHandler 也被分为可以处理入站事件的 Handler 和出站事件的 Handler,当然有些 Handler 既可以处理入站也可以处理出站。

Netty 提供了大量预定义的可以开箱即用的 ChannelHandler 实现,包括用于各种协议(如 HTTP 和 SSL/TLS)的 ChannelHandler。

基于 Netty 的网络应用程序中根据业务需求会使用 Netty 已经提供的 ChannelHandler 或者自行开发 ChannelHandler,这些 ChannelHandler 都放在 ChannelPipeline 中统一管理,事件就会在 ChannelPipeline 中流动,并被其中一个或者多个 ChannelHandler 处理。

它的原理图如下:

2.4 Netty线程模型

模型解读:

1) Netty 抽象出两组线程池BossGroup和WorkerGroup,BossGroup专门负责接收客户端的连接, WorkerGroup专门负责网络的读写

不就是Reactor模型的主从架构吗

2)BossGroup和WorkerGroup类型都是NioEventLoopGroup

NIO是一种IO方式,epoll,BIO都是。所以,其实还有EpollEventLoopGroup,以此类推

3)NioEventLoopGroup 相当于一个事件循环线程组, 这个组中含有多个事件循环线程 ,每一个事件循环线程是NioEventLoop

4)每个NioEventLoop都有一个selector , 用于监听注册在其上的socketChannel的网络通讯

5)每个Boss NioEventLoop线程内部循环执行的步骤有 3 步

  1. 处理accept事件 , 与client 建立连接 , 生成 NioSocketChannel
  2. 将NioSocketChannel注册到某个worker NIOEventLoop上的selector
  3. 继续处理任务队列的任务 , 即runAllTasks

6)每个worker NIOEventLoop线程循环执行的步骤

  1. 轮询注册到自己selector上的所有NioSocketChannel 的read, write事件
  2. 处理 I/O 事件, 即read , write 事件, 在对应NioSocketChannel 处理业务
  3. runAllTasks处理任务队列TaskQueue的任务 ,一些耗时的业务处理一般可以放入线程池中慢慢处理,这样不影响数据在 pipeline 中的流动处理

7)每个worker NIOEventLoop处理NioSocketChannel业务时,会使用 pipeline (管道),管道中维护了很多 handler 处理器用来处理 channel 中的数据

三、Netty核心组件详解(未完待续)

3.1 EventLoopGroup和EventLoop

EventLoop,直译:事件循环;EventLoopGroup直译:事件循环组。虽然我不知道这是啥,但是基本上可以猜测:后者是对前者做管理的类。所以我们主要了解一下EventLoop先。

回想一下我们在 NIO 中是如何处理我们关心的事件的?很简单,就是在一个 while 循环中 select 出事件,然后依次处理每种事件。这不就是【事件循环】嘛。

3.1.1 EventLoop

EventLoop是Netty 的核心接口,用于处理网络连接的生命周期中所发生的事件。它的类结构如下:

再来看看接口定义:

再看看对应的实现类:

看,我们用到的NioEventLoop就在里面了。如果大家翻开里面的源码,会发现,NioEventLoop里面有几个重要的属性,我这边用伪代码写一下:(有一些属性是在父类中的)

java 复制代码
class NioEventLoop {
	Selector selector;
	Thread thread;
	Queue<Runnable> taskQueue;
	SelectedSelectionKeySet selectedKeys;
}

由上面的伪代码可以看到,NioEventLoop 中:

  1. 维护了一个线程任务队列(是否似曾相识?线程池呀!),支持异步提交执行任务,线程启动时会调用 NioEventLoop 的 run 方法,执行 I/O 任务和非 I/O 任务:
    • I/O 任务,即 selectionKey 中 ready 的事件,如 accept、connect、read、write 等,由 processSelectedKeys 方法触发
    • 非 IO 任务,添加到 taskQueue 中的任务,如 register0、bind0 等任务,由 runAllTasks 方法触发
  2. 维护了一个Selector选择器。熟悉NIO的朋友估计就知道这是啥了
  3. 维护了一个SelectionKeySet。这个不知道大家有没有印象,在NIO模型中我说:向Selector注册了Channel和感兴趣事件后就会被包装成一个SelectionKey

可是我们知道的,一台电脑顶多就几千条线程,所以,能分配到一个Netty中的线程数必然也不会多,或者说我们创建出来的EventLoop必然也不会太多。那,Netty号称百万并发量是如何实现的呢?

所以这里势必会有一种对应关系:1个EventLoop管理N个连接(或者说Channel)

线程的分配

异步传输实现只使用了少量的 EventLoop(以及和它们相关联的 Thread),而且在当前的线程模型中,它们可能会被多个 Channel 所共享。这使得可以通过尽可能少量的 Thread 来

3.1.2 NioEventLoopGroup

NioEventLoopGroup,主要管理 eventLoop 的生命周期,可以理解为一个线程池,内部维护了一组线程,每个线程(NioEventLoop)负责处理多个 Channel 上的事件,而一个 Channel 只对应于一个线程。

学习总结

感谢

相关推荐
虽千万人 吾往矣几秒前
golang 熔断限流降级
开发语言·后端·golang
chengxuyuan1213_10 分钟前
python常用基础语法
开发语言·windows·python
山山而川粤43 分钟前
美食推荐系统|Java|SSM|JSP|
java·开发语言·后端·学习·mysql
#sakura1 小时前
javaEE
java·开发语言·java-ee
arong_xu1 小时前
Modern C++ std::atomic简介
开发语言·c++·atomic
wowing-1 小时前
Kotlin快速入门
开发语言·kotlin
AI人H哥会Java2 小时前
【Spring】基于XML的Spring容器配置——Bean的作用域
java·开发语言·后端·spring·架构
紧跟先前的步伐2 小时前
java常用类(下)
java·开发语言·算法
爱数学的程序猿2 小时前
Python入门:7.Pythond的内置容器
开发语言·python