05 netty的基本组件概述和EventLoop(线程池)的介绍
1 Netty的背景
Netty:异步的事件驱动的网络应用框架,该框架用于快速开发可维护的高性能协议服务器和客户端。
- 异步:这里的异步不是异步IO,而是指总体流程上的异步,处理流程由多个线程协同完成
Netty is an asynchronous event-driven network application framework for rapid development of maintainable high performance protocol servers & clients.
Netty的特点:
-
Netty实际上可以看成Java在网络编程方面的标准框架,很多基于Java的项目如果涉及到网络通信,那么该项目底层很有可能使用netty框架进行网络通信功能的实现。
-
Netty也可以看成是多Java的NIO的进一步封装,我们可以采用Java的NIO实现网络通信,但采用Netty的话,我们的开发更为迅速。
Netty在NIO上的改进 |
---|
不需要自己构建网络协议 |
提供解决 TCP 传输问题(粘包、半包)的工具 |
解决epoll 空轮询导致 CPU 100% |
对 NIO的API 进行增强,更易用,如 FastThreadLocal => ThreadLocal,ByteBuf => ByteBuffer |
概述:事件循环机制(消息调度)是一种等待或分派事件(消息)的编程模型。该机制在工作时向外部的“事件提供者”发出请求,然后调用相关的事件处理程序。
In computer science, the event loop is a programming construct or design pattern that waits for and dispatches events or messages in a program. The event loop works by making a request to some internal or external "event provider" (that generally blocks the request until an event has arrived), then calls the relevant event handler ("dispatches the event"). The event loop is also sometimes referred to as the message dispatcher, message loop, message pump, or run loop.)
dispatcher [dɪsˈpætʃɚ] 发报机; 收发; 调度;
2 基本的客户/服务端程序实现
服务端程序
package netty_basic;
import io.netty.bootstrap.ServerBootstrap;
import io.netty.channel.ChannelHandlerContext;
import io.netty.channel.ChannelInboundHandlerAdapter;
import io.netty.channel.ChannelInitializer;
import io.netty.channel.nio.NioEventLoopGroup;
import io.netty.channel.socket.nio.NioServerSocketChannel;
import io.netty.channel.socket.nio.NioSocketChannel;
import io.netty.handler.codec.string.StringDecoder;
// Netty的API实现服务端程序
public class HelloServer {
public static void main(String[] args) {
/*
ServerBootstrap:启动器,负责组装Netty组件,启动服务器
group-NioEventLoopGroup:组中包括BossEventLoop,WorkEventLoop,EventLoop用于处理事件,
在Java的NIO的API中,EventLoop包括selector和thread对象
channel-NioServerSocketChannel.class:服务器Socket Channel的实现方式选择,这里选择的NIO方式
childHandler:bossEventLoop负责处理连接事件,workerEventLoop(child)负责处理读写事件
childHandler通过添加处理程序实现worker的工作流程,
*/
new ServerBootstrap()
.group(new NioEventLoopGroup())
.channel(NioServerSocketChannel.class)
.childHandler(new ChannelInitializer<NioSocketChannel>() {
@Override
protected void initChannel(NioSocketChannel ch) throws Exception {
/*
为服务端读写通道添加具体的处理程序(accept事件发生,与客户端建立连接后才执行):
-StringDecoder:用于解码,将bytebuffer中内容转换为字符串
-ChannelInboundHandlerAdapter:自定义的handler,这里实现的逻辑:当触发读事件时,打印内容
*/
ch.pipeline().addLast(new StringDecoder());
ch.pipeline().addLast(new ChannelInboundHandlerAdapter(){
@Override
public void channelRead(ChannelHandlerContext ctx,Object msg) throws Exception{
System.out.println(msg);
}
});
}
})
.bind(8080);
}
}
客户端程序
package netty_basic;
import io.netty.bootstrap.Bootstrap;
import io.netty.channel.ChannelInitializer;
import io.netty.channel.nio.NioEventLoopGroup;
import io.netty.channel.socket.ServerSocketChannel;
import io.netty.channel.socket.SocketChannel;
import io.netty.channel.socket.nio.NioSocketChannel;
import io.netty.handler.codec.string.StringEncoder;
import java.net.InetSocketAddress;
import java.net.Socket;
//Netty的API实现客户端程序
public class HelloClient {
public static void main(String[] args) throws InterruptedException {
new Bootstrap()
.group(new NioEventLoopGroup())
.channel(NioSocketChannel.class)
.handler(new ChannelInitializer<NioSocketChannel>() {
@Override
protected void initChannel(NioSocketChannel nioSocketChannel) throws Exception {
// 添加客户端通道的处理程序,对String进行编码,这里逻辑与服务端程序要进行对照
nioSocketChannel.pipeline().addLast(new StringEncoder());
}
})
.connect(new InetSocketAddress("localhost",8080))
.sync() // 阻塞方法,只有与服务端建立连接后才会往下执行
.channel()
.writeAndFlush("hello world"); // 写入数据,会调用通道的处理程序(handler)
}
}
执行结果(先运行服务端,在运行客户端)
hello world // 服务端打印hello world
组件关系梳理
上面程序中涉及了channel、EventLoop、Handler等基本的组件,其含义理解如下:
组件 | 直观含义 | 备注 |
---|---|---|
channel | 数据读写通道 | |
handler | 数据的处理程序(工序),多个handler形成pipeline(流水线) | handler分为Inbound和Outbound两个类别,其中Inbound表示入站,Outbound表示出战,对于网络数据流,其数据的读入和写出所采用的处理程序是分开的 |
eventLoop | 处理数据的“工人”,与channel进行绑定 |
直觉上的理解:主机与其他主机进行网络通信的时候会建立多个数据通道(channel),每个数据通道都需要某个
“工人”进行管理(建立/读/写)。eventLoop就是承担“通道管理者”这个角色。每个eventLoop可以管理多个“数据通道”,并且每个channel与evenLoop进行终生绑定。
3 基本组件-EventLoop
基本概念
EventLoop:本质是单线程执行器(维护1个 Selector),用于处理 Channel 上源源不断的 io 事件。
package io.netty.channel;
import io.netty.util.concurrent.OrderedEventExecutor;
public interface EventLoop extends OrderedEventExecutor, EventLoopGroup {
EventLoopGroup parent();
}
Netty实现了EventLoop接口,该接口继承了OrderedEventExecutor, EventLoopGroup这两个接口。从上图可以看到有两条线:
- 一条线是继承自 juc工具包中的ScheduledExecutorService, 因此包含了线程池中所有的方法
- 另一条线是继承自 netty的 OrderedEventExecutor,用于管理实例对象
- 提供了 boolean inEventLoop(Thread thread) 方法判断一个线程是否属于此 EventLoop
- 提供了 parent 方法来看看自己属于哪个 EventLoopGroup
EventLoopGroup :一组 EventLoop,Channel 一般会调用 EventLoopGroup 的 register 方法来绑定其中一个 EventLoop
-
Channel 的 io 事件由组中绑定的 EventLoop 进行处理(保证线程安全)
-
继承自 netty 自己的 EventExecutorGroup
- 实现了 Iterable 接口提供遍历 EventLoop 的能力
- 另有 next 方法获取集合中下一个 EventLoop
EventLoopGroup接口的实现和关系图如下所示:
package io.netty.channel;
import io.netty.util.concurrent.EventExecutorGroup;
public interface EventLoopGroup extends EventExecutorGroup {
EventLoop next();
ChannelFuture register(Channel var1);
ChannelFuture register(ChannelPromise var1);
/** @deprecated */
@Deprecated
ChannelFuture register(Channel var1, ChannelPromise var2);
}
代码实践
目标:测试EventLoopGropu中的EventLoop是循环访问的,并且每个eventLoop是个单线程线程池,能够提交普通任务和定时任务。
EventLoopGroup的默认线程数目设置
private static final int DEFAULT_EVENT_LOOP_THREADS = Math.max(1, SystemPropertyUtil.getInt("io.netty.eventLoopThreads", NettyRuntime.availableProcessors() * 2));
- 没有在配置文件指定的话,通常设为CPU核心数*2
package netty_basic;
import io.netty.channel.EventLoopGroup;
import io.netty.channel.nio.NioEventLoopGroup;
import lombok.extern.slf4j.Slf4j;
import java.util.concurrent.TimeUnit;
@Slf4j
public class TestEventLoop {
public static void main(String[] args) {
// 定义具有两个EventLoop的对象组
EventLoopGroup group = new NioEventLoopGroup(2); // 本质上是线程池,可以提交定时/普通任务,IO事件
for(int i = 0;i < 4;++i) System.out.println(group.next()); // 访问eventLoop对象
// 提交普通任务给group中的1个eventLoop线程池
group.next().execute(()->{
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
log.debug("common task for eventLoop execute");
});
log.debug("main thread");
// 提交定时任务给group中的1个eventLoop的线程池,定时任务可以用于连接的保活
group.next().scheduleAtFixedRate(()->{
log.debug("timed task");
},0,1, TimeUnit.SECONDS);
}
}
执行结果
io.netty.channel.nio.NioEventLoop@6b71769e
io.netty.channel.nio.NioEventLoop@2752f6e2
io.netty.channel.nio.NioEventLoop@6b71769e
io.netty.channel.nio.NioEventLoop@2752f6e2
14:20:34.158 [main] DEBUG netty_basic.TestEventLoop - main thread
14:20:34.161 [nioEventLoopGroup-2-2] DEBUG netty_basic.TestEventLoop - timed task
14:20:35.164 [nioEventLoopGroup-2-2] DEBUG netty_basic.TestEventLoop - timed task
14:20:35.164 [nioEventLoopGroup-2-1] DEBUG netty_basic.TestEventLoop - common task for eventLoop execute
14:20:36.169 [nioEventLoopGroup-2-2] DEBUG netty_basic.TestEventLoop - timed task
14:20:37.174 [nioEventLoopGroup-2-2] DEBUG netty_basic.TestEventLoop - timed task
14:20:38.164 [nioEventLoopGroup-2-2] DEBUG netty_basic.TestEventLoop - timed task
14:20:39.168 [nioEventLoopGroup-2-2] DEBUG netty_basic.TestEventLoop - timed task
14:20:40.173 [nioEventLoopGroup-2-2] DEBUG netty_basic.TestEventLoop - timed task
14:20:41.163 [nioEventLoopGroup-2-2] DEBUG netty_basic.TestEventLoop - timed task
...
- 输出中对EventLoop的遍历本质上是循环遍历即轮询。
- 线程池中的定时任务能够实现1s中执行一次
使用实例2
目标:测试每个客户端是与服务端的1个eventLoop绑定的
注意点:上图中IDEA调试时断点暂停线程默认暂停所有线程,多线程程序进行测试时由于提交任务的线程和执行任务的线程并非同一线程则将其设为暂停当前线程
注意点: 设置同一段客户端程序运行多次(allow parallel run)模拟多个客户端访问服务器
服务端程序
package netty_basic.eventLoop;
import io.netty.bootstrap.ServerBootstrap;
import io.netty.buffer.ByteBuf;
import io.netty.channel.ChannelHandlerContext;
import io.netty.channel.ChannelInboundHandlerAdapter;
import io.netty.channel.ChannelInitializer;
import io.netty.channel.nio.NioEventLoopGroup;
import io.netty.channel.socket.nio.NioServerSocketChannel;
import io.netty.channel.socket.nio.NioSocketChannel;
import lombok.extern.slf4j.Slf4j;
import java.nio.charset.Charset;
import java.nio.charset.StandardCharsets;
// 注意:客户端与服务器的字符集要保持一致,不然会出现乱码
@Slf4j
public class EventLoopServer {
public static void main(String[] args) {
new ServerBootstrap()
.group(new NioEventLoopGroup())
.channel(NioServerSocketChannel.class)
.childHandler(new ChannelInitializer<NioSocketChannel>() {
@Override
protected void initChannel(NioSocketChannel ch) throws Exception {
ch.pipeline().addLast(new ChannelInboundHandlerAdapter(){
@Override
public void channelRead(ChannelHandlerContext ctx,Object msg){
ByteBuf buf = (ByteBuf) msg;
log.debug(buf.toString(StandardCharsets.UTF_8)); //将信息转化为字符串
}
});
}
})
.bind(8080);
}
}
客户端程序
package netty_basic.eventLoop;
import io.netty.bootstrap.Bootstrap;
import io.netty.channel.Channel;
import io.netty.channel.ChannelInitializer;
import io.netty.channel.nio.NioEventLoopGroup;
import io.netty.channel.socket.nio.NioSocketChannel;
import io.netty.handler.codec.string.StringEncoder;
import java.net.InetSocketAddress;
import java.nio.charset.StandardCharsets;
//Netty的API实现客户端程序
public class EventLoopClient {
public static void main(String[] args) throws InterruptedException {
Channel channel = new Bootstrap()
.group(new NioEventLoopGroup())
.channel(NioSocketChannel.class)
.handler(new ChannelInitializer<NioSocketChannel>() {
@Override
protected void initChannel(NioSocketChannel nioSocketChannel) throws Exception {
// 添加客户端通道的处理程序,对String进行编码,这里逻辑与服务端程序要进行对照
nioSocketChannel.pipeline().addLast(new StringEncoder(StandardCharsets.UTF_8));
}
})
.connect(new InetSocketAddress("localhost",8080))
.sync() // 阻塞方法,只有与服务端建立连接后才会往下执行
.channel();
System.out.println(channel);
System.out.println("");
}
}
将客户端代码运行两个模拟两个客户端并向服务端发送信息,此时服务端输出如下:
15:01:02.494 [nioEventLoopGroup-2-3] DEBUG netty_basic.eventLoop.EventLoopServer - hello
15:05:07.445 [nioEventLoopGroup-2-3] DEBUG netty_basic.eventLoop.EventLoopServer - hello1
15:05:22.116 [nioEventLoopGroup-2-3] DEBUG netty_basic.eventLoop.EventLoopServer - hello1
15:05:26.735 [nioEventLoopGroup-2-3] DEBUG netty_basic.eventLoop.EventLoopServer - hello2
15:05:53.103 [nioEventLoopGroup-2-4] DEBUG netty_basic.eventLoop.EventLoopServer - client2:hello1
15:05:56.714 [nioEventLoopGroup-2-4] DEBUG netty_basic.eventLoop.EventLoopServer - client2:hello2
15:05:59.820 [nioEventLoopGroup-2-4] DEBUG netty_basic.eventLoop.EventLoopServer - client2:hello2
15:06:00.508 [nioEventLoopGroup-2-4] DEBUG netty_basic.eventLoop.EventLoopServer - client2:hello2
15:06:01.948 [nioEventLoopGroup-2-4] DEBUG netty_basic.eventLoop.EventLoopServer - client2:hello2
15:06:02.132 [nioEventLoopGroup-2-4] DEBUG netty_basic.eventLoop.EventLoopServer - client2:hello2
从日志信息可以看出,客户端1与nioEventLoopGroup-2-3绑定,客户端2与nioEventLoopGroup-2-4绑定
- Channel 调用 EventLoopGroup 的 register 方法来绑定其中一个 EventLoop
总结
对于EventLoopGroup:
- 每个channel与组中的1个EventLoop进行绑定
- 多个channel建立时,轮流与组中eventLoop进行绑定,实现1个线程(EventLoop)管理多个Channel(如上图中chanenl1和3绑定EventLoop1,而channel2绑定EventLoop2,如果再有新的channel则可以与EventLoop1绑定)
职责划分思想(服务端)
划分1:服务端程序可以采用2个EventLoopGroup进行职责划分
动机:将accept事件与IO事件分开处理。
/**
* Set the EventLoopGroupfor the parent (acceptor) and the child (client). These
*/
public ServerBootstrap group(EventLoopGroup parentGroup, EventLoopGroup childGroup) {
super.group(parentGroup);
ObjectUtil.checkNotNull(childGroup, "childGroup");
if (this.childGroup != null) {
throw new IllegalStateException("childGroup set already");
}
this.childGroup = childGroup;
return this;
}
代码修改
.group(new NioEventLoopGroup(),new NioEventLoopGroup(2)) //
- 定义两个EventLoopGroup进行分工即boss和worker其中boss只负责ServerSocketChannel(服务端)上的accept事件,而worker负责socketChannel(客户端)上的读写事件
划分2:除了boss和worker所对应的EventLoopGroup,定义其他EventLoopGroup去进行执行时间较长的操作
动机:服务端处理某个channel的读写事件处理时,有时业务操作时间比较长,会造成该单线程的EventLoop所管理的其他channel变的读写事件得不到及时处理。这个时候需要将耗时比较长的操作单独分出来交给其他线程处理。具体的做法就是采用多个EventGroup对handler的内容进行拆分。
代码实现实例:
package netty_basic.eventLoop;
import io.netty.bootstrap.ServerBootstrap;
import io.netty.buffer.ByteBuf;
import io.netty.channel.*;
import io.netty.channel.nio.NioEventLoopGroup;
import io.netty.channel.socket.nio.NioServerSocketChannel;
import io.netty.channel.socket.nio.NioSocketChannel;
import lombok.extern.slf4j.Slf4j;
import java.nio.charset.Charset;
import java.nio.charset.StandardCharsets;
// 注意:客户端与服务器的字符集要保持一致,不然会出现乱码
@Slf4j
public class EventLoopServer {
public static void main(String[] args) {
/*
定义两个EventLoopGroup进行分工即boss和worker
其中boss只负责ServerSocketChannel(服务端)上的accept事件
而worker负责socketChannel上的读写事件
*/
// 创建独立的EventLoop用于执行处理程序中执行时间较长的操作
EventLoopGroup anotherGroup = new DefaultEventLoop(); // DefaultEventLoop只能处理普通任务和定时任务
new ServerBootstrap()
.group(new NioEventLoopGroup(),new NioEventLoopGroup(2))
.channel(NioServerSocketChannel.class)
.childHandler(new ChannelInitializer<NioSocketChannel>() {
@Override
protected void initChannel(NioSocketChannel ch) throws Exception {
ch.pipeline().addLast("handler1",new ChannelInboundHandlerAdapter(){
@Override
public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception {
ByteBuf buf = (ByteBuf) msg;
log.debug(buf.toString(StandardCharsets.UTF_8));
ctx.fireChannelRead(msg); // 将消息传递给下一个handler!!!!
}
});
// 采用额外的anotherGroup进行处理
ch.pipeline().addLast(anotherGroup,"handler2",new ChannelInboundHandlerAdapter(){
@Override
public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception {
ByteBuf buf = (ByteBuf) msg;
log.debug(buf.toString(StandardCharsets.UTF_8));
}
});
}
})
.bind(8080);
}
}
模拟两个客户端访问服务端的输出:
16:11:40.917 [nioEventLoopGroup-4-2] DEBUG netty_basic.eventLoop.EventLoopServer - client1:hello
16:11:40.918 [defaultEventLoop-1-1] DEBUG netty_basic.eventLoop.EventLoopServer - client1:hello
16:12:01.469 [nioEventLoopGroup-4-1] DEBUG netty_basic.eventLoop.EventLoopServer - client2:hello
16:12:01.476 [defaultEventLoop-1-1] DEBUG netty_basic.eventLoop.EventLoopServer - client2:hello
从输出可以看出,每个客户端发送的消息经过两个线程(EventLoop)处理。客户端1中是nioEventLoopGroup-4-2和defaultEventLoop-1-1进行处理。而客户端2中是nioEventLoopGroup-4-1和defaultEventLoop-1-1进行处理
EventLoop的切换代码:
ctx.fireChannelRead(msg); // 将消息传递给下一个handler!!!!
原始的pipeline中多个handler都是由同一EventLoop进行处理,而上述代码实现信息传递给其他EventLoop进行处理。
源码分析:EventLoop切换原理
本质:多个单线程线程池协同完成任务,实现任务拆分后分工完成。
调用链:
ctx.fireChannelRead(msg); // 将消息传递给下一个handler!!!!
------------------------------------------------------------------------
接口ChannelHandlerContext定义的方法:
@Override
ChannelHandlerContext fireChannelRead(Object msg);
-------------------------------------------------------------------------
抽象类AbstractChannelHandlerContext(实现ChannelHandlerContext接口)的方法:
@Override
public ChannelHandlerContext fireChannelRead(final Object msg) {
invokeChannelRead(findContextInbound(MASK_CHANNEL_READ), msg);
return this;
}
static void invokeChannelRead(final AbstractChannelHandlerContext next, Object msg) {
final Object m = next.pipeline.touch(ObjectUtil.checkNotNull(msg, "msg"), next);
/*
next.executor():获取下一个handler的EventLoop
*/
EventExecutor executor = next.executor();
// 判断下一个handler的EventLoop是否和当前线程是同一线程
if (executor.inEventLoop()) {
next.invokeChannelRead(m); // 与当前线程是同一线程,则直接调用
} else {
// 不是同一个线程,则将调用的代码封装为任务对象(实现Runable的对象)让下一个handler的EventLoop执行
executor.execute(new Runnable() {
@Override
public void run() {
next.invokeChannelRead(m);
}
});
}
}
上述代码中invokeChannelRead中展示了EventLoop切换的核心逻辑,核心逻辑是判断当前线程是否是下一个handler的EventLoop的线程,是的话就执行,不是则让下一个handler的EventLoop进行execute(提交任务给该线程池)
@Override
public boolean inEventLoop() {
return inEventLoop(Thread.currentThread());
}
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· DeepSeek 开源周回顾「GitHub 热点速览」
· 物流快递公司核心技术能力-地址解析分单基础技术分享
· .NET 10首个预览版发布:重大改进与新特性概览!
· AI与.NET技术实操系列(二):开始使用ML.NET
· .NET10 - 预览版1新功能体验(一)