一、IO读写原理
- 文件的读写还是socket读写,再Java应用层开发,都是 input或者 output处理。用户程序进行的IO读写,会用到 read&write(内核态) 两大系统调用。
read:将数据从内核缓冲区复制到进程缓冲区
write:把数据从进程缓冲区复制到内核缓冲区 - 内核缓冲区 和 进程缓存区
用户进程(N个):处于用户态(用户空间)
系统空间:内核态。在用户态需要访问系统资源,需要借助内核态,系统资源主要有:
1)CPU:控制一个程序的执行
2)输入输出:一切都是流,所有流都需要借助内核态
3)进程管理:进程创建、销毁、阻塞、唤醒之间的调度
4)内存:内存的申请、释放
5)进程间通信:进程之间不能相互访问内存,所以进程
以上所提到的系统资源,再用户进程中是无法被直接访问的,只有通过操作系统来访问,所以把操作系统访问这一功能称之为系统调用。
- 缓冲区的目的,是为了减少频繁的系统IO调用
系统调用需要从用户态切换到内核态,切换之后保存用户进程的数据和状态等信息。结束调用之后会需要恢复之前的信息,为了减少这种损耗的时间,还有损耗性能的时间,所以出现了缓冲区。 - 有了缓冲区,操作系统使用 read从内核缓冲区复制到进程缓冲区,write从进程缓冲区复制到内核缓冲区,只有缓冲区中的数据达到一定的量,再进行IO的系统调用,提升了系统的性能。
- Java IO读写底层的流程图:
二、四种主要的IO模型
概念
- 阻塞IO:需要内核IO操作彻底完成之后,才返回到用户空间,执行用户的操作
非阻塞IO:不需要等待内核IO操作彻底完成之后,才返回到用户空间
阻塞/非阻塞 指的是用户空间程序的执行状态 - 同步IO:是用户空间线程和内核空间线程的交互,用户空间线程是主动发起IO请求的一方,内核空间线程指的是被动接收的一方
异步IO:与上面相反
1.同步阻塞 BIO
- jdk1.4之前,IO模型都采用的BIO模型。
需要先在服务器端启动一个ServerSocket,然后在客户端启动Socket与服务器端进行通信,服务器端调用accept方法来接收客户端的连接请求,一旦接收上连接请求,就可以建立套接字,在这个套接字上进行读写操作,此时不能再接收其他客户端的连接请求,只能等待当前连接的客户端的执行操作完成。 - JDK1.4开始,出现同步阻塞式IO
处理多个客户端请求,使用多线程
优点:用户线程阻塞等待数据期间,不会占用CPU资源
缺点:BIO模型在高并发的场景下是不可用的
BIO编程
- 服务器端
1)创建ServerSocket实例
2)绑定端口
3)通过accept方法监听客户端的连接,有客户端连接会返回socket实例
4)进行读写操作
5)关闭资源 - 服务器端如何设计为可处理很多客户端的连接?
主线程负责接收客户端连接,子线程负责和客户端交互。
代码实现:
//子线程
class HandlerThread extends Thread{
private Socket socket;
public HandlerThread(Socket socket){
this.socket = socket;
}
@Override
public void run() {//重写run方法
try {
//读取客户端的请求信息
OutputStream output = null;
BufferedReader reader = null;
while(true){
reader = new BufferedReader(new InputStreamReader(socket.getInputStream()));
String msg = reader.readLine();
System.out.println("客户端:"+socket.getRemoteSocketAddress()+",发送的信息为:"+msg);
//给客户端回复
output = socket.getOutputStream();//获取输出流
//控制台读取数据
reader = new BufferedReader(new InputStreamReader(System.in));
String info = reader.readLine();
output.write((info+"\n").getBytes());
output.flush();
if("".equals(info) || "exit".equals(info)) break;
}
//关闭资源
reader.close();
output.close();
} catch (IOException e) {
e.printStackTrace();
} finally {
try {
System.out.println("客户端:"+socket.getRemoteSocketAddress()+"关闭了");
socket.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
}
//主线程
public class MyBIOServer {
public static void main(String[] args) {
ServerSocket ssocket = null;
try {
//创建ServerSocket实例
ssocket = new ServerSocket();
//绑定端口
ssocket.bind(new InetSocketAddress(9999));
System.out.println("服务器端启动了...");
while(true){
//通过accept方法监听客户端的连接,有客户端连接会返回socket实例
Socket socket = ssocket.accept(); //这个方法是一个阻塞方法
System.out.println("有新的客户端连接:"+socket.getRemoteSocketAddress());
//将socket实例交给子线程去处理
new HandlerThread(socket).start();
}
} catch (IOException e) {
e.printStackTrace();
} finally {
if(ssocket != null){
try {
ssocket.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
}
}
- 客户端
1)创建socket实例
2)通过connect去指定服务器端的IP和端口
3)进行读写操作
4)关闭资源
代码实现:
public class MyBIOClient {
public static void main(String[] args) {
Socket socket = null;
try {
//创建Socket实例
socket = new Socket();
//连接服务器端,通过connect绑定IP地址和端口号
socket.connect(new InetSocketAddress("127.0.0.1", 9999));
System.out.println("客户端启动了...");
//进行读写操作
while(true){
//写操作
OutputStream output = socket.getOutputStream();
BufferedReader reader = new BufferedReader(new InputStreamReader(System.in));
output.write((reader.readLine()+"\n").getBytes());
output.flush();
//读操作
reader = new BufferedReader(new InputStreamReader(socket.getInputStream()));
String msg = reader.readLine();
System.out.println("服务器响应信息为:"+msg);
}
} catch (IOException e) {
e.printStackTrace();
} finally {
if(socket != null){
try {
socket.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
}
}
2.同步非阻塞 NIO
NIO的内容在NIO模型中具体描述。
3.IO多路复用
- 通过一个新的系统调用 select/epoll 系统调用,实现一个进程监视多个文件描述符(唯一ID)。一旦某个描述符就绪(内核缓冲区可读/可写),内核能够通知用户程序进行IO系统调用。
- 以线程为例:单个线程不断的轮询 select/epoll 系统调用 所负责多个socket连接,当某个socket连接有数据到达,就返回这些可以读写的连接。
- 特点:
与NIO模型类似,多路复用IO需要轮询负责select/epoll查询调用的线程,查找出可以进行IO操作的一个连接,对于每一个可以查询的socket,一般需要设置为non-blocking。 - 优点:
select/epoll 可以同时处理成百上千的连接,与之前的一个线程维护一个连接相比,IO多路复用则不需要创建线程,也就不需要维护,从而减少系统开销。 - 缺点:
select/epoll系统调用,属于阻塞的模式,同步IO。读写事件就绪之后,用户自己进行读写,这个读写过程也是阻塞的。
4.异步非阻塞 AIO
- 用户线程发起系统调用,告知内核启动某个IO操作,用户线程直接返回。内核在整个IO操作(数据准备,数据复制完成之后,调用回调函数通知用户程序(已就绪),用户执行后续的业务操作。
- 特点:
分为两个等待过程,等待数据就绪,等待数据拷贝,而在这两个等待过程中用户线程都不是block的,等这两个操作完成之后,用户线程就会收到一个信号。所以AIO又称之为信号驱动IO。 - 缺点:
需要事件的注册,就需要操作系统,对系统资源和内存资源消耗大。 - java.nio.channels包:
AsynchronousServerSocketChannel 服务器数据通道
AsynchronousSocketChannel 客户端通道
AsynchronousDatagramChannel 基于UDP的通道
AsynchronousFIleChannel 操作文件 - 异步非阻塞,服务器端实现一个一个线程,异步执行当前客户端请求,客户端的请求都是操作系统完成再去通知服务器端处理,所以这个AIO主要用于连接数多同时请求比较复杂的系统架构。
BIO NIO AIO三个网络模型的适用场景:
模型 | 适用场景 |
---|---|
BIO | 连接数目少的系统架构,同时也需要服务器资源充足,虽然效率不高,但是程序… |
NIO | 连接数目比较多以及连接时间短的系统架构,例如聊天服务器,编程比较复杂 |
AIO | 连接数目比较多以及连接时间长的系统架构,充分调用操作系统参与并发,不管是业务逻辑,还是并发逻辑,都需要操作系统的支持,所以不同的操作系统,性能也是不同的 |