目录
- 前言
- 阅读对象
- 阅读导航
- 前置知识
- 课程内容
- 一、Netty简介
- 1.1 Netty是什么
- 1.2 Netty有什么优势
- 二、第一个Netty程序
- 2.1 Netty简单使用示例
- 2.2 代码解读
- 2.3 Netty的特性
- 2.3.1 Netty的事件
- 2.4 Netty线程模型
- 三、Netty核心组件详解(未完待续)
- 3.1 EventLoopGroup和EventLoop
- 3.1.1 EventLoop:事件循环
- 3.1.2 NioEventLoopGroup:事件循环组
- 3.1.3 所谓异步的体现
- 3.2 Channel:通道
- 3.2.1 Channel接口
- 3.3 ChannelPipeline 和 ChannelHandlerContext
- 3.3.1 ChannelPipeline 接口
- 学习总结
- 感谢
前言
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细节很重要:
- Netty是一个【异步事件驱动】的网络应用程序框架(异步,事件驱动,如何理解?)
- Netty 是一个基于NIO的客户、服务器端的编程通信框架。NIO,NIO,NIO,讲三遍
(PS:有心的同学这个时候应该回忆以下,Java的NIO编程里面,有什么组件,或者细节来着?)
1.2 Netty有什么优势
相比传统Java Socket编程、Java NIO,Netty具有如下明显优势
- 提供了更高层次的封装,API使用简单,降低了网络编程开发门槛;
传统的NIO开发,你需要独自考虑、处理网络编程中遇到的一些常见问题。如:【断线重连】、【 网络闪断】、【心跳处理】、【粘包】、【半包读写】、【网络拥塞】和【异常流】
- 功能强大,预制了多种编解码功能, 支持多种主流的协议;
编解码:网络编程一定要处理的环节。因为数据在网络中传输是需要转换为二进制的,不可能是明文
支持的协议:传输层有TCP、UDP、本地传输;应用层有HTTP、WebSocket等
- 定制能力强,通过自定义的ChannelHandler实现更灵活的拓展
- 性能高,对比其他主流的NIO框架,Netty的综合性能更优
- 成熟、稳定。Netty目前基本没有大更新了,基本上已经修复了所有JDK NIO的BUG
- 社区活跃
- 已经经历了大规模商业应用的考验,质量有保证
二、第一个Netty程序
2.1 Netty简单使用示例
话不多说,我们先来简单使用一下,开始我们的第一个Netty程序,然后再一点一点推敲。
先导入pom:
<dependency><groupId>io.netty</groupId><artifactId>netty-all</artifactId><version>4.1.42.Final </version><scope>compile</scope>
</dependency>
然后引入服务端代码:
/*** 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>() {@Overrideprotected 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 {@Overridepublic void channelActive(ChannelHandlerContext ctx) throws Exception {System.out.println("Netty服务器:客户端连接已建立");super.channelActive(ctx);}@Overridepublic 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);}@Overridepublic void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) throws Exception {super.exceptionCaught(ctx, cause);}
}
接着是Netty客户端代码示例:
/*** 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>() {@Overrideprotected 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> {@Overrideprotected void channelRead0(ChannelHandlerContext ctx, ByteBuf msg) throws Exception {System.out.println("客户端收到消息:" + msg.toString(CharsetUtil.UTF_8));}@Overridepublic void channelActive(ChannelHandlerContext ctx) throws Exception {String msgAfterTCP = "你好啊,Netty服务器";ctx.writeAndFlush(Unpooled.copiedBuffer(msgAfterTCP, CharsetUtil.UTF_8));ctx.alloc().buffer();}
}
2.2 代码解读
我们先来简单总结下服务端NettyServer
的流程:
- 先声明一个ServerBootstrap(Bootstrap的一种)、EventLoopGroup。服务端声明了2个EventLoopGroup,其实1个也可以。这个跟Reactor模式有关
- 初始化bootstrap。通过链式调用设置一些属性,比如:group()、localAddress()、childHandler()(其实这些方法,我们可以看成Java POJO中的setXxx() )。最核心的是新增了一个自定义的
NettyClientHandler
- 然后bootstrap.bind()
- 监听通道关闭
- 在finally块内关闭EventLoopGroup
而客户端NettyClient
呢,它的流程如下:
- 先声明一个Bootstrap、EventLoopGroup
- 初始化bootstrap。通过链式调用设置一些属性,比如:group()、channel()、remoteAddress()、handler()(其实这些方法,我们可以看成Java POJO中的setXxx() )。最核心的是新增了一个自定义的
NettyServerHandler
- 然后bootstrap.connect()
- 监听通道关闭
- 在finally块内关闭EventLoopGroup
看看,从代码主流程来看,其实客户端跟服务端基本没什么很大的区别。当然这不是主要的东西。最重要的是,大家发现没有,这里出现了好几个陌生的API,这就是Netty提供给我们的核心组件!这些组件会在后面给大家详解介绍,也是本文的核心所在!这些组件,分别是:EventLoopGroup
、 Bootstrap(ServerBootstrap)
、 NioServerSocketChannel(NioSocketChannel)
、 ChannelHandler
、 ChannelPipeline
(这个需要点进去才能看到)、ByteBuf
。
这些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 事件是按照它们与入站或出站数据流的相关性进行分类的。
- 可能由入站数据或者相关的状态更改而触发的事件包括:
- 连接已被激活或者连接失活;
- 数据读取;
- 用户事件;
- 错误事件
- 出站事件是未来将会触发的某个动作的操作结果,这些动作包括:
- 打开或者关闭到远程节点的连接;
- 将数据写到或者冲刷到套接字
每个事件都可以被分发给 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 步
- 处理accept事件 , 与client 建立连接 , 生成 NioSocketChannel
- 将NioSocketChannel注册到某个worker NIOEventLoop上的selector
- 继续处理任务队列的任务 , 即runAllTasks
6)每个worker NIOEventLoop线程循环执行的步骤
- 轮询注册到自己selector上的所有NioSocketChannel 的read, write事件
- 处理 I/O 事件, 即read , write 事件, 在对应NioSocketChannel 处理业务
- runAllTasks处理任务队列TaskQueue的任务 ,一些耗时的业务处理一般可以放入线程池中慢慢处理,这样不影响数据在 pipeline 中的流动处理
7)每个worker NIOEventLoop处理NioSocketChannel业务时,会使用 pipeline (管道),管道中维护了很多 handler 处理器用来处理 channel 中的数据
三、Netty核心组件详解(未完待续)
我们在【2.2 代码解读】中除了简单分析了Netty服务端客户端使用流程外,还给大家挖掘出了一些Netty的核心组件。分别是:EventLoopGroup
、 Bootstrap(ServerBootstrap)
、 NioServerSocketChannel(NioSocketChannel)
、 ChannelHandler
、 ChannelPipeline
(这个需要点进去才能看到)、ByteBuf
。接下来我们就开始着手研究研究。
这些组件出现的位置大概就是代码流程【自上而下】、【自内而外】吧。
3.1 EventLoopGroup和EventLoop
EventLoop
直译:事件循环;EventLoopGroup
直译:事件循环组。
虽然我不知道这是啥,但是基本上可以猜测:后者是对前者做管理的类。所以我们还是要了解一下EventLoop
先。
回想一下我们在 NIO 中是如何处理我们关心的事件的?很简单,就是在一个 while 循环中 select 出事件,然后依次处理每种事件,这不就是【事件循环】嘛。
3.1.1 EventLoop:事件循环
EventLoop是Netty 的核心接口,用于处理网络连接的生命周期中所发生的事件。它的类结构如下:
再来看看接口定义:
再看看对应的实现类:
看,我们用到的NioEventLoop
就在里面了。如果大家翻开里面的源码,会发现,NioEventLoop
里面有几个重要的属性,我这边用伪代码写一下:(有一些属性是在父类中的)
class NioEventLoop {Selector selector;Thread thread;Queue<Runnable> taskQueue;SelectedSelectionKeySet selectedKeys;
}
由上面的伪代码可以看到,NioEventLoop
中:
- 维护了一条
线程
和任务队列
(是否似曾相识?线程池呀!),支持异步提交执行任务,线程启动时会调用 NioEventLoop 的 run 方法,执行 I/O 任务和非 I/O 任务:- I/O 任务,即 selectionKey 中 ready 的事件,如 accept、connect、read、write 等,由 processSelectedKeys 方法触发
- 非 IO 任务,添加到 taskQueue 中的任务,如 register0、bind0 等任务,由 runAllTasks 方法触发
- 维护了一个
Selector
选择器(多路复用器)。这个在NIO里面,就是循环遍历注册在Selector
上的所有Socket
,然后响应对应事件嘛。所以从这个东西一定能看出一个东西,那就是:【一个EventLoop里面肯定管理了多个Channel
】 - 维护了一个
SelectionKeySet
。这个不知道大家有没有印象,在NIO模型中我说:向Selector
注册了Channel
和感兴趣事件后就会被包装成一个SelectionKey
3.1.2 NioEventLoopGroup:事件循环组
NioEventLoopGroup:事件循环组。它是做什么的呢?其实,它主要干下面几件事情:
- 管理 EventLoop 的生命周期,可以理解为一个线程池,内部维护了一组线程
- 负责为每个新建的
Channel
分配一个 EventLoop
EventLoop的分配
异步传输实现只使用了少量的 EventLoop(以及和它们相关联的 Thread),而且在当前的线程模型中,它们可能会被多个 Channel 所共享。这使得可以通过尽可能少量的 Thread 来支撑大量的 Channel,而不是每个 Channel 分配一个Thread(EventLoop)。
EventLoopGroup 负责为每个新创建的 Channel 分配一个 EventLoop。怎么实现的呢?顺序分配。
在当前实现中,使用顺序循环(round-robin)的方式进行分配以获取一个均衡的分布,并且相同的 EventLoop 可能会被分配给多个 Channel。一旦一个 Channel 被分配给一个 EventLoop,它将在它的整个生命周期中都使用这个EventLoop(以及相关联的 Thread)。
3.1.3 所谓异步的体现
我们在一开始说Netty的时候提到过:Netty是一个【异步事件驱动】的网络应用程序框架。
事件驱动跟Reactor模型有关嘛,看了上面的EventLoop
估计有一点感觉了。那【异步】呢?其实这里已经有体现了。怎么说?
首先,我们说了,NioEventLoop
它是有自己线程的,所以,我们服务端绑定ServerSocketChannel
的时候肯定会给他分配一个EventLoop
。
那么,我们点开来看,服务端绑定的时候是怎样的,可以稍微窥见一点Netty
的异步所在,以及EventLoop
是如何工作的。
但是追踪链比较深,我只能告诉大家,在服务端调用这个bind
的时候,会新建一条Channel
,并且把真正的bind
操作投递给Channel
里面的QueueTask
任务队列里面。
3.2 Channel:通道
我们在之前的NIO里面,包括上文中也提到过,NIO以后使用了Channel
来封装Socket
。但是
3.2.1 Channel接口
Channel即Java NIO编程里面的Socket。Netty 的 Channel 接口所提供的 API,被用于所有的 I/O 操作(bind
、connect
、read
和 write
)。大大地降低了直接使用 Socket 类的复杂性。此外,Channel也是拥有许多预定义的、专门化实现的广泛类层次结构的根。
由于 Channel 是独一无二的,所以为了保证顺序将 Channel 声明为 java.lang.Comparable的一个子接口。因此,如果两个不同的 Channel 实例都返回了相同的散列码,那么AbstractChannel 中的 compareTo()方法的实现将会抛出一个 Error。
说到Channel,如果大伙有手敲过上面的示例代码的话,你会有点印象:诶,我们自己手写ChannelInboudHandler
的时候,貌似看到过不少如下的方法哦:
我想有过自己思考的朋友,估计通过英文多少能猜到了,这些肯定涉及【生命周期】、【事件】之类的说法。下面就是想给大家介绍一下,【Channel的生命周期】
Channel 的生命周期状态
- ChannelUnregistered :Channel 已经被创建,但还未注册到 EventLoop
- ChannelRegistered :Channel 已经被注册到了 EventLoop
- ChannelActive :Channel 处于活动状态(已经连接到它的远程节点)。它现在可以接收和发送数据了
- ChannelInactive :Channel 没有连接到远程节点
当这些状态发生改变时,将会生成对应的事件。这些事件将会被转发给 ChannelPipeline中的 ChannelHandler,其可以随后对它们做出响应。在我们的编程中,关注 ChannelActive 和ChannelInactive 会更多一些。
重要 Channel 的方法
- eventLoop:返回分配给 Channel 的 EventLoop
- pipeline:返回 Channel 的 ChannelPipeline,也就是说每个 Channel 都有自己的ChannelPipeline
- isActive:如果 Channel 是活动的,则返回 true。活动的定义依赖于底层的传输协议。例如,一个 Socket 传输一旦连接到了远程节点便是活动的,而一个 Datagram 传输一旦被打开便是活动的
- localAddress:返回本地的 SokcetAddress
- remoteAddress:返回远程的 SocketAddress
- write:将数据写到远程节点,注意,这个写只是写往 Netty 内部的缓存,还没有真正写往 socket
- flush:将之前已写的数据冲刷到底层 socket 进行传输
- writeAndFlush:一个简便的方法,等同于调用 write()并接着调用 flush()
3.3 ChannelPipeline 和 ChannelHandlerContext
3.3.1 ChannelPipeline 接口
当 Channel 被创建时,它将会被自动地分配一个新的 ChannelPipeline,每个 Channel 都有自己的 ChannelPipeline。这项关联是永久性的。在 Netty 组件的生命周期中,这是一项固定的操作,不需要开发人员的任何干预。
ChannelPipeline 提供了 ChannelHandler 链的容器,并定义了用于在该链上传播入站(也就是从网络到业务处理)和 出站(也就是从业务处理到网络),各种事件流的 API,我们代码中的 ChannelHandler 都是放在 ChannelPipeline 中的。
使得事件流经 ChannelPipeline 是 ChannelHandler 的工作,它们是在应用程序的初始化或者引导阶段被安装的。这些 ChannelHandler 对象接收事件、执行它们所实现的处理逻辑,并将数据传递给链中的下一个 ChannelHandler,而且 ChannelHandler 对象也完全可以拦截事件不让事件继续传递。它们的执行顺序是由它们被添加的顺序所决定的。
说到ChannelPipeline 有一个关于ChannelHandler的生命周期得跟大家伙说一说:ChannelHandler 的生命周期。