什么是NIO?
java 的NIO是从jdk1.4引入的一个新的I/O操作的api。可以替换标准的I/O API,NIO与原来的IO具有同样的功能和目的,但是使用方式完全不一样,NIO是面向缓冲区,基于通道的IO操作,NIO将以更加高效的方式进行文件的读写操作。
传统的IO操作是面向流的。
如果是本地文件,那么需要设置接收文件的输入流,同时指定发送文件的输出流,由输出流向输入流通过流式操作发送数据,从而起到复制文件的作用。
如果是网络传输的话,那么服务端需要阻塞接收客户端的流,如果有流进来就将accept的结果再通过获取流的API(getInputStream或者getOutputStream)操作获取流,然后再读取数据。如果没有流进来,此时服务端一直处于阻塞状态,导致服务端无法进行其他的操作。
面向缓冲区基于通道的NIO是如何进行上述操作的。
对于本地文件来说,NIO通过各种各样的缓冲区比如(ByteBuffer,IntegerBuffer,FloatBuffer,DoubleBuffer,CharBuffer… 除了Boolean类型都有对应的缓冲区
)等缓冲区,通过allocate或者allocateDirect获取指定大小的缓冲区,同时再获取各种流式操作对应的管道(FileChannel,SocketChannel,ServerSocketChannel,DatagramChannel),这些通道都集成了读写操作的API,使我们不必要再创建输出流和输出流的操作,我们可以向缓冲区中写入数据,然后将缓冲区交给通道进行传送,我们也可以通过通道从将缓冲区中的数据读取出来。
简言之: 管道负责连接,缓冲区负责传输数据
缓冲区是如何进行读写
核心方法:
get()
获取缓冲区的数据
put()
存数据到缓冲区
核心属性:
mark:标记如果调用mark() API的话会存储当前操作数据的位置
position: 当前正在操作数据的位置
limit 表示缓冲区可以操作数据的大小(limit位置之后的位置不能进行读写)
capacity 缓冲区的最大容量
String message = "hello world";
ByteBuffer buf =ByteBuffer.allocate(1024);//设置容量
buf.put(((java.lang.String) message).getBytes());//向缓冲区中写入数据
buf.flip();//将缓冲区切换到读模式
byte[] dst = new byte[buf.limit()];//缓冲取可以操作数据的大小
buf.get(dst,0,3);//将buf部分数据存储到dst数组0-3位置的数据
buf.mark();//标记下当前操作的位置 3
String msg = new String(dst,0,3);//转化为字符串
System.out.println(msg);
System.out.println(buf.position()); //如果position的值为字符串的长度的话 就无法再在缓冲区进行读写了
buf.get(dst,3,3);
System.out.println(new String(dst,3,3));
System.out.println(buf.position());
buf.reset();//回复到mark的位置
System.out.println(buf.position());
直接缓冲区和非直接缓冲区
非直接缓冲区:通过allocate()方法分配缓冲区,将缓冲区建立在JVM的内存中。
假如要从磁盘中读取文件,数据不能直接传输到用户地址空间,首先传输到内核地址空间,再从内核地址空间copy到用户地址空间。
直接缓冲区: 通过allocateDirect 分配直接缓冲区,将缓冲区建立在操作系统的物理内存中。效率高。
弊端,分配物理内存和销毁物理内存消耗资源高。销毁或者写到物理磁盘中由操作系统决定。
通道技术
主要通道实现类FileChannel,ServerSocketChannel SocketChannel DatagramChannel
各种流基本上提供了通道的获取api(getChannel)
本地IO
FileInputStream/FileOutputStream
RandomAccessFile
获取通道的三种方式之一
public static void channel(){
//利用通道完成文件的复制
try {
FileInputStream fileInputStream = new FileInputStream("1.jpg");
FileOutputStream fileOutputStream = new FileOutputStream("2.jpg");
//获取通道
FileChannel channelIn = fileInputStream.getChannel();//通道1
FileChannel channelOut = fileOutputStream.getChannel();//通道2
ByteBuffer buf = ByteBuffer.allocate(1024);//缓冲区
while (channelIn.read(buf)!=-1){//将通道内的数据写入到buf中
buf.flip();//开始读缓存 不然无法进行读操作因为limit在限制着
channelOut.write(buf);//将缓存中的内容写到通道中
buf.clear();//清空缓存
}
channelOut.close();
channelIn.close();
fileOutputStream.close();
fileOutputStream.close();
} catch (FileNotFoundException e) {
e.printStackTrace();
} catch (IOException e) {
e.printStackTrace();
}
}
通过通道复制文件的三种方式之二: 通过open方法获取通道 通过MappedBytedBuffer 获取映射文件
//直接映射文件复制文件
public static void channelMap() throws IOException {
FileChannel fileChannelIn = FileChannel.open(Paths.get("1.jpg"), StandardOpenOption.READ);//从本地文件中直接读取
FileChannel fileChannelOut = FileChannel.open(Paths.get("2.jpg"), StandardOpenOption.WRITE,StandardOpenOption.READ,StandardOpenOption.CREATE_NEW);//从本地文件中直接读取 在操作文件的时候表明操作的类型
//内存映射文件
MappedByteBuffer mapIn = fileChannelIn.map(FileChannel.MapMode.READ_ONLY, 0, fileChannelIn.size());
MappedByteBuffer mapOut = fileChannelOut.map(FileChannel.MapMode.READ_WRITE, 0, fileChannelIn.size());
//直接对缓冲区进行数据的读写
byte[] dst = new byte[mapIn.limit()];
mapIn.get(dst);//从缓冲区获取
mapOut.put(dst);//写入到物理内存中 但是神魔时候写入硬盘 由操作系统决定。
}
通过通道复制文件的三种方式之三:
public static void test04() throws IOException {
FileChannel inChannel = FileChannel.open(Paths.get("1.jpg"),StandardOpenOption.READ);
FileChannel outChannel = FileChannel.open(Paths.get("2.jpg"),StandardOpenOption.WRITE);
// inChannel.transferTo(0,inChannel.size(),outChannel);//到哪去
outChannel.transferFrom(inChannel,0,inChannel.size());//从哪来
inChannel.close();
outChannel.close();
}
网络IO
-
阻塞式
Socket
ServerSocket
阻塞式的网络IO通过管道和缓冲区,可以让我们省去创建输入流和输出流,即客户端通过获取管道并且向管道里面的缓冲区中写入内容,交给服务端,服务端阻塞接收,然后再通过管道再将反馈的信息写入到缓冲区 发送个客户端,逻辑变得异常清晰。 -
非阻塞式
NIO的非阻塞模式管理客户端和服务端的数据传输主要是通过selector选择器 ,要明白为什么使用选择器首先弄明白非阻塞解决的的问题是神魔。
非阻塞式要解决的问题是针对服务端秩序阻塞接收信息导致cpu利用率低的问题,
客户端发起对服务端的请求,首先客户端的管道会被注册到selector上,服务端会轮询的检测选择器的状态,
如果发现注册的客户端有连接请求就注册到选择器上,
如果注册的客户端有发送数据的请求,就接收数据进行操作,
即选择器会检测客户端channel的状态(OP_ACCEPT,OP_READ,OP_WRITE,OP_CONNECT)
服务端的操作
public static void server() throws IOException {
//获取通道
ServerSocketChannel serverSocketChannel = ServerSocketChannel.open();
serverSocketChannel.configureBlocking(false);//设置为非阻塞式
//绑定接口
serverSocketChannel.bind(new InetSocketAddress(9000));
//获取选择器
Selector selector = Selector.open();
//将通道注册到选择器上 并且指定监听事件
serverSocketChannel.register(selector, SelectionKey.OP_ACCEPT);//监听连接状态
//选择器轮询式获取连接器上准备就绪的事件
while (selector.select()>0){
Iterator<SelectionKey> selectionKeys = selector.selectedKeys().iterator();
while(selectionKeys.hasNext()){
SelectionKey selectionKey = selectionKeys.next();//获取就绪的事件
if(selectionKey.isAcceptable()){//如果接收就绪就获取连接
SocketChannel accept = serverSocketChannel.accept();
accept.configureBlocking(false);
//将accept注册到selector上 并且监听读操作
accept.register(selector,SelectionKey.OP_READ);
}else if(selectionKey.isReadable()){
SocketChannel socketChannel = (SocketChannel) selectionKey.channel();
ByteBuffer byteBuffer = ByteBuffer.allocate(1024);
int len =0;
while((len = socketChannel.read(byteBuffer))>0){
byteBuffer.flip();
System.out.println(new String(byteBuffer.array(),0,len));
byteBuffer.clear();//清除缓存
}
}
selectionKeys.remove();
}
}
}