1、IO操作流程:以read磁盘文件操作为例
①、将数据从磁盘读取到磁盘驱动
②、从磁盘驱动读取到操作系统内核buffer
③、从操作系统内核buffer读取到用户线程
2、IO与NIO区别
2.1、IO是面向流的单向读/写操作
2.2、NIO是面向缓冲区buffer的,读写操作发生在缓冲区内,是缓冲区内的读写模式进行操作,读模式用于读取缓冲区中的数据,写模式用于向缓冲区写入数据。
3、阻塞式IO与非阻塞式IO
描述的是用户线程调用操作系统内核IO操作的方式
3.1、阻塞:是指调用IO操作需要等待结果的完成才能继续执行,会影响后续的动作执行,对CPU资源利用率不足
案例:小明去火车站排队买票,排队两天才买到票
3.2、非阻塞:是指IO操作被调用后,立即返回一个状态给用户线程,无需等待IO操作彻底完成,但是为了得到结果,仍需要定时重复请求结果数据,造成CPU资源大量消耗
案例:小明到火车站买票,告知没票,每隔3小时去火车站询问一次,两天后买到票
3.3、IO多路复用:异步阻塞 IO 模型,为改善同步非阻塞中线程轮询等待的问题,基于操作系统内核提供的多路分离函数select(),可以实现IO多路复用
使用select以后最大的优势是用户可以在一个线程内同时处理多个socket 的 IO 请求。用户可以注册多个 socket,然后不断地调用 select 读取被激活的 socket,即可达到在同一个线程内同时处理多个 IO 请求的目的。
而在同步阻塞 模型中,必须通过多线程的方式才能达到这个目的。
这里的 select 函数是阻塞的,因此多路 IO 复用模型也被称为异步阻塞 IO 模型。
注意,这里的所说的阻塞是指 select 函数执行时线程被阻塞,而不是指 socket。
4、同步与异步:
描述的是用户线程与操作系统内核的交互方式
4.1、同步:是指用户线程调用内核IO操作需要等待或轮询等待结果,才能继续进行。其中轮询等待的问题可以使用IO多路复用模型实现
4.2、异步:用户调用内核IO操作后,继续执行后续操作,当内核IO操作完成后会通知用户线程或这调用用户线程注册的回调函数
5、NIO缺点:
①、在请求量比较大的情况下,会出现部分请求响应时间比较长的现象
②、不适用于长任务场景,不然会导致其他任务无法执行
6、BIO与NIO的区别:
BIO | NIO |
同步阻塞 | 同步非阻塞 |
单向传输数据 | 可以双向传输数据 |
一对一的连接方式 | 一对多的连接方式 |
面向流操作 | 面向缓冲区操作 |
适合于请求少、长连接场景 | 适合于大量请求、短连接的场景 |
7、buffer 缓冲区
缓冲区本质上是一块可以读写操作的内存区域,这块内存被包装成NIO Buffer对象,并提供了一组方法用于便捷的操作这块内存。
7.1、Java NIO 关键Buffer的实现:
ByteBuffer CharBuffer ShortBuffer IntBuffer LongBuffer DoubleBuffer FloatBuffer
7.2 多级缓存:
物理磁盘IO操作要比内存IO操作慢很多倍,所以一般为了提高性能,通常会对数据进行缓存,JAVA应用程序与物理磁盘之间通常会有多级缓存:
1) Disk Drive Buffer(磁盘缓存):位于磁盘驱动器中的 RAM,将磁盘数据移 动到磁盘缓冲区是一件相当耗时的操作。
2) OS Buffer(系统缓存):操作系统自己缓存,可以在应用程序间共享数据
3) Application Buffer(应用缓存):应用程序的私有缓存。
7.3、Buffer的基本应用:
使用Buffer读写数据一般遵循四个步骤:
①、写入数据到Buffer
②、调用flip()方法,切换读写模式
③、从Buffer中读取数据
④、调用clear()方法清除所有缓存的数据,或者compact()方法清除已经读过的数据
案例:
public class NIODemo {
static void doPrint(Integer positon, Integer limit, Integer capacity){
System.out.println("positon=" + positon);//指针位
System.out.println("limit=" + limit);//限制位
System.out.println("capacity=" + capacity);//容量
}
public static void main(String[] args) {
//创建ByteBuffer
//参数:用于存储数据的数组的长度,字节为单位
ByteBuffer buffer = ByteBuffer.allocate(10);//从JVM中分配一块内存区域
ByteBuffer buffer2 = ByteBuffer.allocateDirect(10);//从OS中分配一块内存区域
System.out.println("写入数据前");
doPrint(buffer.position(), buffer.limit(), buffer.capacity());
//向缓冲区写入数据
buffer.put("hello".getBytes());
System.out.println("写入数据后");
doPrint(buffer.position(), buffer.limit(), buffer.capacity());
// //获取第一个字符数据,需要把position位挪到下标为0的位置
// buffer.position(0);
// byte b = buffer.get();
// //遍历存入的数据,需要先把limit位移动到position位,再把position位归零
// buffer.limit(buffer.position());
// buffer.position(0);
//上面挪动limit和position位称为反转缓冲区,可用flip()代替
buffer.flip();//切换为读模式
System.out.println("读数据前");
doPrint(buffer.position(), buffer.limit(), buffer.capacity());
byte c1=buffer.get();
System.out.println((char)c1);
System.out.println("===读数据之后===");
doPrint(buffer.position(),buffer.limit(),buffer.capacity());
// // while(buffer.position() < buffer.limit()){
// // buffer.hasRemaining() 就是把buffer.position() < buffer.limit()进行了封装而已
// while(buffer.hasRemaining()){
// byte a = buffer.get();
// System.out.println(a);
// }
}
}
7.4、相关API
方法 | 作用 |
allocate(int capacity) | 从JVM中分配内存,创建缓冲区的时候指定缓冲区容量的大小,实际上是指定缓冲区底层的字节数组的大小 |
allocateDirect(int capacity) | 从操作系统中分配内存 |
wrap(byte[] array) | 利用传入的字节数组来构建缓冲区 |
array() | 获取缓冲区底层的字节数组 |
get() | 获取缓冲区中position位置上的字节 |
get(byte[] dst) | 将缓冲区中的数据写到传入的字节数组中 |
get(int index) | 获取指定下标上的字节 |
put(byte b) | 向position位置上放入指定的字节 |
put(byte[] src) | 向position位置上放入指定的字节数组 |
put(byte[] src, int offset, int length) | 向position位置上放入指定的字节数组的部分元素 |
put(ByteBuffer src) | 将字节缓冲区放入 |
put(int index, byte b) | 向指定位置插入指定的字节 |
capacity() | 获取容量位 |
clear() | 清空缓冲区:position = 0; limit = capacity; mark = -1; |
compact() | q清除已经读取过的缓冲区 |
flip() | 反转缓冲区:limit = position; position = 0; mark = -1; 切换读写模式 |
hasRemaing() | 判断position和limit之间是否还有空余 |
limit() | 获取限制位 |
limit(int newLimit) | 设置限制位 |
mark() | 设置标记位 |
position() | 获取操作位 |
position(int newPosition) | 设置操作位 |
remaining() | 获取position和limit之间剩余的元素个数 |
reset() | 重置缓冲区:position = mark |
rewind() | 重绕缓冲区:position = 0; mark = -1 |
8、Channel 通道
NIO是基于通道Channel和缓冲区Buffer进行操作,数据从Channel读取到Buffer中,从Buffer写入到Channel中
NIO中Channel的具体实现类:
①、FileChannel:从文件中读写数据
②、Data'gramChannel:通过UDP协议读写网络中的数据
③、SocketChannel:通过TCP协议读写网络中的数据
④、ServerSocketChannel:可以监听新进来的TCP连接,向Web服务器一样
8.1、FileChannel基本使用
/**
* 一次性全部读取文件内容
* @author zxj
* @date 2020/5/15 13:56
*/
public class FileChannelDemo01 {
public static void main(String[] args) throws IOException {
//创建buffer,在jvm中分配1024b的内存
ByteBuffer buffer = ByteBuffer.allocate(1024);
//创建FileChannel,指定位READ读模式,把数据读取到channel
FileChannel fc = FileChannel.open(Paths.get("data.txt"), StandardOpenOption.READ);
//将通道中的数据读取到buffer
fc.read(buffer);
//转换位读模式
buffer.flip();
//一次全部读取,当buffer的内存足够大时
byte[] arr = buffer.array();
System.out.println(buffer.position());
System.out.println(new String(arr));
//此处不是清除数据,而是标记为无用数据(脏数据)
buffer.clear();
//关闭通道
fc.close();
}
}
/**
* 多次顺序读取文件内容
* @author zxj
* @date 2020/5/15 13:56
*/
public class FileChannelDemo02 {
public static void main(String[] args) throws IOException {
//创建buffer,在jvm中分配2b的内存
ByteBuffer buffer = ByteBuffer.allocate(2);
//创建FileChannel,指定位READ读模式,把数据读取到channel
FileChannel fc = FileChannel.open(Paths.get("data.txt"), StandardOpenOption.READ);
int len = -1;
do {
//将通道中的数据读取到buffer
len = fc.read(buffer);
//转换为读模式
buffer.flip();
while (buffer.hasRemaining()){
//单个字符读取
System.out.println((char) buffer.get());
}
//转换为写模式
buffer.flip();
//每次读数据应将原数据设置为无效
buffer.clear();
}while (len != -1);
//关闭通道
fc.close();
}
}
- 如果是通过FileInputStream获取FileChannel,那么只能进行读取操作
- 如果是通过FileOutputStream获取FileChannel,那么只能进行写入操作
- 如果是通过RandomAccessFile获取FileChannel,那么可以进行读写操作
@Test
public void readFile() throws Exception {
// 创建RandomAccessFile对象。指定模式为读写模式
RandomAccessFile raf = new RandomAccessFile("F:\\a.txt", "rw");
// 获取FileChannel对象
FileChannel fc = raf.getChannel();
// 创建缓冲区用于存储数据
ByteBuffer buffer = ByteBuffer.allocate(10);
// 记录读取的字节个数
int len;
// 读取数据
while ((len = fc.read(buffer)) != -1) {
System.out.println(new String(buffer.array(), 0, len));
buffer.flip();
}
// 关流
raf.close();
}
写入过程
@Test
public void writeFile() throws Exception {
// 创建RandomAccessFile对象。指定模式为读写模式
RandomAccessFile raf = new RandomAccessFile("F:\\test.txt", "rw");
// 获取FileChannel对象
FileChannel fc = raf.getChannel();
// 创建缓冲区,并且将数据放入缓冲区
ByteBuffer src = ByteBuffer.wrap("hello".getBytes());
// 利用通道写出数据
fc.write(src);
// 关流
raf.close();
}
复制文件
@Test
public void copyFile() throws Exception {
// 创建流对象指向对应的文件
FileInputStream in = new FileInputStream("F:\\a.txt");
FileOutputStream out = new FileOutputStream("E:\\a.txt");
// 获取FileChannel对象
FileChannel src = in.getChannel();
FileChannel dest = out.getChannel();
// 准备缓冲区
ByteBuffer buffer = ByteBuffer.allocate(10);
// 读取数据,将读取到的数据写出
while (src.read(buffer) != -1) {
buffer.flip();
dest.write(buffer);
buffer.clear();
}
// 关流
in.close();
out.close();
}
8.2、SocketChannel 基本使用,客户端与服务端通信
/**
* 服务端
* @throws IOException
*/
@Test
public void server() throws IOException {
//创建ServerSocketChannel
ServerSocketChannel ssc = ServerSocketChannel.open();
//指定监听端口,ip为本机
ssc.socket().bind(new InetSocketAddress(9999));
ByteBuffer buffer=ByteBuffer.allocate(1024);
//等待客户端的连接
while(true){
SocketChannel sc =ssc.accept();
int byteReader = sc.read(buffer);
buffer.flip();
System.out.println("server收到的信息:" + new String(buffer.array()));
sc.close();
}
}
/**
* 客户端
*/
@Test
public void client() throws IOException {
SocketChannel sc = SocketChannel.open();
sc.connect(new InetSocketAddress("127.0.0.1", 9999));
ByteBuffer buffer = ByteBuffer.allocate(1024);
buffer.put("hello server!".getBytes());
buffer.flip();
sc.write(buffer);
sc.close();
}
8.3 Selector
Selector 是 Java NIO 中实现多路复用技术的关键,多路复用技术又是提高通 讯性能的一个重要因素。项目中可以基于 selector 对象实现了一个线程管理多 个 channel 对象,多个网络链接的目的。例如:在一个单线程中使用一个 Selector 处理 3 个 Channel,如图所示
为什么使用 Selector?
仅用单个线程来处理多个 Channel 的好处是:只用一个线程处理所有的通道, 可以有效避免线程之间上下文切换带来的开销,而且每个线程都要占用系统的一 些资源(如内存)。因此,使用的线程越少越好。
通道触发了一个事件意思是该事件已经就绪。所以,某个 channel 成功连接到 另一个服务器称为“连接就绪”。一个 server socket channel 准备好接收新 进入的连接称为“接收就绪”。一个有数据可读的通道可以说是“读就绪”。等 待写数据的通道可以说是“写就绪”。
这四种事件用 SelectionKey 的四个常量来表示:
1) SelectionKey.OP_CONNECT
2) SelectionKey.OP_ACCEPT
. 3) SelectionKey.OP_READ
4) SelectionKey.OP_WRITE
/**
* Selector 多路复用选择器
* 用于实现Channel的双向传输
* selector 上注册的通道必须是非阻塞的
*
* @author zxj
* @date 2020/3/17 8:46
*/
public class Server {
public static void main(String[] args) throws IOException {
// 开启服务端通道
ServerSocketChannel ssc = ServerSocketChannel.open();
// 绑定监听端口
ssc.bind(new InetSocketAddress(8070));
// 设置为非阻塞
ssc.configureBlocking(false);
// 开启选择器
Selector selec = Selector.open();
// 将通道注册到选择器上
ssc.register(selec, SelectionKey.OP_ACCEPT);
//一个服务端对应多个客户端
while(true){
// 选择出已经注册的通道
selec.select();
// 获取这次选择出来的通道的事件类型
Set<SelectionKey> keys = selec.selectedKeys();
Iterator<SelectionKey> its = keys.iterator();
while (its.hasNext()){
SelectionKey key = its.next();
// 连接事件
if(key.isAcceptable()){
// 获取通道
ServerSocketChannel sscx = (ServerSocketChannel) key.channel();
// 接收连接
SocketChannel sc = sscx.accept();
sc.configureBlocking(false);
// 将改通道注册到选择器并指定事件类型为“读和写”
sc.register(selec, SelectionKey.OP_READ | SelectionKey.OP_WRITE);
}
//读事件
if(key.isReadable()){
// 获取通道
SocketChannel sc = (SocketChannel) key.channel();
// 读取数据
ByteBuffer buffer = ByteBuffer.allocate(1024);
sc.read(buffer);
System.out.println(new String(buffer.array(), 0, buffer.position()));
// 将此通道上的读事件注销
sc.register(selec, key.interestOps() ^ SelectionKey.OP_READ);
}
//写事件
if(key.isWritable()){
SocketChannel sc = (SocketChannel) key.channel();
// 写出数据,向客户端响应
sc.write(ByteBuffer.wrap("hi client".getBytes()));
// 将此通道上的写事件注销
sc.register(selec, key.interestOps() ^ SelectionKey.OP_WRITE);
}
its.remove();
}
}
}
}
/**
* @author zxj
* @date 2020/3/17 8:46
*/
public class Client {
public static void main(String[] args) throws IOException {
SocketChannel sc = SocketChannel.open();
// sc.configureBlocking(false);
sc.connect(new InetSocketAddress("localhost", 8070));
System.out.println("客户端已连接");
// 向服务器发送数据
sc.write(ByteBuffer.wrap("hello server".getBytes()));
// 读取服务器的响应数据
ByteBuffer buffer = ByteBuffer.allocate(1024);
sc.read(buffer);
System.out.println(new String(buffer.array(), 0, buffer.position()));
sc.close();
}
}