Netty啊,真的是大名鼎鼎!Netty之于Java网络编程,相当于Spring之于Java开发。两者都是Java生态的金字塔尖的【框架】!所以,非常推荐Java程序员学习这个框架。
Netty有多牛逼?据说,曾经在性能上把谷歌公司一个用C++写的网络通信框架都给人干碎了。后来,后者参照了Netty的设计方案,才完成了超越。
需要有一定的网络编程基础。如若没有,请务必要学习下【阅读导航】中提到的系列上一篇文章。
另外,如果你们了解【设计模式】中的【责任链模式】就更好了。因为在Netty的开发中,Handler
使用了【责任链模式】的方式,将各个Handler
链化起来。
而且很负责地告诉大家,好多优秀的Java源码,都有【责任链模式】的影子。所以,去学习吧,能帮助你阅读源码以及提升自己的编程技巧。传送门:《史上最全设计模式导学目录(完整版)》
系列上一篇文章:《【Netty专题】【网络编程】从OSI、TCP/IP网络模型开始到BIO、NIO(Netty前置知识)》
Reactor模型(未完待续)
Netty是由 JBOSS 提供的一个Java开源网络通信框架。它是一个【异步事件驱动】的网络应用程序框架,用于快速开发可维护的高性能协议服务器和客户端。
也就是说,Netty 是一个基于NIO的客户、服务器端的编程通信框架,使用Netty 可以确保你快速和简单的开发出一个网络应用,例如实现了某种协议的客户、服务端应用。Netty相当于简化和流线化了网络应用的编程开发过程,例如:基于TCP和UDP的socket服务开发。
上面有个2细节很重要:
- Netty是一个【异步事件驱动】的网络应用程序框架(异步,事件驱动,如何理解?)
- Netty 是一个基于NIO的客户、服务器端的编程通信框架。NIO,NIO,NIO,讲三遍
(PS:有心的同学这个时候应该回忆以下,Java的NIO编程里面,有什么组件,或者细节来着?)
相比传统Java Socket编程、Java NIO,Netty具有如下明显优势
传统的NIO开发,你需要独自考虑、处理网络编程中遇到的一些常见问题。如:【断线重连】、【 网络闪断】、【心跳处理】、【粘包】、【半包读写】、【网络拥塞】和【异常流】
编解码:网络编程一定要处理的环节。因为数据在网络中传输是需要转换为二进制的,不可能是明文
支持的协议:传输层有TCP、UDP、本地传输;应用层有HTTP、WebSocket等
话不多说,我们先来简单使用一下,开始我们的第一个Netty程序,然后再一点一点推敲。
先导入pom:
<dependency>
<groupId>io.nettygroupId>
<artifactId>netty-allartifactId>
<version>4.1.42.Final version>
<scope>compilescope>
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>() {
@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客户端代码示例:
/**
* 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();
}
}
我们先来简单总结下服务端NettyServer
的流程:
NettyClientHandler
而客户端NettyClient
呢,它的流程如下:
NettyServerHandler
看看,从代码主流程来看,其实客户端跟服务端基本没什么很大的区别。当然这不是主要的东西。最重要的是,大家发现没有,这里出现了好几个陌生的API,这就是Netty提供给我们的核心组件!这些组件会在后面给大家详解介绍,也是本文的核心所在!这些组件,分别是:EventLoopGroup
、 Bootstra(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核心组件详解】中讲到。
我们在最开始介绍Netty的时候,有这么描述过:它是一个【异步事件驱动】的网络应用程序框架。并且向大家抛出了这么个问题:如何理解【异步事件驱动】?
如果你们看了我上一篇文章其实不难理解。【异步事件驱动】=【异步】+【事件驱动】。
只不过稍微有些不同的是:Netty对于事件的定义。
Netty 使用不同的事件来通知我们状态的改变或者是操作的状态。这使得我们能够基于已经发生的事件来触发适当的动作。Netty 事件是按照它们与入站或出站数据流的相关性进行分类的。
每个事件都可以被分发给 ChannelHandler 类中的某个用户实现的方法,既然事件分为入站和出站,用来处理事件的 ChannelHandler 也被分为可以处理入站事件的 Handler 和出站事件的 Handler,当然有些 Handler 既可以处理入站也可以处理出站。
Netty 提供了大量预定义的可以开箱即用的 ChannelHandler 实现,包括用于各种协议(如 HTTP 和 SSL/TLS)的 ChannelHandler。
基于 Netty 的网络应用程序中根据业务需求会使用 Netty 已经提供的 ChannelHandler 或者自行开发 ChannelHandler,这些 ChannelHandler 都放在 ChannelPipeline 中统一管理,事件就会在 ChannelPipeline 中流动,并被其中一个或者多个 ChannelHandler 处理。
模型解读:
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 中的数据
EventLoop
,直译:事件循环;EventLoopGroup
直译:事件循环组。虽然我不知道这是啥,但是基本上可以猜测:后者是对前者做管理的类。所以我们主要了解一下EventLoop
先。
回想一下我们在 NIO 中是如何处理我们关心的事件的?很简单,就是在一个 while 循环中 select 出事件,然后依次处理每种事件。这不就是【事件循环】嘛。
EventLoop是Netty 的核心接口,用于处理网络连接的生命周期中所发生的事件。它的类结构如下:
再来看看接口定义:
再看看对应的实现类:
看,我们用到的NioEventLoop
就在里面了。如果大家翻开里面的源码,会发现,NioEventLoop
里面有几个重要的属性,我这边用伪代码写一下:(有一些属性是在父类中的)
class NioEventLoop {
Selector selector;
Thread thread;
Queue<Runnable> taskQueue;
SelectedSelectionKeySet selectedKeys;
}
由上面的伪代码可以看到,NioEventLoop
中:
线程
和任务队列
(是否似曾相识?线程池呀!),支持异步提交执行任务,线程启动时会调用 NioEventLoop 的 run 方法,执行 I/O 任务和非 I/O 任务:
Selector
选择器。熟悉NIO的朋友估计就知道这是啥了SelectionKeySet
。这个不知道大家有没有印象,在NIO模型中我说:向Selector
注册了Channel
和感兴趣事件后就会被包装成一个SelectionKey
可是我们知道的,一台电脑顶多就几千条线程,所以,能分配到一个Netty中的线程数必然也不会多,或者说我们创建出来的EventLoop必然也不会太多。那,Netty号称百万并发量是如何实现的呢?
所以这里势必会有一种对应关系:1个EventLoop管理N个连接(或者说Channel)
线程的分配
异步传输实现只使用了少量的 EventLoop(以及和它们相关联的 Thread),而且在当前的线程模型中,它们可能会被多个 Channel 所共享。这使得可以通过尽可能少量的 Thread 来
NioEventLoopGroup,主要管理 eventLoop 的生命周期,可以理解为一个线程池,内部维护了一组线程,每个线程(NioEventLoop)负责处理多个 Channel 上的事件,而一个 Channel 只对应于一个线程。