NIO
什么是 NIO
NIO全称Non Blocking IO,同步非阻塞IO框架。服务器实现模式为一个线程可以处理多个请求(连接),客户端发送的连接请求都会注册到多路复用器selector上,多路复用器轮询到连接有IO请求就进行处理
为什么用 NIO
常见的网络服务中,如果每一个客户端都维持一个与登陆服务器的连接。那么服务器将维护多个和客户端的连接以出来和客户端的contnect 、read、write ,特别是对于长链接的服务,有多少个c端,就需要在s端维护同等的IO连接。这对服务器来说是一个很大的开销(著名的C10K问题)
NIO 怎么玩
Reactor模式的单线程版-NIO服务端
public static void main(String[] args) throws IOException {
ServerSocketChannel socketChannel = ServerSocketChannel.open();
// 绑定端口
socketChannel.bind(new InetSocketAddress(8080));
// 声明异步
socketChannel.configureBlocking(false);
// 声明多路复用器,创建 linux下的epoll文件描述对象
Selector selector = Selector.open();
// 注册连接事件
socketChannel.register(selector,SelectionKey.OP_ACCEPT);
while (true){
// 通过操作系统的epoll,选择触发事件的selectionKey 加入set集合中
selector.select();
final Set<SelectionKey> selectionKeys = selector.selectedKeys();
final Iterator<SelectionKey> iterator = selectionKeys.iterator();
// 遍历触发事件的selectionKey,selectionKey中包含 socketChannel
while (iterator.hasNext()){
final SelectionKey next = iterator.next();
// 判断事件类型
if(next.isAcceptable()){
// 如果是连接事件,给对应的SocketChannel注册读取事件
final ServerSocketChannel channel = (ServerSocketChannel) next.channel();
final SocketChannel accept = channel.accept();
accept.configureBlocking(false);
accept.register(selector,SelectionKey.OP_READ);
}else if (next.isReadable()){
// 如果是读取事件,直接读取
final SocketChannel channel = (SocketChannel) next.channel();
ByteBuffer byteBuffer = ByteBuffer.allocate(1024);
final int read = channel.read(byteBuffer);
System.out.println(new String(byteBuffer.array(),0,read));
}
}
// 移除set集合中已经处理的socketChannel,避免二次处理
iterator.remove();
}
}
复制代码
如何运行并测试,该NIO服务端呢?
Windows 环境下,可以通过命令行工具的 telnet 命令,连接服务端并发送数据;telnet localhost 8080
连接服务端ctrl+]
打开发送页面,并通过 send
命令发送消息到服务端
NIO 是如何接受socket请求并读取数据?
在NIO服务端启动时,会通过Selector selector = Selector.open()
创建一个linux epoll/windows select实例对象,然后再selector.select()
时,将事件、channel注册到epoll中并等待事件发生,如果事件发生会从操作系统内部的就绪事件列表中rdList
【当操作系统感知到事件发生时,会将事件加入到就绪事件列表中rdList】,选择对应的事件加入到SelectionKey
中,然后进行处理。
NIO解决了BIO什么痛点,还有什么缺陷
- NIO 解决了
- BIO 线程阻塞,读写阻塞,线程等待时间过长问题
- 客户端连接数过多导致的服务端CPU过高,线程浪费过多问题(C10K)
- NIO 还有以下缺陷
- 没有从本质上解决C10K问题【如果存在10万连接,同时读写数据,单selector,NIO的循环也需要很久】
- selector.select(),存在空轮询bug,当select()方法没有获取事件,也有可能不阻塞,直接向下执行,且一次出现后续会一直出现。
- 代码编写复杂,需要大量异常IO处理
Netty
Netty 是基于NIO的,运用Reactor模型设计的高性能、异步事件驱动的网络应用框架。
Netty 解决了 NIO 的哪些痛点
- 解决了selector.select()的 空轮询bug
- 简化了开发,netty内部已经对断线重连、 网络闪断、心跳处理、半包读写、 网络拥塞和异常流等进行了处理。
- netty内部初始化多个selector,从而避免C10K问题
Netty 怎么玩
Netty 服务端
public class NettyService {
public static int port = 8080;
public static void main(String[] args) {
// 创建接受accept事件的selector线程组
EventLoopGroup bossGroup = new NioEventLoopGroup(1);
// 创建处理read/write等业务处理事件的selector线程组
EventLoopGroup workerGroup = new NioEventLoopGroup(8);
ServerBootstrap bootstrap = new ServerBootstrap();
bootstrap.group(bossGroup, workerGroup)
.channel(NioServerSocketChannel.class)
.childHandler(new ChannelInitializer<SocketChannel>() {
@Override
protected void initChannel(SocketChannel socketChannel) throws Exception {
// 业务处理handler
socketChannel.pipeline().addLast(new DefaultChannelInboundHandlerAdapter());
}
})
.option(ChannelOption.SO_BACKLOG, 128)
.childOption(ChannelOption.SO_KEEPALIVE, true);
try {
ChannelFuture future = bootstrap.bind(port).sync();
future.channel().closeFuture().sync();
} catch (InterruptedException e) {
e.printStackTrace();
}finally {
workerGroup.shutdownGracefully();
bossGroup.shutdownGracefully();
}
}
}
复制代码
服务端业务处理handler
public class DefaultChannelInboundHandlerAdapter extends ChannelInboundHandlerAdapter {
protected static ChannelGroup channels = new DefaultChannelGroup(GlobalEventExecutor.INSTANCE);
@Override
public void channelRegistered(ChannelHandlerContext ctx) throws Exception {
System.out.println("客户端上线:"+ ctx.channel().remoteAddress());
channels.add(ctx.channel());
}
@Override
public void channelUnregistered(ChannelHandlerContext ctx) throws Exception {
System.out.println("客户端下线:"+ ctx.channel().remoteAddress());
channels.remove(ctx.channel());
}
@Override
public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception {
ByteBuf byteBuf = (ByteBuf) msg;
String rtn = "客户端:"+ ctx.channel().remoteAddress()+":"+ byteBuf.toString(CharsetUtil.UTF_8);
channels.forEach(channel -> channel.writeAndFlush(Unpooled.wrappedBuffer(rtn.getBytes(StandardCharsets.UTF_8))));
}
}
复制代码
Netty 客户端
public class NettyClient {
public static void main(String[] args) {
EventLoopGroup workerGroup = new NioEventLoopGroup();
Bootstrap bootstrap = new Bootstrap();
bootstrap.group(workerGroup)
.channel(NioSocketChannel.class)
.option(ChannelOption.SO_KEEPALIVE, true)
.handler(new ChannelInitializer<SocketChannel>() {
@Override
public void initChannel(SocketChannel ch) throws Exception {
ch.pipeline().addLast(new StringEncoder()).addLast(new StringDecoder())
.addLast(new DefaultClientHandlerAdapter());
}
});
try {
ChannelFuture connect = bootstrap.connect("127.0.0.1", 8080).sync();
Channel channel = connect.channel();
System.out.println("===="+channel.localAddress()+"====");
Scanner scanner = new Scanner(System.in);
while (scanner.hasNext()){
String next = scanner.next();
channel.writeAndFlush(next);
}
channel.closeFuture().sync();
} catch (InterruptedException e) {
e.printStackTrace();
}finally {
workerGroup.shutdownGracefully();
}
}
}
复制代码
客户端业务处理handler
public class DefaultClientHandlerAdapter extends ChannelInboundHandlerAdapter{
@Override
public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception {
System.out.println("客户端接受到消息");
System.out.println(msg.toString());
}
@Override
public void channelWritabilityChanged(ChannelHandlerContext ctx) throws Exception {
System.out.println("客户端写入消息");
}
}
复制代码
如何运行并测试,以上netty代码
先运行服务段main方法,然后运行客户端main方法
就可以通过客户端控制台输入内容发送到服务端
Netty 的线程模型
在网上看到一张图片,感觉非常到位!(服务端)
- 根据 new NioEventLoopGroup() 的源码,不难发现NioEventLoopGroup其实是一个线程组对象,内部的线程池对象包含 selector 和 taskQueue;
- bossGroup 负责accept事件,workerGroup负责读写事件
- NioEventLoop 其实是一个线程
- pipeline 是处理事件的业务管道
- ChannelHandler 是业务管道中的具体的处理类,以客户端应用程序为例,如果事件的运动方向是从客户端到服务端的,那么我们称这些事件为出站的,即客户端发送给服务端的数据会通过pipeline中的一系列ChannelOutboundHandler(ChannelOutboundHandler调用是从tail到head方向逐个调用每个handler的逻辑),并被这些Handler处理,反之则称为入站的,入站只调用pipeline里的ChannelInboundHandler逻辑(ChannelInboundHandler调用是从head到tail方向逐个调用每个handler的逻辑)。
Netty 粘包拆包
TCP 做为传输层协议,并不理解上层业务数据的具体含义,它只会根据TCP 缓冲区的大小,进行数据包的划分。比如缓存区的大小是10k,你发送了12k的数据,TCP会将这个12K数据,拆分成10K+2K【拆包】,先发送10k,然后等下一个数据,取8K与上一个数据的2K组成10K【粘包】再发送。
Netty 如何解决
- 消息定长度,传输的消息大小固定长度,如,每次发送100字节,不足的补空字符
- 每次消息末尾添加特殊字符,然后再消息接受端进行分割
- 发送消息的同时发送长度,先发送长度,然后再发送消息。消息接收端,接受到消息长度,然后等待下一个数据包到达,获取指定长度。
Netty提供了多个解码器,可以进行分包的操作,如下:
- LineBasedFrameDecoder (回车换行分包)
- DelimiterBasedFrameDecoder(特殊分隔符分包)
- FixedLengthFrameDecoder(固定长度报文来分包)
Netty 断线自动重连实现
在客户端实现再客户端连接服务端时,添加监听器,监听连接状态,如果连接失败,再次重连
代码实现:
ChannelFuture cf = bootstrap.connect(host, port);
cf.addListener(new ChannelFutureListener() {
@Override
public void operationComplete(ChannelFuture future) throws Exception {
if (!future.isSuccess()) {
//重连交给后端线程执行
future.channel().eventLoop().schedule(() -> {
try {
connect();
} catch (Exception e) {
e.printStackTrace();
}
}, 3000, TimeUnit.MILLISECONDS);
}
}
});
//对通道关闭进行监听
cf.channel().closeFuture().sync();
复制代码
Netty 怎么解决 selector.select() 的空轮询 bug
Netty 创建了一个int selectCnt = 0,在执行select时,selectCnt+1,如果select获取到的事件数量大于0时,selectCnt 设置为 0,当selectCnt大于设置的阈值512【io.netty.selectorAutoRebuildThreshold 可以自己设置】时,Netty会新建一个selector,并将出现bug的selector上的注册事件重新注册到新建的selector上,同时关闭出bug的selector。
源码:
protected void run() {
int selectCnt = 0;
for (;;) {
try {
int strategy;
try {
strategy = selectStrategy.calculateStrategy(selectNowSupplier, hasTasks());
switch (strategy) {
case SelectStrategy.CONTINUE:
continue;
case SelectStrategy.BUSY_WAIT:
case SelectStrategy.SELECT:
// 省略部分为select的一些获取逻辑
default:
}
} catch (IOException e) {
rebuildSelector0();
selectCnt = 0;
handleLoopException(e);
continue;
}
selectCnt++;
cancelledKeys = 0;
needsToSelectAgain = false;
final int ioRatio = this.ioRatio;
boolean ranTasks;
// 省略部分不相干代码
if (ranTasks || strategy > 0) {
if (selectCnt > MIN_PREMATURE_SELECTOR_RETURNS && logger.isDebugEnabled()) {
logger.debug("Selector.select() returned prematurely {} times in a row for Selector {}.",
selectCnt - 1, selector);
}
selectCnt = 0;
} else if (unexpectedSelectorWakeup(selectCnt)) { // 这行代码是关键!
selectCnt = 0;
}
} catch (CancelledKeyException e) {
// Harmless exception - log anyway
if (logger.isDebugEnabled()) {
logger.debug(CancelledKeyException.class.getSimpleName() + " raised by a Selector {} - JDK bug?",
selector, e);
}
} catch (Error e) {
// 省略异常处理代码
}
}
}
复制代码
unexpectedSelectorWakeup方法:
private static final int SELECTOR_AUTO_REBUILD_THRESHOLD
= SystemPropertyUtil.getInt("io.netty.selectorAutoRebuildThreshold", 512);
private boolean unexpectedSelectorWakeup(int selectCnt) {
if (Thread.interrupted()) {
return true;
}
if (SELECTOR_AUTO_REBUILD_THRESHOLD > 0 &&
selectCnt >= SELECTOR_AUTO_REBUILD_THRESHOLD) {
rebuildSelector();
return true;
}
return false;
}
作者:疯狂小周
链接:https://juejin.cn/post/6956516753752981535
来源:掘金