IO体系
反射、代理、多线程这些东西,当然,还有 NIO
在 Java 中,很多大家熟悉的框架都在使用 Netty,而且,这些框架遍布 Java 的各个领域,包括但不仅限于大数据、RPC、消息队列、搜索引擎、数据库等。
IO --> 五种IO模型 --> NIO 模型 --> 基于NIO的网络框架(Netty)
IO--面向流的编程
不可能既是输入又是输出流
- 输入流:打开流,读取数据,关闭流
- 输出流:打开流,写数据,关闭流
- 字节流 InputStream OutputStream 都是抽象
- in:write flush close
- out:
- 字符流 Reader Writer 都是抽象
- re
- wr
底层都是字节流
- 节点流:直接和设别,特定地方读写
- 过滤流:(装饰器模式)节点流作为输入或者输出,使用一个存在的输入或者输出流来创建(作为参数传递进去)
IO流的链:数据--》缓冲输出流--》文件输出流--》文件。装饰器模式的作用,获取一个
装饰器模式
NIO--面向块或者缓冲区编程
1.4出来的
NIO组件回顾
Java NIO,它不仅仅可以用在网络编程中,还能用在文件读写等其它场景
Channel--桥梁
java.nio.channels.Channel
实体之间的桥梁,实体可以是硬件设备,文件,网络套接字或者可执行IO操作的程序(Linux中一切皆文件)
不同实体(文件)类型对应不同的类型的Channel:
- FileChannel : 操作普通文件
- DatagramChannel:UDP协议
- SocketChannel:TCP协议,客户端和服务端之间的Channel
- ServerSocketChannel:TCP协议,用于服务端的Channel
Buffer--数据容器
线性有序序列(数组),存储特定的基本类型。Buffer的类型有CharBuffer,ByteBuffer, ShortBuffer, IntBuffer, LongBUffer, FloatBuffer, DoubleBuffer这7个,没有boolean,对于每一种都有堆内存和直接内存的实现****。(HeapByteBuffer和DirectByteBuffer)
两种模式:读模式和写模式
主要属性:是capacity(读模式和写模式一致,从1开始),limit(读模式表示最大可读到的位置,写模式是可以写到的最大位置,从1开始),position(读模式是下一个可读的位置,写模式是下一个可写的位置。从0开始)。
和Channel的关系:从buffer读取数据到Channel里面,从Channel读取数据到buffer里面
重要的API:
- 分配一个Buffer:
allocate() - 写入数据:
buf.put()或者channel.read(buf),read是read to buffer的意思 - 切换为读模式:
buf.flip()。Buffer从写模式切换为读模式的时候。limit变成position的值,position变成0 - 读取数据:
buf.read()或者channel.write(buf),write 是 write from buf - 重新读取和写入:
rewind(),重置 position 为0,limit 和 capacity 保持不变,可以重新读写数据 - 清空数据:
buf.clear()清空数据 - 压缩数据:
buf.compact()清除已读数据,将未读数据往前移动
package com.deltaqin.io;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.io.RandomAccessFile;
import java.nio.ByteBuffer;
import java.nio.channels.FileChannel;
import java.nio.charset.StandardCharsets;
/**
* @author deltaqin
* @date 2021/4/15 12:55 下午
*/
public class FileChannelDemo {
public static void main(String[] args) throws IOException {
FileChannel fileChannel = new RandomAccessFile("a.txt", "rw").getChannel();
// Byte类型的buffer
ByteBuffer buffer = ByteBuffer.allocate(1024);
while ((fileChannel.read(buffer)) != -1) {
// buffer 默认是写模式,flip 是切换为读模式
buffer.flip();
// buffer是否有数据未读取
while (buffer.hasRemaining()) {
// 未读取数据的长度
int remain = buffer.remaining();
byte[] bytes = new byte[remain];
//读出buffer的数据
buffer.get(bytes);
System.out.println(new String(bytes, StandardCharsets.UTF_8));
}
// 清空buffer,为下一次数据写入准备。将buffer切换为写模式
buffer.clear();
}
}
}
DirectByteBuffer
ByteBuffer buffer = ByteBuffer.allocateDirect__(512);
�
HeapByteBuffer多了一次数据拷贝,堆外内存直接和IO设备交互,
- 为什么OS不直接操作堆内存?如果此时操作的话,和IO的操作时间比较长,导致GC长时间不能执行,直接OOM拷贝时候是不会GC的,堆内存拷贝到OS指定的位置,这个耗时远小于直接和IO设备交互的时间。
- 如何避免内存泄漏?当java的DirectByteBuffer被回收的时候,其内部的成员变量address不再引用直接内存,JNI的方式就会回收堆外内存
- 原生方法native,直接调用malloc方法

