1. 啥是I/O模式?
对于I/O模式,以下馆子做一个类比。下馆子时,我们可能会遇到3种模式:
- 类似于学校饭堂,需要排队,排队点餐结账,打好饭后,找位置干饭;
- 类似于美食广场,点好餐后,餐馆开始做菜(此时你可以做其他事情,没必要等着),当菜做好了,会喊你的号,你要过去店门口,拿好自己的餐,才找位置干饭;
- 类似于餐厅包厢,点菜后,此时你可以继续做其他事情,当菜做好后,菜会直接端上桌;
上面的下馆子的类比关系:
馆子
类比提供服务的服务器
饭菜
类比数据
饭菜可以吃了
类比数据就绪
送饭菜
类比数据的读取
客人
类比等待数据的线程
经典的I/O模式有3种,BIO(阻塞IO)、NIO(非阻塞IO)、AIO(异步IO)。
- BIO:under JDK 1.4 类似于饭堂打饭的模式,阻塞、同步。当数据未就绪(菜没做好),线程发生阻塞,直到数据就绪为止。数据就绪后,唤醒当前线程(同步)读取数据(自己端菜);
- NIO:JDK 1.4 java.nio 类似于美食广场的模式,非阻塞、同步。当数据未就绪时(菜没做好),线程不会发生阻塞,可以执行其他任务。数据就绪后,当前线程(同步)进行数据读取(自己端菜);
- AIO:JDK 1.7 类似于餐厅包厢,非阻塞、异步。当数据未就绪时(菜没做好),线程不会发生阻塞,可以执行其他任务。数据就绪后,由程序的其他线程(异步)进行数据读取(服务员端菜);
2. netty 对IO模式的支持
BIO(OIO deprecate) | NIO(common) | NIO(linux) | NIO(macOS) | AIO(deprecate) |
---|---|---|---|---|
ThreadPerChannelEventLoopGroup | NioEventLoopGroup | EpollEventLoopGroup | KQueueEventLoopGroup | AioEventLoopGroup |
ThreadPerChannelEventLoop | NioEventLoop | EpollEventLoop | KQueueEventLoop | AioEventLoop |
OioServerSocketChannel | NioServerSocketChannel | EpollServerSocketChannel | KQueueServerSocketChannel | AioServerSocketChannel |
OioSocketChannel | NioSocketChannel | EpollSocketChannel | KQueueSocketChannel | AioSocketChannel |
2.1. 为啥BIO不建议使用(deprecate)?
假若连接数在较高时,太多线程处于阻塞状态,线程阻塞会被认为是一种浪费CPU资源的方式,线程太多时,CPU上下文切换成本较高,效率低下。 但是BIO也不是一无是处,假若连接数不高,并发度不高时,BIO不输NIO。
2.2. 为啥会删掉已经做好的AIO?
AIO在windows实现较为成熟,但是在linux实现不够成熟,且linux又常用作于服务器。
linux下,AIO相比NIO的性能提高并不理想。
但是,最近5.0版本又重新开始更新了,5.0版本和4.1版本同时更新,4.1必要的特性会合并到5.0版本。
3. netty对Reactor模式的支持
netty 的NIO线程模式是基于Reactor开发模式实现的,核心流程:
- 注册感兴趣的事件
- 扫描是否有感兴趣的事件发生
- 事件发生后做出对应的处理
channel感兴趣的事件:
type | channel type | OP_ACCEPT | OP_CONNECT | OP_WRITE | OP_READ |
---|---|---|---|---|---|
client | SocketChannel | √ | √ | √ | |
server | ServerSocketChannel | √ | |||
server | SocketChannel | √ | √ |
3.1. Reactor的3种版本
- Reactor 单线程模式
Reactor 单线程
模式下,以下公共皆在同一个线程下执行:
- 接入连接(ServerSocketChannel接受连接SocketChannel)
- 接受请求(SocketChannel#read)
- 请求解码(decode)
- 请求处理(handle)
- 处理结果编码(encode)
- 请求结果写回(SocketChannel#write)
- 断开连接(SocketChannel#close)
就好比,刚起步的餐馆,一人身兼多职:
- 迎客(接入连接)
- 点菜(接受请求)
- 做菜(请求处理)
- 上菜(请求结果写回)
- 送客(断开连接)
- Reactor 多线程模式
本模式下是Reactor 单线程模式的多线程版本。
就好比,餐馆业务繁忙后,自己一个忙不过来,需要请多几个人帮忙,每个人的工作内容都是一样的
- 迎客(接入连接)
- 点菜(接受请求)
- 做菜(请求处理)
- 上菜(请求结果写回)
- 送客(断开连接)
- Reactor 主从模式
Reactor 主从模式,拆分成俩部分:
- 主模式(boss线程池)
- 接入连接(ServerSocketChannel接受连接SocketChannel)
- 从模式(worker线程池)
- 接受请求(SocketChannel#read)
- 请求解码(decode)
- 请求处理(handle)
- 处理结果编码(encode)
- 请求结果写回(SocketChannel#write)
就好比,餐馆为了提升服务品质,需要专业的人才服务对应的岗位:
- 迎客(接入连接)需要形象更好的人
- 点菜(接受请求)需要记性比较好的、推荐菜品、口才、情商比较好的人
- 做菜(请求处理)需要大厨师炒上几手好菜
- 上菜(请求结果写回)需要细心的人
- 送客(断开连接)需要形象更好的人
3.2. netty是怎么使用这3种Reactor模式的呢?
Reactor 单线程模式:
public class SingleThreadReactor {
public static void main(String[] args) throws InterruptedException {
ServerBootstrap serverBootstrap = new ServerBootstrap();
serverBootstrap.channel(NioServerSocketChannel.class);
serverBootstrap.option(NioChannelOption.SO_BACKLOG, 1024);
serverBootstrap.childOption(NioChannelOption.TCP_NODELAY, true);
// 单线程
NioEventLoopGroup bossGroup = new NioEventLoopGroup(1);
try {
// 注册使用的线程池
serverBootstrap.group(bossGroup);
serverBootstrap.childHandler(new ChannelInitializer<NioSocketChannel>() {
@Override
protected void initChannel(NioSocketChannel ch) throws Exception {
ChannelPipeline pipeline = ch.pipeline();
pipeline.addLast("frameDecoder", new OrderFrameDecoder());
pipeline.addLast("frameEncoder", new OrderFrameEncoder());
pipeline.addLast("protocolDecoder", new OrderProtocolDecoder());
pipeline.addLast("protocolEncoder", new OrderProtocolEncoder());
pipeline.addLast(new OrderServerProcessHandler());
}
});
ChannelFuture channelFuture = serverBootstrap.bind(8090).sync();
channelFuture.channel().closeFuture().sync();
} finally {
bossGroup.shutdownGracefully();
}
}
}
Reactor 多线程模式:
public class MultiThreadReactor {
public static void main(String[] args) throws InterruptedException {
ServerBootstrap serverBootstrap = new ServerBootstrap();
serverBootstrap.channel(NioServerSocketChannel.class);
serverBootstrap.option(NioChannelOption.SO_BACKLOG, 1024);
serverBootstrap.childOption(NioChannelOption.TCP_NODELAY, true);
// 多线程
NioEventLoopGroup bossGroup = new NioEventLoopGroup(16);
try {
// 注册使用的线程池
serverBootstrap.group(bossGroup);
serverBootstrap.childHandler(new ChannelInitializer<NioSocketChannel>() {
@Override
protected void initChannel(NioSocketChannel ch) throws Exception {
ChannelPipeline pipeline = ch.pipeline();
pipeline.addLast("frameDecoder", new OrderFrameDecoder());
pipeline.addLast("frameEncoder", new OrderFrameEncoder());
pipeline.addLast("protocolDecoder", new OrderProtocolDecoder());
pipeline.addLast("protocolEncoder", new OrderProtocolEncoder());
pipeline.addLast(new OrderServerProcessHandler());
}
});
ChannelFuture channelFuture = serverBootstrap.bind(8090).sync();
channelFuture.channel().closeFuture().sync();
} finally {
bossGroup.shutdownGracefully();
}
}
}
Reactor 主从模式:
public class MasterSlaveReactor {
public static void main(String[] args) throws InterruptedException {
ServerBootstrap serverBootstrap = new ServerBootstrap();
serverBootstrap.channel(NioServerSocketChannel.class);
serverBootstrap.option(NioChannelOption.SO_BACKLOG, 1024);
serverBootstrap.childOption(NioChannelOption.TCP_NODELAY, true);
// 主线程池
NioEventLoopGroup bossGroup = new NioEventLoopGroup(1);
// 从线程池
NioEventLoopGroup workerGroup = new NioEventLoopGroup(16);
try {
// 注册使用的线程池
serverBootstrap.group(bossGroup, workerGroup);
serverBootstrap.childHandler(new ChannelInitializer<NioSocketChannel>() {
@Override
protected void initChannel(NioSocketChannel ch) throws Exception {
ChannelPipeline pipeline = ch.pipeline();
pipeline.addLast("frameDecoder", new OrderFrameDecoder());
pipeline.addLast("frameEncoder", new OrderFrameEncoder());
pipeline.addLast("protocolDecoder", new OrderProtocolDecoder());
pipeline.addLast("protocolEncoder", new OrderProtocolEncoder());
pipeline.addLast(new OrderServerProcessHandler());
}
});
ChannelFuture channelFuture = serverBootstrap.bind(8090).sync();
channelFuture.channel().closeFuture().sync();
} finally {
bossGroup.shutdownGracefully();
workerGroup.shutdownGracefully();
}
}
}
4. netty对Reactor支持的灵魂N问
4.1. 为什么主reactor大多数情况下,只会用到一个线程,并不需要一个线程池?
主reactor的主要任务是接入连接(ServerSocketChannel#accept)。
一般情况下,我们去构建一个服务,只会提供一个端口port,其他网络服务需要与本服务通信,连上此端口即可。
EventLoop可以理解为包含Selector的事件循环处理器(包含Selector的单线程线程池)。
ServerSocketChannel要想接入连接,需要注册到单个Selector即可,同时需要一个执行线程,这俩个条件单个EventLoop即可满足,因此ServerSocketChannel绑定EventLoop,注册到EventLoop内的Selector,监听OP_ACCEPT即可。
io.netty.channel.AbstractChannel.AbstractUnsafe#register
@Override
public final void register(EventLoop eventLoop, final ChannelPromise promise) {
// 省略部分代码...
// 绑定EventLoop
AbstractChannel.this.eventLoop = eventLoop;
if (eventLoop.inEventLoop()) {
register0(promise);
} else {
try {
eventLoop.execute(new Runnable() {
@Override
public void run() {
register0(promise);
}
});
} catch (Throwable t) {
logger.warn(
"Force-closing a channel whose registration task was not accepted by an event loop: {}",
AbstractChannel.this, t);
closeForcibly();
closeFuture.setClosed();
safeSetFailure(promise, t);
}
}
}
因此,boss线程组只需要构建一个EventLoopGroup即可。
NioEventLoopGroup bossGroup = new NioEventLoopGroup(1);
// 省略部分代码...
serverBootstrap.group(bossGroup, workerGroup);
当然,特殊情况下,也是允许绑定多个端口的。如果不同端口获取的数据格式一致,其实没必要绑定多个端口;如果不同端口获取的数据格式不一致,在从reactor中,对不同类型的Message,分发到不同的Handler处理,逻辑上不太清晰,也不建议这么做。
4.2. Netty是如何为channel分配NioEventLoop(即处理channel编码、解码、处理等等业务的线程)的?
ServerSocketChannel接入连接SocketChannel后,SocketChannel在ServerSocketChannel的pipeline中传播,当传播到ServerBootstrapAcceptor时,SocketChannel会进行一系列的初始化,包括option、attribute、handler的绑定、EventLoop的绑定
io.netty.bootstrap.ServerBootstrap.ServerBootstrapAcceptor#channelRead
@Override
@SuppressWarnings("unchecked")
public void channelRead(ChannelHandlerContext ctx, Object msg) {
final Channel child = (Channel) msg;
// 绑定SocketChannel的handler
child.pipeline().addLast(childHandler);
// 设置SocketChannel的Option,如TCP_NODELAY
setChannelOptions(child, childOptions, logger);
// 设置SocketChannel的属性,辅助作用
setAttributes(child, childAttrs);
try {
// 从Reactor中选择EventLoop,绑定
// 注册到EventLoop的Selector
// 绑定后,监听OP_READ事件
childGroup.register(child).addListener(new ChannelFutureListener() {
@Override
public void operationComplete(ChannelFuture future) throws Exception {
if (!future.isSuccess()) {
forceClose(child, future.cause());
}
}
});
} catch (Throwable t) {
forceClose(child, t);
}
}
怎么还没聊到分配NioEventLoop?来啦来啦
EventLoopGroup在注册SocketChannel时,会进行EventLoop的分配,具体分配代码,我们定位到这里:
io.netty.channel.MultithreadEventLoopGroup#register(io.netty.channel.Channel)
@Override
public ChannelFuture register(Channel channel) {
return next().register(channel);
}
next方法的内部实现,可定位到这里:
io.netty.util.concurrent.DefaultEventExecutorChooserFactory
public final class DefaultEventExecutorChooserFactory implements EventExecutorChooserFactory {
public static final DefaultEventExecutorChooserFactory INSTANCE = new DefaultEventExecutorChooserFactory();
private DefaultEventExecutorChooserFactory() { }
@SuppressWarnings("unchecked")
@Override
public EventExecutorChooser newChooser(EventExecutor[] executors) {
//依据executor的大小(EventLoopGroup的构造器参数可设置大小)是否为2^n,选择不同的EventLoop分配器
if (isPowerOfTwo(executors.length)) {
return new PowerOfTwoEventExecutorChooser(executors);
} else {
return new GenericEventExecutorChooser(executors);
}
}
private static boolean isPowerOfTwo(int val) {
return (val & -val) == val;
}
private static final class PowerOfTwoEventExecutorChooser implements EventExecutorChooser {
private final AtomicInteger idx = new AtomicInteger();
private final EventExecutor[] executors;
PowerOfTwoEventExecutorChooser(EventExecutor[] executors) {
this.executors = executors;
}
@Override
//executors总数是2^n(2,4,8...)才会用, &运算效率更高, 同时当idx累加成最大值之后,更公平。为啥?可看下GenericEventExecutorChooser的实现
public EventExecutor next() {
return executors[idx.getAndIncrement() & executors.length - 1];
}
}
private static final class GenericEventExecutorChooser implements EventExecutorChooser {
private final AtomicInteger idx = new AtomicInteger();
private final EventExecutor[] executors;
GenericEventExecutorChooser(EventExecutor[] executors) {
this.executors = executors;
}
@Override
public EventExecutor next() {
// 递增、取模,取正值,不然可能是负数
// 存在缺点:当idx累加成最大值后,有短暂的不公平:
// 假如executors总数为7
// 1, 2, 1, 0, 6, 5, 4, 3, 2, 1, 0, 6, 5,...
// 当idx达到最大值时为1,继续递增时,就变成2,1,公平性会短暂性缺失
return executors[Math.abs(idx.getAndIncrement() % executors.length)];
}
}
}
4.3. netty是怎么实现多路复用器selector的跨平台?
io.netty.channel.nio.NioEventLoopGroup#NioEventLoopGroup(int, java.util.concurrent.ThreadFactory)
public NioEventLoopGroup(int nThreads, ThreadFactory threadFactory) {
this(nThreads, threadFactory, SelectorProvider.provider());
}
SelectorProvider是rt.jar包下的,会依据不同环境下的jdk,选择对应的Selector。
package sun.nio.ch;
import java.nio.channels.spi.SelectorProvider;
/**
* Creates this platform's default SelectorProvider
*/
public class DefaultSelectorProvider {
/**
* Prevent instantiation.
*/
private DefaultSelectorProvider() { }
/**
* Returns the default SelectorProvider.
*/
public static SelectorProvider create() {
return new sun.nio.ch.WindowsSelectorProvider();
}
}
package sun.nio.ch;
import java.nio.channels.spi.SelectorProvider;
/**
* Creates this platform's default SelectorProvider
*/
public class DefaultSelectorProvider {
/**
* Prevent instantiation.
*/
private DefaultSelectorProvider() { }
/**
* Returns the default SelectorProvider.
*/
public static SelectorProvider create() {
return new sun.nio.ch.KQueueSelectorProvider();
}
}
package sun.nio.ch;
import java.nio.channels.spi.SelectorProvider;
import java.security.AccessController;
import sun.security.action.GetPropertyAction;
/**
* Creates this platform's default SelectorProvider
*/
public class DefaultSelectorProvider {
/**
* Prevent instantiation.
*/
private DefaultSelectorProvider() { }
@SuppressWarnings("unchecked")
private static SelectorProvider createProvider(String cn) {
Class<SelectorProvider> c;
try {
c = (Class<SelectorProvider>)Class.forName(cn);
} catch (ClassNotFoundException x) {
throw new AssertionError(x);
}
try {
return c.newInstance();
} catch (IllegalAccessException | InstantiationException x) {
throw new AssertionError(x);
}
}
/**
* Returns the default SelectorProvider.
*/
public static SelectorProvider create() {
String osname = AccessController.doPrivileged(new GetPropertyAction("os.name"));
if (osname.equals("SunOS"))
return createProvider("sun.nio.ch.DevPollSelectorProvider");
if (osname.equals("Linux"))
return createProvider("sun.nio.ch.EPollSelectorProvider");
return new sun.nio.ch.PollSelectorProvider();
}
}