1.同步异步概念
2.阻塞非阻塞概念
3.常见I/O模型:同步阻塞IO,同步非阻塞IO,异步阻塞IO,异步非阻塞IO
4.UNIX系统下的IO多路复用(OS级别的I/O多路复用是重点,同步非阻塞I/O的应用)***
5.IO中BIO,NIO,AIO简介
6.NIO实现原理(NIO也是同步非阻塞I/O的应用)(NIO阻塞代码实例 NIO非阻塞代码实例 这里的Selector真正体现了多路复用)(重点)***
1.同步异步概念
同步异步是针对应用程序和内核的交互而言的。
同步指的是用户进程触发IO操作,等待/轮询的去查看IO操作是否完成。(同步阻塞IO是等待,同步非阻塞是轮询)
异步指的是用户进程出发IO操作便开始做别的事,IO操作已经完成的时候会得到IO完成的通知。
2.阻塞非阻塞概念
阻塞非阻塞是针对于进程在访问数据的时候,根据IO操作的就绪状态采用的不同操作方式。
阻塞状态下,读取/写入函数将一直等待IO操作就绪。
非阻塞状态下,读取/写入函数会立即返回一个状态值。
3.常见的IO模型
同步阻塞IO:在此种模型下,用户进程在发起一个IO操作以后,必须等待IO操作的完成。只有当真正完成了IO操作以后,用户进程才能运行。
应用:Java.io包下的传统IO模型
同步非阻塞IO:在此种模型下,用户进程在发起一个IO操作以后边可返回做其它事情,但是用户进程需要时不时的询问IO操作是否就绪,这就要求用户进程不断的进行轮询,从而引入不必要的CPU资源浪费。
应用:jdk1.4出现的java.nio包
异步阻塞IO:在此种模型下,用户进程发起一个IO操作以后,不等待内核IO操作的完成,等内核完成IO操作以后通知应用程序。
这就是同步异步的最关键区别:同步必须等待/主动轮询IO操作是否完成。
为什么说是阻塞的呢?因为其是调用了select函数来完成,而select函数本身实现的方式就是阻塞的。其好处为:可以同时监听多个文件句柄,从而提高系统并发性。
同步非阻塞IO:在此种模型下,用户进程只需要发起一个IO操作然后立即返回,等IO操作真正完成以后,应用程序会得到IO操作完成的通知,此时用户进程只需要对数据进行处理,不需要进行实际的IO读写操作,因为真正的IO读取或写入操作已经由内核完成了。
4.UNIX系统下的IO多路复用(OS级别的I/O多路复用是重点,同步非阻塞I/O的应用)
UNIX系统秉承了一切皆文件的思想。
IO多路复用机制是同步非阻塞IO的应用。
它利用单独的线程(内核级)统一检测所有的Socket(套接字),一旦某个Socket有了IO数据,则启动响应的Application处理。
实现原理:在select和poll中利用轮询Socket句柄的方式来实现监测Socket中是否有IO数据到达。
select底层是数组,poll是链表,epoll是哈希表。
select和epoll区别:
- 每次调用select,都需要把fd集合(句柄集合)从用户态拷贝到内核态,这个开销在fd很多时开销很大
- 同时每次调用select都需要在内核遍历传递进来的所有的fd,这个开销在fd很多时也很大
- select支持的文件描述符数量太小了,默认是1024
4.epoll为每个fd指定一个回调函数,当设备就绪,唤醒等待队列上的等待者时,就会调用这个回调函数 - epoll所支持的FD上线是最大可以打开的文件数目
5.BIO,NIO,,AIO简介
BIO:同步并阻塞IO模型的应用,服务器实现模式为一个连接一个线程,即客户端有连接请求时服务器就需要启动一个线程进行处理,如果这个连接不做任何事情会造成不必要的开销,所以可以通过线程池机制改善。
适用场景:适用于连接数目比较小且固定的架构,这种方式对服务器资源要求比较高,并发局限于应用中,JDK1.4之前唯一的选择。
NIO:同步非阻塞IO模型的应用。服务器实现模式为一个请求一个线程。客户端发送的连接请求都会注册到多路复用器上,多路复用器轮询到有I/O请求时才启动一个线程进行处理。
适用场景:适用于连接数目多且连续比较短(轻操作)的架构,比如聊天服务器,并发局限于应用中,编程比较复杂,JDK1.4开始支持。
AIO:异步非阻塞,服务器实现模式为一个有效请求一个线程,客户端的IO请求都是由OS先完成了再通知服务器应用去启动线程进行处理。
适用场景:连接数目多且连接比较长(重操作)的架构,比如相册服务器,充分调用OS参与并发操作,编程比较复杂,JDK7开始支持。
NIO实现原理
NIO是同步非阻塞,一个请求一个线程,客户端发送的连接请求都会注册到多路复用器上,多路复用器轮询到有IO请求时才进行处理。
多路复用原理:它利用单独的线程(内核级)统一检测所有的Socket(套接字),一旦某个Socket有了IO数据,则启动响应的Application处理。
实现原理:在select和poll中利用轮询Socket句柄的方式来实现监测Socket中是否有IO数据到达。
NIO的适用场景:
在文件IO中优势和传统IO不明显,但是如果是网络IO,则是其适用场景。
实现NIO的三要素
Buffer缓冲区,Channel管道,Selector选择器
Buffer是用来存储数据的(数组形式),Channel是用来运输的。
java.nio包下 Buffer为缓冲区抽象类
public abstract class Buffer extends Object{
public final int capacity();//1
public final int limit();//2
public final int position();//3
public final Buffer mark();//4
public final Buffer reset();
public final Buffer flip();//5
}
Capacity说明:容量,缓冲区能够容纳数据元素的最大数量(Buffer缓冲区底层是数组,所以capacity不可变)
Limit说明:界限,表示缓冲区中可以操作的数据大小(limit后面的数据不可以进行读写操作)
position说明:位置,表示缓冲区中正在操作的数据位置
mark说明:标记,表示记录当前position的位置,可以通过reset恢复到mark的位置
flip说明:调用此方法后,会将position置0,limit置为之前position的值。
ByteBuffer缓冲区(Buffer的继承子类)主要方法:(除了以上再加)
put();//将数据写入缓冲区
get();//将数据从缓冲区读出
java.nio.channels.channel接口
主要实现类:FileChannel,SocketChannel,ServerSocketChannel,DatagramChannel
获取Channel对象的方法:
本地IO:FileInputStream/FileOutputStream/RandomAccessFile
网络IO:Socket,ServerSocket,DatagramSocket
Selector适用时不能与FileChannel一起使用,Selector使用场景是非阻塞的,而FileChannel是阻塞场景下的文件IO,而SocketChannel可以是非阻塞的,所以Selector常与SocketChannel连用。
以下代码:
客户端的FileChannel=>SocketChannel=>服务器的FileChannel
阻塞下Channel+Buffer:
//这是客户端代码
public class TestDemo {
public static void main(String[] args) throws Exception {
//1.获取通道,向IP127.0.0.1的9999建立通道
SocketChannel socketChannel=SocketChannel.open(new InetSocketAddress("127.0.0.1", 9999));
//2.发送一张图片给服务器
FileChannel fileChannel=FileChannel.open(Paths.get("E:"+File.separator+"psb.jpg"));
//3.创建Buffer
ByteBuffer buffer=ByteBuffer.allocate(1024);
//4.fileChannel承载本地图片,SocketChannel读取本地图片,两管道配合,传输给服务器
while(fileChannel.read(buffer)!=-1){
//Buffer在被socketChannel读取前切换成读模式
buffer.flip();
socketChannel.write(buffer);
//读完将Buffer切换成写模式,能让管道继续读取文件
buffer.clear();
}
//5.关闭流
fileChannel.close();
socketChannel.close();
}
}
//这里是服务器端
public class Member {
public static void main(String[] args) throws Exception{
//1.获取通道
ServerSocketChannel server=ServerSocketChannel.open();
//2.得到文件通道,将客户端传递过来的图片写到本地项目下(写模式,没有则创建)
FileChannel outChannel=FileChannel.open(Paths.get("2.jpg"), StandardOpenOption.WRITE,StandardOpenOption.CREATE);
//3.绑定连接通道(这里的Socket套接字侧面反映了TCP,UDP)
server.bind(new InetSocketAddress(9999));//监听服务器的6666端口数据请求
//4.获取客户端的连接(阻塞的)
SocketChannel client=server.accept();
//5.同样与服务器端的SocketChannel对应的Buffer缓冲区
ByteBuffer buffer=ByteBuffer.allocate(1024);
//6.将客户端传来的图片保存在本地中
while(client.read(buffer)!=-1){
//在读之前都要切换成读模式
buffer.flip();
outChannel.write(buffer);
//读完切换成写模式,能让管道继续读取文件的数据
buffer.clear();
}
//7.关闭通道
outChannel.close();
client.close();
server.close();
}
}
然后运行服务器端代码,再运行客户端请求。的确将客户端E盘的图片文件通过Socket管道,以FileChannel阻塞方式,上传到了服务器端。
非阻塞下:SocketChannel+Selector+Buffer:
以上阻塞态如果不关闭流,则服务器端一直会读取客户端发来的数据,进而阻塞,所以要使用socketChannel
public class TestDemo {
//这是客户端代码
public static void main(String[] args) throws Exception {
//1.获取通道,向IP127.0.0.1的9999建立通道
SocketChannel socketChannel=SocketChannel.open(new InetSocketAddress("127.0.0.1", 9999));
socketChannel.configureBlocking(false);//非阻塞状态
//2.发送一张图片给服务器
FileChannel fileChannel=FileChannel.open(Paths.get("E:"+File.separator+"psb.jpg"));
//3.创建Buffer
ByteBuffer buffer=ByteBuffer.allocate(1024);
//4.fileChannel承载本地图片,SocketChannel读取本地图片,两管道配合,传输给服务器
while(fileChannel.read(buffer)!=-1){
//Buffer在被socketChannel读取前切换成读模式
buffer.flip();
socketChannel.write(buffer);
//读完将Buffer切换成写模式,能让管道继续读取文件
buffer.clear();
}
//5.关闭流
fileChannel.close();
socketChannel.close();
}
}
public class Member {
public static void main(String[] args) throws Exception{
//这里是服务器端
//1.获取通道
ServerSocketChannel server=ServerSocketChannel.open();
//2.切换成非阻塞模式
server.configureBlocking(false);
//3.绑定连接通道(这里的Socket套接字侧面反映了TCP,UDP)
server.bind(new InetSocketAddress(9999));//监听服务器的6666端口数据请求
//4.获取选择器
Selector selector=Selector.open();
//4.1将通道注册到选择器上,指定接收监听通道"事件,回调接收就绪事件代码
server.register(selector, SelectionKey.OP_ACCEPT);
//5.轮询的获取选择器上已 就绪 的事件(只要select()>0,说明已经就绪)(这里非阻塞才真正体现多路复用)
while(selector.select()>0){
//6.使用Iterator遍历句柄
Iterator<SelectionKey>iterator=selector.selectedKeys().iterator();
//7.获取已经就绪的事件
while(iterator.hasNext()){
SelectionKey selectionKey=iterator.next();
//接收事件就绪
if(selectionKey.isAcceptable()){
//8.获取客户端连接
SocketChannel client=server.accept();
//8.1 切换成非阻塞态 这样才能使用FileChannel
client.configureBlocking(false);
//8.2 注册到选择器上-->拿到客户端的连接为了读取通道的数据(指定监听读就绪事件) 回调读状态代码
client.register(selector, SelectionKey.OP_READ);
}//读事件就绪
else if(selectionKey.isReadable()){
//9.获取当前选择器读就绪状态的通道
SocketChannel client=(SocketChannel)selectionKey.channel();
//9.1读取数据
ByteBuffer buffer=ByteBuffer.allocate(1024);
//9.2得到文件通道,将客户端传递过来的图片写道服务器本地(写模式,没有则创建)
FileChannel outChannel=FileChannel.open(Paths.get("2.jpg"), StandardOpenOption.WRITE,StandardOpenOption.CREATE);
while(client.read(buffer)!=-1){
//在读之前都要切换成读模式
buffer.flip();
outChannel.write(buffer);
//读完切换成写模式,能让管道继续读取文件的数据
buffer.clear();
}
}
//取消选择键(已经处理过的事情,就取消)
iterator.remove();
}
}
//7.关闭通道
//outChannel.close();
//client.close();
server.close();
}
}
同样也上传了图片。这里加入了Selector选择器的NIO非阻塞,才真正实现了IO多路复用,并且通过选择器状态的不同,回调不同的Channel。并且使用Iterator实现了轮询。
JavaNIO与IO的区别:
1.传统的IO面向流,一个字节一个字节处理数据,而NIO是面向缓冲区的,面向内存块处理数据。
2.Java IO的各种流是阻塞的,当一个线程调用read()或write()时,该线程被阻塞,直到有一些数据被读取。
NIO是非阻塞的,使一个线程从某通道发送请求读取数据。
3.Java NIO的选择器允许一个单独的线程来监视多个输入通道。
4.传统IO是单向的流。NIO是双向的Channel管道,读写都是双向的。
分散读取与聚集写入
分散读取(scatter):将一个Channel数据分散读取到多个Buffer中
聚集写入(gather):将多个缓冲区数据集中写入到一个通道中