netty学习(2)

本文详细解析了Netty框架的核心组件和工作原理,包括ThreadFactory、BlockingQueue、Executor框架、EventLoop、ChannelPipeline等关键概念,以及ServerBootstrap的配置与启动流程。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

ThreadFactory很简单,就是一个线程工厂也就是负责生产线程的

ThreadFactory 是一个接口。
public interface ThreadFactory {
Thread newThread(Runnable r);
}
private static final AtomicInteger poolNumber = new AtomicInteger(1);//原子类,线程池编号

匿名内部类
格式
new 类名/接口/抽象类(){
}

BlockingQueue顾名思义:阻塞队列,简单说就是放入,取出数据都会发生阻塞。比如取出数据时发现容器没有数据,就会等待产生阻塞一直等到有数据

执行者框架(Executor framework)或Fork/Join框架(Fork/Join framework),
调用new Thread()创建的线程缺乏管理,被称为野线程,而且可以无限制创建,之间相互竞争,会导致过多占用系统资源导致系统瘫痪。
不利于扩展,比如如定时执行、定期执行、线程中断
引入Executor线程池框架

Executors工厂类

ServerBootstrap用一个ServerSocketChannelFactory 来实例化。

EventLoop 这个相当于一个处理线程,是Netty接收请求和处理IO请求的线程。

EventLoopGroup 可以理解为将多个EventLoop进行分组管理的一个类,是EventLoop的一个组。

ServerBootstrap 从命名上看就可以知道,这是一个对服务端做配置和启动的类。

ChannelPipeline 这是Netty处理请求的责任链,这是一个ChannelHandler的链表,而ChannelHandler就是用来处理网络请求的内容的。

ChannelHandler 用来处理网络请求内容,有ChannelInboundHandler和ChannelOutboundHandler两种,ChannlPipeline会从头到尾顺序调用ChannelInboundHandler处理网络请求内容,从尾到头调用ChannelOutboundHandler处理网络请求内容。这也是Netty用来灵活处理网络请求的机制之一,因为使用的时候可以用多个decoder和encoder进行组合,从而适应不同的网络协议。而且这种类似分层的方式可以让每一个Handler专注于处理自己的任务而不用管上下游,这也是pipeline机制的特点。这跟TCP/IP协议中的五层和七层的分层机制有异曲同工之妙。

ScheduledExecutorService,是基于线程池设计的定时任务类,每个调度任务都会分配到线程池中的一个线程去执行,也就是说,任务是并发执行,互不影响。

SimpleChannelInboundHandler

原子类是靠 sun 基于 CAS 实现的,CAS 是一种乐观锁
内存位置V、旧的预期值A和新值B,当且仅当V符合预期值A时,CAS用新值B原子化地更新V的值,否则,它什么都不做。
 CAS的ABA问题

当然CAS也并不完美,它存在"ABA"问题,假若一个变量初次读取是A,在compare阶段依然是A,但其实可能在此过程中,它先被改为B,再被改回A,而CAS是无法意识到这个问题的。CAS只关注了比较前后的值是否改变,而无法清楚在此过程中变量的变更明细,这就是所谓的ABA漏洞。

num++看似简单的一个操作,实际上是由1.读取 2.加一 3.写入 三步组成的
悲观锁
使用独占锁机制来解决,是一种悲观的并发策略,抱着一副“总有刁民想害朕”的态势,每次操作数据的时候都认为别的线程会参与竞争修改,所以直接加锁。同一刻只能有一个线程持有锁,那其他线程就会阻塞。

乐观锁

先检查当前value是否等于current,如果相等,则意味着value没被其他线程修改过,更新并返回true。如果不相等,compareAndSet则会返回false,然后循环继续尝试更新。

EventLoop——控制流、多线程处理、并发
Channel——Socket;
一个EventLoopGroup包含一个或者多个EventLoop;
一个EventLoop在它的生命周期内只和一个Thread绑定;
所有由EventLoop处理得I/O事件都将在它专有的Thread上处理
一个Channel在它的生命周期内只注册一个EventLoop;
一个EventLoop可能会被分配给一个或多个Channel。

