NIO 是一种同步非阻塞的 IO 模型。同步是指线程不断轮询 IO 事件是否就绪,非阻塞是指线程在等待 IO 的时候,可以同时做其他任务。同步的核心就是 Selector,Selector 代替了线程本身轮询 IO 事件,避免了阻塞同时减少了不必要的线程消耗;非阻塞的核心就是通道和缓冲区,当 IO 事件就绪时,可以通过写道缓冲区,保证 IO 的成功,而无需线程阻塞式地等待。
BIO -blocking IO 同步式阻塞式IO->UDP/TCP
NIO -new IO 同步非阻塞式IO
AIO -AsynchronousIO 异步式非阻塞式IO -->jdk1.8
BIO缺点
1、会产生阻塞行为 receive/accept/connect/read/write
2、一对一的连接:每连接一个客户端,在服务器端就需要开启一个线程去处理请求,在客户端较多的情况下,服务器端就会产生大量的线程-耗费内存
3、连接建立后,如果不发生任何操作,那么会导致服务器中这个线程依然会被占用,耗费服务器资源
4、 无法实现定点操作
NIO优势
1、非阻塞,提高传输效率
2、一对多连接,可以用一个或者少量的服务器中的线程来处理大量的请求,从而节省服务器的内存资源
3、即使已经建立连接,只要没有对应的读写事件,那么依然不能够使用服务器来进行处理
4、利用通道来进行双向传输
5、因为利用缓冲区来存储数据,所以可以对缓冲区中的数据实现定点操作
NIO的三大组件:Buffer缓冲区,Channel通道,Selector多路复用选择器
1、Buffer - 缓冲区
ByteBuffer实现类
容器,存储数据,在底层以数组形式实现
在NIO库中,所有数据都是用缓冲区处理的。在读取数据时,它是直接读到缓冲区中的; 在写入数据时,它也是写入到缓冲区中的;任何时候访问 NIO 中的数据,都是将它放到缓冲区中。而在面向流I/O系统中,所有数据都是直接写入或者直接将数据读取到Stream对象中。
capacity 容量位-指定缓冲区的容量,一旦指定,无法更改
limit 限制位-限制操作位所能达到的高度
position 操作位-指定要操作的位置
mark 标记位-标记位置,认为标记位置之前的数据是已经操作过的没有错误的数据
flip()反转缓冲区,先将限制位挪到操作位上,然后将操作位归零清空标记位
clear()清空缓冲区,将操作位归零,将limit挪到capacity,标记位清空
reset()重置缓冲区,将操作位挪到标记位
rewind()重绕缓冲区,将操作位归零,将标记位清空 用于缓冲区多次读取
Code
// 创建缓冲区对象
// ByteBuffer底层依靠字节数组来存储数据
// ByteBuffer buffer = ByteBuffer.allocate(10);
// 在创建缓冲区的时候传入字节数组,并且先定了字节的数组的大小
// 虽然这种方式给定了数据,但是position依然从第0位开始计算
ByteBuffer buffer = ByteBuffer.wrap("hello".getBytes());
// buffer.put((byte)97);
// 获取操作位
// System.out.println(buffer.position());
// System.out.println(buffer.capacity());
// System.out.println(buffer.limit());
// 添加数据
// buffer.put("abc".getBytes());
// buffer.put("def".getBytes());
// System.out.println(buffer.position());
// 反转缓冲区
// buffer.limit(buffer.position());
// 设置操作位
// buffer.position(0);
// buffer.flip();
// 获取数据
// byte b = buffer.get();
// System.out.println(b);
// 实际上判断操作位是否小于限制位
// while (buffer.hasRemaining()) {
// byte b = buffer.get();
// System.out.println(b);
// }
// 将缓冲区转化为数组
byte[] data = buffer.array();
// buffer.flip();
// System.out.println(new String(data,0,buffer.limit()));
System.out.println(new String(data, 0, buffer.position()));
2、 Channel - 通道
传输数据,是面向缓冲区的,在java中Channel默认也是阻塞的,需要手动将其设置为非阻塞模式。
Channel是双向的,读写必须通过Buffer对象处理
BIO : File、UDP-DatagramSocket、TCP-Socket/ServerSocket
NIO : FileChannel、UDP-DatagramChannel、TCP-SocketChannel/ServerSocketChannel
FileChannel-操作文件。可以利用通道实现相同平台之间的零拷贝技术
FileChannel不能切换到非阻塞模式,因为它不是套接字通道,所以
FileChannel不能和Selector绑定事件
Code
package cn.tedu.nio.channel;
import java.io.IOException;
import java.net.InetSocketAddress;
import java.nio.ByteBuffer;
import java.nio.channels.SocketChannel;
public class ClientDemo {
public static void main(String[] args) throws IOException, InterruptedException {
// 创建客户端的通道
SocketChannel sc = SocketChannel.open();
// NIO默认是阻塞的,手动设置为非阻塞
sc.configureBlocking(false);
// 发起连接 - 即使连接失败,也会继续向下执行
sc.connect(new InetSocketAddress("localhost", 8090));
// 如果单独使用channel,需要将它进行手动阻塞
// 判断连接是否建立
// 这个方法底层会判断连接是否建立,如果建立则继续往下执行
// 如果这个连接没有建立,那么在底层会试图再次建立连接
// 如果试图连接多次失败,那么会抛出异常
while (!sc.finishConnect())
;
// 写出数据
sc.write(ByteBuffer.wrap("hello".getBytes()));
Thread.sleep(10);
// 读取数据
ByteBuffer buffer = ByteBuffer.allocate(1024);
sc.read(buffer);
buffer.flip();
System.out.println(new String(buffer.array(), 0, buffer.limit()));
// 关闭
sc.close();
}
}
##服务器端
package cn.tedu.nio.channel;
import java.io.IOException;
import java.net.InetSocketAddress;
import java.nio.ByteBuffer;
import java.nio.channels.ServerSocketChannel;
import java.nio.channels.SocketChannel;
public class ServerDemo {
public static void main(String[] args) throws IOException, InterruptedException {
// 创建服务器端的通道
ServerSocketChannel ssc = ServerSocketChannel.open();
// 绑定要监听的端口
ssc.bind(new InetSocketAddress(8090));
// 设置为非阻塞
ssc.configureBlocking(false);
// 接受连接
SocketChannel sc = ssc.accept();
// 手动阻塞
while (sc == null)
sc = ssc.accept();
// 准备一个缓冲区用于存储数据
ByteBuffer buffer = ByteBuffer.allocate(1024);
// 读取数据
sc.read(buffer);
buffer.flip();
System.out.println(new String(buffer.array(), 0, buffer.limit()));
sc.write(ByteBuffer.wrap("接收成功~~~".getBytes()));
Thread.sleep(10);
ssc.close();
}
}
3、Selector - 选择器
选择器是NIO的核心,它是channel的管理者
通过selector可以实现利用同一个服务器端来处理多个客户端的数据,实现了用少量的线程处理大量的请求,在底层处理的时候依然是同步的
在NIO中一共有四种事件:
1.SelectionKey.OP_CONNECT:连接事件
2.SelectionKey.OP_ACCEPT:接收事件
3.SelectionKey.OP_READ:读事件
4.SelectionKey.OP_WRITE:写事件
Code
客户端
package cn.tedu.nio.selector;
import java.io.IOException;
import java.net.InetSocketAddress;
import java.nio.ByteBuffer;
import java.nio.channels.SelectionKey;
import java.nio.channels.Selector;
import java.nio.channels.SocketChannel;
import java.util.Iterator;
import java.util.Set;
public class ClientDemo {
public static void main(String[] args) throws IOException {
// 创建客户端的通道
SocketChannel sc = SocketChannel.open();
// 获取选择器
Selector selc = Selector.open();
// 选择器管理的连接要求必须是非阻塞的
sc.configureBlocking(false);
// 将客户端注册到选择器身上,并且申请了一个可连接事件
sc.register(selc, SelectionKey.OP_CONNECT);
// 发起连接
sc.connect(new InetSocketAddress("localhost", 8090));
// 从这儿开始的代码针对多客户端来进行操作的
while (true) {
// 选择出注册过的通道
selc.select();
// 针对通道的不同事件类型进行处理
Set<SelectionKey> keys = selc.selectedKeys();
Iterator<SelectionKey> it = keys.iterator();
while (it.hasNext()) {
// 根据事件类型进行处理
SelectionKey key = it.next();
// 判断是否是可连接事件
if (key.isConnectable()) {
// 从当前事件中获取到对应的通道
SocketChannel scx = (SocketChannel) key.channel();
// 如果是可连接事件,判断连接是否成功
while (!scx.finishConnect())
;
// 如果连接成功了,可能会向服务器端发数据或者读数据
scx.register(selc, SelectionKey.OP_WRITE | SelectionKey.OP_READ);
}
// 判断是否是可写事件
if (key.isWritable()) {
// 从当前事件中获取到对应的通道
SocketChannel scx = (SocketChannel) key.channel();
// 写出数据
scx.write(ByteBuffer.wrap("hello~~~".getBytes()));
// 需要去掉可写事件
// 获取这个通道身上的所有的事件
scx.register(selc, key.interestOps() ^ SelectionKey.OP_WRITE);
}
// 判断是否是可读事件
if (key.isReadable()) {
// 从当前事件中来获取到对应的通道
SocketChannel scx = (SocketChannel) key.channel();
// 读取数据
ByteBuffer buffer = ByteBuffer.allocate(1024);
scx.read(buffer);
buffer.flip();
System.out.println(new String(buffer.array(), 0, buffer.limit()));
// 需要去掉这个可读事件
scx.register(selc, key.interestOps() ^ SelectionKey.OP_READ);
}
// 处理完这一大类事件之后
it.remove();
}
}
}
}
##服务器端
package cn.tedu.nio.selector;
import java.io.IOException;
import java.net.InetSocketAddress;
import java.nio.ByteBuffer;
import java.nio.channels.SelectionKey;
import java.nio.channels.Selector;
import java.nio.channels.ServerSocketChannel;
import java.nio.channels.SocketChannel;
import java.util.Iterator;
import java.util.Set;
public class ServerDemo {
public static void main(String[] args) throws IOException {
// 创建服务器端的通道
ServerSocketChannel ssc = ServerSocketChannel.open();
// 绑定要监听的端口号
ssc.bind(new InetSocketAddress(8090));
// 开启选择器
Selector selc = Selector.open();
ssc.configureBlocking(false);
// 将通道注册到选择器上,需要注册一个可接受事件
ssc.register(selc, SelectionKey.OP_ACCEPT);
while(true){
// 选择出已经注册的连接
selc.select();
// 根据事件的不同进行分别的处理
Set<SelectionKey> keys = selc.selectedKeys();
Iterator<SelectionKey> it = keys.iterator();
while (it.hasNext()) {
// 将事件取出来分别进行处理
SelectionKey key = it.next();
// 判断可接受事件
if(key.isAcceptable()){
// 从事件中获取到对应的通道
ServerSocketChannel sscx = (ServerSocketChannel) key.channel();
// 接受连接
SocketChannel sc = sscx.accept();
sc.configureBlocking(false);
// 注册读写事件
sc.register(selc, SelectionKey.OP_READ | SelectionKey.OP_WRITE);
}
// 判断可读事件
if(key.isReadable()){
// 从事件中获取到对应的通道
SocketChannel sc = (SocketChannel) key.channel();
// 读取数据
ByteBuffer buffer = ByteBuffer.allocate(1024);
sc.read(buffer);
buffer.flip();
System.out.println(new String(buffer.array(),0, buffer.limit()));
// 需要去掉可读事件
sc.register(selc, key.interestOps() ^ SelectionKey.OP_READ);
}
// 判断可写事件
if(key.isWritable()){
// 从事件中获取到对应的通道
SocketChannel sc = (SocketChannel) key.channel();
// 写出数据
sc.write(ByteBuffer.wrap("收到".getBytes()));
// 需要去掉可写事件
sc.register(selc, key.interestOps() ^ SelectionKey.OP_WRITE);
}
// 去掉这一大类的事件
it.remove();
}
}
}
}