1、简介
1.1 Java中的IO介绍
- BIO:BlockingIO,同步式阻塞式IO,即传统的IO,是java中最早期的流
- NIO:Non-BlockingIO,又称New IO,同步式非阻塞IO,是JDK1.4提供的流
- AIO:AsynchronousIO,异步是非阻塞IO,可以认为是NIO的二代版本,是JDK1.8提供的流
1.2 概述
- NIO是JDK1.4出现的一个新的用于进行数据传输的流
- 全称是Non-BlockingIO,是一种同步式非阻塞式的IO,也是一种能供进行多路复用的IO
- NIO中有3大组件:Buffer、Channel、Selector
- NIO在使用的时候可以基于事件驱动方式来实现
1.3 BIO的缺点
- 一对一的连接方式:即每一个连接请求对应一个线程,在请求最大的情况下,会导致服务器端的压力非常大而致整个服务器的效率变低
- 阻塞:当线程在进行read或者write的时候,除非读完或者写完,否则在这个过程中不能发生任何操作
- 单项传输:数据只能从一端传向另一端,如果需要反向传输需要令创建流对象
1.4 NIO的特点
- 一对多的连接方式:利用一个或者少量线程处理大量的连接请求,降低服务器端的压力
- 非阻塞:在线程不能进行read或者write方法的时候,立即返回0,等待下一次操作
- 双向传输:利用通道可以实现数据的双向
1.5 NIO的缺点
- 在请求量比较大的情况下会出现部分请求的响应时间比较长的现象
- 不适用于长任务场景,不然会导致其他的请求无法处理
1.6 BIO和NIO的比较
2. ByteBuffer
2.1 概述
- 字节缓冲区,继承了Buffer类
- 底层是依靠字节数组来存储数据
- 本身是一个抽象类,需要利用其子类创建对象或者是利用其提供的allocate或者wrap方法来创建ByteBuffer对象
- 重要位置:capacity >= limit >= position >=mark
2.2 重要位置
- capacity:容量位,用于标记该缓冲区的容量,在缓冲区创建好之后就不再改变
- limit:限制位,用于限制操作位position所能达到的最大位置。在缓冲区刚创建的时候指向容量位
- position:操作位,用于指向要操作的位置,实际意义类似于数组中的下标,在缓冲区刚创建的时候指向0
- mark:标记位,用于进行标记,在缓冲区刚创建的时候指向-1,默认不启用
2.3 重要方法
2.4 示例
Demo01
package cn.tedu.buffer;
import java.nio.ByteBuffer;
public class ByteBufferDemo {
public static void main(String[] args) {
// 表示给底层的字节数组来指定大小
// 缓冲区在给定之后,长度就不能改变了
ByteBuffer buffer =
ByteBuffer.allocate(10);
// 添加数据
buffer.put("abc".getBytes());
buffer.put((byte) 0);
buffer.put("def".getBytes());
// 将position挪动
// buffer.position(0);
// 获取元素
// 获取的是一个字节
// byte b = buffer.get();
// System.out.println(b);
// byte b2 = buffer.get();
// System.out.println(b2);
// 遍历
// 将limit挪到position的位置上
// 将position归零
// buffer.limit(buffer.position());
// buffer.position(0);
// 上述两部操作称之为翻转缓冲区
// 等价于
buffer.flip();
// while(buffer.position() < buffer.limit()){
// 等价
while(buffer.hasRemaining()){
byte b = buffer.get();
System.out.println(b);
}
}
}
Demo02
package cn.tedu.buffer;
import java.nio.ByteBuffer;
public class ByteBufferDemo2 {
public static void main(String[] args) {
// 适合于数据未知的场景
// ByteBuffer buffer = ByteBuffer.allocate();
// 数据已知
// ByteBuffer buffer =
// ByteBuffer.wrap("hello big2002".getBytes());
// System.out.println(buffer.position());
ByteBuffer buffer = ByteBuffer.allocate(10);
buffer.put("hello".getBytes());
// 获取缓冲区底层的字节数组
byte[] data = buffer.array();
// System.out.println(new String(data, 0,
// buffer.position()));
buffer.flip();
System.out.println(new String(data, 0,
buffer.limit()));
}
}
3. Channel
3.1 简介
- Channel,称之为通道,在NIO中用于完成数据的传输
- 在操作的时候是面向缓冲区进行的
- 可以实现数据的双向传输
- Channel默认是阻塞的,可以手动设置为非阻塞
3.2 FileChannel
3.2.1 概述
- FileChannel,顾名思义是面向文件的通道
- 可以利用FileChannel完成对文件的读写操作
- 利用FileChannel读取文件的时候,是先将文件中的内容映射到虚拟内存中,然后在读取到程序的缓冲区中
- FileChannel不能直接创建,可以利用FileInputStream、FileOutPutStream、RandomAccessFile对象中的个体Channel()方法获取
- 如果是通过FileInputStream获取FileChannel,那么只能进行读取操作
- 如果是通过FileOutputStream获取FileChannel,那么只能进行写入操作
- 如果是通过RandomAccessStream获取FileChannel,那么可以进行读写操作
3.2.2 示例
读取过程
@Test
public void readFile() throws Exception {
// 创建RandomAccessFile对象。指定模式为读写模式
RandomAccessFile raf = new RandomAccessFile("F:\\a.txt", "rw");
// 获取FileChannel对象
FileChannel fc = raf.getChannel();
// 创建缓冲区用于存储数据
ByteBuffer buffer = ByteBuffer.allocate(10);
// 记录读取的字节个数
int len;
// 读取数据
while ((len = fc.read(buffer)) != -1) {
System.out.println(new String(buffer.array(), 0, len));
buffer.flip();
}
// 关流
raf.close();
}
写入过程
@Test
public void writeFile() throws Exception {
// 创建RandomAccessFile对象。指定模式为读写模式
RandomAccessFile raf = new RandomAccessFile("F:\\test.txt", "rw");
// 获取FileChannel对象
FileChannel fc = raf.getChannel();
// 创建缓冲区,并且将数据放入缓冲区
ByteBuffer src = ByteBuffer.wrap("hello".getBytes());
// 利用通道写出数据
fc.write(src);
// 关流
raf.close();
}
复制文件
@Test
public void copyFile() throws Exception {
// 创建流对象指向对应的文件
FileInputStream in = new FileInputStream("F:\\a.txt");
FileOutputStream out = new FileOutputStream("E:\\a.txt");
// 获取FileChannel对象
FileChannel src = in.getChannel();
FileChannel dest = out.getChannel();
// 准备缓冲区
ByteBuffer buffer = ByteBuffer.allocate(10);
// 读取数据,将读取到的数据写出
while (src.read(buffer) != -1) {
buffer.flip();
dest.write(buffer);
buffer.clear();
}
// 关流
in.close();
out.close();
}
3.3 UDP
3.3.1 概述
- 用于进行UDP收发的通道
- 是无连接的网络协议,只能进行发送和接受的操作
- 基本类是DatagramChannel,是一个抽象类
3.3.2 示例
发送端
@Test
public void send() throws IOException {
// 开启通道
DatagramChannel dc = DatagramChannel.open();
// 准备数据
ByteBuffer buffer = ByteBuffer.wrap("hello".getBytes());
// 发送数据
dc.send(buffer, new InetSocketAddress("localhost", 8090));
// 关闭通道
dc.close();
}
接收端
@Test
public void recieve() throws IOException {
// 开启通道
DatagramChannel dc = DatagramChannel.open();
// 绑定连接地址和端口号
dc.socket().bind(new InetSocketAddress(8090));
// 准备缓冲区
ByteBuffer buffer = ByteBuffer.allocate(1024);
// 接收数据
dc.receive(buffer);
System.out.println(new String(buffer.array(), 0, buffer.position()));
// 关闭通道
dc.close();
}
3.4 TCP
3.4.1 概述
- 用于进行TCP通信的通道
- 需要进行连接的网络协议
- 提供了连接、接收、读取、写入操作
- 客户端通道是SocketChannel,服务器端通道是ServerSocketChannel
3.4.2 示例
客户端
@Test
public void client() 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".getBytes()));
// 关闭通道
sc.close();
}
服务器端
@Test
public void server() throws IOException {
// 开启服务器端通道
ServerSocketChannel ssc = ServerSocketChannel.open();
// 绑定要监听的端口
ssc.socket().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();
}
4. Selector
4.1 概述
- Selector,称之为多路复用选择器
- 对通道进行选择,需要基于事件进行驱动
- 针对了四类事件:connect、accept、read、write,四类事件定义在SelectionKey中
- 可以实现利用一个或者少量线程处理大量请求
- 适用于大量的短任务场景,不适用于长任务场景
- Selector针对的必须是非阻塞的通道
4.2 示例
作用:针对通道的指定事件来进行选择
Selector在使用的时候针对非阻塞通道进行操作
客户端
package cn.tedu.selector;
import java.io.IOException;
import java.net.InetSocketAddress;
import java.nio.ByteBuffer;
import java.nio.channels.SocketChannel;
public class Client {
public static void main(String[] args) throws IOException {
SocketChannel sc = SocketChannel.open();
sc.connect(new InetSocketAddress("localhost",8090));
sc.write(ByteBuffer.wrap("hello server".getBytes()));
//读取数据
ByteBuffer buffer = ByteBuffer.allocate(1024);
sc.read(buffer);
System.out.println(
new String(buffer.array(),0,buffer.position()));
}
}
服务器端
package cn.tedu.selector;
import java.io.IOException;
import java.net.InetSocketAddress;
import java.nio.ByteBuffer;
import java.nio.channels.*;
import java.util.Iterator;
import java.util.Set;
public class Srever {
public static void main(String[] args) throws IOException {
//开启服务器通道
ServerSocketChannel ssc = ServerSocketChannel.open();
//设置非阻塞
ssc.configureBlocking(false);
//绑定端口
ssc.bind(new InetSocketAddress(8090));
//开启选择器
Selector selc = Selector.open();
//将通道注册到选择器上
ssc.register(selc, SelectionKey.OP_ACCEPT);
//模拟:服务器开启之后不关闭
while(true){
//随着运行时间的延长,接收到的请求会越来越多
//需要针对这些请求来进行选择,将能触发时间的请求留下
//将不能触发事件的请求过滤掉
selc.select();
//选完之后能够留下来的请求都是有用的请求
//connect/read/write
//获取请求的事件类型
Set<SelectionKey> set = selc.selectedKeys();
//需要针对请求的不同类型类进行分门别类的处理
Iterator<SelectionKey> it = set.iterator();
while(it.hasNext()){
SelectionKey key = it.next();
//触发服务器的accept操作 - 说明客户端一定调用了connect方法
if (key.isAcceptable()){
//从事件中获取通道
ServerSocketChannel sscx = (ServerSocketChannel) key.channel();
//接收连接
SocketChannel sc = sscx.accept();
sc.configureBlocking(false);
//根据需求确定,如果需要读操作,那么就给READ
//如果需要写操作,那就给WRITE
//如果存在多个register,那么后边的会覆盖前边的
sc.register(selc,
// SelectionKey.OP_READ + SelectionKey.OP_WRITE);
// SelectionKey.OP_READ | SelectionKey.OP_WRITE);
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()));
//读取完成之后,需要将READ事件从通道身上移除掉
//key.interestOps() 获取到所有事件
sc.register(selc,
key.interestOps() - SelectionKey.OP_READ);
// key.interestOps() ^ SelectionKey.OP_READ);
}
if (key.isWritable()){
SocketChannel sc = (SocketChannel) key.channel();
sc.write(ByteBuffer.wrap("收到数据啦~~".getBytes()));
sc.register(selc,
key.interestOps() - SelectionKey.OP_WRITE);
}
//处理完成之后,需要将这一大类时间移除掉
it.remove();
}
}
}
}