NIO
文章目录
1. 四种IO模式
1.0 缓冲区
- 内核缓冲区
- 进程缓冲区
缓冲区的出现是为了缓解CPU和内存处理速度的差异,减少频繁的读写操作
Linux操作系统中,有两个缓冲区,内核缓冲区和进程缓冲区(也叫用户缓冲区)
内核只有一个内核缓冲区,每个用户进程都有自己的进程缓冲区
read和write操作
read操作,进程向内核发出请求,内核进行从物理设备上读取数据放到内核缓冲区,然后把数据从内核缓冲区复制到进程缓冲区
write操作,不是直接把数据从内存中写入物理磁盘,而是从进程缓冲区将数据复制到内核缓冲区,然后再由内核写入物理磁盘
1.1 同步阻塞式
传统的IO模式,用户线程一旦发起一次read那么一直在等待,直到内核IO操作彻底完成,期间不能做其他事,并发量太低
1.2 同步非阻塞式
同步非阻塞式IO再同步阻塞式基础上,将socket设置为NOBOLOCK,线程发一次read请求后立刻返回,但是会轮询,一次一次的发送read请求,直到内核IO完成,真正的读取到了数据,每一个线程都会去轮询会占用大量CPU资源
1.3 IO多路复用
经典的Reactor设计模式,Java中的Selector和Linux中的epoll都是这种模式
使用IO多路复用的前提是内核需要支持select函数,使用select函数可以避免同步非阻塞IO中的线程轮询CPU占用高问题。
用户首先将需要进行IO操作的socket添加到select,然后阻塞等待select系统调用返回。当内核IO完成,通知socket可以进行read,然后用户线程正是发起read,读取数据并执行。
多路分离函数select
流程来看,如果只有单个线程和同步阻塞式IO没有什么区别,甚至还多了socket监控,还有select函数的操作。但是如果并发用户很庞大,依然只有一个select线程进行轮询,用户可以通过注册多个socket然后去调用select,资源开销其实是很小的,达到同一个线程内同时处理多个IO请求的目的,如果是同步阻塞式IO就需要开启多线程才能达到这种效果。
但是select函数优势不仅于此,虽然上述方式允许单线程内处理多个IO请求,但是每个IO请求的过程还是阻塞的(再select函数上阻塞),平均时间可能比同步阻塞还长。如果说用户再注册完socket或者IO请求,然后就可以做自己的事情,等到数据再来时进行处理,就可以提高CPU的利用率。
IO多路复用
如此一来,Reactor线程负责调用内核的select函数检查socket状态,当有socket被激活时,通知相应的线程。这里阻塞也仅仅只发生在了select函数执行过程中,而不是socket中。
1.4 异步IO
经典的Proactor设计模式
有一个专门的服务进程,nodejs就是如此,专门来处理请求
2. NIO
NIO叫No-Blocking IO同步非阻塞式IO,Java中的NIO叫做New IO
传统IO和NIO最重要的区别是,数据打包和传输的方式。传统IO通过流的方式处理数据,NIO通过块的方式处理数据。
面向流的IO一次处理一个字节数据,方便创建过滤器,缺点是太慢
面向块的IO一次处理一个数据块,数度很快,但是麻烦
jdk包中的io也集成了NIO的特性,java.io.*包中有一些类已经以块的形式读写数据
2.1 通道与缓冲区
通道
通道Channel是对原IO包中的模拟,可以通过它读取和写入数据
和流的区别:流是单向流动,通道是全双工,可以同时读写
通道包括以下类型
- Filechannel:从文件中读写数据
- DatagramChannel:通过UDP读写网络数据
- SocketChannel:通过TCP读写网络数据
- ServerSocketChannel:监听新的TCP连接,每一个连接都会创建一个SocketChannel
缓冲区
无论读写都需要将数据先放入缓冲区
缓冲区本质上是一个数组,但也不仅仅是一个数组,缓冲区提供了对数据的结构化访问,还可以跟踪系统的读写进程
缓冲区类型对应着其中基本数据类型(除了布尔类型)
- ByteBuffer
- MappedByteBuffer(专用于内存映射)
- CharBuffer
- ShortBuffer
- LongBuffer
- FloatBuffer
- DoubleBuffer
Buffer类中所有定义的缓冲区都具有四个属性来提供关于其包含的数据元素的信息
四个属性:capacity,limit,position,mark,并遵循mark <= position <= limit <= capacity
解释:
- capacity:容量,可以容纳的最大数据量,在缓冲区创建时被设定并且不能被改变
- limit:表示缓冲区的当前终点,不能对缓冲区超过极限的位置进行读写,是可以修改的
- position:当前位置的索引,下一个要被读或写的元素的索引,是个游标
- mark:标记,调用mark()来设置mark=posotion,再调用reset()可以让position恢复到标记的位置
API
public abstract class Buffer {
//JDK1.4时,引入的api
public final int capacity( );//返回此缓冲区的容量
public final int position( );//返回此缓冲区的位置
public final Buffer position (int newPositio);//设置此缓冲区的位置
public final int limit( );//返回此缓冲区的限制
public final Buffer limit (int newLimit);//设置此缓冲区的限制
public final Buffer mark( );//在此缓冲区的位置设置标记
public final Buffer reset( );//将此缓冲区的位置重置为以前标记的位置
public final Buffer clear( );//清除此缓冲区, 即将各个标记恢复到初始状态,但是数据并没有真正擦除, 后面操作会覆盖
public final Buffer flip( );//反转此缓冲区
public final Buffer rewind( );//重绕此缓冲区
public final int remaining( );//返回当前位置与限制之间的元素数
public final boolean hasRemaining( );//告知在当前位置和限制之间是否有元素
public abstract boolean isReadOnly( );//告知此缓冲区是否为只读缓冲区
//JDK1.6时引入的api
public abstract boolean hasArray();//告知此缓冲区是否具有可访问的底层实现数组
public abstract Object array();//返回此缓冲区的底层实现数组
public abstract int arrayOffset();//返回此缓冲区的底层实现数组中第一个缓冲区元素的偏移量
public abstract boolean isDirect();//告知此缓冲区是否为直接缓冲区
}
clear()和compact()区别:
clear会清空缓冲区所有数据,compact会只清除已经读过的数据,未读的数据放在起始位置
flip()和rewind()区别:
看源码
public final Buffer flip() {
limit = position;
position = 0;
mark = -1;
return this;
}
public final Buffer rewind() {
position = 0;
mark = -1;
return this;
}
差别在limit是否改变,所以rewind更多看作是对缓冲区的重新读取
状态改变举例:
- 新建一个8个字节大小的缓冲区,此时position是0,而limit=capacity=8,capacity不会改变
- 从输入通道中读取5个字节写入缓冲区,此时position=5,limit不变
- 在将缓冲区的数据写到输出通道之前,需要先调用flip()方法,这个方法将limit设置为当前position,并将position设置为0
- 从缓冲区中取4个字节到输出缓冲中,此时position设为4
- 最后需要调用clear()方法清空缓冲区,此时position和limit都被设置为最初位置
2.2 API
- 单通道读
public class NIOFileReadTest {
public static void main(String[] args) throws IOException {
//通过randomAccessFile拿到file文件,设置读写模式
RandomAccessFile file = new RandomAccessFile("D:\\ProgrammingFiles\\TestFiles\\wordcount.java","rw");
//拿到channel对象
FileChannel channel = file.getChannel();
//拿到buffer,设置分配大小
ByteBuffer buffer = ByteBuffer.allocate(1024);
//读取数据到buffer
int bytesRead = channel.read(buffer);
while (bytesRead != -1) {
System.out.println("----->"+bytesRead);
//读写反转
buffer.flip();
while (buffer.hasRemaining()) {
System.out.println((char) buffer.get());
}
buffer.clear();
bytesRead = channel.read(buffer);
}
file.close();
}
}
- 单通道写
public class NIOFileWriteTest {
public static void main(String[] args) throws IOException {
//通过RandomAccessFile,InputStream,OutputStream获取channel
RandomAccessFile reader = new RandomAccessFile("D:\\ProgrammingFiles\\TestFiles\\niofilewrite.txt", "rw");
FileChannel channel = reader.getChannel();
//拿buffer,准备数据
ByteBuffer buffer = ByteBuffer.allocate(1024);
String newData = "ricardo贾";
//写入数据
buffer.clear();
buffer.put(newData.getBytes(StandardCharsets.UTF_8));
buffer.flip();
while (buffer.hasRemaining()){
channel.write(buffer);
}
//关闭channel
reader.close();
}
}
- transferTo和TransferFrom实现通道数据传输,下面的例子完成文件拷贝
public class FileCopyTest {
public static void main(String[] args) throws IOException {
RandomAccessFile from = new RandomAccessFile("D:\\ProgrammingFiles\\TestFiles\\niofilewrite.txt", "rw");
RandomAccessFile to = new RandomAccessFile("D:\\ProgrammingFiles\\TestFiles\\transferFrom.txt", "rw");
FileChannel fromChannel = from.getChannel();
FileChannel toChannel = to.getChannel();
//transferTo(fromChannel,toChannel);
transferFrom(fromChannel,toChannel);
}
static void transferTo(FileChannel from,FileChannel to) throws IOException {
long position = 0;
long size = from.size();
//开始位置position,传输长度
from.transferTo(position,size,to);
from.close();
to.close();
}
static void transferFrom(FileChannel from,FileChannel to) throws IOException {
long position = 0;
long size = from.size();
to.transferFrom(from,position,size);
from.close();
to.close();
}
}
- 单缓冲区实现文件拷贝
public class SingleBufferCopyFileTest {
public static void main(String[] args) throws IOException {
//拿到channel
RandomAccessFile reader = new RandomAccessFile("D:\\ProgrammingFiles\\TestFiles\\niofilewrite.txt", "rw");
RandomAccessFile writer = new RandomAccessFile("D:\\ProgrammingFiles\\TestFiles\\singlebuffer.txt", "rw");
FileChannel readerChannel = reader.getChannel();
FileChannel writerChannel = writer.getChannel();
//生成buffer
ByteBuffer buffer = ByteBuffer.allocateDirect(1024);
//从输入channel读取数据到buffer
while (true) {
int read = readerChannel.read(buffer);
if (read == -1) {
break;
}
//将buffer读写反转
buffer.flip();
//从buffer输出数据到channel
writerChannel.write(buffer);
//清空缓冲区
buffer.clear();
}
//关闭channel
reader.close();
writer.close();
}
}
- allocate()和allocateDirect()的区别在于分配内存的位置不同,前者内存分配在JVM堆中,后者内存分配系统分配内存
- 速度方面allocateDirect()分配内存慢,但IO快,allocate()分配内存快,但IO慢,当数据量很大时allocateDirect()速度要远好于allocate()
- ServerSocketChannel实现监听
public class SocketChannelTest {
public static final String GREETING = "hello java nio.\r\n";
public static void main(String[] args) throws IOException, InterruptedException {
//获取端口
int port = 8888;
if (args.length > 0) {
port = Integer.parseInt(args[0]);
}
//包装数据到buffer
ByteBuffer wrap = ByteBuffer.wrap(GREETING.getBytes(StandardCharsets.UTF_8));
//打开ServerSocketChannel.open()
ServerSocketChannel channel = ServerSocketChannel.open();
//绑定端口
channel.bind(new InetSocketAddress(port));
//设置非阻塞模式
channel.configureBlocking(false);
//开始监听
while (true) {
System.out.println("Waiting for connection...");
//如果是阻塞模式,就会在accept()阻塞,直到获取到连接
SocketChannel accept = channel.accept();
//这里我们设置了非阻塞模式,就会
if (accept == null) {
System.out.println("null");
Thread.sleep(2000);
} else {
System.out.println("Incoming connection from "+accept.socket().getRemoteSocketAddress());
wrap.rewind();
accept.write(wrap);
accept.close();
}
}
}
}
- SocketChannel
public class SocketChannelTest {
public static void main(String[] args) throws IOException {
//两种创建SocketChannel的方法
// SocketChannel channel = SocketChannel.open();//无实质连接
// channel.connect(new InetSocketAddress("www.baidu.com",80));
SocketChannel channel = SocketChannel.open(new InetSocketAddress("www.baidu.com", 80));
ByteBuffer buffer = ByteBuffer.allocate(1024);
//channel.configureBlocking(false);
channel.read(buffer);
channel.close();
System.out.println("read over!");
}
}
- DatagramChannel
@Test
public void TestSend() throws IOException, InterruptedException {
DatagramChannel channel = DatagramChannel.open();
InetSocketAddress sendAddress = new InetSocketAddress("127.0.0.1", 9999);
while (true) {
channel.send(ByteBuffer.wrap("我发了个包".getBytes(StandardCharsets.UTF_8)),sendAddress);
System.out.println("我发出去了");
Thread.sleep(1000);
}
}
@Test
public void TestRecive() throws IOException {
DatagramChannel channel = DatagramChannel.open();
InetSocketAddress recvPort = new InetSocketAddress(9999);
channel.bind(recvPort);
ByteBuffer buffer = ByteBuffer.allocate(512);
while (true) {
buffer.clear();
SocketAddress receive = channel.receive(buffer);
buffer.flip();
System.out.println(receive.toString()+" ");
System.out.println(StandardCharsets.UTF_8.decode(buffer));
}
}
测试先开接收端,再开发送端
接受端
发送端
2.3 Selector
多路复用器,Selector对各个Channel进行轮询,这样每个Channel就可以非阻塞式的运行,降低了对资源的占用
可选择通道SelectableChannel
-
不是所有的Channel都能被Selector复用的,FileChannel就无法被选择器复用,想要选择器复用的Channel必须继承SelectableChannel这个抽象类
-
SelectableChannel类提供了实现通道的可选择性所需要的公共方法,比如register
-
一个通道可以被注册到多个选择器上,但每对每个选择器而言只能注册一次,通道和选择器之间的关系,使用注册的方式完成,在注册的时候,需要指定通道的哪些操作,是selector感兴趣的
注册通道到选择器
通过Channel.register(Selector sel, int ops)方法将一个通道注册到选择器上。第一个参数是注册到哪个选择器上,第二个参数指定选择器关心的通道操作,一共有四种
- 可读:SelectionKey.OP_READ
- 可写:SelectionKey.OP_WRITE
- 连接:SelectionKey.OP_CONNECT
- 接收:SelectionKey.OP_ACCEPT
可以同时有多个通道操作,用|运算符
如:int key = SelectionKey.OP_READ | SelectionKey.OP_WRITE
选择器轮询的不是操作,而是某个操作的就绪状态
public void Test1() throws IOException {
//获取Selector选择器
Selector selector = Selector.open();
//获取通道
ServerSocketChannel serverSocketChannel = ServerSocketChannel.open();
//设置非阻塞
serverSocketChannel.configureBlocking(false);
//绑定链接
serverSocketChannel.bind(new InetSocketAddress(9999));
//将通道注册到选择器上,并指定监听事件为接收事件
serverSocketChannel.register(selector, SelectionKey.OP_ACCEPT);
}
注意与Selector一起使用的时候,通道必须属于非阻塞模式,否则抛异常,也就是说FileChannel无法和Selector一起使用
2.4 轮询查询就绪操作
Selector中的select(),就绪的状态集合保存在一个Set<SelectionKey>的集合中,select()返回的int值代表从上一次调用select方法到这次调用中间有几个通道准备就绪,如果返回不是0,说明有通道准备就绪,那么准备就绪的选择键就会在Set集合中,拿到它的迭代器就可以根据操作的类型进行对应的操作
Set<SelectionKey> set = selector.selectedKeys();
Iterator<SelectionKey> iterator = set.iterator();
while (iterator.hasNext()) {
SelectionKey key = iterator.next();
if (key.isAcceptable()) {
// accept
} else if (key.isConnectable()) {
// connect
} else if (key.isReadable()) {
// read
} else if (key.isWritable()) {
// write
}
iterator.remove();
}
2.5 停止选择的方法
wakeup()立刻返回阻塞状态的select方法
close()关闭Selector,所有注册在该选择器上的通道都被注销