Netty所有的I/O操作都是异步的。因为一个操作可能不会立即返回,所以我们需要一种用于在之后得某个时间点确定其结果的方法。为此,Netty提供了ChannelFuture接口,其addListener()方法注册了一个ChannelFutureListener,以便在某个操作完成时(无论是否成功)得到通知。

BootStrap在netty的应用程序中负责引导服务器和客户端。netty包含了两种不同类型的引导:

  1. 使用服务器的ServerBootStrap,用于接受客户端的连接以及为已接受的连接创建子通道。
  2. 用于客户端的BootStrap,不接受新的连接,并且是在父通道类完成一些操作。

一个servier接口,里面有两种方法 start()和shutdown()
一个虚拟类 BaseServer()实现这个接口,虚拟类有
地址,日志,
原子类 AtomicInteger index
protected DefaultEventLoopGroup defLoopGroup; 本地线程池
protected NioEventLoopGroup bossGroup; boss线程池 处理客户端的连接请求 并将accept的连接注册到subReactor的其中一个线程上; //boss用来接收进来的连接
protected NioEventLoopGroup workGroup; work线程池 负责处理已建立的客户端通道上的数据读写; //用来处理已经被接收的连接;//还有一块ThreadPool是具体的处理业务逻辑的线程池,一般情况下可以复用subReactor
protected NioServerSocketChannel ssch; 是一个可以监听新进来的TCP连接的通道, 就像标准IO中的ServerSocket一样。用于服务端非阻塞地接收TCP连接
protected ChannelFuture cf; ChannelFuture的作用是用来保存Channel异步操作的结果。 异步通知
protected ServerBootstrap b; ServerBootstrap负责初始化netty服务器,并且开始监听端口的socket请求。ServerBootstrap.bind(int)负责绑定端口,当这个方法执行后,ServerBootstrap就可以接受指定端口上的socket连接了。一个ServerBootstrap可以绑定多个端口。
初始化init()
结束退出shutdown()

在使用匿名内部类时,要记住以下几个原则:

a·匿名内部类不能有构造方法。
      b·匿名内部类不能定义任何静态成员、方法和类。
      c·匿名内部类不能是public,protected,private,static。
      d·只能创建匿名内部类的一个实例。
      e·一个匿名内部类一定是在new的后面,用其隐含实现一个接口或实现一个类。
      f·因匿名内部类为局部内部类,所以局部内部类的所有限制都对其生效。

DefaultEventLoopGroup(8, new ThreadFactory(){
incrementAndGet()
以原子方式将当前值加 1。 返回的是新值(即加1后的值) 前者,先+1,再返回
getAndIncrement()
以原子方式将当前值加 1。 返回旧值(即加1前的原始值) 先返回,再 +1

});

一个RunTime就代表一个运行环境。
RunTIme.getRuntime():该方法用于返回当前应用程序的运行环境对象。
.availableProcessors() 返回到Java虚拟机的可用的处理器数量。 此方法返回到虚拟机的最大可用的处理器数量;决不会小于一个

boss
work  *10

HappyChatServer 继承BaseServer
ScheduledExecutorService 将定时任务与线程池功能结合使用
ScheduledExecutorService executorService = Executors.newScheduledThreadPool(2);
创建定长线程池。
@Override
start()
b.group(bossGroup, workGroup)
.channel(NioServerSocketChannel.class) //配置ServerBootstrap的channel(),指定通道channel的类型,由于是服务端,故而是NioServerSocketChannel;
.option(ChannelOption.SO_KEEPALIVE, true) 配置ServerSocketChannel的选项
.option(ChannelOption.TCP_NODELAY, true)
.option(ChannelOption.SO_BACKLOG, 1024)
.localAddress(new InetSocketAddress(port))
.childHandler(new ChannelInitializer() {
//设置子通道也就是SocketChannel的处理器, 其内部是实际业务开发的"主战场
//该函数的主要作用是设置channelHandler来处理客户端的请求的channel的IO。
@Override
protected void initChannel(SocketChannel ch) throws Exception {
ch.pipeline().addLast(defLoopGroup,
new HttpServerCodec(), //请求解码器
new HttpObjectAggregator(65536),//将多个消息转换成单一的消息对象
new ChunkedWriteHandler(), //支持异步发送大的码流,一般用于发送文件流
new IdleStateHandler(60, 0, 0), //检测链路是否读空闲
new UserAuthHandler(), //处理握手和认证
new MessageHandler() //处理消息的发送
);
}
});

