一、NIO与IO的区别
区别主要如下:
IO | NIO |
面向流 | 面向缓冲区 |
阻塞IO | 非阻塞IO |
无选择器 | 有选择器 |
- 传统的IO流,可以理解为水流,需要在文件系统与程序之间建立水管,然后数据就在这水管中流通,而且这流动是单向的。
- NIO传输数据也需要在文件系统和程序之间建立通道,但是这个通道和水管不同。可以把这个通道理解为铁路,人不会直接在铁路上跑,数据也不会直接在这通道上传输。而是通过缓冲区将数据装起来,然后用缓冲区在这通道中传输数据。这个缓冲区就可以理解为火车,火车装了人,在铁路上跑,缓冲区装了数据,在通道上跑,而且这个过程是双向的。
二、通道和缓冲区
上面说了NIO是利用通道和缓冲区进行数据传输的,那下面就来了解一下。
1、缓冲区(Buffer):
一个用于特定基本数据类型的容器。由 java.nio 包定义的,所有缓冲区都是 Buffer 抽象类的子类。简单的说,缓冲区就是用于存储数据的。八种基本类型中,除了boolean没有对应的缓冲区,其他的都有,分别是:ByteBuffer、ShortBuffer、IntBuffer、LongBuffer、FloatBuffer、DoubleBuffer 、CharBuffer。
(1). 缓冲区的四个核心属性:
- capacity:容量
- limit:界限
- position:当前位置
- mark:标记
(2). 缓冲区的核心方法:
- put():存入数据到缓冲区中
- get():获取缓冲区中的数据
- allocate:分配指定大小的缓冲区
看例子:
@Test public void test1(){ String str = "abcde"; //1.分配一个指定大小的缓冲区 ByteBuffer buffer = ByteBuffer.allocate(1024); System.out.println("==================allocate================"); System.out.println(buffer.position());//0 System.out.println(buffer.limit());//1024 System.out.println(buffer.capacity());//1024 //2.利用put()把数据存入缓冲区 buffer.put(str.getBytes()); System.out.println("===================put===================="); System.out.println(buffer.position());//5 System.out.println(buffer.limit());//1024 System.out.println(buffer.capacity());//1024 //3.切换到读数据模式 buffer.flip(); System.out.println("===================flip===================="); System.out.println(buffer.position());//0 System.out.println(buffer.limit());//5 System.out.println(buffer.capacity());//1024 //4.读取缓冲区的数据 byte[] dst = new byte[buffer.limit()]; buffer.get(dst); System.out.println(new String(dst,0,dst.length));//abcde System.out.println("===================get===================="); System.out.println(buffer.position());//5 System.out.println(buffer.limit());//5 System.out.println(buffer.capacity());//1024 //6.清空缓冲区(但是缓冲区中的数据依然存在,只不过里面的数据处于被遗忘状态) buffer.clear(); System.out.println("===================clear===================="); System.out.println(buffer.position());//0 System.out.println(buffer.limit());//1024 System.out.println(buffer.capacity());//1024 System.out.println((char)buffer.get());//a }
(3). 直接缓冲区和非直接缓冲区:
- 非直接缓冲区:通过allocate()方法分配缓冲区,将缓冲区建立在JVM内存中。
- 直接缓冲区:通过allocateDirect()方法分配直接缓冲区,将缓冲区建立在物理内存中。
@Test public void test2(){ //分配直接缓冲区 ByteBuffer buffer = ByteBuffer.allocateDirect(1024); System.out.println(buffer.isDirect());//判断是否是直接缓冲区 }
2、通道(Channel):
Channel 表示 IO 源与目标打开的连接。 上面说了,通道就是铁路,缓冲区就是火车,所以这两个要搭配使用。
(1). Channel接口主要实现类:
- FileChannel:用于读取、写入、映射和操作文件的通道。
- DatagramChannel:通过 UDP 读写网络中的数据通道。
- SocketChannel:通过 TCP 读写网络中的数据。
- ServerSocketChannel:可以监听新进来的 TCP 连接,对每一个新进来的连接都会创建一个 SocketChannel。
(2). 获取通道:
- 1.java对支持通道的类 (FileInputStream 、FileOutputStream 、RandomAccessFile 、DatagramSocket 、Socket 、ServerSocket) 提供了getChannel()方法获取。
- 2.在jdk1.7中可以使用静态方法open()获取。
- 3.使用Files工具类的newByteChannel()方法获取。
(3). 通道之间的数据传输:
- transferTo()、transferForm():将数据从源通道传输到其他 Channel 中。
(4). 通道与缓冲区之间的数据传输:
- inChannel.write(buffer):将缓冲区数据写入通道。
- inChannel.read(buffer):将通道中的数据读到缓冲区。
看例子:利用通道完成文件的复制。
方式一:非直接缓冲区
@Test public void test() throws IOException { FileInputStream fis = new FileInputStream("1.jpg");//读取项目中的1.jpg图片 FileOutputStream fos = new FileOutputStream("2.jpg");//复制1.jpg,重命名为2.jpg //1.获取通道 FileChannel inChannel = fis.getChannel(); FileChannel outChannel = fos.getChannel(); //2.分配缓冲区 ByteBuffer buffer = ByteBuffer.allocate(1024); //3.将通道中的数据存入缓冲区 while (inChannel.read(buffer) != -1){ buffer.flip();//切换成读数据模式 //4.将缓冲区中的数据写入通道中 outChannel.write(buffer); buffer.clear(); } outChannel.close(); inChannel.close(); fos.close();; fis.close(); }
上面代码使用通道和缓冲区进行了图片的复制,将1.jpg复制并重命名为2.jpg。
方式二:直接缓冲区
@Test public void test3() throws IOException { FileChannel inChannel = FileChannel.open(Paths.get("1.jpg"), StandardOpenOption.READ); FileChannel outChannel = FileChannel.open(Paths.get("3.jpg"), StandardOpenOption.WRITE,StandardOpenOption.READ,StandardOpenOption.CREATE_NEW); //inChannel.transferTo(0,inChannel.size(),outChannel); outChannel.transferFrom(inChannel,0,inChannel.size()); inChannel.close(); outChannel.close(); }
3、分散读取与聚集写入:
- 分散读取(Scattering Reads):将通道中的数据分散到多个缓冲区中去。
- 聚集写入(Gathering Writes):将多个缓冲区中的数据聚集到通道中。
看例子:
//分散读取与聚集写入 @Test public void test4() throws IOException { RandomAccessFile raf = new RandomAccessFile("1.jpg","rw"); //1.获取通道 FileChannel channel = raf.getChannel(); //2.分配缓冲区 ByteBuffer buffer1 = ByteBuffer.allocate(128);//缓冲区1 ByteBuffer buffer2 = ByteBuffer.allocate(1024);//缓冲区2 //3.分散读取 ByteBuffer[] bufs = {buffer1,buffer2};//将两个缓冲区加到数组中 channel.read(bufs);//读取缓冲区数据 for (ByteBuffer byteBuffer : bufs){ byteBuffer.flip();//修改成读模式 } System.out.println(new String(bufs[0].array(),0,bufs[0].limit())); System.out.println("============================"); System.out.println(new String(bufs[1].array(),0,bufs[1].limit())); //4.聚集写入 RandomAccessFile raf1 = new RandomAccessFile("5.jpg","rw"); FileChannel channel1 = raf1.getChannel(); channel1.write(bufs); }
4、编码与解码:
- 编码:将字符串转成字节数组。
- 解码:将字节数组转成字符串。
看例子:
//字符集(GBK编GBK解,不会乱码,GBK编UTF8解就会乱码) @Test public void test5() throws IOException { Charset cs1 = Charset.forName("GBK");//指定编码 CharsetEncoder ce = cs1.newEncoder();//获取编码器 CharsetDecoder cd = cs1.newDecoder();//获取解码器 CharBuffer cBuf = CharBuffer.allocate(1024); cBuf.put("花自飘零水自流"); cBuf.flip(); //编码 ByteBuffer bBuf = ce.encode(cBuf); for (int i=0; i<14;i++){ System.out.println(bBuf.get()); } System.out.println("==========================="); //解码 bBuf.flip(); CharBuffer cBuf2 = cd.decode(bBuf); System.out.println(cBuf2.toString()); }
三、NIO的网络通信
1、阻塞和非阻塞:
- 传统的 IO 流都是阻塞式的。也就是说,当一个线程调用 read() 或 write() 时,该线程被阻塞,直到有一些数据被读取或写入,该线程在此期间不 能执行其他任务。因此,性能很低。
- Java NIO 是非阻塞模式的。当线程从某通道进行读写数据时,若没有数据可用时,该线程可以进行其他任务。因此,性能比传统IO更好。
2、NIO进行网络通信的三个核心:
- 通道(Channel):负责连接。
- 缓冲区(Buffer):负责数据的存取。
- 选择器(Selector):是 SelectableChannel 的多路复用器。用于监控 SelectableChannel 的 IO 状况。
3、使用NIO进行网络通信案例(阻塞式):
//客户端 @Test public void client() throws IOException { //1.获取通道 SocketChannel sChannel = SocketChannel.open(new InetSocketAddress("127.0.0.1", 9898)); FileChannel inChannel = FileChannel.open(Paths.get("1.jpg"), StandardOpenOption.READ); //2.分配缓冲区 ByteBuffer buf = ByteBuffer.allocate(1024); //3.读取本地文件,并发送到服务端 while(inChannel.read(buf) != -1){ buf.flip(); sChannel.write(buf); buf.clear(); } sChannel.shutdownOutput(); //4.接收服务端的反馈 int len = 0; while((len = sChannel.read(buf)) != -1){ buf.flip(); System.out.println(new String(buf.array(), 0, len)); buf.clear(); } //5.关闭通道 inChannel.close(); sChannel.close(); } //服务端 @Test public void server() throws IOException{ //1.获取通道 ServerSocketChannel ssChannel = ServerSocketChannel.open(); FileChannel outChannel = FileChannel.open(Paths.get("6.jpg"), StandardOpenOption.WRITE, StandardOpenOption.CREATE); //2.绑定连接 ssChannel.bind(new InetSocketAddress(9898)); //3.获取客户端连接通道 SocketChannel sChannel = ssChannel.accept(); //4.分配指定大小的缓冲区 ByteBuffer buf = ByteBuffer.allocate(1024); //5.接收客户端数据并保存到本地 while(sChannel.read(buf) != -1){ buf.flip(); outChannel.write(buf); buf.clear(); } //6.发送反馈给客户端 buf.put("服务端接收数据成功".getBytes()); buf.flip(); sChannel.write(buf); //7.关闭通道 sChannel.close(); outChannel.close(); ssChannel.close(); }
4、使用NIO进行网络通信案例(非阻塞式):
//客户端 @Test public void client() throws IOException { //1. 获取通道 SocketChannel sChannel = SocketChannel.open(new InetSocketAddress("127.0.0.1", 9998)); //2. 切换非阻塞模式 sChannel.configureBlocking(false); //3. 分配指定大小的缓冲区 ByteBuffer buf = ByteBuffer.allocate(1024); //4. 发送数据给服务端 Scanner scan = new Scanner(System.in); while (scan.hasNext()) { String str = scan.next(); buf.put((new Date().toString() + "\n" + str).getBytes()); buf.flip(); sChannel.write(buf); buf.clear(); } //5. 关闭通道 sChannel.close(); } //服务端 @Test public void server() throws IOException { //1. 获取通道 ServerSocketChannel ssChannel = ServerSocketChannel.open(); //2. 切换非阻塞模式 ssChannel.configureBlocking(false); //3. 绑定连接 ssChannel.bind(new InetSocketAddress(9998)); //4. 获取选择器 Selector selector = Selector.open(); //5. 将通道注册到选择器上, 并且指定“监听接收事件” ssChannel.register(selector, SelectionKey.OP_ACCEPT); //6. 轮询式的获取选择器上已经“准备就绪”的事件 while (selector.select() > 0) { //7. 获取当前选择器中所有注册的“选择键(已就绪的监听事件)” Iterator<SelectionKey> it = selector.selectedKeys().iterator(); while (it.hasNext()) { //8. 获取准备“就绪”的是事件 SelectionKey sk = it.next(); //9. 判断具体是什么事件准备就绪 if (sk.isAcceptable()) { //10. 若“接收就绪”,获取客户端连接 SocketChannel sChannel = ssChannel.accept(); //11. 切换非阻塞模式 sChannel.configureBlocking(false); //12. 将该通道注册到选择器上 sChannel.register(selector, SelectionKey.OP_READ); } else if (sk.isReadable()) { //13. 获取当前选择器上“读就绪”状态的通道 SocketChannel sChannel = (SocketChannel) sk.channel(); //14. 读取数据 ByteBuffer buf = ByteBuffer.allocate(1024); int len = 0; while ((len = sChannel.read(buf)) > 0) { buf.flip(); System.out.println(new String(buf.array(), 0, len)); buf.clear(); } } //15. 取消选择键 SelectionKey it.remove(); } } }
非阻塞模式的服务端写法更麻烦一点,都有详细注释。
总结:
总的来说,NIO性能比传统IO要好很多,主要就是理解通道和缓冲区这两个概念。而阻塞和非阻塞主要是用于网络编程。