一、五种IO类型
1、阻塞IO
用户进程一直等待数据准备好,在复制完成之前都是阻塞的
2、非阻塞IO
用户进程需要不断轮询查看是否数据准备好
优化了提升并发连接数量,但是每一个请求都需要创建一个socket建立连接,每个线程都需要去遍历轮询,导致cpu的消耗,直到数据返回成功
3、IO复用
(1)select/poll
单个线程可以处理多个网络连接。相对于BIO多了一步,注册到selector的过程,进程被selector函数阻塞。
select单个进程所能打开的fd()【就是文件描述符,linux内把所有的外部设备都看成一个文件操作】,对于每一个文件的读写操作都会调用内核提供的系统命令来返回给fd,对于socket的读写也会返回一个fd,所以放fd的时候,说明数据是可读或者是可写。
所以这个模型在单进程里,操作的连接数默认是1024,可以修改,但是会带来网络性能的下降,因为会去加大轮询次数,带来网络延迟,因为只有少数连接处于活跃状态,而每次轮询是查询所有的连接。
jdk1.6之前是使用这种模型
(2)epoll
jdk1.6后使用此模型。解决select-poll的缺陷
- 对单个进程锁打开的连接数没有限制(连接需要占用内存,1TB大概10w个连接)
- 利用每个fd上的callback函数来实现异步回调,省去了轮询的开销
- mmap:通过内核和用户空间映射同一块内存空间,来减少内存复制
4、异步IO
二、NIO
1、了解
基于通道和缓冲区操作的:
- 通道:一个新的原始IO
- 缓冲支持:负责数据存储和传输的缓冲区
- 具体:数据从通道读取到缓冲,数据从缓冲区写入通道
非阻塞的:
- 针对网络的,当线程从通道读取数据到缓冲区,线程依然可以进行其他事情(1.6以后epoll模型)
选择器:
- 用于监听多个通道的事件,例如链接打开、数据到达
- 单个线程可以监听多个数据通道
2、Channel
FileChannel:从文件中读写数据(不适合Selector,因为不能非阻塞)
DatagramChannel:通过UDP协议读写网络中的数据
SocketChannel:通过TCP协议读写网络中的数据
ServerSocketChannel:监听一个TCP连接,对于每一个新的客户端连接都会创建一个SocketChannel。
3、Buffer
buffer是一个对象,包含需要写入或者刚读出的数据,常用的缓冲区类型是ByteBuffer
public class aaa {
public static void main(String[] args) throws IOException {
try {
FileInputStream fis = new FileInputStream(new File("D:/test.txt"));
FileOutputStream fos = new FileOutputStream(new File("D:/test.txt"));
FileChannel fin = fis.getChannel();
FileChannel fout = fos.getChannel();
//初始化一个缓冲区
ByteBuffer buffer = ByteBuffer.allocate(1024);
//读取数据到缓冲区
fin.read(buffer);
//从读转化为写
buffer.flip();
fout.write(buffer);
//重置缓冲区
buffer.clear();
} catch (FileNotFoundException e) {
e.printStackTrace();
}
}
}
4、IO和NIO的区别
类型 | 操作区域 | 处理数据 | IO |
IO | 面向最初的数据源 | 每次读取时=读取所有字节或字符,无缓存 无法前后移动读取流中的数据 | 当一个线程在读或者写的时候,当数据被完全读取/写入之后,并且数据未准备好时,线程不能做其他任务,只能一直等待。 当线程处于活跃状态并且外部未准备好时,阻塞。 |
NIO | 面相缓冲区 | 先将数据读取到缓冲区 可在缓冲区前后移动数据流 | 当一个线程向某个通道发送请求时,当数据被完全读取/写入,并且数据未准备好时,线程可以操作其他任务,直到数据准备后再切换回原通道,继续读写,也就是selector的使用。 外部准备好时才唤醒线程,则不会阻塞。 |
5、缓冲区的内部细节
缓冲区本质是一块可以写入数据,以及从中读取数据的内存,实际上也是一个byte[]数据,只是在NIO中被封装成了NIO Buffer对象,并提供了一组方法来访问这个内存块。
- capacity:容量
- position:位置。读是跟踪从缓冲区读取了多少数据;写是数据放入数组哪个位置;
- limit:写是还有多少数据取出来写到通道;读是还有多少空间可以放出去;
例如:写入
初始话时,capacity是8,就是一个8长度的格子,当写入“hello”的时候,就是ficn.reds("hello);时,postion和limit都会指向4(从0开始),当flip()时,position指向0,limit是4,0-4就是要写入的字符,最后write()写入。
6、零拷贝的原理
IO流程:内核给磁盘发送命令需要读取磁盘的数据,在DMA的控制下,把磁盘上的数据读入到内核缓冲区,内核把数据从内核缓冲区复制到用户缓冲区。用户缓冲区再将数据拷贝到Socket buffer(也是内核),最后发送到网卡缓冲区,四个步骤。
设计到一个用户态到内核态的切换,影响cpu的性能。
从内核态到用户缓冲区没有用,为什么要设计呢
为了提升IO性能,假设应用程序进行读,内核缓冲区对于读相当于一个缓冲空间,当用户只读取一小部分数据的时候,但是内核从磁盘会读取一块数据,下次用户再读其他的数据的时候,在内核缓冲区已经存在就不需要再去磁盘获取,从这个角度是提升了性能的。
使用内存映射缓存mmap,通过内核和用户空间映射同一块内存空间,来减少内存复制。
零拷贝就是减少拷贝次数,减少内核态和用户态之间的数据的复制,提升IO性能。
public class aaa {
public static void main(String[] args) throws IOException {
try {
FileChannel in = FileChannel.open(Paths.get("D:/logo.png"), StandardOpenOption.READ);
FileChannel out = FileChannel.open(Paths.get("D:/logo_cp.png"), StandardOpenOption.READ, StandardOpenOption.CREATE, StandardOpenOption.WRITE);
MappedByteBuffer inMap = in.map(FileChannel.MapMode.READ_ONLY, 0, in.size());
MappedByteBuffer outMap = in.map(FileChannel.MapMode.READ_WRITE, 0, in.size());
byte[] bytes = new byte[inMap.limit()];
inMap.get(bytes);
outMap.put(bytes);
in.close();
out.close();
} catch (FileNotFoundException e) {
e.printStackTrace();
}
}
}
public class ccc {
public static void main(String[] args) {
try {
SocketChannel socketChannel = SocketChannel.open();
socketChannel.connect(new InetSocketAddress("localhost", 9090));
FileChannel channel = new FileInputStream("D:/1.txt").getChannel();
int position = 0;
long size = channel.size();
while (size > 0) {
long tf = channel.transferTo(position, size, socketChannel);
if (tf > 0) {
position += tf;
size = tf;
}
}
socketChannel.close();
channel.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
public class ddd {
public static void main(String[] args) {
try {
ServerSocketChannel channel = ServerSocketChannel.open();
channel.socket().bind(new InetSocketAddress(9090));
SocketChannel accept = channel.accept();
ByteBuffer allocate = ByteBuffer.allocate(1024);
int r = 0;
FileChannel fileChannel = new FileOutputStream("D:/text_cp.txt").getChannel();
while (r != -1){
r = accept.read(allocate);
allocate.flip();
fileChannel.write(allocate);
allocate.clear();
}
} catch (IOException e) {
e.printStackTrace();
}
}
}
7、SocketChannel和ServerSocketChannel
服务端
public class aaa {
public static void main(String[] args) throws IOException {
try {
//支持两种模式:阻塞、非阻塞
ServerSocketChannel open = ServerSocketChannel.open();
open.configureBlocking(false);
//绑定端口号
open.socket().bind(new InetSocketAddress(9090));
while (true) {
SocketChannel accept = open.accept();
//存在连接
if (accept != null) {
ByteBuffer buffer = ByteBuffer.allocate(1024);
accept.read(buffer);
System.out.println(new String(buffer.array()));
//再把消息写回到客户端
Thread.sleep(10000);
buffer.filp();
accept.write(buffer);
} else {
Thread.sleep(1000);
System.out.println("无客户端连接");
}
}
} catch (FileNotFoundException | InterruptedException e) {
e.printStackTrace();
}
}
}
客户端
public class aaa {
public static void main(String[] args) throws IOException {
try {
SocketChannel open = SocketChannel.open();
//把客户端设置为非阻塞,在非阻塞模式下,不一定是等到连接建立之后再往下执行
open.configureBlocking(false);
open.connect(new InetSocketAddress("localhost", 9090));
if(open.isConnectionPending()) {
open.finishConnect();
}
ByteBuffer buffer = ByteBuffer.allocate(1024);
buffer.flip();
open.write(buffer);
//读取服务端返回的数据
buffer.clear();
//非阻塞模式,这里不阻塞
int r = open.read(buffer);
if(r > 0) {
System.out.println("收到服务端的消息" + new String(buffer.array()));
}
} catch (FileNotFoundException e) {
e.printStackTrace();
}
}
}
8、选择器Selector
一个单独的线程可以管理多个channel,从而管理多个网络连接。
public class aaa {
static Selector selector;
public static void main(String[] args) throws IOException {
//创建一个多路复用器
Selector selector = Selector.open();
ServerSocketChannel channel = ServerSocketChannel.open();
//连接的非阻塞
channel.configureBlocking(false);
channel.socket().bind(new InetSocketAddress(9090));
//监听连接事件,会返回一个SelectionKey,通道的唯一标识
channel.register(selector, SelectionKey.OP_ACCEPT);
//轮询
while (true) {
//阻塞,所有注册到复用器上事件
selector.select();
//一旦某个channel准备就绪,就返回他们的key
Set<SelectionKey> selectionKeys = selector.selectedKeys();
Iterator<SelectionKey> iterator = selectionKeys.iterator();
while (iterator.hasNext()) {
//一定是已经就绪的通道
SelectionKey next = iterator.next();
//拿到通道后,可以处理了,避免重复处理
iterator.remove();
if(next.isAcceptable()){
//连接事件
HandleAccept(next);
}else if(next.isReadable()){
//读事件
HandleRead(next);
}
}
}
}
private static void HandleAccept(SelectionKey selectionKey) throws IOException {
ServerSocketChannel serverSocketChannel = (ServerSocketChannel)selectionKey.channel();
SocketChannel accept = serverSocketChannel.accept();
//IO的非阻塞
accept.configureBlocking(false);
accept.write(ByteBuffer.wrap("Server write".getBytes()));
//注册的是accept的读事件
accept.register(selector, SelectionKey.OP_ACCEPT);
}
private static void HandleRead(SelectionKey selectionKey) throws IOException {
SocketChannel socketChannel = (SocketChannel)selectionKey.channel();
ByteBuffer buffer = ByteBuffer.allocate(1024);
socketChannel.read(buffer);
System.out.println("Server receive msg: " + new String(buffer.array()));
}
}
public class bbb {
static Selector selector;
public static void main(String[] args) {
try {
selector = Selector.open();
SocketChannel socketChannel = SocketChannel.open();
socketChannel.configureBlocking(false);
socketChannel.connect(new InetSocketAddress("localhost", 9090));
socketChannel.register(selector, SelectionKey.OP_CONNECT);
while (true) {
selector.select();
Set<SelectionKey> selectionKeys = selector.selectedKeys();
Iterator<SelectionKey> iterator = selectionKeys.iterator();
while (iterator.hasNext()) {
SelectionKey next = iterator.next();
iterator.remove();
if(next.isConnectable()){
//连接事件
HandleConnect(next);
}else if(next.isReadable()){
//读事件
HandleRead(next);
}
}
}
} catch (IOException e) {
e.printStackTrace();
}
}
private static void HandleConnect(SelectionKey selectionKey) throws IOException {
SocketChannel socketChannel = (SocketChannel)selectionKey.channel();
if(socketChannel.isConnectionPending()){
socketChannel.finishConnect();
}
socketChannel.configureBlocking(false);
socketChannel.write(ByteBuffer.wrap("Client receive".getBytes()));
socketChannel.register(selector, SelectionKey.OP_READ);
}
//读取服务端返回的数据
private static void HandleRead(SelectionKey selectionKey) throws IOException {
SocketChannel socketChannel = (SocketChannel)selectionKey.channel();
ByteBuffer buffer = ByteBuffer.allocate(1024);
socketChannel.read(buffer);
System.out.println("Client receive msg: " + new String(buffer.array()));
}
}
9、BIO\NIO\AIO的区别
BIO | NIO | AIO | |
阻塞 | 阻塞:一个线程执行IO操作会被阻塞 | 非阻塞:线程可以同时处理多个IO请求 | 非阻塞 |
同步 | 同步:需要等待IO操作完成后才能继续执行 | 异步:轮询方式=channel+buffer+selector | 回调机制 |
处理 | 面向流 | 缓冲区 | 面向事件 |
并发 | 低,需要创建大量线程 | 通过单线程或少量线程处理 | 同左 |
场景 | 连接数少且吞吐量不高 | 连接数较多但请求量较小 | 高并发 |