Netty是一个基于NIO异步通信框架
Netty框架是由许多组件,优化的数据结构所构建成。
正是通过灵活的组件构建,优化后的数据结构,进而才能保证Netty框架面对高并发场景具有一定的能力
Netty重要的组件有:Channel,EventLoop,Unsafe,ChannelPipeline,Bootstrap,ServerBootstrap等
Channel:
Netty最核心的组件为:Channel
1.通过Channel管道可以设置自定义封装的参数,也可以设置TCP-操作系统级别的参数【TCP参数通常在linux等OS操作系统配置文件中可以设置】,在Netty中,这些TCP参数通常以SO_xxxx开头的
2.Channel同样是Netty框架各个组件结构的串联者
EventLoop:
EventLoop实际上是一个独立的线程,是一个单线程池
你可以把它视作是一个类似于new Thread()创建的线程,但是EventLoop更加贴合Netty体系。EventLoop可以处理连接操作,IO操作,普通任务,定时任务。
Unsafe:
Unsafe是线程不安全的。它提供了直接访问底层数据的能力,用于高效地进行网络数据的读写操作。Unsafe主要用于解决Java NIO中的一些性能瓶颈和限制
Channel的流转通信需要EventLoop线程去做,Channel的IO读写需要Unsafe具体去做,无论是EventLoop还是Unsafe,都是围绕着Channel去做工作的。
当管道Channel建立完成后,后续引入ChannelPipeline体系处理工作:
ChannelPipeline:
该组件包含ChannelContext和ChannelHandler。
ChannelHandler是我们日常程序员开发最重要,打交道最多的地方。
Bootstra,ServerBootstrap:
后续再通过Bootstrap,ServerBootstrap进行整合Channel,EventLoop,Unsafe,ChannelPipeline等。
优化的数据结构包括:Selector,FastThreadLocal,HashWheelTimer等
Selector:
Netty的Selector就是一个IO多路复用器,但是Netty的Selector相对于JavaNIO中原生的Selector而言,做了性能优化。JavaNIO的Selector底层是基于Set集合的,但是Netty的Selector是基于数组的
FastThreadLocal:
在多线程环境下,你一定会需要ThreadLocal做线程数据独享的,但是Java体系的ThreadLocal存在性能瓶颈。在高并发多线程环境下,Netty使用FastThreadLocal这一数据结构优化原生的ThreadLocal,能够更加应对高并发多线程的场景
HashWheelTimer:
HashWheelTimer和Timer一样,都是存储定时任务的数据结构,然后到特定时刻后,就会触发调度相对应的定时任务。比如说:19点51分需要调度定时任务1,而19点52分需要调度定时任务2。
该数据结构也是对Java原生数据结构Timer的优化。原生的Timer是基于二叉树的,对每一个加入的定时任务的调度时间控制的十分精确。但是HashWheelTimer是基于哈希时间轮[一个环形的数组],HashWheelTimer降低了对定时任务的调度的精度,但是极大的优化了性能。【具体见之后的讲解或之前的总结】
//方式1:使用NIO进行编写服务器端
//Selector对象会被封装到EventLoop类的成员变量,在构造方法中进行初始化
Selector selector = Selector.open();
//完成ServerSocketChannel创建的过程---->【初始化操作】
ServerSocketChannel serverSocketChannel = ServerSocketChannel.open();
serverSocketChannel.configureBlocking(false);
//完成ServerSocketChannel注册的工作---->【注册操作】
SelectionKey selectionKey = serverSocketChannel.register(selector, 0, null);
//设置事件
selectionKey.interestOps(SelectionKey.OP_ACCEPT);
//断开绑定的工作
serverSocketChannel.bind(new InetSocketAddress(8000));
//方式2:使用Netty编写服务端
ServerBootstrap serverBootstrap = new ServerBootstrap();
serverBootstrap.channel(NioServerSocketChannel.class);
serverBootstrap.group(new NioEventLoopGroup());
serverBootstrap.childHandler(new ChannelInitializer() {
@Override
protected void initChannel(NioSocketChannel ch) throws Exception {
ch.pipeline().addLast(new LoggingHandler());
}
});
Channel channel = serverBootstrap.bind(8000).sync().channel();
channel.closeFuture().sync();
对比NIO与Netty的编码:
1.原有的NIO代码 在netty编程“消失”---》 NIO代码被Netty封装了起来。
2.Netty中组件NioServerSocketChannel 作用:首先它是一个Channel,Channel是各个组件的串联者,Channel同样是Pipeline的管理者。
先抛出结论:
Netty是NIO代码的封装。
NIO代码和Netty毫无关系,那么Netty的NioServerSocketChannel如何和Nio体系整合的呢?是通过serverSocketChannel.register(selector,0,附件对象),把NioServerSocketChannel传入到附件对象所在的位置,通过附件机制进行绑定整合的。【后续会验证】
NioServerSocketChannel体系图:
父类AbstractNioMessageChannel:
继承该类使得NioServerSocketChannel具有读写操作Netty的网络数据的能力,当然它的底层是通过Unsafe为基础进行网络读写的。
接口AttributeMap:该接口用于在netty中给Channel(SSC或SC)设置属性,所谓属性就是一系列的参数,参数包括netty自定义的参数或TCP-OS系统级别的参数。其实就是一个Map,key键值存储参数名,value值存储的是参数名所对应的参数值。
提出问题:
既然说Netty会封装NIO的代码,那么NIO代码都被封装到哪里了?
如下:
还是那个问题:NIO与Netty毫无关系,那么最核心的NioServerSocketChannel如何和NIO整合的?
根据上述那个结论:通过SelectionKey selectionKey = serverSocketChannel.register(selector, 0, null)这句NIO代码的附件机制传递NioServerSocketChannel给Netty体系,完成整合,后续debug源码会看到的。
测试代码如下:
package com.messi.netty_source_03.Test01;
import io.netty.bootstrap.ServerBootstrap;
import io.netty.channel.Channel;
import io.netty.channel.ChannelInitializer;
import io.netty.channel.EventLoopGroup;
import io.netty.channel.nio.NioEventLoopGroup;
import io.netty.channel.socket.nio.NioServerSocketChannel;
import io.netty.channel.socket.nio.NioSocketChannel;
import io.netty.handler.logging.LoggingHandler;
/**
* @Description TODO
* @Author etcEriksen
* @Date 2024/1/5 17:08
* @Version 1.0
*/
public class NettyServer {
public static void main(String[] args) throws InterruptedException {
EventLoopGroup eventLoopGroup = new NioEventLoopGroup();
ServerBootstrap serverBootstrap = new ServerBootstrap();
serverBootstrap.channel(NioServerSocketChannel.class);
serverBootstrap.group(eventLoopGroup);
serverBootstrap.childHandler(new ChannelInitializer() {
@Override
protected void initChannel(NioSocketChannel ch) throws Exception {
ch.pipeline().addLast(new LoggingHandler());
}
});
Channel channel = serverBootstrap.bind(8000).sync().channel();
channel.closeFuture().sync();
}
}
源码debug流程:
1.
2.final ChannelFuture regFuture = initAndRegister();
initAndRegister()是异步方法,异步结果使用ChannelFuture接收,ChannelFuture就是底层的promise。该异步结果是如何设置的?异步开启的新线程进行执行对应的业务逻辑,让该异步线程会设置异步执行的结果给promise。main主线程接收到异步线程设置的promise,并且以ChannelFuture类型的regFuture进行接收。
3.进入initAndRegister();方法
4.channelFactory.newChannel():创建NioServerSocketChannel对象
5.进入init(channel)方法:完成NioServerSocketChannel的初始化
6.ChannelFuture regFuture = config().group().register(channel)
进而group方法:分配创建一个EventLoopGroup线程池
进入register方法:
此时线程栈也会切换到新创建的NioEventLoop线程
进入register0方法:
进入doRegister方法:该方法就是把当前NioServerSocketChannel对象注册到当前异步开启的新线程NioEventLoop的Selector多路复用器上
进入pipeline.invokeHandlerAddedIfNeeded()方法:
进入safeSetSuccess方法:
public class NettyServer {
public static void main(String[] args) throws InterruptedException {
EventLoopGroup eventLoopGroup = new NioEventLoopGroup();
ServerBootstrap serverBootstrap = new ServerBootstrap();
serverBootstrap.channel(NioServerSocketChannel.class);
serverBootstrap.group(eventLoopGroup);
serverBootstrap.childHandler(new ChannelInitializer() {
@Override
protected void initChannel(NioSocketChannel ch) throws Exception {
ch.pipeline().addLast(new LoggingHandler());
}
});
Channel channel = serverBootstrap.bind(8000).sync().channel();
channel.closeFuture().sync();
}
}
过程如下:
1.
2.
3.
4.
5.进入bind方法
6.进入invokeBind
7.
8.
9.
10.不再继续深入追了,一步步回退一下
11.
12.
13.进入readIfIsAutoRead方法
14.
15.
14.重点:完成通道对应【SelectionKey的创建和相关事件的注册】
详细说说SelectionKey:
这里其实还是封装的NIO代码。但是Netty做了很多优化,比如说在存储SelectionKey时,Netty使用的是数组进行存储,而NIO使用的是Set进行存储。
言归正传,SelectionKey是啥?比如说:当一个服务端启动并且注册,则是你把一个ServerSocketChannel注册到Selector上并且注册Accepte连接事件,此时Netty就会给你分配一个SelectionKey,该SelectionKey就是用于标识这个channel通道和该通道注册监听的事件的。同理如果该服务端监控到连接Accept事件的发生,那么SSC会给每一个客户端连接对应分配一个SocketChannel对象,我们同样会把SocketChannel对象注册到Selector上并且注册监听read或write事件,此时Netty也会对应给该SocketChannel分配一个SelectionKey用于标识该channel并且注册监听该事件。
当一个服务端接收多个客户端连接时,SSC会分配多个SC对象,那么多个SC对象注册到Selector则会产生多个SelectionKey,每一个客户端连接对应一个SelectionKey。但是只有一个服务端并且只注册到Selector一次,所以服务端只对应一个SelectionKey。
注释:
虽然说Selector是监控器,但是当监控到事件触发后,真正处理事件逻辑代码的是线程,线程在netty做了封装,也就是EventLoop。EventLoop可不止一个new Thread()这么简单。后续慢慢展开总结。