一、概述
1.1、IO 的作用:传输数据
1.2、JDK中 IO 的分类:
- BIO - BlockingIO - 同步阻塞式IO
- NIO - NewIO - NonBlockingIO - 同步非阻塞式IO,在JDK1.4中出现 — tomcat
- AIO - AsynchronousIO - 异步非阻塞式IO,在JDK1.7中出现 - AIO本身是在NIO的基础上进行了改进,因此称之为NIO.2
注: 在现在开发中,如果需要进行大量的短任务,那么使用NIO;如果需要进行长任务,会使用BIO。
1.3、概念回顾 (同步、异步、阻塞、非阻塞)
- 同步:一个对象或者一段逻辑在一个时间段内只能被一个线程占用
- 异步:一个对象或者一段逻辑在一个时间段内允许被多个线程抢占
- 阻塞:线程在获取到结果之前会持续等待,既不往下执行代码也不报错
- 非阻塞:线程无论是否获取到结果,都会继续往下执行或者报错
二、BIO的缺点
- 阻塞:阻塞必然导致效率降低
- 一对一连接:每次客户端发起连接,服务器端都需要产生一个线程去处理这个连接。如果有大量的客户端发起连接,那么就意味着服务器端需要产生大量线程去处理请求。服务器端的线程如果过多,就会导致服务器的卡顿甚至崩溃
- 无效连接:在BIO中,无法处理无效的空连接。如果有大量的无线连接产生,就会大量占用服务器端的线程。此时依然可能会导致服务器的卡顿甚至于崩溃
三、NIO的三大组件:
3.1、Buffer - 缓冲区
- 作用:临时存储数据
- Buffer底层是基于数组来进行存储的,只能存储基本类型的数据。Buffer针对八种基本类型,提供了7个实现类,唯独没有针对boolean类型的实现类
- 重要位置:capacity >= limit >= position
- capacity:容量位。用于标记缓冲区的容量,指向缓冲区的最后一位。容量位不可变
- position:操作位。类似于数组中的下标,position指向哪一位就会读写哪一位。每次读写完成之后position会自动的后挪一位。在缓冲区刚创建的时候,position默认执行第0位
- limit:限制位。用于限定position所能达到的最大下标的。当limit和position重合的时候,表示所有的数据都已经被读写完。在缓冲区刚创建的时候,limit默认和capacity重合
3.2、Channel - 通道
- 作用:传输数据
- Channel可以实现双向传输
- Channel默认是阻塞的,但是可以手动设置为非阻塞
3.2.1、常见的Channel
- 文件:FileChannel
- UDP:DatagramChannel
- TCP:SocketChannel,ServerSocketChannel
注:利用FileChannel实现"Zero-copy"(零拷贝)机制 - 零拷贝指的并不是没有数据传输而是没有状态的转化Channel都是面向Buffer进行操作。
3.3、Selector - 多路复用选择器
- 作用:针对事件(客户端和服务器端之间能够产生的操作)来进行选择。
- 实际生产过程中,会考虑将选择器放在服务器端来设置。
- Selector是面向Channel操作,要求Channel必须是非阻塞的。
四、代码实例:
3.1、Buffer - 缓冲区
public class ByteBufferDemo2 {
public static void main(String[] args) {
// byte[] arr = new byte[n] - 数据未知
// ByteBuffer buffer = ByteBuffer.allocate();
// 数据已知
// byte[] arr = {n1, n2, n3 ...}
// ByteBuffer buffer = ByteBuffer.wrap("hello".getBytes());
// buffer.put("a".getBytes());
ByteBuffer buffer = ByteBuffer.allocate(1024);
buffer.put("hello big2007 ~~~".getBytes());
// 获取底层对应的字节数组
// 如果改变数组中的数据,缓冲区中的数据也会跟着变
byte[] arr = buffer.array();
// arr[0] = 97;
// System.out.println(new String(arr, 0, buffer.position()));
buffer.flip();
System.out.println(new String(arr, 0, buffer.limit()));
}
}
3.2、Channel - 通道
3.2.1、Server 服务端
public class Server {
public static void main(String[] args) throws IOException {
// 开启服务器端通道
ServerSocketChannel ssc = ServerSocketChannel.open();
// 绑定端口
ssc.bind(new InetSocketAddress(8090));
// 设置为非阻塞
ssc.configureBlocking(false);
// 接收连接
SocketChannel sc = ssc.accept();
// 判断是否接收到了连接
while (sc == null)
sc = ssc.accept();
// 连接建立
// 读取数据
ByteBuffer buffer = ByteBuffer.allocate(1024);
sc.read(buffer);
// 解析数据
System.out.println(new String(buffer.array(), 0, buffer.position()));
// 关流
ssc.close();
}
}
3.3.2、Client 客户端
public class Client {
public static void main(String[] args) throws IOException {
// 开启客户端通道
SocketChannel sc = SocketChannel.open();
// 手动设置为非阻塞
sc.configureBlocking(false);
// 发起连接 - 阻塞
// 非阻塞:无论是否建立连接,都会继续往下执行
sc.connect(new InetSocketAddress("localhost", 8090));
// 判断连接是否建立
// 如果多次连接没有成功,说明这个连接无法建立
while (!sc.isConnected()) {
// 试图重新建立连接
// 这个方法会自动进行计数,多次计数之后如果仍然无法建立连接,会抛出异常
sc.finishConnect();
}
// 连接建立
// 写出数据
sc.write(ByteBuffer.wrap("hello server".getBytes()));
// 关闭连接
sc.close();
}
}
3.3、Selector - 多路复用选择器
3.3.1、Server 服务端
public class Server {
public static void main(String[] args) throws IOException {
// 开启服务器端的通道
ServerSocketChannel ssc = ServerSocketChannel.open();
// 绑定端口
ssc.bind(new InetSocketAddress(8070));
// 设置非阻塞
ssc.configureBlocking(false);
// 获取选择器
// 实际过程中,将选择器设置为单例的
Selector sel = Selector.open();
// 将通道注册到选择器上,指定要注册的事件
ssc.register(sel, SelectionKey.OP_ACCEPT);
// 实际生产过程中,服务器开了应该不会关闭
// 用while(true)来模拟服务器一直开着
while (true) {
// 当服务器一直开着的时候,随着运行时间的延长,服务器就会接收到越来越多的请求
// 不代表这些请求都是有用请求 - 需要进行选择,选出有用的请求
sel.select();
// 确定这一些请求可能会出发的操作:accept/read/write
Set<SelectionKey> set = sel.selectedKeys();
// 遍历集合,根据请求的类型不同来分别处理
Iterator<SelectionKey> it = set.iterator();
while (it.hasNext()) {
// 获取到请求类型
SelectionKey key = it.next();
// 可接受事件
if (key.isAcceptable()) {
// 先从选择器中获取到通道
ServerSocketChannel sscx = (ServerSocketChannel) key.channel();
// 接收请求
SocketChannel sc = sscx.accept();
// 设置非阻塞
sc.configureBlocking(false);
// 注册可读事件
// sc.register(sel, SelectionKey.OP_READ);
// 注册可写事件
// sc.register(sel, SelectionKey.OP_WRITE);
// 在注册事件的时候,后注册的事件会覆盖之前注册的事件
// sc.register(sel, SelectionKey.OP_READ + SelectionKey.OP_WRITE);
// sc.register(sel, SelectionKey.OP_READ | SelectionKey.OP_WRITE);
sc.register(sel, 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()));
// 注销可读事件
// key.interestOps()获取到当前通道上的所有事件 --- 再从这些事件中抠掉可读事件
// sc.register(sel, key.interestOps() - SelectionKey.OP_READ);
sc.register(sel, key.interestOps() ^ SelectionKey.OP_READ);
}
// 可写事件
if (key.isWritable()) {
// 获取通道
SocketChannel sc = (SocketChannel) key.channel();
// 写出数据
sc.write(ByteBuffer.wrap("hello client".getBytes()));
// 注销可写事件
sc.register(sel, key.interestOps() - SelectionKey.OP_WRITE);
}
// 移除
it.remove();
}
}
}
}
3.3.2、Client 客户端
public class Client {
public static void main(String[] args) throws IOException {
// 开启客户端通道
SocketChannel sc = SocketChannel.open();
// 发起连接
sc.connect(new InetSocketAddress("localhost", 8070));
// 写出数据
sc.write(ByteBuffer.wrap("hi from client".getBytes()));
// 读取数据
ByteBuffer buffer = ByteBuffer.allocate(1024);
sc.read(buffer);
System.out.println(new String(buffer.array(), 0, buffer.position()));
// 关流
sc.close();
}
}
• 由 ChiKong_Tam 写于 2020 年 12 月 28 日