1.NIO简介
- Java BIO:同步阻塞(传统阻塞型):服务器实现模式为一个连接一个线程,即客户端有连接请求时服务器端就需要启动一个线程进行处理,如果这个连接不做任何事情会造成不必要的线程开销。
- Java NIO:同步非阻塞,服务器实现模式为一个线程处理多个请求(连接),即客户端发送的请求都会注册到多路复用器上,多路复用器轮询到连接有I/O请求就进行处理。
2.NIO的应用场景
高并发网络应用:NIO特别适用于需要处理大量并发连接的网络应用,如聊天服务器、游戏服务器等。在这些场景中,传统的阻塞IO模型会因为线程资源有限而成为性能瓶颈。分布式系统中的通信:在分布式系统中,服务之间的通信需要高效的网络传输机制。NIO可以帮助减少线程资源的使用,提高通信效率。文件操作:NIO也可以用于文件的读写操作,特别是在处理大文件或需要并发读取文件时,NIO的效率更高。实时通讯服务:例如,实时消息推送服务、实时交易系统等,这些系统需要快速响应,并且能够处理大量的并发请求。I/O密集型应用:对于需要频繁进行I/O操作的应用,NIO可以减少线程阻塞时间,提高CPU利用率。
由于NIO的复杂性,实际应用中通常会使用Netty这样的高级框架来简化开发。以下是使用Java NIO的一个简单代码示例:
import java.net.InetSocketAddress;
import java.nio.ByteBuffer;
import java.nio.channels.ServerSocketChannel;
import java.nio.channels.SocketChannel;
public class NIOServer {
public static void main(String[] args) throws Exception {
ServerSocketChannel serverSocketChannel = ServerSocketChannel.open();
serverSocketChannel.bind(new InetSocketAddress(8080));
ByteBuffer buffer = ByteBuffer.allocate(1024);
while (true) {
SocketChannel socketChannel = serverSocketChannel.accept(); // 非阻塞
socketChannel.configureBlocking(false);
int bytesRead = socketChannel.read(buffer); // 非阻塞
if (bytesRead > 0) {
buffer.flip();
// 处理读取到的数据...
buffer.clear();
}
}
}
}
3.NIO有哪些核心功能?
- 通道(Channel):用于数据传输的通道,类似于流Stream,但提供了非阻塞读写操作。
- 缓冲区(Buffer):每个通道都维护了一个数据缓冲区;通道可以读写缓冲区中的数据,是双向的;客户端也可以读取缓冲区的数据;缓冲区是通道与客户端之间的缓冲区。
- 选择器(Selector):用于检查一个或多个通道是否准备好进行读写操作的事件选择机制。这使得单个线程能够管理多个通道,从而减少了线程资源的使用。
NIO之所以能够支持上万个连接同时在线,是因为它的选择机制。通过选择器,NIO可以在少量的线程中处理大量的连接,因为每个线程只需处理哪些就绪的连接,而不需要为每个连接都分配一个线程。
以下是一个使用NIO的简单示例代码:
Selector selector = Selector.open();
ServerSocketChannel serverSocket = ServerSocketChannel.open();
serverSocket.bind(new InetSocketAddress(port));
serverSocket.configureBlocking(false);
serverSocket.register(selector, SelectionKey.OP_ACCEPT);
while (true) {
selector.select(); // 阻塞直到至少有一个通道在你注册的事件上就绪了
Set<SelectionKey> selectedKeys = selector.selectedKeys();
Iterator<SelectionKey> iter = selectedKeys.iterator();
while (iter.hasNext()) {
SelectionKey key = iter.next();
if (key.isAcceptable()) {
// 接受连接操作
}
if (key.isReadable()) {
// 读取操作
}
if (key.isWritable()) {
// 写入操作
}
if (key.isConnectable()) {
// 连接就绪操作
}
iter.remove();
}
}
在这个示例中,一个线程通过选择器管理多个通道,从而能够高效地处理多个并发连接。
4.NIO的选择器(Selector)
NIO的选择器(Selector)是java NIO(New I/O)的核心组件,它提供了一种机制,允许单线程或少量线程管理多个通道(Channel)的I/O操作。选择器的目的是实现I/O多路复用,这意味着它能够检测多个通道上是否有至少一个通道准备好进行I/O操作(如读取或写入)。
以下是选择器的一些关键特性:
- 事件驱动:选择器是基于事件驱动的。当通道上发生特定的事件(如数据可读或可写)时,选择器能够检测到这些事件,并通知应用程序。
- 通道注册:为了使用选择器,必须先将通道注册到选择器上,并指定通道感兴趣的事件类型(如SelectionKey.OP_READ或SelectionKey.OP_WRITE)。
- 非阻塞I/O:与选择器一起使用的通道通常配置为非阻塞模式。这意味着如果通道当前没有数据可读或可写,I/O操作将立即返回,而不是阻塞。
- 轮询机制:选择器通过调用select()方法来轮询注册在其上的通道。这个调用会阻塞,直到至少有一个通道准备好I/O操作。
- 就绪状态:一旦select()方法返回,应用程序可以通过迭代选择键(SelectionKey)集合来识别哪些通道处于就绪状态,并执行相应的I/O操作。
- 操作系统支持:选择器的实现依赖于底层的操作系统机制。例如,在Linux上,它通常使用epoll机制,而在Windows上,它可能依赖于IOCP(I/O Completion Ports)。
5.NIO服务器端交互流程:
启动线程:服务器端启动一个线程;选择器(Selector)遍历通道(Channel):线程通过选择器不同的遍历各个通道,如果发现有客户端对应的通道有网络请求,那么开始处理该通道相关业务逻辑;通道(Channel)与缓冲区(Buffer)交互:通道可以读写缓冲区中的数据;缓冲区(Buffer)与客户端交互:缓冲区与客户端进行数据读写交互;
6.NIO缓冲区
缓冲区机制:缓冲区向上与通道进行数据读写交互,向下与客户端进行数据读写交互,客户端与通道不直接进行通信;
-
缓冲区的作用:数据缓存:缓冲区用于在内存中临时存储从通道读取的数据或者将要写入通道的数据,这样可以减少对磁盘或网络的直接I/O操作次数,提高性能。减少上下文切换:通过缓冲区,可以减少应用程序与操作系统之间的上下文切换次数,因为数据可以在缓冲区中积累到一定量后再进行读写操作。提高数据处理效率:缓冲区基于数组实现,可以高效地进行数据的随机访问,这对处理某些需要随机读写的数据结构非常有用。支持批量操作:缓冲区支持批量读写操作,这意味着可以一次性读取或写入大量数据,这对于提高吞吐量非常有帮助。
-
缓冲区的一些基本属性:
capacity:缓冲区的大小,即它能够容纳的数据量。position:下一个要被读或写的元素的索引。limit:缓冲区中不可读写数据的边界索引。mark:用于标记当前position的位置,方便之后恢复。
-
缓冲区的基本操作包括:
put():向缓冲区写入数据。get():从缓冲区中读取数据。flip():切换读写模式,将position设置为0,limit设置为当前position。clear():清空缓冲区,将position设置为0,limit设置为capacity。compact():压缩缓冲区,将未读的数据复制到缓冲区的起始位置,然后切换到写入模式。
以下是一个简单使用ByteBuffer的例子:
import java.nio.ByteBuffer;
public class BufferExample {
public static void main(String[] args) {
// 创建一个容量为1024字节的缓冲区
ByteBuffer buffer = ByteBuffer.allocate(1024);
// 向缓冲区中写入数据
buffer.put("Hello, NIO".getBytes());
// 切换到读模式
buffer.flip();
byte[] data = new byte[buffer.limit()];
// 从缓冲区中读数据
buffer.get(data);
// 输出读取的数据
System.out.println(new String(data));
}
}
7.BIO、NIO、AIO(也叫NIO2)的区别
- BIO(Blocking IO):同步阻塞IO,每个请求都需要一个线程处理,如果请求没有准备好,线程会阻塞。
- NIO(Non-blocking IO):同步非阻塞IO,使用一个线程管理多个请求,通过选择器(Selector)来检查哪个请求准备好了。
- AIO(Asynchronous IO):异步非阻塞IO,真正的异步IO操作,不需要使用选择器,当IO操作完成时会通知应用程序。
8.什么是零拷贝(Zero Copy)
- 零拷贝是一种IO操作优化技术,它减少了数据在用户态和内核态之间的拷贝次数。在Java中,FileChannel的transferTo和transferFrom方法可以实现零拷贝。
- 优点:减少CPU的使用,减少了上下文的切换,提高了系统的吞吐量。
9.Buffer分类与使用:
- buffer是NIO中用来缓存的数据对象,分为直接缓冲区(Direct Buffer)和堆缓冲区。
- 直接缓冲区:在Java堆外分配内存,可以减少Java堆和本地栈之间来回复制数据,但分配和销毁的成本更高。直接缓冲区不会对垃圾收集器造成影响,但是直接缓冲区的分配和释放会占用操作系统的内存。
- 堆缓冲区:在Java堆内分配内存,分配和销毁的成本低。
10.Java IO的常用API
- InputStream/OutputStream和Reader/Writer是Java IO的基本抽象类,分别处理字节流和字符流。
- InputStream/OutputStream通常用于处理原始字节数据,而Reader/Writer提供了字符集转换功能。
11.NIO高性能操作的原理
- NIO使用了选择器(Selector)和多路复用机制,允许单线程管理多个通道,从而提高了处理多个IO操作的效率。
12.什么是Netty?
Netty是一个提供异步事件驱动的网络应用程序框架和工具,用来快速开发高性能、高可靠性的网络服务器和客户端程序。它基于JAVA NIO来实现。
Netty的使用通常涉及到以下步骤:
- 创建线程组(Boss和Worker线程组)。
- 设置通道类型(NioSocketChannel或NioServerSocketChannel)。
- NioSocketChannel:主要负责连接数据的传输,类似于Java底层的Socket。它既可以用于客户端也可以用于服务端,但通常与ServerSocketChannel配合使用,以实现客户端与服务器之间的通信。每个连接的两端(客户端和服务端)都有一个NioSocketChannel负责数据的传输。NioSocketChannel支持阻塞和非阻塞两种模式,能够适应不同的应用场景需求。
- NioServerSocketChannel:专门用于服务端,负责监听传入的连接请求。与NioSocketChannel不同,它不负责数据传输,而是负责创建新的NioSocketChannel对象来处理传入的连接。它本身只应用于服务端,通过绑定到一个端口来等待客户端的连接请求。
- 添加处理器(ChannelHandler)来处理业务逻辑,如编解码器和自定义处理器。
- 设置通道选项,如TCP参数。
- 绑定端口并启动服务。
服务端示例:
import io.netty.bootstrap.ServerBootstrap;
import io.netty.channel.ChannelFuture;
import io.netty.channel.ChannelInitializer;
import io.netty.channel.ChannelOption;
import io.netty.channel.EventLoopGroup;
import io.netty.channel.nio.NioEventLoopGroup;
import io.netty.channel.socket.SocketChannel;
import io.netty.channel.socket.nio.NioServerSocketChannel;
import io.netty.handler.codec.string.StringDecoder;
import io.netty.handler.codec.string.StringEncoder;
public class NettyServer {
public static void main(String[] args) {
// 创建Boss线程组用于接收连接
EventLoopGroup bossGroup = new NioEventLoopGroup(1);
// 创建Worker线程组用于处理已接收连接的IO操作
EventLoopGroup workerGroup = new NioEventLoopGroup();
try {
ServerBootstrap b = new ServerBootstrap();
b.group(bossGroup, workerGroup)
.channel(NioServerSocketChannel.class)
.childHandler(new ChannelInitializer<SocketChannel>() {
@Override
public void initChannel(SocketChannel ch) throws Exception {
ch.pipeline().addLast(new EchoServerHandler());
}
})
.option(ChannelOption.SO_BACKLOG, 128)
.childOption(ChannelOption.SO_KEEPALIVE, true);
// 绑定端口并开始接收进来的连接
ChannelFuture f = b.bind(port).sync();
f.channel().closeFuture().sync();
} finally {
bossGroup.shutdownGracefully();
workerGroup.shutdownGracefully();
}
}
}
客户端示例:
EventLoopGroup group = new NioEventLoopGroup();
try {
Bootstrap b = new Bootstrap();
b.group(group)
.channel(NioSocketChannel.class)
.handler(new ChannelInitializer<SocketChannel>() {
@Override
public void initChannel(SocketChannel ch) throws Exception {
ch.pipeline().addLast(new EchoClientHandler());
}
});
// 尝试建立连接
ChannelFuture f = b.connect(host, port).sync();
f.channel().closeFuture().sync();
} finally {
group.shutdownGracefully();
}
EchoServerHandler和EchoClientHandler是自定义的ChannelHandler,用于处理业务逻辑。
13.Netty的组件
- ServerBootStrap/BootStrap
- ServerBootStrap: 服务端启动配置引导类
- BootStrap:客户端启动配置引导类
- Channel Netty 网络通信组件,能够用于执行网络I/O操作。Channel为用户提供:
- 当前网络连接的通道状态(例如是否打开?是否已连接?)
- 网络连接配置参数(例如接收缓冲区大小)
- 提供异步的网络I/O 操作(如建立连接、读写、绑定端口),异步调用意味着任何I/O调用都将立即返回,并且不保证在调用结束时请求的I/O操作已完成。
- 调用立即返回一个ChannelFuture实例,通过注册监听器到ChannelFuture上,可以I/O操作成功或失败回调通知调用方。
- Selector
- netty基于selector对象实现多路复用,单个线程就可以对多个客户端channel事件进行监听。
- NioEventLoop
- NioEventLoop内部维护了一个线程和一个任务队列,还有一个Selector对象,支持异步提交任务,现成启动后会调用Loop的run方法,执行异步队列中的非I/O任务和I/O任务。
- IO任务
- accecpt、connect、read、write等
- 非IO任务
- 注册、绑定等
- NioEventLoopGroup
- 内部维护多个NioEventLoop,负责处理多个channel事件,但是一个channel只会交给一个NioEventLoop处理
- ChannelHandler
- ChannelHandler处在pipline中,它有两种类型:进站和出站,当服务端收到消息时,所有的进站handler将会被执行,当服务端往客户端写消息时,所有的出站handler将会执行。
- ChannelPipline
- 这就是上面写到的pipeline,一个pipline中会维护很多handler,保存一个handler list,这里涉及到一个设计模式:职责链设计模式。