java中有几种类型的流?JDK为每种类型的流提供了一些抽象类以供继承,请说出他们分别是哪些类?
Java中的流分为两种,一种是字节流,另一种是字符流,分别由四个抽象类来表示(每种流包括输入和输出两种所以一共四个):InputStream,OutputStream,Reader,Writer。Java中其他多种多样变化的流均是由它们派生出来的.
字符流和字节流是根据处理数据的不同来区分的。
字节流按照8位传输,字节流是最基本的,所有文件的储存是都是字节(byte)的储存,在磁盘上保留的并不是文件的字符而是先把字符编码成字节,再储存这些字节到磁盘。
1.字节流可用于任何类型的对象,包括二进制对象,而字符流只能处理字符或者字符串;
2. 字节流提供了处理任何类型的IO操作的功能,但它不能直接处理Unicode字符,而字符流就可以。
读文本的时候用字符流,例如txt文件。读非文本文件的时候用字节流,例如mp3。理论上任何文件都能够用字节流读取,但当读取的是文本数据时,为了能还原成文本你必须再经过一个转换的工序,相对来说字符流就省了这个麻烦,可以有方法直接读取。
字符流处理的单元为2个字节的Unicode字符,分别操作字符、字符数组或字符串,而字节流处理单元为1个字节, 操作字节和字节数组。所以字符流是由Java虚拟机将字节转化为2个字节的Unicode字符为单位的字符而成的,所以它对多国语言支持性比较好!
1.字节流:继承于InputStream \ OutputStream。
- java.lang.Object
- java.io.InputStream
All Implemented Interfaces:
已知直接子类:
AudioInputStream , ByteArrayInputStream , FileInputStream , FilterInputStream , InputStream , ObjectInputStream , PipedInputStream , SequenceInputStream , StringBufferInputStream
// OutputStream提供的方法: /
void write(int b);//写入一个字节的数据
void write(byte[] buffer);//将数组buffer的数据写入流
void write(byte[] buffer,int offset,int len);//从buffer[offset]开始,写入len个字节的数据
void flush();//强制将buffer内的数据写入流
void close();//关闭流
// InputStream提供的方法: /
int read();//读出一个字节的数据,如果已达文件的末端,返回值为-1
int read(byte[] buffer);//读出buffer大小的数据,返回值为实际所读出的字节数
//从输入流读取最多 len字节的数据到一个字节数组。
int read(byte[] buffer,int offset,int len);
int available();//返回流内可供读取的字节数目
long skip(long n);//跳过n个字节的数据,返回值为实际所跳过的数据数
void close();//关闭流
2.字符流,继承于InputStreamReader \ OutputStreamWriter。
Class Reader
- java.lang.Object
- java.io.Reader
All Implemented Interfaces:
Closeable , AutoCloseable , Readable
已知直接子类:
BufferedReader , CharArrayReader , FilterReader , InputStreamReader , PipedReader , StringReader
字符流的类:1),BufferedReader是一种过滤器(filter)(extends FilterReader)。过滤器用来将流的数据加以处理再输出。构造函数为:
BufferedReader(Reader in):生成一个缓冲的字符输入流,in为一个读取器
BufferedReader(Reader in,int size):生成一个缓冲的字符输入流,并指定缓冲区的大小为size
public class IOStreamDemo {
public void samples() throws IOException {
//1. 这是从键盘读入一行数据,返回的是一个字符串
BufferedReader stdin =new BufferedReader(new InputStreamReader(System.in));
System.out.print("Enter a line:");
System.out.println(stdin.readLine());
//2. 这是从文件中逐行读入数据
BufferedReader in = new BufferedReader(new FileReader("IOStreamDemo.java"));
String s, s2 = new String();
while((s = in.readLine())!= null)
s2 += s + "\n";
in.close();
//3. 这是从一个字符串中逐个读入字节
StringReader in1 = new StringReader(s2);
int c;
while((c = in1.read()) != -1)
System.out.print((char)c);
//4. 这是将一个字符串写入文件
try {
BufferedReader in2 = new BufferedReader(new StringReader(s2));
PrintWriter out1 = new PrintWriter(new BufferedWriter(new FileWriter("IODemo.out")));
int lineCount = 1;
while((s = in2.readLine()) != null )
out1.println(lineCount++ + ": " + s);
out1.close();
} catch(EOFException e) {
System.err.println("End of stream");
}
}
}
对于上面的例子,需要说明的有以下几点:
1. InputStreamReader是InputStream和Reader之间的桥梁,由于System.in是字节流,需要用它来包装之后变为字符流供给BufferedReader使用。
2. PrintWriter out1 = new PrintWriter(new BufferedWriter(new FileWriter("IODemo.out")));
这句话体现了Java输入输出系统的一个特点,为了达到某个目的,需要包装好几层。首先,输出目的地是文件IODemo.out,所以最内层包装的是FileWriter,建立一个输出文件流,接下来,我们希望这个流是缓冲的,所以用BufferedWriter来包装它以达到目的,最后,我们需要格式化输出结果,于是将PrintWriter包在最外层。
Java流有着另一个重要的用途,那就是利用对象流对对象进行序列化。
在一个程序运行的时候,其中的变量数据是保存在内存中的,一旦程序结束这些数据将不会被保存,一种解决的办法是将数据写入文件,而Java中提供了一种机制,它可以将程序中的对象写入文件,之后再从文件中把对象读出来重新建立。这就是所谓的对象序列化。Java中引入它主要是为了RMI(Remote Method Invocation)和Java Bean所用,不过在平时应用中,它也是很有用的一种技术。
package com.gqz.io;
import java.io.BufferedInputStream;
import java.io.BufferedOutputStream;
import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.ObjectInputStream;
import java.io.ObjectOutputStream;
import java.util.Date;
/*
* 数据流
* 1、ObjectInputStream
* 2、ObjectOutputStream
*/
public class ObjectTest {
public static void main(String[] args) throws IOException, ClassNotFoundException {
//写出 --> 序列化
ByteArrayOutputStream baos=new ByteArrayOutputStream();
ObjectOutputStream oos=new ObjectOutputStream(new BufferedOutputStream(baos));
//操作基本数据类型 + 数据
oos.writeUTF("不负天地,不负自己");
oos.writeInt(9696);
oos.writeBoolean(true);
oos.writeChar('A');
//对象
oos.writeObject("Struggle");
oos.writeObject(new Date());
//写一个对象
Employee emp = new Employee("马云",400);
oos.writeObject(emp);
oos.flush();
byte[] datas = baos.toByteArray();
//读取 --> 反序列化
ByteArrayInputStream bais = new ByteArrayInputStream(datas);
ObjectInputStream ois = new ObjectInputStream(new BufferedInputStream(bais));
//读取基本数据类型 读的顺序与的顺序一致
String msg = ois.readUTF();
int age = ois.readInt();
boolean flag = ois.readBoolean();
char ch = ois.readChar();
System.out.println(msg);
System.out.println(age);
System.out.println(flag);
System.out.println(ch);
//对象数据的还原
Object str = ois.readObject();
Object date = ois.readObject();
Object employee = ois.readObject();
if(str instanceof String) {
String strObj = (String)str;
System.out.println(strObj);
}
if(date instanceof Date) {
Date dateObj = (Date)date;
System.out.println(dateObj);
}
if(employee instanceof Employee) {
Employee empObj = (Employee)employee;
System.out.println(empObj.getName()+"---->"+empObj.getSalay()); //name字段透明!
}
}
}
//javabean 封装数据
class Employee implements java.io.Serializable {
private transient String name; //该数据不需要序列化
private double salay;
public Employee() {
}
public Employee(String name, double salay) {
super();
this.name = name;
this.salay = salay;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public double getSalay() {
return salay;
}
public void setSalay(double salay) {
this.salay = salay;
}
}
下面就利用InputStream和OutputStream进行文件的拷贝:
package com.gqz.io;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
public class Copy {
/**
*
* @Title: copy
* @Description: copy文件拷贝 字节方式
* @author ganquanzhong
* @date 2019年6月27日 下午3:22:09
* @param srcPath
* @param destPath
*/
public static void copy(String srcPath,String destPath) {
// 1、创建源
File src = new File(srcPath);//源头
File dest = new File(destPath);//目的地
// 2、选择流
InputStream is = null; // 输入流 原文件
OutputStream os = null; // 输出文件 复制文件
//3、操作 文件拷贝
try {
is = new FileInputStream(src);
// FileOutputStream构造方法 append true追加到末尾
//os = new FileOutputStream(dest, true);
os = new FileOutputStream(dest);
// 每次读多大
byte[] flush = new byte[1024 * 5];
int len = -1;
while ((len = is.read(flush)) != -1) {
os.write(flush, 0, flush.length);
os.flush();
}
} catch (FileNotFoundException e) {
e.printStackTrace();
} catch (IOException e) {
// TODO Auto-generated catch block
e.printStackTrace();
} finally {
// 4、释放资源 先打开的后关闭
try {
if (null != os) {
os.close();
}
} catch (Exception e) {
e.printStackTrace();
}
try {
if (null != is) {
is.close();
}
} catch (Exception e) {
e.printStackTrace();
}
}
}
//测试
public static void main(String[] args) {
copy("E:/a.txt", "E:/copy.txt");
}
}
java IO流之-----File类
File类描述的是一个文件或文件夹,只是指定一个路径。(文件夹也可以称为目录)。该类的出现是对文件系统的中的文件以及文件夹进行对象的封装。可以通过对象的思想来操作文件以及文件夹。可以用面向对象的处理问题,通过该对象的方法,可以得到文件或文件夹的信息方便了对文件与文件夹的属性信息进行操作。文件包含很多的信息: 如文件名、创建修改时间、大小、可读可写属性等。
基本API
通过将给定路径来创建一个新File实例。
new File(String pathname);
根据parent路径名字符串和child路径名创建一个新File实例。parent是指上级目录的路径,完整的路径为parent+child.
new File(String parent, String child);
根据parent抽象路径名和child路径名创建一个新File实例。 parent是指上级目录的路径,完整的路径为parent.getPath()+child. 说明:如果指定的路径不存在(没有这个文件或是文件夹),不会抛异常,这时file.exists()返回false。
new File(File parent, String child);
创建:
createNewFile() //在指定位置创建一个空文件,成功就返回true,如果已存在就不创建然后返回false
mkdir() //在指定位置创建目录,这只会创建最后一级目录,如果上级目录不存在就抛异常。
mkdirs() //在指定位置创建目录,这会创建路径中所有不存在的目录。
renameTo(File dest) //重命名文件或文件夹,也可以操作非空的文件夹,文件不同时相当于文件的剪切,剪切时候不能操作非空的文件夹。移动/重命名成功则返回true,失败则返回false。
删除:
delete() //删除文件或一个空文件夹,如果是文件夹且不为空,则不能删除,成功返回true,失败返回false。
deleteOnExit() //在虚拟机终止时,请求删除此抽象路径名表示的文件或目录,保证程序异常时创建的临时文件也可以被删除
判断:
exists() //文件或文件夹是否存在。
isFile() //是否是一个文件,如果不存在,则始终为false。
isDirectory() //是否是一个目录,如果不存在,则始终为false。
isHidden() //是否是一个隐藏的文件或是否是隐藏的目录。
isAbsolute() //测试此抽象路径名是否为绝对路径名。
获取:
getName() //获取文件或文件夹的名称,不包含上级路径。
getPath() //返回绝对路径,可以是相对路径,但是目录要指定
getAbsolutePath() //获取文件的绝对路径,与文件是否存在没关系
length() // 获取文件的大小(字节数),如果文件不存在则返回0L,如果是文件夹也返回0L。
getParent() //返回此抽象路径名父目录的路径名字符串;如果此路径名没有指定父目录,则返回null。
lastModified() //获取最后一次被修改的时间。
staic File[] listRoots() //列出所有的根目录(Window中就是所有系统的盘符)
list() //返回目录下的文件或者目录名,包含隐藏文件。对于文件这样操作会返回null。
list(FilenameFilter filter) //返回指定当前目录中符合过滤条件的子文件或子目录。对于文件这样操作会返回null。
listFiles() //返回目录下的文件或者目录对象(File类实例),包含隐藏文件。对于文件这样操作会返回null。
listFiles(FilenameFilter filter) //返回指定当前目录中符合过滤条件的子文件或子目录。对于文件这样操作会返回null。
路径问题
对于UNIX平台,绝对路径名的前缀是"/"。相对路径名没有前缀。
对于Windows平台,绝对路径名的前缀由驱动器号和一个":"组成,例"c:\\..."。相对路径没有盘符前缀。
更专业的做法(根据系统判断separator是什么)是使用File.separatorChar或者File.separator(前者为char,后者为String),这个值就会根据系统得到的相应的分割符。
常见File类的面试题目:
1、File类型中定义了什么方法来创建一级目录
mkdirs() //在指定位置创建目录,这会创建路径中所有不存在的目录。
2、File类型中定义了什么方法来判断一个文件是否存在
exists() //文件或文件夹是否存在。
3、如何用Java代码列出一个目录下所有的文件?
使用listFiles获取文件夹下面的所有文件,遍历;
//只查询当前目录
public class Main {
public static void main(String[] args) {
File f = new File("d:");
for(File temp : f.listFiles()) {
if(temp.isFile()) {
System.out.println(temp.getName());
}
}
}
}
如果需要对文件夹继续展开
public static void main(String[] args) {
showDirectory(new File("d:\\file"));
}
public static void showDirectory(File f) {
walkDirectory(f, 0);
}
private static void walkDirectory(File f, int level) {
if(f.isDirectory()) {
for(File temp : f.listFiles()) {
//使用递归
walkDirectory(temp, level + 1);
}
}
else {
for(int i = 0; i < level - 1; i++) {
System.out.print("\t");
}
//输出文件名
System.out.println(f.getName());
}
}
在Java 7中可以使用NIO.2的API来做同样的事情,代码如下所示:
public static void main(String[] args) throws IOException {
Path initPath = Paths.get("/Users/Hao/Downloads");
Files.walkFileTree(initPath, new SimpleFileVisitor<Path>() {
@Override
public FileVisitResult visitFile(Path file, BasicFileAttributes attrs)
throws IOException {
System.out.println(file.getFileName().toString());
return FileVisitResult.CONTINUE;
}
});
}
文件分块读取 RandomAccessFile
package com.gqz.io;
import java.io.File;
import java.io.IOException;
import java.io.RandomAccessFile;
public class RandomAccessFileTest {
public static void main(String[] args) throws IOException {
// 分多少块
File src = new File("src/com/gqz/io/Copy.java");
// 总长度
long len = src.length();
// 每块大小
int blockSize = 1024;
// 块数:多少块
int size = (int) Math.ceil(len * 1.0 / blockSize);
//System.out.println(size);
// 其实位置和实际大小
int beginPos = 0;
int actualSize = (int) (blockSize > len ? len : blockSize);
for (int i = 0; i < size; i++) {
beginPos = i * blockSize;// 位置
if (i == size - 1) {// 最后一块
actualSize = (int) len;
} else {
actualSize = blockSize;
len -= actualSize; // 剩余量
}
System.out.println("第"+(i+1 )+ "块 起始位置-->" + beginPos + "读取大小-->" + actualSize);
split(i, beginPos, actualSize);
}
}
/**
*
* @Title: split
* @Description: TODO(这里用一句话描述这个方法的作用)
* @author ganquanzhong
* @date 2019年7月1日 下午2:30:29
* @param i
* @param beginPos 指定读入的位置
* @param actualSize 实际大小
* @throws IOException
*/
public static void split(int i,int beginPos,int actualSize) throws IOException {
RandomAccessFile raf = new RandomAccessFile(new File("src/com/gqz/io/Copy.java"), "r");
raf.seek(beginPos);
// 读取
byte[] datas = new byte[1024];// 缓冲容器
int len = -1;
while ((len = raf.read(datas)) != -1) {
if (actualSize >= len) {
System.out.println(new String(datas, 0, len));
actualSize -= len;
} else {
System.out.println(new String(datas, 0, actualSize));
break;
}
}
raf.close();
}
// 分块思想:起始、实际大小
public static void test2() throws IOException {
RandomAccessFile raf = new RandomAccessFile(new File("src/com/gqz/io/Copy.java"), "r");
// 指定读入的位置
int beginPos = 2;
// 实际大小
int actualSize = 1026;
// 随机读
raf.seek(beginPos);
// 读取
byte[] datas = new byte[1024];// 缓冲容器
int len = -1;
while ((len = raf.read(datas)) != -1) {
if (actualSize >= len) {
System.out.println(new String(datas, 0, len));
actualSize -= len;
} else {
System.out.println(new String(datas, 0, actualSize));
break;
}
}
raf.close();
}
// 指定起始位置、读取剩余所有内容
public static void test() throws IOException {
RandomAccessFile raf = new RandomAccessFile(new File("src/com/gqz/io/Copy.java"), "r");
// 随机读 指定读入的位置
raf.seek(4);
// 读取
byte[] datas = new byte[201];// 缓冲容器
int len = -1;
while ((len = raf.read(datas)) != -1) {
System.out.println(new String(datas, 0, datas.length));
}
raf.close();
}
}
下面进行知识扩展: BIO、NIO、AIO
BIO
BIO 全称Block-IO 是一种阻塞同步的通信模式。我们常说的Stock IO 一般指的是BIO。是一个比较传统的通信方式,模式简单,使用方便。但并发处理能力低,通信耗时,依赖网速。
BIO 设计原理:
服务器通过一个Acceptor线程负责监听客户端请求和为每个客户端创建一个新的线程进行链路处理。典型的一请求一应答模式。若客户端数量增多,频繁地创建和销毁线程会给服务器打开很大的压力。后改良为用线程池的方式代替新增线程,被称为伪异步IO。
服务器提供IP地址和监听的端口,客户端通过TCP的三次握手与服务器连接,连接成功后,双放才能通过套接字(Stock)通信。
小结:BIO模型中通过Socket和ServerSocket完成套接字通道的实现。阻塞,同步,建立连接耗时。
BIO服务器代码,负责启动服务,阻塞服务,监听客户端请求,新建线程处理任务。
import java.io.IOException;
import java.net.ServerSocket;
import java.net.Socket;
import java.util.concurrent.ArrayBlockingQueue;
import java.util.concurrent.ThreadPoolExecutor;
import java.util.concurrent.TimeUnit;
/**
* IO 也称为 BIO,Block IO 阻塞同步的通讯方式
* 比较传统的技术,实际开发中基本上用Netty或者是AIO。熟悉BIO,NIO,体会其中变化的过程。作为一个web开发人员,stock通讯面试经常问题。
* BIO最大的问题是:阻塞,同步。
* BIO通讯方式很依赖于网络,若网速不好,阻塞时间会很长。每次请求都由程序执行并返回,这是同步的缺陷。
* BIO工作流程:
* 第一步:server端服务器启动
* 第二步:server端服务器阻塞监听client请求
* 第三步:server端服务器接收请求,创建线程实现任务
*/
public class ITDragonBIOServer {
private static final Integer PORT = 8888; // 服务器对外的端口号
public static void main(String[] args) {
ServerSocket server = null;
Socket socket = null;
ThreadPoolExecutor executor = null;
try {
server = new ServerSocket(PORT); // ServerSocket 启动监听端口
System.out.println("BIO Server 服务器启动.........");
/*--------------传统的新增线程处理----------------*/
/*while (true) {
// 服务器监听:阻塞,等待Client请求
socket = server.accept();
System.out.println("server 服务器确认请求 : " + socket);
// 服务器连接确认:确认Client请求后,创建线程执行任务 。很明显的问题,若每接收一次请求就要创建一个线程,显然是不合理的。
new Thread(new ITDragonBIOServerHandler(socket)).start();
} */
/*--------------通过线程池处理缓解高并发给程序带来的压力(伪异步IO编程)----------------*/
executor = new ThreadPoolExecutor(10, 100, 1000, TimeUnit.SECONDS, new ArrayBlockingQueue<Runnable>(50));
while (true) {
socket = server.accept(); // 服务器监听:阻塞,等待Client请求
ITDragonBIOServerHandler serverHandler = new ITDragonBIOServerHandler(socket);
executor.execute(serverHandler);
}
} catch (IOException e) {
e.printStackTrace();
} finally {
try {
if (null != socket) {
socket.close();
socket = null;
}
if (null != server) {
server.close();
server = null;
System.out.println("BIO Server 服务器关闭了!!!!");
}
executor.shutdown();
} catch (IOException e) {
e.printStackTrace();
}
}
}
}
BIO服务端处理任务代码,负责处理Stock套接字,返回套接字给客户端,解耦。
import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStreamReader;
import java.io.PrintWriter;
import java.net.Socket;
import com.itdragon.util.CalculatorUtil;
public class ITDragonBIOServerHandler implements Runnable{
private Socket socket;
public ITDragonBIOServerHandler(Socket socket) {
this.socket = socket;
}
@Override
public void run() {
BufferedReader reader = null;
PrintWriter writer = null;
try {
reader = new BufferedReader(new InputStreamReader(this.socket.getInputStream()));
writer = new PrintWriter(this.socket.getOutputStream(), true);
String body = null;
while (true) {
body = reader.readLine(); // 若客户端用的是 writer.print() 传值,那readerLine() 是不能获取值,细节
if (null == body) {
break;
}
System.out.println("server服务端接收参数 : " + body);
writer.println(body + " = " + CalculatorUtil.cal(body).toString());
}
} catch (IOException e) {
e.printStackTrace();
} finally {
if (null != writer) {
writer.close();
}
try {
if (null != reader) {
reader.close();
}
if (null != this.socket) {
this.socket.close();
this.socket = null;
}
} catch (IOException e) {
e.printStackTrace();
}
}
}
}
BIO客户端代码,负责启动客户端,向服务器发送请求,接收服务器返回的Stock套接字。
import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStreamReader;
import java.io.PrintWriter;
import java.net.Socket;
import java.util.Random;
/**
* BIO 客户端
* Socket : 向服务端发送连接
* PrintWriter : 向服务端传递参数
* BufferedReader : 从服务端接收参数
*/
public class ITDragonBIOClient {
private static Integer PORT = 8888;
private static String IP_ADDRESS = "127.0.0.1";
public static void main(String[] args) {
for (int i = 0; i < 10; i++) {
clientReq(i);
}
}
private static void clientReq(int i) {
Socket socket = null;
BufferedReader reader = null;
PrintWriter writer = null;
try {
socket = new Socket(IP_ADDRESS, PORT); // Socket 发起连接操作。连接成功后,双方通过输入和输出流进行同步阻塞式通信
reader = new BufferedReader(new InputStreamReader(socket.getInputStream())); // 获取返回内容
writer = new PrintWriter(socket.getOutputStream(), true);
String []operators = {"+","-","*","/"};
Random random = new Random(System.currentTimeMillis());
String expression = random.nextInt(10)+operators[random.nextInt(4)]+(random.nextInt(10)+1);
writer.println(expression); // 向服务器端发送数据
System.out.println(i + " 客户端打印返回数据 : " + reader.readLine());
} catch (Exception e) {
e.printStackTrace();
} finally {
try {
if (null != reader) {
reader.close();
}
if (null != socket) {
socket.close();
socket = null;
}
} catch (IOException e) {
e.printStackTrace();
}
}
}
}
NIO
NIO 全称New IO,也叫Non-Block IO 是一种非阻塞同步的通信模式。
NIO 设计原理:
NIO 相对于BIO来说一大进步。客户端和服务器之间通过Channel通信。NIO可以在Channel进行读写操作。这些Channel都会被注册在Selector多路复用器上。Selector通过一个线程不停的轮询这些Channel。找出已经准备就绪的Channel执行IO操作。
NIO 通过一个线程轮询,实现千万个客户端的请求,这就是非阻塞NIO的特点。
1)缓冲区Buffer:它是NIO与BIO的一个重要区别。BIO是将数据直接写入或读取到Stream对象中。而NIO的数据操作都是在缓冲区中进行的。缓冲区实际上是一个数组。Buffer最常见的类型是ByteBuffer,另外还有CharBuffer,ShortBuffer,IntBuffer,LongBuffer,FloatBuffer,DoubleBuffer。
2)通道Channel:和流不同,通道是双向的。NIO可以通过Channel进行数据的读,写和同时读写操作。通道分为两大类:一类是网络读写(SelectableChannel),一类是用于文件操作(FileChannel),我们使用的SocketChannel和ServerSocketChannel都是SelectableChannel的子类。
3)多路复用器Selector:NIO编程的基础。多路复用器提供选择已经就绪的任务的能力。就是Selector会不断地轮询注册在其上的通道(Channel),如果某个通道处于就绪状态,会被Selector轮询出来,然后通过SelectionKey可以取得就绪的Channel集合,从而进行后续的IO操作。服务器端只要提供一个线程负责Selector的轮询,就可以接入成千上万个客户端,这就是JDK NIO库的巨大进步。
说明:这里的代码只实现了客户端发送请求,服务端接收数据的功能。其目的是简化代码,方便理解。github源码中有完整代码。
小结:NIO模型中通过SocketChannel和ServerSocketChannel完成套接字通道的实现。非阻塞/阻塞,同步,避免TCP建立连接使用三次握手带来的开销。
NIO服务器代码,负责开启多路复用器,打开通道,注册通道,轮询通道,处理通道。
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.ServerSocketChannel;
import java.nio.channels.SocketChannel;
import java.util.Iterator;
/**
* NIO 也称 New IO, Non-Block IO,非阻塞同步通信方式
* 从BIO的阻塞到NIO的非阻塞,这是一大进步。功归于Buffer,Channel,Selector三个设计实现。
* Buffer : 缓冲区。NIO的数据操作都是在缓冲区中进行。缓冲区实际上是一个数组。而BIO是将数据直接写入或读取到Stream对象。
* Channel : 通道。NIO可以通过Channel进行数据的读,写和同时读写操作。
* Selector : 多路复用器。NIO编程的基础。多路复用器提供选择已经就绪状态任务的能力。
* 客户端和服务器通过Channel连接,而这些Channel都要注册在Selector。Selector通过一个线程不停的轮询这些Channel。找出已经准备就绪的Channel执行IO操作。
* NIO通过一个线程轮询,实现千万个客户端的请求,这就是非阻塞NIO的特点。
*/
public class ITDragonNIOServer implements Runnable{
private final int BUFFER_SIZE = 1024; // 缓冲区大小
private final int PORT = 8888; // 监听的端口
private Selector selector; // 多路复用器,NIO编程的基础,负责管理通道Channel
private ByteBuffer readBuffer = ByteBuffer.allocate(BUFFER_SIZE); // 缓冲区Buffer
public ITDragonNIOServer() {
startServer();
}
private void startServer() {
try {
// 1.开启多路复用器
selector = Selector.open();
// 2.打开服务器通道(网络读写通道)
ServerSocketChannel channel = ServerSocketChannel.open();
// 3.设置服务器通道为非阻塞模式,true为阻塞,false为非阻塞
channel.configureBlocking(false);
// 4.绑定端口
channel.socket().bind(new InetSocketAddress(PORT));
// 5.把通道注册到多路复用器上,并监听阻塞事件
/**
* SelectionKey.OP_READ : 表示关注读数据就绪事件
* SelectionKey.OP_WRITE : 表示关注写数据就绪事件
* SelectionKey.OP_CONNECT: 表示关注socket channel的连接完成事件
* SelectionKey.OP_ACCEPT : 表示关注server-socket channel的accept事件
*/
channel.register(selector, SelectionKey.OP_ACCEPT);
System.out.println("Server start >>>>>>>>> port :" + PORT);
} catch (IOException e) {
e.printStackTrace();
}
}
// 需要一个线程负责Selector的轮询
@Override
public void run() {
while (true) {
try {
/**
* a.select() 阻塞到至少有一个通道在你注册的事件上就绪
* b.select(long timeOut) 阻塞到至少有一个通道在你注册的事件上就绪或者超时timeOut
* c.selectNow() 立即返回。如果没有就绪的通道则返回0
* select方法的返回值表示就绪通道的个数。
*/
// 1.多路复用器监听阻塞
selector.select();
// 2.多路复用器已经选择的结果集
Iterator<SelectionKey> selectionKeys = selector.selectedKeys().iterator();
// 3.不停的轮询
while (selectionKeys.hasNext()) {
// 4.获取一个选中的key
SelectionKey key = selectionKeys.next();
// 5.获取后便将其从容器中移除
selectionKeys.remove();
// 6.只获取有效的key
if (!key.isValid()){
continue;
}
// 阻塞状态处理
if (key.isAcceptable()){
accept(key);
}
// 可读状态处理
if (key.isReadable()){
read(key);
}
}
} catch (IOException e) {
e.printStackTrace();
}
}
}
// 设置阻塞,等待Client请求。在传统IO编程中,用的是ServerSocket和Socket。在NIO中采用的ServerSocketChannel和SocketChannel
private void accept(SelectionKey selectionKey) {
try {
// 1.获取通道服务
ServerSocketChannel serverSocketChannel = (ServerSocketChannel) selectionKey.channel();
// 2.执行阻塞方法
SocketChannel socketChannel = serverSocketChannel.accept();
// 3.设置服务器通道为非阻塞模式,true为阻塞,false为非阻塞
socketChannel.configureBlocking(false);
// 4.把通道注册到多路复用器上,并设置读取标识
socketChannel.register(selector, SelectionKey.OP_READ);
} catch (IOException e) {
e.printStackTrace();
}
}
private void read(SelectionKey selectionKey) {
try {
// 1.清空缓冲区数据
readBuffer.clear();
// 2.获取在多路复用器上注册的通道
SocketChannel socketChannel = (SocketChannel) selectionKey.channel();
// 3.读取数据,返回
int count = socketChannel.read(readBuffer);
// 4.返回内容为-1 表示没有数据
if (-1 == count) {
selectionKey.channel().close();
selectionKey.cancel();
return ;
}
// 5.有数据则在读取数据前进行复位操作
readBuffer.flip();
// 6.根据缓冲区大小创建一个相应大小的bytes数组,用来获取值
byte[] bytes = new byte[readBuffer.remaining()];
// 7.接收缓冲区数据
readBuffer.get(bytes);
// 8.打印获取到的数据
System.out.println("NIO Server : " + new String(bytes)); // 不能用bytes.toString()
} catch (IOException e) {
e.printStackTrace();
}
}
public static void main(String[] args) {
new Thread(new ITDragonNIOServer()).start();
}
}
NIO客户端代码,负责连接服务器,声明通道,连接通道
import java.io.IOException;
import java.net.InetSocketAddress;
import java.nio.ByteBuffer;
import java.nio.channels.SocketChannel;
public class ITDragonNIOClient {
private final static int PORT = 8888;
private final static int BUFFER_SIZE = 1024;
private final static String IP_ADDRESS = "127.0.0.1";
public static void main(String[] args) {
clientReq();
}
private static void clientReq() {
// 1.创建连接地址
InetSocketAddress inetSocketAddress = new InetSocketAddress(IP_ADDRESS, PORT);
// 2.声明一个连接通道
SocketChannel socketChannel = null;
// 3.创建一个缓冲区
ByteBuffer byteBuffer = ByteBuffer.allocate(BUFFER_SIZE);
try {
// 4.打开通道
socketChannel = SocketChannel.open();
// 5.连接服务器
socketChannel.connect(inetSocketAddress);
while(true){
// 6.定义一个字节数组,然后使用系统录入功能:
byte[] bytes = new byte[BUFFER_SIZE];
// 7.键盘输入数据
System.in.read(bytes);
// 8.把数据放到缓冲区中
byteBuffer.put(bytes);
// 9.对缓冲区进行复位
byteBuffer.flip();
// 10.写出数据
socketChannel.write(byteBuffer);
// 11.清空缓冲区数据
byteBuffer.clear();
}
} catch (IOException e) {
e.printStackTrace();
} finally {
if (null != socketChannel) {
try {
socketChannel.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
}
}
AIO
AIO 也叫NIO2.0 是一种非阻塞异步的通信模式。在NIO的基础上引入了新的异步通道的概念,并提供了异步文件通道和异步套接字通道的实现。
AIO 并没有采用NIO的多路复用器,而是使用异步通道的概念。其read,write方法的返回类型都是Future对象。而Future模型是异步的,其核心思想是:去主函数等待时间。
小结:AIO模型中通过AsynchronousSocketChannel和AsynchronousServerSocketChannel完成套接字通道的实现。非阻塞,异步。
AIO服务端代码,负责创建服务器通道,绑定端口,等待请求。
import java.net.InetSocketAddress;
import java.nio.channels.AsynchronousChannelGroup;
import java.nio.channels.AsynchronousServerSocketChannel;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
/**
* AIO, 也叫 NIO2.0 是一种异步非阻塞的通信方式
* AIO 引入了异步通道的概念 AsynchronousServerSocketChannel和AsynchronousSocketChannel 其read和write方法返回值类型是Future对象。
*/
public class ITDragonAIOServer {
private ExecutorService executorService; // 线程池
private AsynchronousChannelGroup threadGroup; // 通道组
public AsynchronousServerSocketChannel asynServerSocketChannel; // 服务器通道
public void start(Integer port){
try {
// 1.创建一个缓存池
executorService = Executors.newCachedThreadPool();
// 2.创建通道组
threadGroup = AsynchronousChannelGroup.withCachedThreadPool(executorService, 1);
// 3.创建服务器通道
asynServerSocketChannel = AsynchronousServerSocketChannel.open(threadGroup);
// 4.进行绑定
asynServerSocketChannel.bind(new InetSocketAddress(port));
System.out.println("server start , port : " + port);
// 5.等待客户端请求
asynServerSocketChannel.accept(this, new ITDragonAIOServerHandler());
// 一直阻塞 不让服务器停止,真实环境是在tomcat下运行,所以不需要这行代码
Thread.sleep(Integer.MAX_VALUE);
} catch (Exception e) {
e.printStackTrace();
}
}
public static void main(String[] args) {
ITDragonAIOServer server = new ITDragonAIOServer();
server.start(8888);
}
}
AIO服务器任务处理代码,负责,读取数据,写入数据
import java.nio.ByteBuffer;
import java.nio.channels.AsynchronousSocketChannel;
import java.nio.channels.CompletionHandler;
import java.util.concurrent.ExecutionException;
import com.itdragon.util.CalculatorUtil;
public class ITDragonAIOServerHandler implements CompletionHandler<AsynchronousSocketChannel, ITDragonAIOServer> {
private final Integer BUFFER_SIZE = 1024;
@Override
public void completed(AsynchronousSocketChannel asynSocketChannel, ITDragonAIOServer attachment) {
// 保证多个客户端都可以阻塞
attachment.asynServerSocketChannel.accept(attachment, this);
read(asynSocketChannel);
}
//读取数据
private void read(final AsynchronousSocketChannel asynSocketChannel) {
ByteBuffer byteBuffer = ByteBuffer.allocate(BUFFER_SIZE);
asynSocketChannel.read(byteBuffer, byteBuffer, new CompletionHandler<Integer, ByteBuffer>() {
@Override
public void completed(Integer resultSize, ByteBuffer attachment) {
//进行读取之后,重置标识位
attachment.flip();
//获取读取的数据
String resultData = new String(attachment.array()).trim();
System.out.println("Server -> " + "收到客户端的数据信息为:" + resultData);
String response = resultData + " = " + CalculatorUtil.cal(resultData);
write(asynSocketChannel, response);
}
@Override
public void failed(Throwable exc, ByteBuffer attachment) {
exc.printStackTrace();
}
});
}
// 写入数据
private void write(AsynchronousSocketChannel asynSocketChannel, String response) {
try {
// 把数据写入到缓冲区中
ByteBuffer buf = ByteBuffer.allocate(BUFFER_SIZE);
buf.put(response.getBytes());
buf.flip();
// 在从缓冲区写入到通道中
asynSocketChannel.write(buf).get();
} catch (InterruptedException e) {
e.printStackTrace();
} catch (ExecutionException e) {
e.printStackTrace();
}
}
@Override
public void failed(Throwable exc, ITDragonAIOServer attachment) {
exc.printStackTrace();
}
}
AIO客户端代码,负责连接服务器,声明通道,连接通道
import java.net.InetSocketAddress;
import java.nio.ByteBuffer;
import java.nio.channels.AsynchronousSocketChannel;
import java.util.Random;
public class ITDragonAIOClient implements Runnable{
private static Integer PORT = 8888;
private static String IP_ADDRESS = "127.0.0.1";
private AsynchronousSocketChannel asynSocketChannel ;
public ITDragonAIOClient() throws Exception {
asynSocketChannel = AsynchronousSocketChannel.open(); // 打开通道
}
public void connect(){
asynSocketChannel.connect(new InetSocketAddress(IP_ADDRESS, PORT)); // 创建连接 和NIO一样
}
public void write(String request){
try {
asynSocketChannel.write(ByteBuffer.wrap(request.getBytes())).get();
ByteBuffer byteBuffer = ByteBuffer.allocate(1024);
asynSocketChannel.read(byteBuffer).get();
byteBuffer.flip();
byte[] respByte = new byte[byteBuffer.remaining()];
byteBuffer.get(respByte); // 将缓冲区的数据放入到 byte数组中
System.out.println(new String(respByte,"utf-8").trim());
} catch (Exception e) {
e.printStackTrace();
}
}
@Override
public void run() {
while(true){
}
}
public static void main(String[] args) throws Exception {
for (int i = 0; i < 10; i++) {
ITDragonAIOClient myClient = new ITDragonAIOClient();
myClient.connect();
new Thread(myClient, "myClient").start();
String []operators = {"+","-","*","/"};
Random random = new Random(System.currentTimeMillis());
String expression = random.nextInt(10)+operators[random.nextInt(4)]+(random.nextInt(10)+1);
myClient.write(expression);
}
}
}
常见面试题
1 IO,NIO,AIO区别
IO 阻塞同步通信模式,客户端和服务器连接需要三次握手,使用简单,但吞吐量小
NIO 非阻塞同步通信模式,客户端与服务器通过Channel连接,采用多路复用器轮询注册的Channel。提高吞吐量和可靠性。
AIO 非阻塞异步通信模式,NIO的升级版,采用异步通道实现异步通信,其read和write方法均是异步方法。
2 Stock通信的伪代码实现流程
服务器绑定端口:server = new ServerSocket(PORT)
服务器阻塞监听:socket = server.accept()
服务器开启线程:new Thread(Handle handle)
服务器读写数据:BufferedReader PrintWriter
客户端绑定IP和PORT:new Socket(IP_ADDRESS, PORT)
客户端传输接收数据:BufferedReader PrintWriter
3 TCP协议与UDP协议有什么区别
TCP : 传输控制协议是基于连接的协议,在正式收发数据前,必须和对方建立可靠的连接。速度慢,合适传输大量数据。
UDP : 用户数据报协议是与TCP相对应的协议。面向非连接的协议,不与对方建立连接,而是直接就把数据包发送过去,速度快,适合传输少量数据。
4 什么是同步阻塞BIO,同步非阻塞NIO,异步非阻塞AIO
同步阻塞IO : 用户进程发起一个IO操作以后,必须等待IO操作的真正完成后,才能继续运行。
同步非阻塞IO: 用户进程发起一个IO操作以后,可做其它事情,但用户进程需要经常询问IO操作是否完成,这样造成不必要的CPU资源浪费。
异步非阻塞IO: 用户进程发起一个IO操作然后,立即返回,等IO操作真正的完成以后,应用程序会得到IO操作完成的通知。类比Future模式。
总结
1 BIO模型中通过Socket和ServerSocket完成套接字通道实现。阻塞,同步,连接耗时。
2 NIO模型中通过SocketChannel和ServerSocketChannel完成套接字通道实现。非阻塞/阻塞,同步,避免TCP建立连接使用三次握手带来的开销。
3 AIO模型中通过AsynchronousSocketChannel和AsynchronousServerSocketChannel完成套接字通道实现。非阻塞,异步。