super.group(parentGroup); 这个方法调用了ServerBootstrap的父类AbstractBootstrap的group(EventLoopGroup group) 方法
.handler(new LoggingHandler()) // (5)设置ServerSocketChannel的处理器
.childOption(ChannelOption.SO_KEEPALIVE, true); 配置子通道也就是SocketChannel的选项
ChannelFuture f = b.bind(port).sync(); // (9)(9)、 绑定并侦听某个端口
f.channel().closeFuture().sync();
boss这个EventLoopGroup作为一个acceptor负责接收来自客户端的请求,然后分发给worker这个EventLoopGroup来处理所有的事件event和channel的IO。

ChannelOption
SO_KEEPALIVE 对应于套接字选项中的SO_KEEPALIVE,该参数用于设置TCP连接,当设置该选项以后,连接会测试链接的状态,这个选项用于可能长时间没有数据交流的连接。当设置该选项以后,如果在两小时内没有数据的通信时,TCP会自动发送一个活动探测数据报文。
TCP_NODELAY 对应于套接字选项中的TCP_NODELAY,该参数的使用与Nagle算法有关,Nagle算法是将小的数据包组装为更大的帧然后进行发送,而不是输入一次发送一次,因此在数据包不足的时候会等待其他数据的到了,组装成大的数据包进行发送,虽然该方式有效提高网络的有效负载,但是却造成了延时,而该参数的作用就是禁止使用Nagle算法,使用于小数据即时传输,于TCP_NODELAY相对应的是TCP_CORK,该选项是需要等到发送的数据量最大的时候,一次性发送数据,适用于文件传输。
SO_BACKLOG 对应的是tcp/ip协议listen函数中的backlog参数,函数listen(int socketfd,int backlog)用来初始化服务端可连接队列,服务端处理客户端连接请求是顺序处理的,所以同一时间只能处理一个客户端连接,多个客户端来的时候,服务端将不能处理的客户端连接请求放在队列中等待处理,backlog参数指定了队列的大小

Socket属于IO的一种,nio提供了ServerSocketChannel和SocketChannel。
普通Socket和NioSocket的区别:
    普通Socket是客户端发出一次请求、服务端接收到后响应、客户端接收到服务端的响应才能再次请求。
    NioSocket是引入了三个概念:Channel、Selector、Buffer。Buffer是将很多请求打包,一次性发出去,有Selector扮演选择器的角色,将请求分转给对应的通道(Channel)进行处理响应。

SocketChannel主要是用来基于TCP通信的通道。
SocketChannel是用来连接Socket套接字
SocketChannel主要用途用来处理网络I/O的通道
SocketChannel是基于TCP连接传输
SocketChannel实现了可选择通道,可以被多路复用的

SocketChannel具有以下的特征:

对于已经存在的socket不能创建SocketChannel
SocketChannel中提供的open接口创建的Channel并没有进行网络级联,需要使用connect接口连接到指定地址
未进行连接的SocketChannle执行I/O操作时,会抛出NotYetConnectedException
SocketChannel支持两种I/O模式:阻塞式和非阻塞式
SocketChannel支持异步关闭。如果SocketChannel在一个线程上read阻塞,另一个线程对该SocketChannel调用shutdownInput,则读阻塞的线程将返回-1表示没有读取任何数据;如果SocketChannel在一个线程上write阻塞,另一个线程对该SocketChannel调用shutdownWrite,则写阻塞的线程将抛出AsynchronousCloseException
SocketChannel支持设定参数

ChannelInitializer类
这个类其实就是往pipeline中添加了很多的channelHandler。