堆外内存的地址放在抽象类里面,

DirectByteBuffer(int cap) { // package-private
super(-1, 0, cap, cap);
boolean pa = VM.isDirectMemoryPageAligned();
int ps = Bits.pageSize();
long size = Math.max(1L, (long)cap + (pa ? ps : 0));
//判断是否有足够的直接内存空间分配,可通过‐XX:MaxDirectMemorySize=<size>参数指定直接内存最大可分配空间,
// 如果不指定默认为最 大堆内存大小,
//在分配直接内存时如果发现空间不够会显示调用System.gc()触发一次full gc回收掉一部分无用的直接内存的引用对象,
// 同时直接内存也会被释放掉
//如果释放完分配空间还是不够会抛出异常java.lang.OutOfMemoryError
Bits.reserveMemory(size, cap);
long base = 0;
try {
// 调用unsafe本地方法分配直接内存
base = unsafe.allocateMemory(size);
} catch (OutOfMemoryError x) {
// 分配失败,释放内存
Bits.unreserveMemory(size, cap);
throw x;
}
unsafe.setMemory(base, size, (byte) 0);
if (pa && (base % ps != 0)) {
// Round up to page boundary
address = base + ps - (base & (ps - 1));
} else {
address = base;
}
// 使用Cleaner机制注册内存回收处理函数,当直接内存引用对象被GC清理掉时,
// 会提前调用这里注册的释放直接内存的Deallocator线程对象的run方法
cleaner = Cleaner.create(this, new Deallocator(base, size, cap));
att = null;
}
Selector
SelectableChannel 对象(s) 的多路复用器 ,一个Selector可以关联到多个SelectableChannel。
注意是SelectableChannel的多路复用器, 不可以跟FileChannel配合使用,FileChannel不是SelectableChannel。只有和网络编程有关的Channel才是Selectable的
一个 Selector 可以为多个 Channel 服务,监听它们准备好的事件。
- Selector 创建的方式有两种,一种是调用Selector.open()方法,一种是调用SelectorProvider.openSelector()方法,其中 SelectorProvider 是自定义的。
- 设置了Channel是非阻塞的之后,开始讲Channel注册到Selector里面,注册之后返回一个SelectionKey的对象,SelectionKey 相当于一个牌子将Channel和Selector绑定在一起,保存感兴趣的事件。
- 事件是Channel感兴趣的事情,比如读事件,写事件,java在SelectionKey中定义了四种事件。四种事件的二进制位是错开的嘿嘿,可以直接使用或运算实现监听多种感兴趣的事件
- 读事件:SelectionKey.OP_READ = 1 << 0 = 0000 0001
- 写事件:SelectionKey.OP_WRITE = 1 << 2 = 0000 0100
- 连接事件:SelectionKey.OP_CONNECT = 1 << 3 = 0000 1000
- 接收连接事件:SelectionKey.OP_ACCEPT = 1 << 4 = 0001 0000
- Selector.select()只是一直阻塞询问一次,轮询需要在外面加上while(true)。selectNow() 不阻塞直接返回,select(timeOut)是阻塞一段时间直接返回。
- Selector.selectKeys()返回就绪的事件的set集合,也就是Set
,然后遍历这些事件拿到想要的结果
Set<SelectionKey> selectionKeys = selector.selectedkeys();
Iterator<SelectionKey> keyIterator = selectionKeys.iterator();
while(keyIterator.hasNext()){
SelectionKey key = keyIterator.next();
if(key.isAcceptable()) {
} else if (key.isReadable()) {
} else if (key.isWritable()) {
} else if (key.isConnectable()) {
}
keyIterator.remove();
}
Channel、Buffer、Selector 联合使用
package com.deltaqin.io;
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;
import java.util.Set;
/**
* @author deltaqin
* @date 2021/4/15 10:17 上午
*/
public class NIODemo {
public static void main(String[] args) throws IOException {
Selector selector = Selector.open();
ServerSocketChannel serverSocketChannel = ServerSocketChannel.open();
serverSocketChannel.bind(new InetSocketAddress(8001));
serverSocketChannel.configureBlocking(false);
// 将Channel注册到selector上,并注册Accept事件
serverSocketChannel.register(selector, SelectionKey.OP_ACCEPT);
System.out.println("start!!!");
while (true) {
// 如果使用的是select(timeout)或selectNow()需要判断返回值是否大于0
selector.select();
Set<SelectionKey> selectionKeys = selector.selectedKeys();
Iterator<SelectionKey> iterator = selectionKeys.iterator();
while (iterator.hasNext()) {
SelectionKey selectionKey = iterator.next();
if (selectionKey.isAcceptable()) {
// 注意强转
ServerSocketChannel serverSocketChannel1 = (ServerSocketChannel) selectionKey.channel();
// socketChannel获取
SocketChannel socketChannel = serverSocketChannel1.accept();
System.out.println("accept a socket" + socketChannel.getRemoteAddress());
socketChannel.configureBlocking(false);
// 将SocketChannel注册到Selector上,并注册读事件
socketChannel.register(selector, SelectionKey.OP_READ);
} else if (selectionKey.isReadable()) {
// 注意强转
SocketChannel socketChannel = (SocketChannel) selectionKey.channel();
ByteBuffer buffer = ByteBuffer.allocate(1024);
int len = socketChannel.read(buffer);
// 拷贝的阻塞
if (len > 0) {
buffer.flip();
byte[] bytes = new byte[buffer.remaining()];
buffer.get(bytes);
// 回车的换行会传递,这里是Mac。替换为空
String con = new String(bytes, "utf-8").replace("\n", "");
System.out.println("receive :" + con);
}
}
// 读取之后
iterator.remove();
}
}
}
}
为什么会有不同的IO模型
- 首先明确什么用户和内核空间,为什么这样划分:操作系统了为了保证安全,不允许普通用户进程直接操作系统的核心,也就是内核,所以系统划分为两个空间,用户进程只可以通过系统调用来切换到内核态让内核执行
- 其次明确数据是如何传递的,内核和用户态传输数据的两个阶段是什么:网络通信中,我们的数据首先发送给网卡,内核将数据拷贝到内核空间之后再拷贝到用户空间,处理之后原路返回。(也就是两个阶段:等待内核空间准备数据,将数据从内核空间拷贝到用户空间)
- 最后,根据上面的两个阶段:准备数据和拷贝数据,Unix将IO的模型分为五个,阻塞IO,非阻塞IO,IO多路复用,信号驱动IO,异步IO
说说常见的五种IO模型
- 阻塞IO:一直阻塞到用户空间得到内核空间拷贝给自己的数据(准备+拷贝都阻塞)
- 非阻塞IO:用户进程不断询问内核数据准备好没,像自旋一样不断重试,(准备不阻塞,拷贝阻塞)
- IO多路复用:IO操作注册到Selector里面返回,阻塞转移到了Selector,数据准备好之后加入一个list(用户调用epoll_wait得到lis,内核不会自己通知,只是调用的时候不会再遍历了直接返回准备好的list),最后拷贝数据也是阻塞的(准备不阻塞(转移到了Selector阻塞),拷贝阻塞)
- 信号驱动IO:系统调用注册信号说自己需要什么数据之后立即返回,内核准备好之后通知用户进程,用户进程阻塞拷贝数据(准备阶段不阻塞,拷贝阶段阻塞)
- 异步IO:内核准备数据之后自己将数据拷贝到用户空间之后通知用户进程直接使用(准备和拷贝都不阻塞)
阻塞型 IO、非阻塞型、IO 多路复用、信号驱动 IO 都是同步 IO,只有最后一种才是异步 IO。
服务器通常都是 linux,所以现在大部分框架都不是很支持异步 IO,包括 Netty 之前实现了一版,但是后面给废弃掉了。
说说阻塞非阻塞同步异步的区别
- 阻塞和非阻塞的意思是你的请求是立即返回还是被挂起等待直到结果返回。(关心线程是否被挂起)
- 同步是指调用者会被阻塞直到IO完成,返回结果。异步是指不会阻塞,结果通过通知或者回调返回(关心什么时候返回数据)
说说java的BIO,NIO,AIO
- BIO:java最原始的IO模型,阻塞型IO,用户发起请求会阻塞当前线程直到内核空间将数据拷贝到用户空间。BIO是面向流的,流是单向的,所以有输入和输出流。流只能同步读写。流一般和字节数组或者字符数组配合使用,
- NIO:New IO , 在java中使用多路复用实现,在java.nio实现,JDK1.4引入。NIO是面向缓存区的,效率更高。Channel是双向的,所以即可读也可写。Channel可以异步读写。Channel一般和Buffer配合使用
- AIO:异步IO,也就是NIO2,在jdk1.7引入到java.nio
BIO和NIO的区别
写一下BIO ,NIO AIO 的示例代码
BIO
几个关键点:
- 使用的API 是网络net包里面的ServerSocket以及Socket,建立服务端套接字之后相当于是开了一个监听的端口,要accept才是真正的开始阻塞
- while(true) 循环用来阻塞accept获取Socket连接
- 一个连接一个线程new Thread
package com.deltaqin.io;
import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStreamReader;
import java.net.ServerSocket;
import java.net.Socket;
/**
* @author deltaqin
* @date 2021/4/15 9:58 上午
*/
public class BIODemo {
public static void main(String[] args) throws IOException {
ServerSocket serverSocket = new ServerSocket(8000);
System.out.println("start!!!");
while (true) {
Socket socket = serverSocket.accept();
System.out.println("accepted a socket:" + socket);
new Thread(()->{
try {
BufferedReader bufferedReader = new BufferedReader(
new InputStreamReader(socket.getInputStream()));
String msg;
while ((msg = bufferedReader.readLine()) != null) {
System.out.println(msg);
}
} catch (IOException e) {
e.printStackTrace();
}
}).start();
}
}
}
上面是服务端程序,启动之后,使用nc建立socket之后,发送数据,可以在输出中看到接受到数据
➜ nc localhost 8000
hello

