BIO,NIO和AIO的区别
一.Java的I/O演进之路
Java共支持3种网络编程的I/O模型:BIO,NIO,AIO
BIO:
同步并阻塞(传统阻塞型),服务器实现模式为一个连接一个线程,即客户端有连接请求时服务端就需要启动一个线程进行处理,如果这个连接不做任何事情会造成不必要的线程开销。
NIO:
同步非阻塞,服务器实现模式为一个线程处理多个请求(连接),即客户端发送的连接请求都会注册到多路复用器上,多路复用器轮询到链接有I/O请求就进行处理。
AIO:
异步非阻塞,服务器实现模型为一个有效请求一个线程,客户端发I/O请求都是由操作系统先完成了再通知服务器应用去启动线程进行处理,一般适用于连接数较多且连接时间较长的应用。
二.BIO深入刨析
1.BIO概述
BIO(Blocking I/O)就是传统的Java I/O编程,其相关的类和接口都在java.io包下。BIO是同步阻塞的,服务器实现模式为一个连接一个线程,即客户端有连接请求时服务端就需要启动一个线程进行处理,如果这个连接不做任何事情会造成不必要的线程开销,可以通过线程池机制改善(实现多个客户端连接服务器)
BIO编程流程的梳理:
- 服务端启动一个ServerSocket,注册端口,调用accept方法监听客户端的Socket连接。
- 客户端启动Socket对服务端进行通信,默认情况下服务器端需要对每个客户建立一个线程与之通讯。
2.BIO案例
服务端:
public class Server {
public static void main(String[] args) throws IOException {
System.out.println("===服务端启动===");
// 1.定义一个ServerSocket对象进行服务端的端口注册
ServerSocket ss = new ServerSocket(9999);
// 2.监听客户端的Socket连接请求
Socket socket = ss.accept();
// 3.从Socket管道中得到一个字节输入流对象
InputStream is = socket.getInputStream();
// 4.把字节输入流包装成一个缓冲字符输入流
BufferedReader br = new BufferedReader(new InputStreamReader(is));
String msg;
while ((msg = br.readLine()) != null) {
System.out.println("服务端接收到:" + msg);
}
}
}
客户端:
public class Client {
public static void main(String[] args) throws IOException {
System.out.println("===客户端启动===");
// 1.创建Socket对象请求服务端的连接
Socket socket = new Socket("127.0.0.1", 9999);
// 2.从Socket对象中获取一个字节输出流
OutputStream os = socket.getOutputStream();
// 3.把字节输出流包装成一个打印流
PrintStream ps = new PrintStream(os);
ps.println("Hello World!服务端,你好!");
ps.flush();
}
}
总结:
在以上通信中,服务端会一直等待客户端的消息,如果客户端没有进行消息的发送,服务端会一直进入阻塞状态。
3.伪异步I/O
1),概述:
伪异步I/O采用线程池和任务队列实现,当客户端接入时,将客户端Socket封装成一个Task(该任务实现java.lang.Runnable
线程任务接口)交给后端的线程池中进行处理。JDK的线程池可以设置消息队列的大小和最大线程数,因此,它们的资源占用是可控的,无论多少个客户端并发访问,都不会导致资源的耗尽和宕机。
2),代码实现:
客户端:
public class Client {
public static void main(String[] args) {
try {
Socket socket = new Socket("127.0.0.1", 9999);
OutputStream os = socket.getOutputStream();
PrintStream ps = new PrintStream(os);
Scanner sc = new Scanner(System.in);
while (true) {
System.out.print("请说:");
String msg = sc.nextLine();
ps.println(msg);
ps.flush();
}
} catch (IOException e) {
e.printStackTrace();
}
}
}
线程池处理类:
public class SocketServerPoolHandler {
/**
* 1.创建一个线程池的成员变量用于存储一个线程池对象
*/
private ExecutorService executorService;
/**
* 2.创建这个类的时候就需要初始化线程池对象
*/
public SocketServerPoolHandler(int maxThreadNum, int queueSize) {
executorService = new ThreadPoolExecutor(3, maxThreadNum,
120, TimeUnit.SECONDS, new ArrayBlockingQueue<>(queueSize));
}
/**
* 3.提供一个方法来提交任务给线程池的任务队列来暂存,等着线程池来处理
*/
public void execute(Runnable target) {
executorService.execute(target);
}
}
服务端:
public class Server {
public static void main(String[] args) {
try {
// 1.注册端口
ServerSocket ss = new ServerSocket(9999);
// 2.定义一个循环接收客户端的Socket连接请求
// 初始化一个线程池对象
SocketServerPoolHandler poolHandler = new SocketServerPoolHandler(3, 10);
while (true) {
Socket socket = ss.accept();
// 3.把Socket对象交给一个线程池进行处理
// 把Socket封装成一个任务对象交给线程池处理
Runnable target = new ServerRunnableTarget(socket);
poolHandler.execute(target);
}
} catch (IOException e) {
e.printStackTrace();
}
}
}
public class ServerRunnableTarget implements Runnable {
private Socket socket;
public ServerRunnableTarget(Socket socket) {
this.socket = socket;
}
@Override
public void run() {
// 处理接收的客户端Socket通信需求
try {
InputStream is = socket.getInputStream();
BufferedReader br = new BufferedReader(new InputStreamReader(is));
String msg;
while ((msg = br.readLine()) != null) {
System.out.println("服务端接收到:" + msg);
}
} catch (IOException e) {
e.printStackTrace();
}
}
}
结果:
启动服务端并启动了多个客户端发送消息,由于核心线程数=最大线程数=3,当客户端数>3时,客户端的Socket任务会到线程池的阻塞队列中等待,关闭客户端,当客户端数<=3时,Socket任务将会被服务端处理
3),小结
- 伪异步I/O采用了线程池实现,因此避免了为每个请求创建一个独立线程造成资源耗尽的问题,但由于底层依然是采用了同步阻塞模型,因此无法从根部上解决问题。
- 如果单个消息处理的缓慢,或者服务器线程池中的全部线程都被阻塞,那么后续的Socket的I/O消息都将在队列中排队。新的Socket请求将被拒绝,客户端会发生大量连接超时
多线程BIO通信模型图:
三.NIO深入刨析
1.NIO概述
- NIO(Non-Blocking IO)是从Java 1.4版本开始引入的一个新的IO API,可以替代标准的Java IO API。NIO与原来的IO由同样的作用和目的,但是使用的方式完全不同,NIO支持面向缓冲区的,基于通道的IO操作。NIO将以更加高效的方式进行文件的读写操作。NIO可以理解为非阻塞IO,传统的IO的read和write只能阻塞执行,线程在读写IO期间不能干其他事情,比如调用socket.read()时,如果服务器一直没有数据传输过来,线程就一直阻塞,而NIO中可以配置socket为非阻塞模式。
- NIO相关类都被放在
java.nio
包及子包下,并且对原java.io
包中的很多类进行改写.。 - NIO有三大核心部分:Channel(通道),Buffer(缓冲区),Selector(选择器)。
- Java NIO的非阻塞模式,使一个线程从某通道发送请求或者读取数据,但是它仅能得到目前可用的数据,如果目前没有数据可用时,就什么都不会获取,而不是保持线程阻塞,所以直至数据变的可以读取数据之前,该线程可以继续做其他的事情。非阻塞也是如此,一个线程请求写入一些数据到某通道,但不需要等待它完全写入,这个线程同时可以去别的事情。
- 通俗理解:NIO可以做到用一个线程来处理多个操作。假设有1000个请求过来,根据实际情况,可以分配20或者80个线程来处理。不像之前的阻塞IO那样,非得分配1000个
2.NIO和BIO的比较:
-
BIO以流的方式处理数据,而NIO以块的方式处理数据,块I/O的效率比流I/O高很多。
-
BIO是阻塞的,NIO是非阻塞的。
-
BIO基于字节流和字符流进行操作,而NIO基于Channel(通道)和Buffer(缓冲区)进行操作,数据总是从通道读取到缓冲区中,或者从缓冲区写入到通道中。Selector(选择器)用于监听多个通道的事件(比如:连接请求,数据到达等),因此使用单个线程就可以监听多个客户端通道。
NIO BIO 面向缓冲区(Buffer) 面向流(Stream) 非阻塞(Non Blocking IO) 阻塞IO(Blocking IO) 选择器(Selector)
3、NIO三大核心原理示意图
NIO有三大核心部分:Channel(通道)、Buffer(缓冲区)、Selector(选择器)
Buffer缓冲区:
缓冲区本质上是一块可以写入数据,然后可以从中读取数据的内存。这块内存被包装为NIO Buffer对象,并提供了一组方法,用来方便的访问该块内存。相比直接对数组的操作,Buffer API更加容易操作和管理。
Channel(通道):
Java NIO的通道类似流,但又有些不同:即可以从通道中读取数据,又可以写数据到通道。但流的(input或output)读写通常是单向的。通道可以非阻塞读取和写入通道,通道可以支持读取或写入缓冲区,也支持异步的读写。
Selector选择器:
Selecctor是一个Java NIO的组件,可以检查一个或多个NIO通道,并确定哪些通道已经准备好进行读写。这样,一个单独的线程就可以管理多个Channel,从而管理多个网络连接,提高效率。
- 每个channel都会对应一个Buffer。
- 一个线程对应一个Selector,一个Selector对应多个channel(连接)。
- 程序切换到那个channel由事件决定。
- Selector会根据不同的事件,再各个通道上切换。
- Buffer就是一个内存块,底层是一个数组。
- 数据的读写是通过Buffer完成的,BIO中要么是输入流,要么是输出流,不能双向,但是NIO的Buffer既可以读也可以写。
- Java NIO系统的核心在于:通道和缓冲区。通道表示打开到IO设备(例如:文件,套接字)的连接。若需要使用NIO系统,需要获取用于连接IO设备的通道以及用于容纳数据的缓冲区。然后操作缓冲区。对数据进行处理。简而言之,channel负责传输,Buffer负责存取数据。
4.NIO核心一:缓冲区(Buffer)
1),Buffer概述:
Buffer是一个用于特定基本数据类型的容器。由java.nio
包定义的,所有缓冲区都是Buffer抽象类的子类。Java NIO中的Buffer主要用于与NIO通道进行交互,数据是从通道读入缓冲区,从缓冲区写入到通道中。
2),Buffer类及其子类:
Buffer就像一个数组,可以保存多个相同类型的数据。根据数据类型不同,有以下Buffer常用子类:
- ByteBuffer
- CharBuffer
- ShortBuffer
- IntBuffer
- LongBuffer
- FloatBuffer
- DoubleBuffer
上述Buffer类都采用相似的方法进行管理数据,只是各自管理的数据类型不同而已。都是通过如下方法获取一个Buffer对象:
static XxxBuffer allocate(int capacity) //创建一个容量为capacity的XxxBuffer对象
3),缓冲区的基本属性:
- 容量(capacity):作为一个内存块,Buffer具有一定的固定大小,也称为容量,缓冲区容量不能为负,并且创建后不能更改。
- 限制(limit):表示缓冲区中可以操作数据的大小(limit后数据不能进行读写)。缓冲区的限制不能为负,并且不能大于其容量。写入模式,限制等于Buffer的容量。读取模式下,limit等于写入的数据量。
- 位置(position):下一个要读取或写入的数据的索引。缓冲区的位置不能为负,并且不能大于其限制。
- 标记(mark)与重置(reset):标记是一个索引,通过Buffer中的mark()方法只当Buffer中一个特定的position,之后可以通过调用reset()方法恢复到这个position。
- 标记,位置,限制,容量遵守一下不变式: 0<=mark<=position<=limit<=capacity
4),Buffer常见的方法:
Buffer clear() //清空缓冲区并返回对缓冲区的引用(缓冲区中的数据依然存在,但是处于被遗忘状态)
Buffer flip() //为将缓冲区的界限设置为当前位置,并将当前位置重置为0
int capacity() //返回Buffer的capacity大小
boolean hasRemaining() //判断缓冲区中是否还有元素
int limit() //返回Buffer的界限(limit)的位置
Buffer limit(int n) //将设置缓冲区界限为n,并返回一个具有新limit的缓冲区对象
Buffer mark() //对缓冲区设置标记
int position() //返回缓冲区的当前位置position
Buffer position(int n) //将设置缓冲区的当前位置为n,并返回修改后的Buffer对象
int remaining() //返回position和limit之间的元素个数
Buffer reset() //将位置position转到以前设置的mark所在的位置
Buffer rewind() //将位置设为0,取消设置的mark
5),缓冲区的数据操作:
Buffer所有子类提供了两个用于数据操作的方法:get()和put()
获取Buffer中的数据:
get() //读取单个字节
get(byte[] dst) //批量读取多个字节到dst中
get(int index) //读取指定索引位置的字节(不会移动position)
放入数据到Buffer中:
put(byte b) //将给定单个字节写入缓冲区的当前位置
put(byte[] src) //将src中的字节写入缓冲区的当前位置
put(int index, byte b) //将指定字节写入缓冲区的索引位置(不会移动position)
使用Buffer读写数据一般遵循以下四个步骤:
写入数据到Buffer
调用flip()方法,转换为读取模式
从Buffer中读取数据
调用buffer.clear()方法或者buffer.compact()方法清除缓冲区
6),Buffer案例:
@Test
public void test01() {
// 1.分配一个缓冲区,容量设置成10
ByteBuffer buffer = ByteBuffer.allocate(10);
System.out.println(buffer.position()); // 0
System.out.println(buffer.limit()); // 10
System.out.println(buffer.capacity()); // 10
System.out.println("--------------------");
// 2.put()往缓冲区中添加数据
String name = "hello";
buffer.put(name.getBytes());
System.out.println(buffer.position()); // 5
System.out.println(buffer.limit()); // 10
System.out.println(buffer.capacity()); // 10
System.out.println("--------------------");
// 3.flip()为将缓冲区的界限设置为当前位置,并将当前位置重置为0 可读模式
buffer.flip();
System.out.println(buffer.position()); // 0
System.out.println(buffer.limit()); // 5
System.out.println(buffer.capacity()); // 10
System.out.println("--------------------");
// 4.get()数据的读取
char ch = (char) buffer.get();
System.out.println(ch);
System.out.println(buffer.position()); // 1
System.out.println(buffer.limit()); // 5
System.out.println(buffer.capacity()); // 10
System.out.println("--------------------");
}
@Test
public void test02() {
// 1.分配一个缓冲区,容量设置成10 put()往缓冲区中添加数据
ByteBuffer buffer = ByteBuffer.allocate(10);
String name = "hello";
buffer.put(name.getBytes());
System.out.println(buffer.position()); // 5
System.out.println(buffer.limit()); // 10
System.out.println(buffer.capacity()); // 10
System.out.println("--------------------");
// 2.clear()清除缓冲区中的数据 并没有真正清除数据,只是让position的位置恢复到初始位置,后续添加数据的时候才会覆盖每个位置的数据
buffer.clear();
System.out.println(buffer.position()); // 0
System.out.println(buffer.limit()); // 10
System.out.println(buffer.capacity()); // 10
System.out.println((char) buffer.get()); // h
System.out.println("--------------------");
// 3.定义一个缓冲区
ByteBuffer buf = ByteBuffer.allocate(10);
String n = "hello";
buf.put(n.getBytes());
buf.flip();
// 读取数据
byte[] b = new byte[2];
buf.get(b);
System.out.println(new String(b));
System.out.println(buf.position()); // 2
System.out.println(buf.limit()); // 5
System.out.println(buf.capacity()); // 10
System.out.println("--------------------");
buf.mark(); // 标记此刻这个位置 2
byte[] b2 = new byte[3];
buf.get(b2);
System.out.println(new String(b2));
System.out.println(buf.position()); // 5
System.out.println(buf.limit()); // 5
System.out.println(buf.capacity()); // 10
System.out.println("--------------------");
buf.reset(); // 回到标记位置
if (buf.hasRemaining()) {
System.out.println(buf.remaining()); // 3
}
}
7),直接与非直接缓冲区:
ByteBuffer可以是两种类型,一种是基于直接内存(也就是非堆内存);另一种是非直接内存(也就是堆内存)。对于直接内存来说,JVM将会在IO操作上具有更高的性能,因为他直接作用与本地系统的IO操作。而非直接内存,也就是堆内存中的数据,如果要做IO操作,会先从本进程内存复制到直接内存,再利用本地IO处理。
从数据流的角度,非直接内存是下面这样的作用链:
本地IO-->直接内存-->非直接内存-->直接内存-->本地IO
而直接内存是:
本地IO-->直接内存-->本地IO
很明显,在做IO处理时,比如网络发送大量数据时,直接内存会具有更高的效率。直接内存使用allocateDirect()创建,但是它比申请普通的堆内存需要耗费更高的性能。不过,这部分的数据是在JVM之外的,因此他不会占用应用的内存。所以,当有很大的数据要缓存,并且它的生命周期有很长,那么就比较适合是使用直接内存。只是一般来说,如果不是能带来很明显的性能提升,还是推荐使用堆内存。字节缓冲区是直接缓冲区还是非直接缓冲区可调用其isDirect()方法来确定。
直接缓冲区使用场景:
- 有很大的数据需要存储,它的生命周期很长。
- 适合频繁的IO操作,比如网络并发场景。
5.NIO核心二:通道(channel)
1),Channel概述:
通道(Channel):由java.nio.channels包定义。Channel表示IO源与目标打开的连接。Channel类似于传统的流。只不过Channel本身不能直接访问数据,Channel只能与Buffer进行交互。
NIO的通道类似于流,但有些区别如下:
- 通道可以同时进行读写,而流只能读或者写。
- 通道可以实现异步的读写数据。
- 通道可以从缓冲区读写数据。
BIO中的stream是单向的,例如FileInputStream对象只能进行读取数据的操作,而NIO中的Channel是双向的,可以读操作,也可以写操作
Channel在NIO中是一个接口
public interface Channel extends Closeable
2),常用的Channel实现类:
FileChannel:用于读取、写入、映射和操作文件的通道
DatagramChannel:通过UDP读写网络中的数据通道
SocketChannel:通过TCP读写网络中的数据
ServerSocketChannel:可以监听新进来的TCP连接,对每一个新进来的连接都会创建一个SocketChannel(ServerSocketChannel类似ServerSocket,SocketChannel类似Socket)
3),FileChannel类:
获取通道的一种方式是对支持的对象调用getChannel()方法。支持通道的类如下:
- FileInputStream
- FileOutputStream
- RandomAccessFile
- DatagramSocket
- Socket
- ServerSocket
获取通道的其他方式是使用Files类的静态方法newByteChannel()
获取字节通道,或者通过通道的静态方法open()
打开并返回指定通道
4),FileChannel的常用方法:
int read(ByteBuffer dst) // 从Channel中读取数据到ByteBuffer
long read(ByteBuffer[] dsts) // 将Channel中的数据分散到ByteBuffer[]
int write(ByteBuffer src) // ByteBuffer中的数据写入到Channel
long write(ByteBuffer[] srcs) // 将ByteBuffer[]中的数据聚集到Channel
long position() // 返回此通道的文件位置
FileChannel position(long p) // 设置此通道的文件位置
long size() // 返回此通道的文件的当前大小
FileChannel truncate(long s) // 将此通道的文件截取为给定大小
void force(boolean metaData) // 强制将所有对此通道的文件更新写入到存储设备中
5),FileChannel案例:
本地文件写数据:
@Test
public void write() {
try {
// 1.字节输出流通向目标文件
FileOutputStream fos = new FileOutputStream("data01.txt");
// 2.得到字节输出流对应的通道Channel
FileChannel channel = fos.getChannel();
// 3.分配缓冲区
ByteBuffer buffer = ByteBuffer.allocate(1024);
buffer.put("Hello World!".getBytes());
// 4.把缓冲区切换成写出模式
buffer.flip();
channel.write(buffer);
channel.close();
System.out.println("写数据到文件中!");
} catch (IOException e) {
e.printStackTrace();
}
}
本地读文件:
@Test
public void read() {
try {
// 1.定义一个文件字节输入流与源文件接通
FileInputStream fis = new FileInputStream("data01.txt");
// 2.需要得到文件输入流的文件通道
FileChannel channel = fis.getChannel();
// 3.定义一个缓冲区
ByteBuffer buffer = ByteBuffer.allocate(1024);
// 4.读取数据到缓冲区
channel.read(buffer);
buffer.flip();
// 5.读取出缓冲区中的数据并输出
String rs = new String(buffer.array(), 0, buffer.remaining());
System.out.println(rs);
} catch (IOException e) {
e.printStackTrace();
}
}
使用Buffer完成文件复制:
@Test
public void copy() {
try {
// 源文件
File srcFile = new File("data01.txt");
// 目标文件
File destFile = new File("data02.txt");
// 得到字节输入流
FileInputStream fis = new FileInputStream(srcFile);
// 得到字节输出流
FileOutputStream fos = new FileOutputStream(destFile);
// 得到文件通道
FileChannel isChannel = fis.getChannel();
FileChannel osChannel = fos.getChannel();
// 分配缓冲区
ByteBuffer buffer = ByteBuffer.allocate(1024);
while (true) {
// 必须先清空缓冲区再写入数据到缓冲区
buffer.clear();
// 开始读取一次数据
int flag = isChannel.read(buffer);
if (flag == -1) {
break;
}
// 已经读取了数据,把缓冲区的模式切换成可读模式
buffer.flip();
// 把数据写出
osChannel.write(buffer);
}
isChannel.close();
osChannel.close();
System.out.println("复制完成!");
} catch (IOException e) {
e.printStackTrace();
}
}
分散(Scatter)和聚集(Gather)
- 分散读取:是指把Channel通道的数据读入到多个缓冲区中。
- 聚集写入:是指把多个Buffer中的数据聚集到Channel。
@Test
public void test() {
try {
// 1.字节输入管道
FileInputStream fis = new FileInputStream("data01.txt");
FileChannel isChannel = fis.getChannel();
// 2.字节输出管道
FileOutputStream fos = new FileOutputStream("data03.txt");
FileChannel osChannel = fos.getChannel();
// 3.定义多个缓冲区做数据分散
ByteBuffer buffer1 = ByteBuffer.allocate(6);
ByteBuffer buffer2 = ByteBuffer.allocate(1024);
ByteBuffer[] buffers = {buffer1, buffer2};
// 4.从通道中读取数据分散到各个缓冲区
isChannel.read(buffers);
// 5.从每个缓冲区中查询是否有数据读取到
for (ByteBuffer buffer : buffers) {
// 切换到读数据模式
buffer.flip();
System.out.println(new String(buffer.array(), 0, buffer.remaining()));
}
// 6.聚集写入到通道
osChannel.write(buffers);
isChannel.close();
osChannel.close();
System.out.println("复制完成!");
} catch (IOException e) {
e.printStackTrace();
}
}
transferFrom():
从通道中去复制原通道数据
@Test
public void test02() {
try {
// 1.字节输入管道
FileInputStream fis = new FileInputStream("data01.txt");
FileChannel isChannel = fis.getChannel();
// 2.字节输出管道
FileOutputStream fos = new FileOutputStream("data04.txt");
FileChannel osChannel = fos.getChannel();
// 3.复制数据
osChannel.transferFrom(isChannel, isChannel.position(), isChannel.size());
isChannel.close();
osChannel.close();
System.out.println("复制完成!");
} catch (IOException e) {
e.printStackTrace();
}
}
transferTo:
把原通道数据复制到目标通道:
@Test
public void test03() {
try {
// 1.字节输入管道
FileInputStream fis = new FileInputStream("data01.txt");
FileChannel isChannel = fis.getChannel();
// 2.字节输出管道
FileOutputStream fos = new FileOutputStream("data05.txt");
FileChannel osChannel = fos.getChannel();
// 3.复制数据
isChannel.transferTo(isChannel.position(), isChannel.size(), osChannel);
isChannel.close();
osChannel.close();
System.out.println("复制完成!");
} catch (IOException e) {
e.printStackTrace();
}
}
6.NIO核心三:选择器(Selector)
1),概述:
选择器是非阻塞IO的核心,是SelectableChannel对象的多路复用器,Selector可以同时监控多个SelectableChannel的IO状况,也就是说,利用Selector可使一个单独的线程管理多个Channel
- NIO用非阻塞的IO方式。可以用一个线程,处理多个客户端的连接,就会使用到Selector。
- Selector能够检测多个注册的通道上是否有事件发生(多个Channel以事件的方式可以注册到同一个Selector),如果有事件发生,便获取事件然后针对每个事件进行相应的处理。这样就可以用一个单线程去管理多个通道,也就是管理多个连接和请求。
- 只有在连接或通道有读写事件发生时,才会进行读写,就大大的减少了系统开销,并且不必为每个连接都创建一个线程,不用去维护多线程。
- 避免了多线程之间的上下文切换导致的开销。
2),Selector的应用:
创建Selector:通过调用Selector.open()
方法创建一个Selector
Selector selector = Selector.open();
向选择器注册通道:SelectableChannel.register(Selector sel, int ops)
// 1.获取通道
ServerSocketChannel ssChannel = ServerSocketChannel.open();
// 2.切换非阻塞模式
ssChannel.configureBlocking(false);
// 3.绑定连接
ssChannel.bind(new InetSocketAddress(9898));
// 4.获取选择器
Selector selector = Selector.open();
// 5.将通道注册到选择器上,并且指定监听接收事件
ssChannel.register(selector, SelectionKey.OP_ACCEPT);
当调用register(Selector sel, int ops)
将通道注册选择器时,选择器对通道的监听事件,需要通过第二个参数ops指定。可以监听的事件类型(使用SelectionKey的四个常量表示):
- 读:
SelectionKey.OP_READ
(1) - 写:
SelectionKey.OP_WRITE
(4) - 连接:
SelectionKey.OP_CONNECT
(8) - 接收:
SelectionKey.OP_ACCEPT
(16)
若注册时不止监听一个事件,则可以使用位或操作符连接
int interestSet = SelectionKey.OP_READ | SelectionKey.OP_WRITE
7.NIO非阻塞式网络通信原理分析
1),Selector特点说明:
Selector可以实现:一个I/O线程可以并发处理N个客户端连接和读写操作,这从根本上解决了传统同步阻塞I/O一连接一线程的模型,架构的性能,弹性伸缩能力和可靠性都得到了极大的提升。
2),服务端流程:
当客户端连接服务端时,服务端会通过ServerSocketChannel得到SocketChannel:获取通道
ServerSocketChannel ssChannel = ServerSocketChannel.open();
切换非阻塞模式
ssChannel.configureBlocking(false);
绑定连接
ssChannel.bind(new InetSocketAddress(9999));
将通道注册到选择器上,并且指定监听接收事件
ssChannel.register(selector, SelectionKey.OP_ACCEPT);
轮询式的获取选择器上已经准备就绪的事件
// 轮询式的获取选择器上已经准备就绪的事件
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();
}
}
3),客户端流程:
获取通道
SocketChannel sChannel = SocketChannel.open(new InetSocketAddress("127.0.0.1", 9999));
切换非阻塞模式
sChannel.configureBlocking(false);
分配指定大小的缓冲区
ByteBuffer buf = ByteBuffer.allocate(1024);
发送数据给服务端
Scanner scan = new Scanner(System.in);
while (scan.hasNext()) {
String str = scan.nextLine();
buf.put((new SimpleDateFormat("yyyy/MM/dd HH:mm:ss").format(System.currentTimeMillis())
+ "\n" + str).getBytes());
buf.flip();
sChannel.write(buf);
buf.clear();
}
// 关闭通道
sChannel.close();
8、NIO非阻塞式网络通信入门案例
1),服务端实现:
public class Server {
public static void main(String[] args) throws IOException {
// 1.获取通道
ServerSocketChannel ssChannel = ServerSocketChannel.open();
// 2.切换非阻塞模式
ssChannel.configureBlocking(false);
// 3.绑定连接
ssChannel.bind(new InetSocketAddress(9999));
// 4.获取选择器
Selector selector = Selector.open();
// 5.将通道注册到选择器上,并且指定监听接收事件
ssChannel.register(selector, SelectionKey.OP_ACCEPT);
// 6.使用Selector选择器轮询已经就绪好的事件
while (selector.select() > 0) {
// 7.获取选择器中的所有注册的通道中已经就绪好的事件
Iterator<SelectionKey> it = selector.selectedKeys().iterator();
// 8.开始遍历这些准备好的事件
while (it.hasNext()) {
SelectionKey sk = it.next();
// 9.判断这个事件具体是什么
if (sk.isAcceptable()) {
// 10.直接获取当前接入的客户端通道
SocketChannel channel = ssChannel.accept();
// 11.切换非阻塞模式
channel.configureBlocking(false);
// 12.将该通道注册到选择器上
channel.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, buf.remaining()));
buf.clear();
}
}
// 15.取消选择键SelectionKey
it.remove();
}
}
}
}
2),客户端实现:
public class Client {
public static void main(String[] args) throws IOException {
// 1.获取通道
SocketChannel sChannel = SocketChannel.open(new InetSocketAddress("127.0.0.1", 9999));
// 2.切换非阻塞模式
sChannel.configureBlocking(false);
// 3.分配指定大小的缓冲区
ByteBuffer buf = ByteBuffer.allocate(1024);
// 4.发送数据给服务端
Scanner scan = new Scanner(System.in);
while (scan.hasNext()) {
String str = scan.nextLine();
buf.put((new SimpleDateFormat("yyyy/MM/dd HH:mm:ss").format(System.currentTimeMillis())
+ "\n" + str).getBytes());
buf.flip();
sChannel.write(buf);
buf.clear();
}
// 5.关闭通道
sChannel.close();
}
}
NIO非阻塞通信模型图:
四.AIO简介
AIO用来解决数据复制阶段的阻塞问题。
- 同步意味着,在进行读写操作时,线程需要等待结果,还是相当于闲置。
- 异步意味着,在进行读写操作时,线程不必等待结果,而是将来由操作系统通过回调方式由另外的线程来获得结果。
异步模型需要底层操作系统(Kernel)提供支持
1.windows系统通过IOCP实现了真正的异步。
2.Linux系统异步IO再2.6版本引入,但其底层实现还是用多路复用模拟了异步IO,性能没有优势。
AIO编程
Java AIO(NIO.2) :异步非阻塞,服务器实现模式为一个有效请求一个线程,客户端的I/O请求都是由OS先完成了在通知服务器应用去启动线程进行处理。
AIO
异步非阻塞,基于NIO的,可以称之为NIO2.0
BIO NIO AIO
Socket SocketChannel AsynchronousSocketChannel
ServerSocket ServerSocketChannel AsynchronousServerSocketChannel
与NIO不同,当进行读写操作时,只需直接调用API的read或write方法即可,这两种方式都是异步的,对于读操作而言,当有流可读取时,操作系统会将刻度的流传入read方法的缓冲区,对于写操作,当操作系统将write方法传递的流写入完毕时,操作系统主动通知应用程序。
既可以理解为,read/write方法都是异步的,完成后会主动调用回调函数。在JDK1.7中,这部分内容被称为NIO.2,主要在Java.nio.channels包下增加了下面四个异步通道:
AsynchronousSocketChannel
AsynchronousServerSocketChannel
AsynchronousFileChannel
AsynchronousDatagramChannel
文件AIO
AsynchronousFileChannel
@Slf4j
public class AioDemo1 {
public static void main(String[] args) throws IOException {
try{
AsynchronousFileChannel s =
AsynchronousFileChannel.open(
Paths.get("1.txt"), StandardOpenOption.READ);
ByteBuffer buffer = ByteBuffer.allocate(2);
log.debug("begin...");
s.read(buffer, 0, null, new CompletionHandler<Integer, ByteBuffer>() {
@Override
public void completed(Integer result, ByteBuffer attachment) {
log.debug("read completed...{}", result);
buffer.flip();
debug(buffer);
}
@Override
public void failed(Throwable exc, ByteBuffer attachment) {
log.debug("read failed...");
}
});
} catch (IOException e) {
e.printStackTrace();
}
log.debug("do other things...");
System.in.read();
}
}
输出:
13:44:56 [DEBUG] [main] c.i.aio.AioDemo1 - begin...
13:44:56 [DEBUG] [main] c.i.aio.AioDemo1 - do other things...
13:44:56 [DEBUG] [Thread-5] c.i.aio.AioDemo1 - read completed...2
+-------------------------------------------------+
| 0 1 2 3 4 5 6 7 8 9 a b c d e f |
+--------+-------------------------------------------------+----------------+
|00000000| 61 0d |a. |
+--------+-------------------------------------------------+----------------+
可看到:
- 响应文件读取成功的是另一个线程Thread-5
- 主线程并没有IO操作阻塞
五.BIO,NIO,AIO总结
BIO,NIO,AIO:
- Java BIO : 同步并阻塞,服务器实现模式为一个连接一个线程,即客户端有连接请求时服务器端就需要启动一个线程进行处理,如果这个连接不做任何事情会造成不必要的线程开销,当然可以通过线程池机制改善。
- Java NIO : 同步非阻塞,服务器实现模式为一个请求一个线程,即客户端发送的连接请求都会注册到多路复用器上,多路复用器轮询到连接有I/O请求时才启动一个线程进行处理。
- Java AIO(NIO.2) : 异步非阻塞,服务器实现模式为一个有效请求一个线程,客户端的I/O请求都是由OS先完成了再通知服务器应用去启动线程进行处理。
BIO,NIO,AIO使用场景分析:
- BIO适用于连接数目少且固定的架构,这种方式对服务器资源要求比较高,并发局限于应用中.
- NIO方式适用于连接数目多且连接比较短(轻操作)的架构,比如聊天服务器。
- AIO方式适用于连接数目多且连接比较长(重操作)的架构,比如相册服务器,充分调用OS参与并发操作,编程比较复杂。