ChannelPipeline数据管道是ChannelHandler数据处理器的容器,负责ChannelHandler的管理和事件的拦截与调度.
pipeline的概念可以从这里抽象出来:将一件需要重复做的事情(这里指为客户准备一份精美的食物)切割成各个不同的阶段(这里是四个阶段:盘子,薯条,豌豆,饮料),每一个阶段由独立的单元负责(四个生产者分别负责不同的环节)。所有待执行的对象依次进入作业队列(这里是所有的客户排好队依次进入服务,除了开始和结尾的一段时间,任意时刻,四个客户被同时服务)。对应到CPU中,每一条指令的执行过程可以切割成:fetch instruction、decode it、find operand、perform action、store result 5个阶段
netty在服务端端口绑定和新连接建立的过程中会建立相应的channel,而与channel的动作密切相关的是pipeline这个概念,pipeline像是可以看作是一条流水线,原始的原料(字节流)进来,经过加工,最后输出

EventLoopGroup 说白了,就是一个死循环,不停地检测IO事件,处理IO事件,执行任务
ServerBootstrap 是服务端的一个启动辅助类,通过给他设置一系列参数来绑定端口启动服务
bossGroup的作用就是不断地accept到新的连接,将新的连接丢给workerGroup来处理
.channel(NioServerSocketChannel.class) 表示服务端启动的是nio相关的channel,

