(一)NIO编程之NIO与BIO
BIO编程
IO 有的称之为 basic(基本) IO,有的称之为 block(阻塞) IO,主要应用于文件 IO 和网络 IO,
这里不再说文件 IO, 大家对此都非常熟悉,本次主要讲解网络 IO。
在 JDK1.4 之前,我们建立网络连接的时候只能采用 BIO,需要先在服务端启动一个ServerSocket,然后在客户端启动 Socket 来对服务端进行通信,默认情况下服务端需要对每个请求建立一个线程等待请求,而客户端发送请求后,先咨询服务端是否有线程响应,如果没有则会一直等待或者遭到拒绝,如果有的话,客户端线程会等待请求结束后才继续执行,这就是阻塞式 IO。
接下来通过一个例子复习回顾一下 BIO 的基本用法(基于 TCP)。
package com.bestqiang.bio;
import java.io.InputStream;
import java.io.OutputStream;
import java.net.ServerSocket;
import java.net.Socket;
//BIO 服务器端程序
public class TCPServer {
public static void main(String[] args) throws Exception {
//1.创建 ServerSocket 对象
ServerSocket ss = new ServerSocket(9999);
while (true) {
//2.监听客户端
Socket s = ss.accept(); //阻塞
//3.从连接中取出输入流来接收消息
InputStream is = s.getInputStream(); //阻塞
byte[] b = new byte[10];
is.read(b);
String clientIP = s.getInetAddress().getHostAddress();
System.out.println(clientIP + "说:" + new String(b).trim());
//4.从连接中取出输出流并回话
OutputStream os = s.getOutputStream();
os.write("没钱".getBytes());
//5.关闭
s.close();
}
}
}
上述代码编写了一个服务器端程序,绑定端口号 9999,accept 方法用来监听客户端连接,
如果没有客户端连接,就一直等待,程序会阻塞到这里。
package com.bestqiang.bio;
import java.io.InputStream;
import java.io.OutputStream;
import java.net.Socket;
import java.util.Scanner;
//BIO 客户端程序
public class TCPClient {
public static void main(String[] args) throws Exception {
while (true) {
//1.创建 Socket 对象
Socket s = new Socket("127.0.0.1", 9999);
//2.从连接中取出输出流并发消息
OutputStream os = s.getOutputStream();
System.out.println("请输入:");
Scanner sc = new Scanner(System.in);
String msg = sc.nextLine();
os.write(msg.getBytes());
//3.从连接中取出输入流并接收回话
InputStream is = s.getInputStream(); //阻塞
byte[] b = new byte[20];
is.read(b);
System.out.println("老板说:" + new String(b).trim());
//4.关闭
s.close();
}
}
}
上述代码编写了一个客户端程序,通过 9999 端口连接服务器端,getInputStream 方法用来
等待服务器端返回数据,如果没有返回,就一直等待,程序会阻塞到这里。运行效果如下图
所示:
NIO编程
什么是NIO?
java.nio 全称 java non-blocking IO,是指 JDK 提供的新 API。从 JDK1.4 开始,Java 提供了一系列改进的输入/输出的新特性,被统称为 NIO(即 New IO)。新增了许多用于处理输入输出的类,这些类都被放在 java.nio 包及子包下,并且对原 java.io 包中的很多类进行改写,新增了满足 NIO 的功能。
NIO 和 BIO 有着相同的目的和作用,但是它们的实现方式完全不同,BIO 以流的方式处理数据,而 NIO 以块的方式处理数据,块 I/O 的效率比流 I/O 高很多。另外,NIO 是非阻塞式的,这一点跟 BIO 也很不相同,使用它可以提供非阻塞式的高伸缩性网络。NIO 主要有三大核心部分:Channel(通道),Buffer(缓冲区), Selector(选择器)。传统的 BIO基于字节流和字符流进行操作,而 NIO 基于 Channel(通道)和 Buffer(缓冲区)进行操作,数据总是从通道读取到缓冲区中,或者从缓冲区写入到通道中。Selector(选择区)用于监听多个通道的事件(比如:连接请求,数据到达等),因此使用单个线程就可以监听多个客户端通道。
文件IO
概述和核心 API
缓冲区(Buffer):实际上是一个容器,是一个特殊的数组,缓冲区对象内置了一些机
制,能够跟踪和记录缓冲区的状态变化情况。Channel 提供从文件、网络读取数据的渠道,
但是读取或写入的数据都必须经由 Buffer,如下图所示:
在 NIO 中,Buffer 是一个顶层父类,它是一个抽象类
常用的 Buffer 子类有:
- ByteBuffer,存储字节数据到缓冲区
- ShortBuffer,存储字符串数据到缓冲区
- CharBuffer,存储字符数据到缓冲区
- IntBuffer,存储整数数据到缓冲区
- LongBuffer,存储长整型数据到缓冲区
- DoubleBuffer,存储小数到缓冲区
- FloatBuffer,存储小数到缓冲区
对于 Java 中的基本数据类型,都有一个 Buffer 类型与之相对应,最常用的自然是ByteBuffer 类(二进制数据),该类的主要方法如下所示:
- public abstract ByteBuffer put(byte[] b); 存储字节数据到缓冲区
- public abstract byte[] get(); 从缓冲区获得字节数据
- public final byte[] array(); 把缓冲区数据转换成字节数组
- public static ByteBuffer allocate(int capacity); 设置缓冲区的初始容量
- public static ByteBuffer wrap(byte[] array); 把一个现成的数组放到缓冲区中使用
- public final Buffer flip(); 翻转缓冲区,重置位置到初始位置
通道(Channel):类似于 BIO 中的 stream,例如 FileInputStream 对象,用来建立到目标(文件,网络套接字,硬件设备等)的一个连接,但是需要注意:BIO 中的 stream 是单向的,例如 FileInputStream 对象只能进行读取数据的操作,而 NIO 中的通道(Channel)是双向的,既可以用来进行读操作,也可以用来进行写操作。常用的 Channel 类有:
FileChannel、DatagramChannel、ServerSocketChannel 和 SocketChannel。FileChannel 用于文件的数据读写,DatagramChannel 用于 UDP 的数据读写,ServerSocketChannel 和 SocketChannel 用于 TCP 的数据读写。
这里我们先讲解 FileChannel 类,该类主要用来对本地文件进行 IO 操作,主要方法如下所示:
- public int read(ByteBuffer dst) ,从通道读取数据并放到缓冲区中
- public int write(ByteBuffer src) ,把缓冲区的数据写到通道中
- public long transferFrom(ReadableByteChannel src, long position, long count),从目标通道中复制数据到当前通道
- public long transferTo(long position, long count, WritableByteChannel target),把数据从当前通道复制给目标通道
下面来使用NIO进行简单编程,熟悉API基本用法
package com.bestqiang.nio.file;
import org.junit.Test;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.nio.ByteBuffer;
import java.nio.channels.FileChannel;
/** * @author BestQiang */
// 通过NIO实现文件IO
public class TestNIO {
@Test //向本地文件中写数据
public void test1() throws Exception {
//1. 创建输出流
FileOutputStream fileOutputStream = new FileOutputStream("basic.txt");
//2. 从流中得到一个通道
FileChannel fileChannel = fileOutputStream.getChannel();
//3. 提供一个缓冲区
ByteBuffer buffer = ByteBuffer.allocate(1024);
//4. 向缓冲区中存入数据
String str = "Hello,NIO";
buffer.put(str.getBytes());
//** 反转缓冲区
buffer.flip();
//5. 把缓冲区写到通道中
fileChannel.write(buffer);
//6. 关闭
fileOutputStream.close();
}
@Test // 从本地文件中读取数据
public void test2() throws Exception{
File file = new File("basic.txt");
// 1. 创建输入流
FileInputStream inputStream = new FileInputStream(file);
// 2.从流中得到一个通道
FileChannel channel = inputStream.getChannel();
// 3.获取缓冲区
ByteBuffer buffer = ByteBuffer.allocate((int) file.length());
// 4.从通道取得数据到缓冲区
int read = channel.read(buffer);
System.out.println(new String(buffer.array()));
inputStream.close();
}
@Test // 使用NIO实现文件复制
public void test3() throws Exception{
// 1.创建两个流
FileInputStream inputStream = new FileInputStream("basic.txt");
FileOutputStream outputStream = new FileOutputStream("basic2.txt");
// 2.得到两个通道
FileChannel inputStreamChannelchannel = inputStream.getChannel();
FileChannel outputStreamChannelchannel = outputStream.getChannel();
// 3.复制
inputStreamChannelchannel.transferTo(0, inputStreamChannelchannel.size(),outputStreamChannelchannel);
inputStream.close();
outputStream.close();
}
}
NIO 中的通道是从输出流对象里通过 getChannel 方法获取到的,该通道是双向的,既可以读,又可以写。在往通道里写数据之前,必须通过 put 方法把数据存到 ByteBuffer 中,然后通过通道的 write 方法写数据。在 write 之前,需要调用 flip 方法翻转缓冲区,把内部重置到初始位置,这样在接下来写数据时才能把所有数据写到通道里。
关于buffer.flip()方法,jdk的源码已经说得很清楚
上面说翻转这个缓冲区,将极限位置设置为当前位置,将当前位置设置为0,如果mark被定义,就终止。
在经过put操作后,应该翻转缓冲区,然后再进行使用。