1. NIO概念入门
1.1 IO底层流程
概念: OS可以执行所有的CPU指令,包括很多危险操作,所以为了程序的健壮性,CPU指令需要被分类:
- 内核态:位于内核内存空间,间由OS直接使用。
- 用户态:位于用户内存空间,如果想访问内核态指令,必须向OS申请。
IO的数据读取真实流程:磁盘上的数据读取到内核空间,然后再读取到用户空间。
1.2 IO常见模型
概念:
- IO模型:阻塞式IO模型:当用户发起一个read请求后,必须等到read出了结果,才可以去做别的事情。
- NIO模型:非阻塞式IO模型:当用户发起一个read请求后,立刻得到结果,这个结果是你要读的硬盘数据是否已经加载到了内核空间:
- 如果已经加载到了,开始从内核空间读取数据到用户空间(这个过程是阻塞的)。
- 如果没有加载到,每隔一段时间就重问一遍(间隔轮询)。
- 这种模式下,因为每次都会立刻得到结果而无需等待,所以不会阻塞,线程可以做其他的事情。
- NIO复用模型:同时执行并监听多个NIO,哪个NIO的内核空间从硬盘上成功加载到了数据,执行哪个NIO。
- AIO,异步IO模型:多个IO独立进行工作,互不干扰。
1.3 NIO
概念: NIO(Non-Block IO)是从jdk1.4版本开始引入的一个新的IO流API,可以替代标准的Java IO API,自带一个缓冲区,可以进行更加高效的文件读写操作。
- 对比IO:
- NIO与原来的IO有同样的作用和目的,但是使用的方式完全不同。
- IO面向流,NIO面向缓冲区。
- IO是阻塞的,读或者写的时候都会阻塞当前线程,NIO是非阻塞的,可以同时读和写。
- IO是单向的,要么读,要么写,NIO是双向的,同一个NIO实例,既可以读也可以写。
- 在Java API中提供了两套NIO,一套是针对标准输入输出NIO,另一套就是网络编程NIO。
2. NIO核心之Buffer缓冲
概念:
- Buffer是NIO的核心之一,可以保存多个相同类型的数据,且支持双向操作。
- Buffer根据数据类型不同(boolean除外) ,有以下Buffer的常用子类:
- ByteBuffer/ShortBuffer/IntBuffer/LongBuffer
- FloatBuffer/DoubleBuffer/CharBuffer
2.1 Buffer创建方式
概念: Buffer有两种类型:
- 直接缓冲区:DirectBuffer
- DirectBuffer位于计算机本地内存(native),与JVM无关,创建和销毁的开销比较大,也不容易被控制。
- DirectBuffer的传输效率高,因为本地内存和内核空间因为都在同一OS中,可以进行直接进行通信。
- DirectBuffer的内存回收工作需要自己想办法解决。
- 创建方式:
XXXBuffer.allocateDirect(1024):必须指定初始缓冲区容量字节大小。
- 非直接缓冲区:HeapBuffer
- HeapBuffer也叫Java堆缓冲区,位于JVM的堆内存中,创建和销毁的开销比较小,容易控制。
- HeapBuffer的传输效率低,因为JVM属于另一块虚拟的OS,想要和本机OS的内核空间进行数据传输,需要先再开辟一块本地内存作为中间媒介,这块本地内存被称为中间缓冲区。
- HeapBuffer的内存回收工作由JVM的GC管理,不用我们操心。
- 创建方式:
XXXBuffer.allocate(1024):必须指定初始缓冲区容量字节大小。
buffer.isDirect():判断一个缓冲区实例是否是直接缓冲区。
源码: /javase-advanced/
- src:
c.y.nio.BufferTest.build()
/**
* @author yap
*/
public class BufferTest {
@Test
public void build() {
ByteBuffer byteBuffer = ByteBuffer.allocate(1024);
System.out.println(byteBuffer.isDirect() ? "direct..." : "heap...");
}
}
2.2 Buffer核心属性
概念: Buffer有四个核心属性:
capacity: 最大容量:只能装这么多个字节的数据,一旦声明不能改变。- 设置:必须在构造缓冲区的时候指定。
- 获取:
Buffer capacity()
limit: 操作上限:你只能操作这么多个字节的数据,默认等于最大容量。- 设置:
void limit(int pos) - 获取:
int limit()
- 设置:
position: 当前位置:你已经操作到了这个位置,默认为0。- 设置:
Buffer position(int pos) - 获取:
int position()
- 设置:
mark: 位置备份:你在这个位置做了标记,标记为-1时失效。- 设置:
void mark():将position备份给mark。 - 获取:
void reset():将position移动到mark位置。
- 设置:
四个核心值的大小关系:0 <= mark <= position <= limit <= capacity
源码: /javase-advanced/
- src:
c.y.nio.BufferTest.coreAttributes()
/**
* @author yap
*/
public class BufferTest {
@Test
public void coreAttributes() {
ByteBuffer byteBuffer = ByteBuffer.allocate(1024);
byteBuffer.position(0).limit(1024);
System.out.println("capacity: " + byteBuffer.capacity());
System.out.println("position: " + byteBuffer.position());
System.out.println("limit: " + byteBuffer.limit());
}
}
2.3 Buffer基础操作
概念: 以 ByteBuffer 为例:
ByteBuffer put(byte[] src): 将字节数组src的全部内容传输到缓冲区。byte[] array():返回缓冲区内容的字节数组形式,该操作不移动position。Buffer flip():翻转为读:limit=position,position=0,mark=-1。byte get():获取缓冲区中的数据,每获取一个字节,position+1。byte get(index i):获取缓冲区中指i号置上的字节,该操作不移动position。Buffer rewind():倒带重读:position=0,mark=-1。Buffer clear():清除重置:position=0,limit=capacity,mark=-1。boolean hasRemaining():缓冲区中是否还有未读数据。int remaining():返回缓冲区中未读数据的字节数。
源码: /javase-advanced/
- src:
c.y.nio.BufferTest.bufferApi()
/**
* @author yap
*/
public class BufferTest {
@Test
public void bufferApi() {
ByteBuffer byteBuffer = ByteBuffer.allocate(1024);
byteBuffer.put("hello".getBytes(), 0, 5);
byteBuffer.put("world".getBytes());
System.out.println("data: " + new String(byteBuffer.array()));
byteBuffer.flip();
System.out.print("flip: ");
System.out.print((char) byteBuffer.get(3));
System.out.print((char) byteBuffer.get());
System.out.println((char) byteBuffer.get());
byteBuffer.rewind();
System.out.print("rewind: ");
System.out.print((char) byteBuffer.get());
System.out.println((char) byteBuffer.get());
byteBuffer.clear();
System.out.print("clear: ");
System.out.print((char) byteBuffer.get());
System.out.println((char) byteBuffer.get());
System.out.print("remaining: ");
if (byteBuffer.hasRemaining()) {
System.out.println(byteBuffer.remaining());
}
}
}
3. NIO核心之Channel通道
概念: 阻塞式IO的两端是App和硬盘数据,而非阻塞式NIO的两端是Buffer和硬盘数据(Buffer在App中),通过一个 java.nio.channels.FileChannel 接口来进行通信.
- 构造:
基础IO流实例.getChannel()。 - 方法:以ByteBuffer为例:
int read(ByteBuffer dst):将数据读到buffer中,读完返回-1。int write(ByteBuffer src):将buffer中的内容写出去。
3.1 FileChannel
概念: FileChannel是Channel的文件IO实现类,负责连接硬盘文件数据和App中的Buffer。
源码: /javase-advanced/
- src:
c.y.nio.FileChannelTest
/**
* @author yap
*/
public class FileChannelTest {
/**
* copy `nio-src.txt` to `nio-dest.txt`
*/
@Test
public void fileChannel() throws IOException {
String srcPath = "D:" + File.separator + "java-io" + File.separator + "nio-src.txt";
String destPath = "D:" + File.separator + "java-io" + File.separator + "nio-dest.txt";
FileInputStream fis = new FileInputStream(srcPath);
FileOutputStream fos = new FileOutputStream(destPath);
FileChannel fisChannel = fis.getChannel();
FileChannel fosChannel = fos.getChannel();
ByteBuffer byteBuffer = ByteBuffer.allocate(1024);
while (true) {
byteBuffer.clear();
int read = fisChannel.read(byteBuffer);
if (read == -1) {
break;
}
byteBuffer.flip();
fosChannel.write(byteBuffer);
}
fosChannel.close();
fisChannel.close();
fis.close();
fos.close();
System.out.println("copy over...");
}
}
3.2 SocketChannel
概念: SocketChannel是Channel的Socket实现类,是负责连接TCP客户端的通道,和它相对应的是ServerSocketChannel,是负责连接TCP服务端的通道。
- ServerSocketChannel相关API方法:
static ServerSocketChannel open():打开一个服务端通道。ServerSocketChannel bind(SocketAddress local):对服务端通道绑定一个Socket地址。new InetSocketAddress(int port):绑定只需要指定端口号,IP可选。
SocketChannel accept():服务端通道等待接收数据,该方法会阻塞当前线程。
- SocketChannel相关API方法:
static SocketChannel open(SocketAddress remote):打开一个客户端通道并连接到指定Socket地址。new InetSocketAddress(String host, int port):必须指定IP和端口。
源码: /javase-advanced/
- src:
c.y.nio.BlockSocketServer
package com.yap.nio;
import java.io.IOException;
import java.net.InetSocketAddress;
import java.net.SocketAddress;
import java.nio.ByteBuffer;
import java.nio.channels.ServerSocketChannel;
import java.nio.channels.SocketChannel;
/**
* @author yap
*/
public class BlockSocketServer {
public static void main(String[] args) throws IOException {
int port = 9999;
ServerSocketChannel serverSocketChannel = ServerSocketChannel.open();
SocketAddress socketAddress = new InetSocketAddress(port);
serverSocketChannel.bind(socketAddress);
System.out.println("ready to accept data...");
SocketChannel socketChannel = serverSocketChannel.accept();
ByteBuffer byteBuffer = ByteBuffer.allocate(1024);
while (socketChannel.read(byteBuffer) != -1) {
byteBuffer.flip();
for (int i = 0, j = byteBuffer.limit(); i < j; i++) {
System.out.print((char) byteBuffer.get());
}
System.out.println();
byteBuffer.clear();
}
socketChannel.close();
serverSocketChannel.close();
}
}
- src:
c.y.nio.BlockSocketClient
package com.yap.nio;
import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStreamReader;
import java.net.InetSocketAddress;
import java.net.SocketAddress;
import java.nio.ByteBuffer;
import java.nio.channels.SocketChannel;
/**
* @author yap
*/
public class BlockSocketClient {
public static void main(String[] args) throws IOException {
String ip = "127.0.0.1";
int port = 9999;
SocketAddress socketAddress = new InetSocketAddress(ip, port);
SocketChannel socketChannel = SocketChannel.open(socketAddress);
ByteBuffer byteBuffer = ByteBuffer.allocate(1024);
System.out.println("please input your message...");
BufferedReader br = new BufferedReader(new InputStreamReader(System.in));
String str;
while ((str = br.readLine()) != null) {
byteBuffer.put(("=> " + str).getBytes());
byteBuffer.flip();
socketChannel.write(byteBuffer);
byteBuffer.clear();
if ("exit".equalsIgnoreCase(str)) {
break;
}
}
br.close();
socketChannel.close();
}
}
4. NIO核心之Selector选择
概念: Selector可以监控多个Channel状态并作出对应处理,是设计非阻塞IO模型的最佳选择。
- Channel对应状态有四种
CONNECT:Channel已经准备好完成连接序列了(IP,端口等搭建完成)。ACCEPT:Channel可以调用accept()了。READ:Channel可以调用 `read(
WRITE:Channel可以调用write()了。
- 构造:
static Selector open():开启一个Selector实例。 - Channel相关API方法:
SelectableChannel configureBlocking(boolean block):配置Channel是否为可阻塞Channel。SelectionKey register(Selector sel, int ops):将Channel注册给Selector:- param1: 将Channel注册给哪个Selector。
- param2: 此Channel感兴趣的状态,只有在这个状态下开可以调用
channel()获取我。
- Selector常用API方法:
int select():返回选择器中已经处于就绪状态(准备好进行IO操作)的状态个数,这个状态被记作是一个SelectionKey,至少选择一个才会返回,否则阻塞。Set<SelectionKey> selectedKeys():返回选择器中已经处于就绪状态(可以进行IO操作)的Set集合。
- SelectionKey常用API方法:
boolean isAcceptable():判断是否是OP_ACCEPT状态。boolean isReadable():判断是否是OP_READ状态。SelectableChannel channel():获取当前状态对应的Channel。
源码: /javase-advanced/
- src:
c.y.nio.NonBlockSocketServer
package com.yap.nio;
import java.io.IOException;
import java.net.InetSocketAddress;
import java.net.SocketAddress;
import java.nio.ByteBuffer;
import java.nio.channels.SelectionKey;
import java.nio.channels.Selector;
import java.nio.channels.ServerSocketChannel;
import java.nio.channels.SocketChannel;
import java.util.Iterator;
/**
* @author yap
*/
public class NonBlockSocketServer {
public static void main(String[] args) throws IOException {
int port = 9002;
ServerSocketChannel serverSocketChannel = ServerSocketChannel.open();
SocketAddress socketAddress = new InetSocketAddress(port);
serverSocketChannel.bind(socketAddress);
serverSocketChannel.configureBlocking(false);
System.out.println("ready to accept data...");
Selector selector = Selector.open();
serverSocketChannel.register(selector, SelectionKey.OP_ACCEPT);
SocketChannel socketChannel = null;
while (selector.select() > 0) {
Iterator<SelectionKey> it = selector.selectedKeys().iterator();
while (it.hasNext()) {
SelectionKey selectionKey = it.next();
if (selectionKey.isConnectable()) {
socketChannel = serverSocketChannel.accept();
socketChannel.configureBlocking(false);
System.out.println(socketChannel.getLocalAddress());
}
if (selectionKey.isAcceptable()) {
socketChannel = serverSocketChannel.accept();
socketChannel.configureBlocking(false);
socketChannel.register(selector, SelectionKey.OP_READ);
} else if (selectionKey.isReadable()) {
socketChannel = (SocketChannel) selectionKey.channel();
ByteBuffer byteBuffer = ByteBuffer.allocate(1024);
while (socketChannel.read(byteBuffer) > 0) {
byteBuffer.flip();
for (int i = 0, j = byteBuffer.limit(); i < j; i++) {
System.out.print((char) byteBuffer.get());
}
System.out.println();
byteBuffer.clear();
}
}
it.remove();
}
}
if (socketChannel != null) {
socketChannel.close();
}
serverSocketChannel.close();
}
}
- src:
c.y.nio.NonBlockSocketClient
package com.yap.nio;
import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStreamReader;
import java.net.InetSocketAddress;
import java.net.SocketAddress;
import java.nio.ByteBuffer;
import java.nio.channels.SocketChannel;
/**
* @author yap
*/
public class NonBlockSocketClient {
public static void main(String[] args) throws IOException {
String ip = "127.0.0.1";
int port = 9002;
SocketAddress socketAddress = new InetSocketAddress(ip, port);
SocketChannel socketChannel = SocketChannel.open(socketAddress);
socketChannel.configureBlocking(false);
ByteBuffer byteBuffer = ByteBuffer.allocate(1024);
System.out.println("please input your message...");
BufferedReader br = new BufferedReader(new InputStreamReader(System.in));
String str;
while ((str = br.readLine()) != null) {
byteBuffer.put(("=> " + str).getBytes());
byteBuffer.flip();
socketChannel.write(byteBuffer);
byteBuffer.clear();
}
br.close();
socketChannel.close();
}
}