文章目录
一、NIO
NIO支持面向缓冲区的、基于通道的IO操作。NIO以更高效的方式进行文件的读写操作。传统的IO的read和write只能阻塞执行,线程在读写IO期间不能干其他事情,比如调用socket.read()时,如果服务器一直没有数据传输过来,线程就一直阻塞,但NIO中可以配置socket为非阻塞模式。
NIO的三大核心:Channel(通道)、Buffer(缓冲区)、Selector(选择器)。NIO基于通道和缓冲区进行操作,数据总是从通道读取到缓冲区中,或者从缓冲区写入通道中。选择器用于监听多个通道的事件,如连接请求、数据到达等,因此可以用单个线程就可以监听多个用客户端通道。
NIO是可以做到用一个线程来处理多个操作的。假设有1000个请求,根据实际情况,可以分配20或者80个线程进行处理。不像BIO,一定要分配1000个线程来进行处理。
1.1 缓冲区(Buffer)
- 容量(capacity):作为一个内存块,缓冲区具有一定的大小,成为容量,缓冲区容量不能为负,并且创建后不能更改。
- 限制(limit):表示缓冲区中可以操作数据的大小,limit后数据不能进行读写。写入模式下,限制等于buffer的容量;读取模式下,限制等于写入的数据量
- 位置(position):下一个要读取或写入的数据的索引。
- 标记(mark)与重置(reset):标记是一个索引,通过mark()方法指定Buffer中一个特定的position,之后可以通过调用reset()方法恢复到这个position
常见方法:
- Buffer clear():清空缓冲区并返回对缓冲区的引用
- Buffer flip():将缓冲区的界限设置为当前位置,并将当前位置重置为0
- int capacity():返回缓冲区的容量大小
- boolean hasRemaining():判断缓冲区中是否还有元素
- int limit():返回缓冲区的界限的位置
- Buffer limit(int n):设置缓冲区界限为n,返回一个具有新limit的缓冲区对象
- Buffer mark():对缓冲区设置标记
- int position():返回缓冲区的当前位置
- Buffer position(int n):设置当前缓冲区的位置为n,并返回修改后的Buffer对象
- int remaining():返回position和limit之间的元素个数
- Buffer reset():将position转到以前设置的mark位置
- Buffer rewind():将位置设置为0,并取消设置mark
获取Buffer中的数据:
- get():读取单个字节
- get(byte[] dst):批量读取多个字节到dst中
- get(int index):读取指定索引位置的字节(不会移动position)
向Buffer中充入数据:
- put(byte b):将给定的单个字节写入缓冲区的当前位置
- put(byte[] src):将src中的字节写入缓冲区的当前位置
- put(int index,byte b):将指定字节写入缓冲区的索引位置(不会移动position)
package NIO;
import java.nio.ByteBuffer;
import java.nio.charset.StandardCharsets;
public class BufferTest {
public static void main(String[] args) {
ByteBuffer buffer = ByteBuffer.allocate(10);
System.out.println(buffer.position());//0
System.out.println(buffer.limit());//10
System.out.println(buffer.capacity());//10
//2、使用put向缓冲区充入数据
String name = "itheima";
buffer.put(name.getBytes());
System.out.println(buffer.position());//7
System.out.println(buffer.limit());//10
System.out.println(buffer.capacity());//10
//3、Buffer flip():可读模式
buffer.flip();
System.out.println(buffer.position());//0
System.out.println(buffer.limit());//7
System.out.println(buffer.capacity());//10
//4、get获取数据
char ch = (char)buffer.get();
System.out.println(ch);
System.out.println(buffer.position());//1
System.out.println(buffer.limit());//7
System.out.println(buffer.capacity());//10
}
}
1.2 通道(Channel)
- 通道可以同时进行读写,而流只能读或者只能写
- 通道可以实现异步读写数据
- 通道可以从缓冲读数据,也可以写数据到缓冲
Channel在NIO中是一个接口:public interface Channel extends Closeable{}
常用的Channel实现类:
- FileChannel:用于读取、 写入、映射和操作文件的通道
- DatagramChannel:通过UDP读写网络中的数据通道
- SocketChannel:通过TCP读写网络中的数据
- ServerSocketChannel:可以监听新进来的TCP连接,对每一个新进来的连接都会创建一了SocketChannel
FileChannel的常用方法:
- int read(ByteBuffer dst):从Channel中读取数据到ByteBuffer
- long read(ByteBuffer[] dsts):将Channel中的数据读取到数组中
- 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):强制将所有对此通道的文件更新写入到存储设备中
1.2.1 通过FileChannel向文件中写入数据
package NIO;
import java.io.FileNotFoundException;
import java.io.FileOutputStream;
import java.io.IOException;
import java.nio.ByteBuffer;
import java.nio.channels.FileChannel;
public class FileChannelTest1 {
public static void main(String[] args) {
try {
// 1、字节输出流定位到目标文件
FileOutputStream fos = new FileOutputStream("c:\\d.txt");
//2、得到字节输出流对应的通道
FileChannel channel = fos.getChannel();
//3、分配缓冲区
ByteBuffer buffer = ByteBuffer.allocate(1024);
buffer.put("hello".getBytes());
//4、向缓冲区写入数据
buffer.flip();
channel.write(buffer);
channel.close();
} catch (FileNotFoundException e) {
e.printStackTrace();
} catch (IOException e) {
e.printStackTrace();
}
}
}
1.2.2 通过FileChannel读取文件
package NIO;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.nio.Buffer;
import java.nio.ByteBuffer;
import java.nio.channels.FileChannel;
public class FileChannelTest2 {
public static void main(String[] args) {
try {
//1、定义一个文件字节输入流
FileInputStream is = new FileInputStream("c:\\d.txt");
//2、得到文件字节输入流的文件通道
FileChannel channel = is.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 (Exception e) {
e.printStackTrace();
}
}
}
1.2.3 通过FileChannel赋值文件
package NIO;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.FileOutputStream;
import java.nio.ByteBuffer;
import java.nio.channels.FileChannel;
public class FileChannelTest3 {
public static void main(String[] args) {
File srcFile = new File("c:\\d.txt");
File desFile = new File("c:\\e.txt");
try {
FileInputStream fis = new FileInputStream(srcFile);
FileOutputStream fos = new FileOutputStream(desFile);
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();
}
} catch (Exception e) {
e.printStackTrace();
}
}
}
1.2.4 TransferFrom和TranserTo
package NIO;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.FileOutputStream;
import java.io.IOException;
import java.nio.channels.FileChannel;
public class FileChannelTest4 {
public static void main(String[] args) throws IOException {
FileInputStream fis = new FileInputStream("c:\\d.txt");
FileChannel isChannel = fis.getChannel();
FileOutputStream fos = new FileOutputStream("c:\\e.txt");
FileChannel osChannel = fos.getChannel();
//第一种
osChannel.transferFrom(isChannel,isChannel.position(),isChannel.size());
//第二种
isChannel.transferTo(isChannel.position(),isChannel.size(),osChannel);
isChannel.close();
osChannel.close();
}
}
1.3 选择器(Selector)
Selector是SelectableChannel对象的多路复用器,Selector可以同时监控说个SelectorChannel的IO状况,也就是说,利用一个Selector可使一个单独的线程管理多个Channel。Selector是非阻塞IO的核心。
创建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 , SelevtionKey.OP_ACCEPT);
监听的事件:
- 读:SelectionKey.OP_READ
- 写:SelectionKey.OP_WRITE
- 连接:SelectionKey.OP_CONNECT
- 接收:SelectionKey.OP_ACCEPT
- 若注册时不止监听一个事件,则可以用位或操作符连接
1.4 NIO通信实例
服务器端:
package NIO;
import java.io.IOException;
import java.net.InetSocketAddress;
import java.net.Socket;
import java.nio.ByteBuffer;
import java.nio.channels.*;
import java.util.Iterator;
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、使用选择器轮询就绪事件
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 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();
}
}
it.remove();//处理完毕之后需要移出当前事件
}
}
}
}
客户端:
package NIO;
import java.io.IOException;
import java.net.InetSocketAddress;
import java.nio.ByteBuffer;
import java.nio.channels.SocketChannel;
import java.nio.charset.StandardCharsets;
import java.util.Scanner;
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 sc = new Scanner(System.in);
while(true){
System.out.println("请输入:");
String msg = sc.nextLine();
buf.put(msg.getBytes());
buf.flip();
sChannel.write(buf);
buf.clear();
}
}
}
二、练习
1、如果是 Windows 系统,以递归方式读取 C 盘中所有的目录和文件,并打印出每个文 件的大小和每个目录中文件的数量
package File;
import java.io.File;
public class Test12_01 {
public static void main(String[] args) {
test("C:\\",0);
}
private static void test(String fileDir , int num){
File file = new File(fileDir);
File[] files = file.listFiles();
if(files == null)
return;
else {
for(File f : files){
if(f.isFile()){
System.out.println(f.getName()+"的大小为:"+f.length());
num++;//目录中文件数量加1
}else{
test(f.getName(),0);
}
}
}
System.out.println(file.getName()+"中文件的数量为:"+num);
}
}
2、在网络上下载一个大文件(任何文件都可以,可以是 exe 文件,也可以是日志、电影 或其它类型的大文件),然后使用 Java 标准 I/O 模型读取并复制成另一份文件: 用字节流实现,用字符流实现。
字节流:
package File;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.FileOutputStream;
import java.io.IOException;
public class Test12_02 {
public static void main(String[] args) throws IOException {
FileInputStream fis = new FileInputStream("D:\\1.rmvb");
FileOutputStream fos = new FileOutputStream("F:\\1.rmvb");
byte[] bytes = new byte[1024];
int len;
while((len = fis.read(bytes))!=-1)
fos.write(bytes,0,len);
fos.close();;
fis.close();;
}
}
字符流:
package File;
import java.io.*;
public class Test12_02 {
public static void main(String[] args) throws IOException {
FileReader fr = new FileReader("D:\\1.rmvb");
FileWriter fw = new FileWriter("F:\\1.rmvb");
int len = 0;
char[] buf = new char[1024];
while((len = fr.read(buf))!=-1){
fw.write(buf,0,len);
}
fw.flush();
fw.close();
fr.close();
}
}
3、针对练习 2,用 NIO 重新实现,然后比较一下执行效率
package File;
import java.io.*;
import java.nio.channels.FileChannel;
public class Test12_02 {
public static void main(String[] args) throws IOException {
FileInputStream fis = new FileInputStream("D:\\1.rmvb");
FileChannel isChannel = fis.getChannel();
FileOutputStream fos = new FileOutputStream("F:\\1.rmvb");
FileChannel osChannel = fos.getChannel();
osChannel.transferFrom(isChannel,isChannel.position(),isChannel.size());
isChannel.close();
osChannel.close();
}
}
4、创建一个文本文件,然后随意输入一些格式固定的编号,例如: 100011720988、100009077459 100009077473 … 然后将下面的这个 URL 地址用这些编号替换:https://item.csdn.com/xxx.html,也 就是将 xxx 替换为对应的编号,例如:https://item.csdn.com/100011720988.html。 最后再借助开源的二维码组件,将这些 URL 批量生成为二维码图片(图片存储目录自 己指定)。
package File;
import com.google.zxing.BarcodeFormat;
import com.google.zxing.EncodeHintType;
import com.google.zxing.MultiFormatWriter;
import com.google.zxing.client.j2se.MatrixToImageWriter;
import com.google.zxing.common.BitMatrix;
import com.google.zxing.qrcode.decoder.ErrorCorrectionLevel;
import java.io.*;
import java.nio.file.Path;
import java.util.HashMap;
public class Test12_04 {
public static void main(String[] args) throws IOException, IOException {
/**
* 生成新文件并且添加内容
*/
File file = new File("F:\\number.txt");
FileWriter writer = null;
writer = new FileWriter(file,false);
if (file.exists()){
System.out.println("创建成功!");
}
String result = "100011720988";
writer.append(result);
writer.flush();
/**
* 读取文件内容并打印
*/
InputStreamReader isr = new InputStreamReader(new FileInputStream(file), "utf-8");
BufferedReader br = new BufferedReader(isr);
String lineTxt = null;
String line1 = br.readLine();
System.out.println(line1);
/**
* 修改文件内容
*/
String str = "https://item.csdn.com/"+result+".html";
writer.append(str);
writer.flush();
String line2 = br.readLine();
System.out.println(line2);
/**
* 生成二维码
*/
int width = 300; //二维码图片的宽度
int height = 300; //二维码图片的高度
String format = "png"; //二维码格式
String content = line2;
//定义二维码内容参数
HashMap hints = new HashMap();
//设置字符集编码格式
hints.put(EncodeHintType.CHARACTER_SET, "UTF-8");
//设置容错等级,在这里我们使用M级别
hints.put(EncodeHintType.ERROR_CORRECTION, ErrorCorrectionLevel.M);
//设置边框距
hints.put(EncodeHintType.MARGIN, 2);
try {
//指定二维码内容
BitMatrix bitMatrix = new MultiFormatWriter().encode(content, BarcodeFormat.QR_CODE, width, height,hints);
//指定生成图片的保存路径
Path newfile = new File("F:\\imooc.png").toPath();
//生成二维码
MatrixToImageWriter.writeToPath(bitMatrix, format, newfile);
} catch (Exception e) {
e.printStackTrace();
}
}
}