IO操作主要可分为两阶段
1)把磁盘或者网络数据加载到内核的内存空间
2)把内核的内存空间数据复制到用户进程的内存空间中
阻塞、非阻塞的区别是在于第一阶段,即数据准备阶段。如果在数据准备时,主线程必须等待,就为阻塞;不需要一直等待可以执行其他操作,就是非阻塞。
同步、异步的区别在于第二阶段,如果是用户进程需要主动复制数据到用户内存,则为同步;如果由内核完成数据报复制之后主动返回数据则为异步
前面说到,java中I/O编程,大致可以分为三种,阻塞IO(BIO)、非阻塞IO(NIO)和异步IO(AIO)。
BIO
BIO就是同步阻塞IO。在传统的同步阻塞模型开发中,ServerSocket负责绑定IP地址,启动监听端口;Socket负责发起连接操作。连接成功后,双方通过输入和输出流进行同步阻塞式通信。 传统的socket通讯都是阻塞的,服务端接收到客户端请求直到复制完数据都是阻塞的
AIO
异步非阻塞IO,式真正的异步IO,将数据报复制等操作交给内核完成,用户进程可以处理其他事情而不需要干涉
底层过程同 NIO,区别在于,AIO 使用的命令是 epoll
,使用事件驱动的方式来代替轮询的方式,当监听的 I/O 准备好了,采用事件驱动(事件回调)的方式通知进程去获取数据
NIO
同步非阻塞的IO,底层是采用操作系统的IO多路复用模型,通过操作系统的select()/epoll()方法监听多个通道,一旦有一个channel数据报准备好,就通知应用程序去复制数据报。
非阻塞体现:一个 select 处理多个客户应用进程的 I/O,如果第一个 I/O 数据没有准备好,那么就去处理第二个客户端的 I/O,依此类推,客户端之间谁的数据先准备好就先处理谁的,不存在第二个要等第一个处理完才能开始处理的情况;
IO多路复用是阻塞在select,epoll这样的系统调用之上,而没有阻塞在真正的I/O系统调用之上。
提供了与传统BIO模型中的Socket和ServerSocket相对应的SocketChannel和ServerSocketChannel两种不同的套接字通道实现。两种通道都支持阻塞和非阻塞两种模式,默认采用阻塞的实现方式
1.缓冲区 Buffer
在 NIO 中,所有数据都是用缓冲区处理的。在读取数据时,它是直接读到缓冲区中的; 在写入数据时,它也是写入到缓冲区中的。所有的缓冲区类型都继承于抽象类 Buffer,比方说:ByteBuffe、CharBuffer、 ShortBuffer、IntBuffer、LongBuffer、FloatBuffer、DoubleBuffer.都实现了相同的Buffer接口。
缓冲区本质上是一个数组,并提供了跟踪和记录缓冲区的状态变化的信息。
其中最重要的属性是下面三个,它们一起合作完成对缓冲区内部状态的变化跟踪:
position:当前操作数据所在的位置,也可以理解做游标,当调用 get()/put()方法读取或者写入缓冲区的时候,position会自 动更新,在新创建一个 Buffer 对象时,position 初始值为0
limit:指定还有多少数据需要取出(在从缓冲区写入通道时),或者还有多少空间可以放入数据(在从通道读入缓冲区时)。
读取缓冲区的数据时,如果limit > position 则认为在缓冲区还有数据可以读取
capacity:指定可以存储在缓冲区中的最大数据容量,实际上,它指定了底层数组的大小
这三个属性之间满足:0 <=position <= limit <=capacity 的关系
使用缓冲区读取和写入数据通常遵循以下四个步骤:
- 写数据到缓冲区;
- 调用buffer.flip()方法;
- 从缓冲区中读取数据;
- 调用buffer.clear()
在向buffer中写入数据时,position会记录下当前数据写入的位置,如果写入完成需要读取数据,那么就需要通过flip()方法将Buffer从写模式切换到读模式,其实就是锁定操作范围,让数据操作范围索引只能在position - limit 之间,源码如下。读取完所有的数据后,就需要清空缓冲区,使得buffer可以再次被写入
//完成两件事:
//1. 把limit 设置为当前的 position 值
//2. 把position 设置为0
public final Buffer flip() {
limit = position;
position = 0;
mark = -1;
return this;
}
2.通道 Channel
我们对数据的读取和写入要通过Channel,它就像水管一样,是一个通道。通道区别于流的地方在于通道是双向的,可以用于读、写或者同时读写操作。 NIO中,任何时候读取数据,都不是直接从通道读取,而是从通道读取到缓冲区。然后再操作缓冲区中的数据
操作系统底层的通道一般都是全双工的,所以全双工的Channel比Stream能更好的映射底层操作系统的API。
Channel主要分两大类:
SelectableChannel:用户网络读写
FileChannel:用于文件操作
后面代码会涉及的ServerSocketChannel和SocketChannel都是SelectableChannel的子类。
3.多路复用器 Selector
Selector是Java NIO 编程的基础。
Selector--选择器,顾名思义就是提供选择已经就绪的任务的能力:Selector会不断轮询注册在其上的Channel,查看是否某个Channel读或者写事件就绪。我们可以通过SelectionKey获取就绪Channel的集合,然后根据其状态进行后续的操作。
一个Selector可以同时轮询多个Channel,因为JDK使用了epoll()代替传统的select实现,所以没有最大连接句柄1024/2048的限制。所以,只需要一个线程负责Selector的轮询,就可以接入成千上万的客户端。
服务端
/**
* 2020/3/8
* created by chenpp
*/
public class NioServer {
private int port;
private static Selector selector = null;
/**
* 指定端口号启动服务
* */
public boolean startServer(int port){
try {
this.port = port;
selector = Selector.open();
//打开监听通道
ServerSocketChannel server = ServerSocketChannel.open();
//绑定端口
server.bind(new InetSocketAddress(this.port));
//默认configureBlocking为true,如果为 true,此通道将被置于阻塞模式;如果为 false.则此通道将被置于非阻塞模式
server.configureBlocking(false);
//创建选择器
selector = Selector.open();
//监听客户端连接请求
server.register(selector, SelectionKey.OP_ACCEPT);
System.out.println("服务端启动成功,监听端口:" + port);
}catch (Exception e){
System.out.println("服务器启动失败");
return false;
}
return true;
}
public void listen() throws IOException {
while(true){
//阻塞方法,轮询注册的channel,当至少一个channel就绪的时候才会继续往下执行
int keyCount = selector.select();
System.out.println("当前有:"+keyCount+"channel有事件就绪");
//获取就绪的SelectionKey
Set<SelectionKey> keys = selector.selectedKeys();
Iterator<SelectionKey> it = keys.iterator();
SelectionKey key = null;
//迭代就绪的key
while(it.hasNext()){
key = it.next();
it.remove();
//SelectionKey相当于是一个Channel的表示,标记当前channel处于什么状态
// 按照channel的不同状态处理数据
process(key);
}
}
}
private void process(SelectionKey key) throws IOException {
//该channel已就绪,可接收消息
if(key.isAcceptable()){
System.out.println("accept事件就绪...");
doAccept(key);
}else if(key.isReadable()){
System.out.println("read事件就绪...");
doRead(key);
}else if(key.isWritable()){
System.out.println("write事件就绪...");
doWrite(key);
}
}
private void doWrite(SelectionKey key) throws IOException {
//获取对应的socket
SocketChannel socket = (SocketChannel)key.channel();
//获取key上的附件
String content = (String)key.attachment();
socket.write(ByteBuffer.wrap(content.getBytes()));
socket.close();
}
private void doRead(SelectionKey key) throws IOException {
//获取对应的socket
SocketChannel socket = (SocketChannel)key.channel();
//设置一个读取数据的Buffer 大小为1024
ByteBuffer buff = ByteBuffer.allocate(1024);
StringBuilder content = new StringBuilder();
while(socket.read(buff) > 0) {
buff.flip();
content.append(new String(buff.array(),"utf-8"));
}
//注册selector,并设置为可写模式
key = socket.register(selector,SelectionKey.OP_WRITE);
//在key上携带一个附件(附近就是之后要写的内容)
key.attach("服务端已收到:"+content);
System.out.println("读取内容:" + content);
}
private void doAccept(SelectionKey key) throws IOException {
//获取对应的channel
ServerSocketChannel server = (ServerSocketChannel)key.channel();
//从channel中获取socket信息
SocketChannel socket = server.accept();
//设置为非阻塞模式
socket.configureBlocking(false);
//注册selector,并设置为可读模式
socket.register(selector, SelectionKey.OP_READ);
}
}
/**
* 2020/3/8
* created by chenpp
*/
public class NioServerStarter {
public static void main(String[] args) throws IOException {
NioServer nioServer = new NioServer();
nioServer.startServer(8080);
nioServer.listen();
}
}
客户端
package com.chenpp.nio;
import javax.sound.midi.SoundbankResource;
import java.io.IOException;
import java.net.InetSocketAddress;
import java.nio.ByteBuffer;
import java.nio.channels.SelectionKey;
import java.nio.channels.Selector;
import java.nio.channels.SocketChannel;
import java.util.Iterator;
import java.util.Set;
import java.util.UUID;
/**
* 2020/3/8
* created by chenpp
*/
public class NioClient {
private static Selector selector = null;
public void start(String ip, int port) throws IOException {
//创建选择器
selector = Selector.open();
//打开监听通道
SocketChannel socketChannel = SocketChannel.open();
socketChannel.configureBlocking(false);
//连接对应的服务器 ip , port
socketChannel.connect(new InetSocketAddress(ip, port));
//注册select为连接状态
socketChannel.register(selector, SelectionKey.OP_CONNECT);
System.out.println("客户端,启动成功...");
}
public void listen() throws IOException {
while (true) {
//阻塞方法,轮询注册的channel,当至少一个channel就绪的时候才会继续往下执行
selector.select();
//获取就绪的SelectionKey
Set<SelectionKey> keys = selector.selectedKeys();
Iterator<SelectionKey> it = keys.iterator();
SelectionKey key = null;
//迭代就绪的key
while (it.hasNext()) {
key = it.next();
it.remove();
//SelectionKey相当于是一个Channel的表示,标记当前channel处于什么状态
// 按照channel的不同状态处理数据
process(key);
}
}
}
private void process(SelectionKey key) throws IOException {
//channel处于可连接状态,发送消息给服务端
if (key.isConnectable()) {
System.out.println("connect事件就绪 ....");
SocketChannel clientChannel = (SocketChannel) key.channel();
if (clientChannel.isConnectionPending()) {
clientChannel.finishConnect();
}
clientChannel.configureBlocking(false);
String name = UUID.randomUUID().toString();
System.out.println("客户端发送数据:{}" + name);
ByteBuffer buffer = ByteBuffer.wrap(name.getBytes());
clientChannel.write(buffer);
clientChannel.register(key.selector(), SelectionKey.OP_READ);
} else if (key.isReadable()) {
//获取对应的socket
System.out.println("read事件就绪 ....");
SocketChannel socket = (SocketChannel) key.channel();
//设置一个读取数据的Buffer 大小为1024
ByteBuffer buff = ByteBuffer.allocate(1024);
StringBuilder content = new StringBuilder();
int len = socket.read(buff);
if (len > 0) {
buff.flip();
content.append(new String(buff.array(), "utf-8"));
//让客户端读取下一次read
System.out.println("客户端收到反馈:" + content);
key.interestOps(SelectionKey.OP_READ);
}else if(len <= 0){
key.cancel();
socket.close();
}
}
}
}
/**
* 2020/3/8
* created by chenpp
*/
public class NioClientStarter {
public static void main(String[] args) throws IOException {
NioClient client = new NioClient();
client.start("localhost",8080);
client.listen();
}
}
NIO工作原理:
- 由一个专门的线程来处理所有的 IO 事件,并负责分发。
- 事件驱动机制:事件到达的时候触发,而不是同步的去监视事件。
参考: