PS: 之前一直想了解这个NIO到底是什么东西,奈何目前用不到,听说现在许多框架都在用,而且面试的时候也有被问道,感觉还是去多了解了解底层怎么实现的~~这是我的第100篇博客!!!
一. NIO与IO区别
NIO主要有三大核心部分:Channel(通道),Buffer(缓冲区), Selector
二. 缓冲区
缓冲区(Buffer) :一个用于特定基本数据类型的容器。由 java.nio 包定义的,所有缓冲区都是 Buffer 抽象类的子类。
Java NIO 中的 Buffer 主要用于与 NIO 通道进行交互,数据是从通道读入缓冲区,从缓冲区写入通道中的。
2.1 直接缓冲区与非直接缓冲区
- 非直接缓冲区:通过allocate()方法分配缓冲区,将缓冲区建立在JVM的内存中
- 直接缓冲区:通过allocateDirect()方法分配直接缓冲区,将缓冲区建立在物理内存中。可以提高效率
这里探究验证了一下底层Buffer的原理
//分配指定大小的缓冲区
ByteBuffer byteBuffer = ByteBuffer.allocate(1024);
String s=new String("abcde");
//缓冲区存数据
System.out.println("PUT操作");
byteBuffer.put(s.getBytes());
System.out.println(byteBuffer.position()); //5
System.out.println(byteBuffer.limit()); //1024
System.out.println(byteBuffer.capacity()); //1024
System.out.println("***************************");
System.out.println("filp操作");
byteBuffer.flip();
System.out.println(byteBuffer.position()); //0
System.out.println(byteBuffer.limit()); //5
System.out.println(byteBuffer.capacity()); //1024
System.out.println("***************************");
System.out.println("get操作");
byte[] b=new byte[byteBuffer.limit()];
byteBuffer.get(b,0,4);
if(byteBuffer.hasRemaining()){
//判断缓冲区是否有剩余
System.out.println("缓冲区还有几个剩余的: "+byteBuffer.remaining());
}
System.out.println(byteBuffer.position()); //5
System.out.println(byteBuffer.limit()); //5
System.out.println(byteBuffer.capacity()); //1024
System.out.println("***************************");
System.out.println("rwind操作可重复读"); //又把position置于开头
byteBuffer.rewind();
System.out.println(byteBuffer.position()); //0
System.out.println(byteBuffer.limit()); //5
System.out.println(byteBuffer.capacity()); //1024
System.out.println("***************************");
System.out.println("mark操作记录当前的position位置"); //又把position置于开头
byteBuffer.mark();
byte[] bytes=new byte[1024];
byteBuffer.get(bytes,0,2); //取走前两个,position为2
System.out.println("取走前两个,position为"+byteBuffer.position()); //2
byteBuffer.reset(); //恢复到mark位置的position 为0
System.out.println("恢复到mark位置的position 为"+byteBuffer.position()); //0
System.out.println("***************************");
System.out.println("clear操作清空(非真清空,相当于把position和limit恢复开始状态)数组中的数据依然存在"); //又把position置于开头
byteBuffer.clear();
System.out.println(byteBuffer.position()); //0
System.out.println(byteBuffer.limit()); //1024
System.out.println(byteBuffer.capacity()); //1024
System.out.println("***************************");
//获取第一个bytez转化为char还是能输出
System.out.println("获取第一个bytez转化为char还是能输出"+(char)byteBuffer.get());
if(byteBuffer.hasRemaining()){
//判断缓冲区是否有剩余
System.out.println("缓冲区还有几个剩余的: "+byteBuffer.remaining());
}
ByteBuffer byteBuffer=ByteBuffer.allocateDirect(1024);//创建一个直接缓冲区
System.out.println(byteBuffer.isDirect());//判断是否为直接缓冲区
三. 通道
通道:由java.nio.channels包定义。
Channel表示IO源与目标打开的连接。
Channel类似于传统的“流”。但其自身不能直接访问数据,Channel只能与Buffer进行交互。
java.nio.channels.Channel 接口
-
FileChannel:用于读取、写入、映射和操作文件的通道。
-
SocketChannel:通过 TCP 读写网络中的数据。
-
ServerSocketChannel:可以监听新进来的 TCP 连接,对每一个新进来的连接都会创建一个 SocketChannel。
-
DatagramChannel:通过 UDP 读写网络中的数据通道。
3.1 java针对支持通道的类提供了getChannel()方法
- 本地IO:
FileInputStream/FileOutputStream
long start=System.currentTimeMillis();
FileInputStream fis=null;
FileOutputStream fos=null;
//获取通道
FileChannel inchannel = null;
FileChannel outchannel = null;
try {
fis = new FileInputStream("E:\\image/1.jpg");
fos = new FileOutputStream("E:\\image/3.jpg");
inchannel = fis.getChannel();
outchannel = fos.getChannel();
//设置非直接缓冲区大小
ByteBuffer byteBuffer=ByteBuffer.allocate(1024);
while(inchannel.read(byteBuffer)!=-1){
//将通道里的数据存入缓冲区
byteBuffer.flip(); //切换读取模式;
outchannel.write(byteBuffer); // //将缓冲区中的数据写入通道中
byteBuffer.clear(); 清空缓冲区
}
}catch (Exception e){
e.printStackTrace();
}finally{
if(outchannel!=null){
try {
outchannel.close();
} catch (IOException e) {
e.printStackTrace();
}
}
if(inchannel!=null){
try {
inchannel.close();
} catch (IOException e) {
e.printStackTrace();
}
}
if(fos!=null){
try {
fos.close();
} catch (IOException e) {
e.printStackTrace();
}
}
if(fis!=null){
try {
fis.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
long end=System.currentTimeMillis();
System.out.println("耗费时间:"+(end-start));//耗费时间:1094
RandomAccessFile
RandomAccessFile raf=new RandomAccessFile("E:\\image/1234.txt","rw");
//建立通道
FileChannel inChannel = raf.getChannel();
//创建缓存区数组 ->分散读取
ByteBuffer byteBuffer=ByteBuffer.allocate(1024);
ByteBuffer byteBuffer1=ByteBuffer.allocate(100);
ByteBuffer[] buffers={
byteBuffer,byteBuffer1};
//读取到缓存区
inChannel.read(buffers);
for (ByteBuffer buffer : buffers) {
buffer.flip(); //切换为读模式
}
//查看各自通道有多少
System.out.println(new String(buffers[0].array(),0,buffers[0].limit()));
System.out.println("****************************************************");
System.out.println(new String(buffers[1].array(),0,buffers[1].limit()));
//聚集写入
RandomAccessFile raf2=new RandomAccessFile("E:\\image/12345.txt","rw");
FileChannel outChannel = raf2.getChannel();
outChannel.write(buffers);
inChannel.close();
outChannel.close();
- 网络IO:
Socket
ServerSocket
DatagramSocket
3.2 在JDK 1.7 中的NIO.2 针对各个通道提供了静态方法 open()
long start=System.currentTimeMillis();
//通过静态方法 open()创建通道
FileChannel inChannel=FileChannel.open(Paths.get("E:\\image/1.jpg"), StandardOpenOption.READ); //读模式
FileChannel outChannel=FileChannel.open(Paths.get("E:\\image/4.jpg"),StandardOpenOption.WRITE,StandardOpenOption.READ,StandardOpenOption.CREATE); //读写模式;存在就报错,不存在就创建
//内存映射文件
MappedByteBuffer inMappedBuf=inChannel.map(FileChannel.MapMode.READ_ONLY,0,inChannel.size());
MappedByteBuffer outMappedBuf=outChannel.map(FileChannel.MapMode.READ_WRITE,0,inChannel.size());
//进行写操作
byte[] bytes=new byte[inMappedBuf.limit()];
inMappedBuf.get(bytes);
outMappedBuf.put(bytes);
//关闭通道
inChannel.close();
outChannel.close();
long end=System.currentTimeMillis();
System.out.println("耗时: "+(end-start));
直接通过通道复制
long start=System.currentTimeMillis();
FileChannel inChannel=FileChannel.open(Paths.get("E:\\image/1.jpg"), StandardOpenOption.READ); //读模式
FileChannel outChannel=FileChannel.open(Paths.get("E:\\image/4.jpg"),StandardOpenOption.WRITE,StandardOpenOption.READ,StandardOpenOption.CREATE); //读写模式;存在就覆盖,不存在就创建
inChannel.transferTo(0,inChannel.size(),outChannel);
inChannel.close();
outChannel.close();
long end=System.currentTimeMillis();
System.out.println("耗时: "+(end-start));
3.3 在JDK 1.7 中的NIO.2 的Files工具类的newByteChannel()
3.4 测试结果
四. NIO 的阻塞式与非阻塞式网络通信
传统的 IO 流都是阻塞式的。也就是说,当一个线程调用 read() 或 write()时,该线程被阻塞,直到有一些数据被读取或写入,该线程在此期间不能执行其他任务。因此,在完成网络通信进行 IO 操作时,由于线程会阻塞,所以服务器端必须为每个客户端都提供一个独立的线程进行处理,当服务器端需要处理大量客户端时,性能急剧下降。
Java NIO 是非阻塞模式的。当线程从某通道进行读写数据时,若没有数据可用时,该线程可以进行其他任务。线程通常将非阻塞 IO 的空闲时间用于在其他通道上执行 IO 操作,所以单独的线程可以管理多个输入和输出通道。因此,NIO 可以让服务器端使用一个或有限几个线程来同时处理连接到服务器端的所有客户端。
选择器(Selector)
选择器(Selector) 是 SelectableChannle 对象的多路复用器,Selector 可以同时监控多个 SelectableChannel 的 IO 状况,也就是说,利用 Selector可使一个单独的线程管理多个 Channel。Selector 是非阻塞 IO 的核心。
使用NIO 完成网络通信的三个核心:
1. 通道(Channel):负责连接
* java.nio.channels.Channel 接口:
* |--SelectableChannel
* |--SocketChannel
* |--ServerSocketChannel
* |--DatagramChannel
*
* |--Pipe.SinkChannel
* |--Pipe.SourceChannel
*
2. 缓冲区(Buffer):负责数据的存取
3. 选择器(Selector):是 SelectableChannel 的多路复用器。用于监控SelectableChannel的IO状况
4.1 阻塞式IO
//阻塞式通信客户端
@Test
public void client() throws IOException {
//获取通道
SocketChannel socketChannel=SocketChannel.open(new InetSocketAddress("127.0.0.1",9777));
//文件通道
FileChannel fileChannel=FileChannel.open(Paths.get("E:\\image/1.jpg"), StandardOpenOption.READ);
//缓冲区
ByteBuffer byteBuffer=ByteBuffer.allocate(1024);
//读取缓冲区
while (fileChannel.read(byteBuffer)!=-1){
byteBuffer.flip(); //切换读模式
socketChannel.write(byteBuffer);
byteBuffer.clear();
}
//关闭写模式
socketChannel.shutdownOutput();
//获取服务端信息
int len=0;//记录长度
while((len=socketChannel.read(byteBuffer))!=-1){
byteBuffer.flip(); //切换读模式
System.out.println(new String(byteBuffer.array(),0,len));// 输出
byteBuffer.clear(); //清空缓冲区(复位)
}
socketChannel.close();
fileChannel.close();
}
//阻塞式通信服务端
@Test
public void server() throws IOException {
ServerSocketChannel serverSocketChannel=ServerSocketChannel.open(); //打开服务端通道
serverSocketChannel.bind( new InetSocketAddress(9777)); //绑定端口
FileChannel outChannel=FileChannel.open(Paths.get("E:\\image/19.jpg"),StandardOpenOption.WRITE,StandardOpenOption.CREATE); //文件写入
//服务端处于阻塞状态
SocketChannel socketChannel=serverSocketChannel.accept();
//定义缓冲区
ByteBuffer byteBuffer=ByteBuffer.allocate(1024);
while (socketChannel.read(byteBuffer)!=-1){
byteBuffer.flip();
outChannel.write(byteBuffer);
byteBuffer.clear();
}
//发送反馈给客户端
byteBuffer.put("服务端接收完毕".getBytes());
byteBuffer.flip();
socketChannel.write(byteBuffer);//写回去给客户端
socketChannel.close();
outChannel.close();
serverSocketChannel.close();
}
4.2 非阻塞式IO(ServerSocketChannel/SocketChannel)
//非阻塞式通信客户端
@Test
public void Nonclient() throws IOException{
SocketChannel socketChannel=SocketChannel.open(new InetSocketAddress("127.0.0.1",9777));
//设置非阻塞式模式
socketChannel.configureBlocking(false);
//设置缓冲区
ByteBuffer byteBuffer=ByteBuffer.allocate(1024);
//缓冲区设置当前时间
byteBuffer.put(LocalDateTime.now().toString().getBytes());
byteBuffer.flip();//转换读模式
socketChannel.write(byteBuffer); //将缓冲区数据发送给客户端
byteBuffer.clear();
//关闭通道
socketChannel.close();
}
//非阻塞式通信服务端
@Test
public void Nonserver() throws IOException{
//获取通道
ServerSocketChannel serverSocketChannel=ServerSocketChannel.open();
//切换非阻塞模式
serverSocketChannel.configureBlocking(false);
//绑定链接
serverSocketChannel.bind(new InetSocketAddress(9777));
//获取选择器
Selector selector=Selector.open();
//将通道注册到选择器上,并监听接受事件
serverSocketChannel.register(selector, SelectionKey.OP_ACCEPT);
//轮询式的选择 选择器中的事件
while (selector.select()>0){
//用迭代器迭代
Iterator<SelectionKey> iterator=selector.selectedKeys().iterator();
while(iterator.hasNext()){
//获取准备就绪的事件
SelectionKey sk=iterator.next();
//判断具体的是什么事件
if(sk.isAcceptable()){
//若接受就绪,获取客户端连接
SocketChannel channel = serverSocketChannel.accept();
//切换非阻塞模式
channel.configureBlocking(false);
//注册到选择器上
channel.register(selector,SelectionKey.OP_READ);
}else if(sk.isReadable()){
SocketChannel socketChannel= (SocketChannel) sk.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();//复位
}
}
iterator.remove();//取消选择selectionKey
}
}
}
4.3 非阻塞式IO(DatagramChannel)
//DatagramChannel 数据报通道客户端
@Test
public void DatagramChannelClient() throws IOException{
DatagramChannel datagramChannel=DatagramChannel.open();
//设置非阻塞通道
datagramChannel.configureBlocking(false);
//开辟缓冲区
ByteBuffer byteBuffer=ByteBuffer.allocate(1024);
//输入信息(带时间格式)
Scanner scanner=new Scanner(System.in);
while (scanner.hasNext()){
String msg=scanner.next();
byteBuffer.put((LocalDateTime.now().toString()+"\n"+msg).getBytes());
byteBuffer.flip();
datagramChannel.send(byteBuffer,new InetSocketAddress("127.0.0.1",9777));//向目的地发送数据
byteBuffer.clear(); //清空复位
}
datagramChannel.close();//关闭
}
//DatagramChannel 数据报通道服务端
@Test
public void DatagramChannelServer() throws IOException{
DatagramChannel datagramChannel=DatagramChannel.open();
datagramChannel.configureBlocking(false);
datagramChannel.bind(new InetSocketAddress(9777));
Selector selector=Selector.open();
datagramChannel.register(selector,SelectionKey.OP_READ);
while (selector.select()>0){
Iterator<SelectionKey> iterator = selector.selectedKeys().iterator();
while (iterator.hasNext()){
SelectionKey selectionKey = iterator.next();
if(selectionKey.isReadable()){
ByteBuffer byteBuffer=ByteBuffer.allocate(1024);
datagramChannel.receive(byteBuffer);
byteBuffer.flip();
System.out.println(new String(byteBuffer.array(),0,byteBuffer.limit()));
byteBuffer.clear();
}
iterator.remove();
}
}
}
4.4 测试
如果 IDEA 使用@Test单元测试控制台输入不了
首先找到你IDEA 安装目录
最底下增加一句,然后重启IDEA 即可
-Deditable.java.test.console=true
测试结果
客户端1号
客户端2号
服务端接受结果
五. 管道
public class TestPipe {
@Test
public void test1()throws IOException{
//1.获取管道
Pipe pipe=Pipe.open();
//2.将缓冲区中的数据写入管道
ByteBuffer buf=ByteBuffer.allocate(1024);
Pipe.SinkChannel sinkChannel=pipe.sink();
buf.put("通过单向管道发送数据".getBytes());
buf.flip();
sinkChannel.write(buf);
//3.读取缓冲区中的数据
Pipe.SourceChannel sourceChannel=pipe.source();
buf.flip();
int len=sourceChannel.read(buf);
System.out.println(new String(buf.array(),0,len));
sourceChannel.close();
sinkChannel.close();
}
}