前言
NIO在JDK1.4的时候开始登场,在JDK1.7的时候又出了NIO.2又称为AIO。而NIO的实现离不开操作系统对IO多路复用模型的支持,如果对IO模型的不太熟悉可以看这里。如果对IO操作中,读/写数据的流程不太熟悉,可以看这里。另外,本文没有对具体源码做分析。
NIO和BIO的区别
NIO 与传统 I/O(BIO) 不同,它是基于块(Block)的,它以块为基本单位处理数据。而在IO中,基于流(stream)进行IO操作,如字节流以8位(一个字节)为单位进行IO操作,字符流以16个位为单位(Java采用Unicode字符集,一个字符占用两个字节)进行IO操作。
以网络IO为例子,当有30个客户端请求连接服务端时,假设服务端用一个线程来处理。
- 按顺序处理1号到30号线程,即使1号线程,等了好久才有数据发送。
如果服务端采用多线程的方式,那么可以一下子创建30个线程,分别处理这个30个请求,但这种方式显然不太乐观,特别在面对百万客户端甚至更多。
而在NIO中,通过引入Selector等组件,帮服务端和客户端进行解耦。客户端的请求事件在Selector中进行注册,Selector帮我们监听连接中,哪个连接的数据已经到达,再通知到服务端线程。
此种状态下,服务端可能只需少量线程就能处理多个客户端的请求了。这种方式又叫做IO多路复用,这种设计模式又叫反应器模式。
NIO实现
服务端栗子
public static void main(String[] args) throws Exception {
// 1.得到一个ServerSocketChannel对象
ServerSocketChannel serverSocketChannel = ServerSocketChannel.open();
// 2.得到一个选择器Selector对象
Selector selector = Selector.open();
// 3.绑定客户端的端口号
serverSocketChannel.bind(new InetSocketAddress(8000));
// 4.设置阻塞方式
serverSocketChannel.configureBlocking(false);
// 5.把ServerSocketChannel对象注册给Selector
serverSocketChannel.register(selector, SelectionKey.OP_ACCEPT);
// 6.监控客户端
while (true) {
// 表示没有客户端尝试连接
if (selector.select(2000) == 0) {
System.out.println("无连接");
continue;
}
// 如果有的话,得到所有的SelectionKey,判断通道里的事件类型
Set<SelectionKey> selectionKeys = selector.selectedKeys();
Iterator<SelectionKey> iterator = selectionKeys.iterator();
while (iterator.hasNext()) {
SelectionKey selectionKey = iterator.next();
// 判断selectionKey的事件类型
if (selectionKey.isAcceptable()) {
// 客户端连接事件
SocketChannel socketChannel = serverSocketChannel.accept();
socketChannel.configureBlocking(false);
socketChannel.register(selector, SelectionKey.OP_READ, ByteBuffer.allocate(1024));
}
if (selectionKey.isReadable()) {
// 读取客户端数据事件
SocketChannel channel = (SocketChannel) selectionKey.channel();
ByteBuffer buffer = (ByteBuffer) selectionKey.attachment();
channel.read(buffer);
System.out.println("客户端发送的数据" + new String(buffer.array()));
}
iterator.remove();
}
}
}
客户端栗子
public static void main(String[] args) throws Exception{
// 1.先得到一个网络通道
SocketChannel socketChannel = SocketChannel.open();
// 2.设置阻塞方式为非阻塞
socketChannel.configureBlocking(false);
// 3.设置连接的服务器的IP和端口号
InetSocketAddress inetSocketAddress = new InetSocketAddress("127.0.0.1", 8000);
// 4.连接服务器
if(!socketChannel.connect(inetSocketAddress)){
// 如果没连接上, 不会一直阻塞, 可以处理其他任务
while (!socketChannel.finishConnect()){
System.out.println("处理其他任务....");
}
}
// 5.得到一个缓冲区并存入数据
String msg = "hello world!";
ByteBuffer byteBuffer = ByteBuffer.wrap(msg.getBytes());
// 6.发送数据
socketChannel.write(byteBuffer);
// 7.不能关闭连接,让客户端保持连接
System.in.read();
}
缓冲区
NIO种提供很多种类型的缓冲区:
图片来自这位博主的文章中
在这些个缓冲区中,有如下几个重要属性值:
- capacity:容量,表示的是这个缓冲区包含元素的个数
- limit:在 Buffer 上进行的读写操作都不能越过这个下标,代表 buffer 中有效数据的长度。
- position:表示当前的读或写到达哪个位置了。
关于缓冲区的读取,需要注意一下这几个属性位置关系:
- 初始化一个容量为7的缓冲区
- 往缓冲区写入2513,此时的位置变换如图
- 此时,如果我们只想按顺序读取部分数据,我们需要调用byteBuffer.flip()方法将 buffer 从写入操作转换成读取操作,不然从position位置开始读取的数据是空的。调用byteBuffer.flip()后如下:
- 同理 ,从读取操作转换成写入操作,需要使用buffer.clear()进行切换,但此时buffer中的数据并不会删除,只是修改了 position 、limit、和 mark 的位置。