每一个连接都分配一个线程,客户端一直增加,资源耗尽就会挂掉
NIO
- Selector
- ServerSocketChannel
- register():将Channel注册到Selector
- while(true):循环select获取SelectionKey
- select()
- iterator
- selectionKey.isAcceptable()
- accept之后将SocketChannel注册到Selector
- selectionKey.isReadable()
- 获取SocketChannel之后开始读取传递过来的数据
- selectionKey.isAcceptable()
Java 中的 NIO 使用的是 IO 多路复用技术实现的,多个连接,服务器只用一个线程通过while循环来处理(拿到就绪的请求开始处理)
下面演示的demo在接收数据的时候使用的是当前的线程,这个只是适合连接少的时候,selectKeys返回太多的话,由于拷贝数据是阻塞的,所以有性能问题,可以使用线程池来创建拷贝数据的任务
NIO相比于BIO主要是简化了每次新的连接来了就是一个线程,这里可以看到只是使用了一个线程
这里的while循环就是阻塞的Selector。不断轮询内核有无数据就绪的Channel
package com.deltaqin.io;
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;
import java.util.Set;
/**
* @author deltaqin
* @date 2021/4/15 10:17 上午
*/
public class NIODemo {
public static void main(String[] args) throws IOException {
Selector selector = Selector.open();
ServerSocketChannel serverSocketChannel = ServerSocketChannel.open();
serverSocketChannel.bind(new InetSocketAddress(8001));
serverSocketChannel.configureBlocking(false);
// 将Channel注册到selector上,并注册Accept事件
serverSocketChannel.register(selector, SelectionKey.OP_ACCEPT);
System.out.println("start!!!");
while (true) {
// 如果使用的是select(timeout)或selectNow()需要判断返回值是否大于0
selector.select();
Set<SelectionKey> selectionKeys = selector.selectedKeys();
Iterator<SelectionKey> iterator = selectionKeys.iterator();
while (iterator.hasNext()) {
SelectionKey selectionKey = iterator.next();
if (selectionKey.isAcceptable()) {
// 注意强转
ServerSocketChannel serverSocketChannel1 = (ServerSocketChannel) selectionKey.channel();
// socketChannel获取
SocketChannel socketChannel = serverSocketChannel1.accept();
System.out.println("accept a socket" + socketChannel.getRemoteAddress());
socketChannel.configureBlocking(false);
// 将SocketChannel注册到Selector上,并注册读事件
socketChannel.register(selector, SelectionKey.OP_READ);
} else if (selectionKey.isReadable()) {
// 注意强转
SocketChannel socketChannel = (SocketChannel) selectionKey.channel();
ByteBuffer buffer = ByteBuffer.allocate(1024);
int len = socketChannel.read(buffer);
// 拷贝的阻塞
if (len > 0) {
buffer.flip();
byte[] bytes = new byte[buffer.remaining()];
buffer.get(bytes);
// 回车的换行会传递,这里是Mac。替换为空
String con = new String(bytes, "utf-8").replace("\n", "");
System.out.println("receive :" + con);
}
}
// 读取之后
iterator.remove();
}
}
}
}
AIO
使用的时候需要重写channel的回调函数,main线程执行之后就会停止,所以要在最后加一个读取阻塞。
会将数据写到用户空间之后通知开始执行回调函数
package com.deltaqin.io;
import java.io.IOException;
import java.net.InetSocketAddress;
import java.nio.ByteBuffer;
import java.nio.channels.AsynchronousServerSocketChannel;
import java.nio.channels.AsynchronousSocketChannel;
import java.nio.channels.CompletionHandler;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.Future;
/**
* @author deltaqin
* @date 2021/4/15 10:58 上午
*/
public class AIODemo {
public static void main(String[] args) throws IOException {
AsynchronousServerSocketChannel asynchronousServerSocketChannel =
AsynchronousServerSocketChannel.open();
asynchronousServerSocketChannel.bind(new InetSocketAddress(8003));
System.out.println("start!!!");
asynchronousServerSocketChannel.accept(null, new CompletionHandler<AsynchronousSocketChannel, Object>() {
@Override
public void completed(AsynchronousSocketChannel asynchronousSocketChannel, Object o) {
try{
System.out.println("accept a socket" + asynchronousSocketChannel.getRemoteAddress());
// 使用serverSocket阻塞获取
asynchronousServerSocketChannel.accept(null, this);
// 内核将数据拷贝到用户空间
while (true) {
ByteBuffer byteBuffer = ByteBuffer.allocate(1024);
Future<Integer> future = asynchronousSocketChannel.read(byteBuffer);
if (future.get() > 0) {
byteBuffer.flip();
byte[] bytes = new byte[byteBuffer.remaining()];
byteBuffer.get(bytes);
String con = new String(bytes, "utf-8");
if (con.equals("\n")) {
continue;
}
System.out.println("receive :" + con);
}
}
}catch (IOException e) {
e.printStackTrace();
} catch (InterruptedException e) {
e.printStackTrace();
} catch (ExecutionException e) {
e.printStackTrace();
}
}
@Override
public void failed(Throwable throwable, Object o) {
System.out.println("failed");
}
});
System.in.read();
}
}
NIO框架
NIO和java中的NIO分别指的是什么?
- 广义上来说,NIO 包括 Non-blocking IO(非阻塞 IO)、IO Multiplexing(IO 多路复用)、Signal-Driven IO(信号驱动 IO)。狭义上来说,Non-blocking 非阻塞,也就是非阻塞IO。
- Java NIO 中的N是指New,即新的意思,Java NIO 即新的 IO,是相对于 OIO (Old IO) 或者 BIO (Blocking IO) 来说的,在 Java 的实现中是基于** IO Multiplexing **模型实现的。
为什么有了NIO框架,自带的NIO不香吗?
- API 复杂难用,尤其是 Buffer 的指针切来切去
- 需要掌握丰富的知识,比如多线程和网络编程
- 可靠性无法保证,断线重连、半包粘包、网络拥塞统统需要自己考虑
- 空轮询 bug,CPU 又 100% 了,一直未根除此问题
都有什么常见的NIO框架(发展史)
- JDKNIO(2002)
- Netty2(2004):Netty2 号称是 Java 社区中第一个基于事件驱动的网络应用框架
<dependency>
<groupId>net.gleamynode</groupId>
<artifactId>netty2</artifactId>
<version>1.9.2</version>
</dependency>
- Grizzly(2004):应用服务器,为了打败 Tomcat 而生的。使用了最新的 NIO 技术,并封装成了完整的框架 ——Grizzly,提供了高性能的 API,并隐藏了编程的复杂性,使得它一出世就很火。比如 Jetty、Comet 等都是基于 Grizzly 构建的。文档少,更新慢,社区不活跃
- Tomcat(2005):6.0 版本,它也开始支持 NIO 了,同样使得它的性能有了质的飞跃。但是,Tomcat 的 NIO 通信层并没有从它本身的代码中解耦出来
- MINA(2005):MINA 可帮助用户轻松开发高性能和高可伸缩性的网络应用程序,支持各种传输协议,比如 TCP 和 UDP 等。
- MINA 的功能繁杂,复杂性高,性能差
- MINA 的功能过于耦合
- Netty 的 API 更整洁、更文档化
- Netty 的更新周期短
- MINA3 会破坏 API 的兼容性
- Netty3(2008):全新的 Netty,背靠 JBoss,并借鉴了 MINA 中很多优秀的设计,更新频繁,社区也异常活跃,占领了网络通信的半壁江山。
<dependency>
<groupId>org.jboss.netty</groupId>
<artifactId>netty</artifactId>
<version>3.0.0.Final</version>
</dependency>
- Netty4(2012):JBoss独立出来,很多改进,比如线程模型、池化的 Buffer 等,性能提升到了极致。Netty 彻底独立
<dependency>
<groupId>io.netty</groupId>
<artifactId>netty-all</artifactId>
<version>4.0.0.Final</version>
</dependency>
- Netty5:JDK 的新特性,比如 ForkJoinPool
Netty的应用
- 框架:spring cloud gateway, dubbo, grpc
- 大数据:Hadoop,flink
- 消息队列:rocketMQ,ActiveMQ
- 搜索引擎:ES
- 分布式协调器:zookeeper
- 数据库:Neo4j
- 工具类:async-http-client
- 负载均衡:ribbon
posted on 2025-10-13 01:08 chuchengzhi 阅读(10) 评论(0) 收藏 举报
浙公网安备 33010602011771号