channel在netty里面是一大核心概念,可以理解为一条channel就是一个连接或者一个服务端bind动作,后面会细说
.handler(new SimpleServerHandler() 表示服务器启动过程中,需要经过哪些流程,这里SimpleServerHandler最终的顶层接口为ChannelHander,是netty的一大核心概念,表示数据流经过的处理器,可以理解为流水线上的每一道关卡
childHandler(new ChannelInitializer)…表示一条新的连接进来之后,该怎么处理,也就是上面所说的,老板如何给工人配活
ChannelFuture f = b.bind(8888).sync(); 这里就是真正的启动过程了,绑定8888端口,等待服务器启动完毕,才会进入下行代码
f.channel().closeFuture().sync(); 等待服务端关闭socket
bossGroup.shutdownGracefully(); workerGroup.shutdownGracefully(); 关闭两组死循环

bind方法是入口
validate() 验证服务启动需要的必要参数,然后调用doBind()
doBinde()内两大核心方法 initAndRegister(),初始化和注册 以及doBind0()
(1)initAndRegister()
1.new一个channel
2.init这个channel
3.将这个channel register到某个对象

什么是channel 是在服务启动的时候创建,我们可以和普通Socket编程中的ServerSocket对应上,表示服务端绑定的时候经过的一条流水线
channel是通过一个 channelFactory new出来的

通过反射的方式来创建一个对象,而这个class就是我们在ServerBootstrap中传入的NioServerSocketChannel.class
最终创建channel相当于调用默认构造函数new出一个 NioServerSocketChannel对象

NioServerSocketChannel的默认构造函数,SelectorProvider.openServerSocketChannel()创建一条server端channel

ChannelConfig 也是netty里面的一大核心模块

netty启动一个服务所经过的流程
1.设置启动类参数,最重要的就是设置channel
2.创建server对应的channel,创建各大组件,包括ChannelConfig,ChannelId,ChannelPipeline,ChannelHandler,Unsafe等
3.初始化server对应的channel,设置一些attr,option,以及设置子channel的attr,option,给server的channel添加新channel接入器,并出发addHandler,register等事件
4.调用到jdk底层做端口绑定,并触发active事件,active触发的时候,真正做服务端口绑定

netty最核心的就是reactor线程,对应项目中使用广泛的NioEventLoop

reactor 线程的启动
NioEventLoop的run方法是reactor线程的主体,在第一次添加任务的时候被启动

外部线程在往任务队列里面添加任务的时候执行 startThread() ,netty会判断reactor线程有没有被启动,如果没有被启动,那就启动线程再往任务队列里面添加任务
SingleThreadEventExecutor 在执行doStartThread的时候,会调用内部执行器executor的execute方法,将调用NioEventLoop的run方法的过程封装成一个runnable塞到一个线程中去执行

默认情况下,ThreadPerTaskExecutor 在每次执行execute 方法的时候都会通过DefaultThreadFactory创建一个FastThreadLocalThread线程,而这个线程就是netty中的reactor线程实体

netty的reactor线程在添加一个任务的时候被创建,该线程实体为 FastThreadLocalThread,最后线程执行主体为NioEventLoop的run方法。

NioEventLoop 的run方法
for (;?{}
对三个步骤不断循环
1.首先轮询注册到reactor线程对用的selector上的所有的channel的IO事件
2.处理产生网络IO事件的channel
3.处理任务队列
select操作
wakenUp 表示是否应该唤醒正在阻塞的select操作,可以看到netty在进行一次新的loop之前,都会将wakeUp 被设置成false,标志新的一轮loop的开始,具体的select操作我们也拆分开来看
1.定时任务截止事时间快到了,中断本次轮询
2.轮询过程中发现有任务加入,中断本次轮询

3.阻塞式select操作
执行到这一步,说明netty任务队列里面队列为空,并且所有定时任务延迟时间还未到(大于0.5ms),于是,在这里进行一次阻塞select操作,截止到第一个定时任务的截止时间
阻塞select操作结束之后,netty又做了一系列的状态判断来决定是否中断本次轮询,中断本次轮询的条件有

轮询到IO事件 (selectedKeys != 0)
oldWakenUp 参数为true
任务队列里面有任务(hasTasks)
第一个定时任务即将要被执行 (hasScheduledTasks())
用户主动唤醒(wakenUp.get())

不管是boos线程还是worker线程,所做的事情均分为以下三个步骤

轮询注册在selector上的IO事件
处理IO事件
执行异步task

新连接的建立
1.检测到有新的连接
2.将新的连接注册到worker线程组
3.注册新连接的读事件

当服务端绑启动之后,服务端的channel已经注册到boos reactor线程中,reactor不断检测有新的事件,直到检测出有accept事件发生

NioEventLoop.java

所有的channel底层都会有一个与unsafe绑定,每种类型的channel实际的操作都由unsafe来实现
服务端对应的channel的unsafe是 NioMessageUnsafe
read方法必须是reactor线程调用,然后拿到channel对应的pipeline和 RecvByteBufAllocator.Handle
调用 doReadMessages 方法不断地读取消息,用 readBuf 作为容器,这里,其实可以猜到读取的是一个个连接,然后调用 pipeline.fireChannelRead(),将每条新连接经过一层服务端channel的洗礼
之后清理容器,触发 pipeline.fireChannelReadComplete()

1.doReadMessages(List)
2.pipeline.fireChannelRead(NioSocketChannel)

1.doReadMessages()
netty调用jdk底层nio的边界 javaChannel().accept();,由于netty中reactor线程第一步就扫描到有accept事件发生,因此,这里的accept方法是立即返回的,返回jdk底层nio创建的一条channel
netty将jdk的 SocketChannel 封装成自定义的 NioSocketChannel,加入到list里面,这样外层就可以遍历该list,做后续处理

服务端的创建过程中会创建netty中一系列的核心组件,包括pipeline,unsafe等等
jdk nio里面熟悉的影子—— SelectionKey.OP_READ,一般在原生的jdk nio编程中,也会注册这样一个事件,表示对channel的读感兴趣

1.channel 继承 Comparable 表示channel是一个可以比较的对象
2.channel 继承AttributeMap表示channel是可以绑定属性的对象,在用户代码中,我们经常使用channel.attr(…)方法就是来源于此
3.ChannelOutboundInvoker是4.1.x版本新加的抽象,表示一条channel可以进行的操作
4.DefaultAttributeMap用于AttributeMap抽象的默认方法,后面channel继承了直接使用
5.AbstractChannel用于实现channel的大部分方法,其中我们最熟悉的就是其构造函数中,创建出一条channel的基本组件
6.AbstractNioChannel基于AbstractChannel做了nio相关的一些操作,保存jdk底层的 SelectableChannel,并且在构造函数中设置channel为非阻塞
7.最后,就是两大channel,NioServerSocketChannel,NioSocketChannel对应着服务端接受新连接过程和新连接读写过程

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值