BIO,NIO,AIO总结
BIO
阻塞IO, 最常见的就是Socket连接了。
上代码:
服务端:
public static void main(String[] args) {
try {
ServerSocket serverSocket = new ServerSocket(9998, 100, InetAddress.getByName("127.0.0.1"));
System.out.println("等待连接到达..........");
while (true){
// 是阻塞的,直到客户端连接到达
Socket socket = serverSocket.accept();
System.out.println("接收到一个连接");
// 多线程处理,可以让socket接收更多的客户端,而不至于阻塞在等待客户端回写数据的地方
new Thread(new Runnable() {
@Override
public void run() {
try {
handler(socket);
} catch (IOException e) {
e.printStackTrace();
}
}
});
// 如果使用同步的,下面等待客户端响应数据的地方一直阻塞住,如果这时候有另外一个客户端连接过来,服务端是没法连接上的
// 新的客户端那边会发送数据成功,但是这边接收不到,就丢了数据
// handler(socket);
}
} catch (IOException e) {
e.printStackTrace();
}
}
private static void handler(Socket socket) throws IOException {
byte []leng=new byte[1024];
// 这个方法也是阻塞的,如果客户端不发送数据,一直等待
System.out.println("等待客户端输入数据..........");
socket.getInputStream().read(leng);
System.out.println(new String(leng));
System.out.println("客户端输入数据结束。");
}
客户端:
public static void main(String[] args) {
try {
Socket socket = new Socket(InetAddress.getByName("127.0.0.1"), 9998);
OutputStream outputStream = socket.getOutputStream();
outputStream.write("你好服务端".getBytes());
outputStream.flush();
} catch (Exception e) {
e.printStackTrace();
}
}
BIO(Blocking IO)
同步阻塞模型,一个客户端连接对应一个处理线程。
IO模型:
缺点:
1:IO代码里read操作是阻塞操作,如果连接不做数据读写操作会导致线程阻塞,浪费资源。
2:如果线程很多,会导致服务器线程太多,压力太大。
应用场景:
BIO方式用于连接数目不多且固定的架构,这种方式对服务器资源要求比较高,但是程序简单易于理解。
NIO(Non Blocking IO)
同步非阻塞IO,服务器实现模式为一个线程可以处理多个请求(连接),客户端发送的连接请求都会注册到多路复用器selector上,多路复用器轮询到连接有IO请求就进行处理。
I/O多路复用底层一般用Linux API (select ,poll,epoll)来实现,他们的区别如下表:最新的也是效率最高的是epoll,从主动轮询变成被动接收消息通知。
select | poll | epoll(jdk1.5以上) | |
操作方式 | 遍历 | 遍历 | 回调 |
底层实现 | 数组 | 链表 | 哈希表 |
IO效率 | 每次调用都进行线程遍历,时间复杂度O(n) | 每次调用都进行线程遍历,时间复杂度O(n) | 事件通知方式,每当有IO事件就绪,系统注册的回调函数就会被调用,时间复杂度O(1) |
最大连接 | 有上限 | 无上限 | 无上限 |
应用场景:
NIO方式适用于连接数目多且连接比较短(轻操作)的架构,比如聊天服务器,弹幕系统,服务器间通讯,编程比较复杂,JDK1.4开始支持。
NIO的三大组件:Channel(通道) ,Buffer(缓冲区),Selector(选择器)
IO模型:
1:channel类似于流,每个channel对应一个buffer缓冲区,buffer底层是个数组。
2:channel会注册到selector上,由selector根据channel读写事件的发生将其交由某个空闲的线程处理。
3:selector可以对应一个或多个线程。
4:NIO的buffer和channel都是既可以读也可以写。
服务端:单线程
public static void main(String[] args) throws IOException {
// 创建一个在本地端口进行监听的服务Scoket通道,并设置为非阻塞的
ServerSocketChannel ssc = ServerSocketChannel.open();
// 必须配置为非阻塞才能往selector上注册,否则会报错,selector模式本身就是非阻塞模式
ssc.configureBlocking(false);
// 绑定本地的一个端口号
ssc.socket().bind(new InetSocketAddress(1009));
// 创建一个选择器selector 对于linux操作系统来说 会调用epoll_create的内核函数。创建一个epoll的实例。
Selector selector = Selector.open();
// 把ServerSocketChannel 注册到selector上,并且selector对客户端的accept事件感兴趣,就是监听这个事件
// 每个注册channel都对应一个key 注册表也是根据这个key找到对应的channel
SelectionKey selectionKey = ssc.register(selector, SelectionKey.OP_ACCEPT);
while (true){
System.out.println("等待事件发生");
// 轮询监听key select 是阻塞的, BIO 中 accept()是阻塞的 ,多路复用器,这里不再阻塞之后,就可以得到那些有IO事件发生的连接,比如连接事件,读取事件。那些没有IO事件发生的连接这里不会获取出来。
// 底层调用linux操作系统 内核epoll_ctl 函数,开始真正的监听事件,然后调用epoll_wait函数等待事件的发生
selector.select();
System.out.println("有事件发生了");
// 有客户端请求,被轮询监听到了
Iterator<SelectionKey> iterator = selector.selectedKeys().iterator();
// 如果有多个事件同时到达,会按顺序处理 如果处理的时间过长,有新的事件到达还会处理吗????????
while (iterator.hasNext()){
SelectionKey key = iterator.next();
// 删除本次已经处理的key,防止下次select重复处理
iterator.remove();
handle(key);
}
}
}
private static void handle(SelectionKey key) throws IOException {
// 根据传入的key 判断是哪一种事件
if (key.isAcceptable()){
System.out.println("有客户端连接事件发生了");
// 知道它是连接事件,所以可以知道它是什么类型的 这个事件是服务端这边的事件 所以这个channel是服务端这边的 ServerSocketChannel
ServerSocketChannel ssc = (ServerSocketChannel) key.channel();
// NIO 非阻塞体现: 此处accept方法是阻塞的,但是这里因为是发生了了连接事件,所以这个方法会马上执行完,不会阻塞
// 处理完连接请求 不会继续等待客户端的数据
SocketChannel sc = ssc.accept();
sc.configureBlocking(false);
// 通过Selector监听Channel时对读事件感兴趣
sc.register(key.selector(),SelectionKey.OP_READ);
}else if (key.isReadable()){
System.out.println("有客户端有可读事件发生");
// 因为这个事件是客户端发送请求的,所以这个通道是客户端那边的通道 SocketChannel
// 每个channel都有自己绑定的key
SocketChannel sc= (SocketChannel) key.channel();
//
ByteBuffer buffer = ByteBuffer.allocate(1024);
// NIO非阻塞的体现: 首先read方法不会阻塞,其次是这种事件响应模型,当调用到read方法时肯定是客户端发生了发送数据的事件
int len = sc.read(buffer);
if (len!=-1){
System.out.println("客户端读取到的数据:"+new String(buffer.array(),0,len));
}
ByteBuffer bufferWrite = ByteBuffer.wrap("helloClient".getBytes());
// 通道是双向的,可以从客户端读和向客户端写
sc.write(bufferWrite);
// 设置一下channel的监听事件 是通过key来绑定的
key.interestOps(SelectionKey.OP_READ| SelectionKey.OP_WRITE);
sc.close();
}
}
客户端:
public static void main(String[] args) {
NIOClient nioClient = new NIOClient();
try {
nioClient.initClient("127.0.0.1",1009);
nioClient.connect();
} catch (IOException e) {
e.printStackTrace();
}
}
private void connect() throws IOException {
// 轮询访问selector
while(true){
// 选择一组可以进行IO操作的事件,放在slector中,客户端的该方法不会阻塞,
// 这里和服务端的方法不一样,查看API注释可以知道,当至少有一个通道被选中时,
// selector的wakeup方法被调用 方法返回 而对于客户端来说,通道一直是被选中的
this.selector.select();
// 获取slector中选中的项的迭代器
Iterator<SelectionKey> iterator = this.selector.selectedKeys().iterator();
while (iterator.hasNext()){
SelectionKey key= iterator.next();
// 删除已选的key ,以免重复处理。
iterator.remove();
// 连接事件发生
if (key.isConnectable()){
SocketChannel channel= (SocketChannel) key.channel();
// 如果正在连接 则完成连接
if (channel.isConnectionPending()){
channel.finishConnect();
}
// 设置成非阻塞的
channel.configureBlocking(false);
// 在这里可以给服务器发送信息
ByteBuffer buffer = ByteBuffer.wrap("你好服务端".getBytes());
channel.write(buffer);
// 在和服务端连接成功之后 为了可以接收到服务端的信息 需要给通道设置读的事件
channel.register(this.selector,SelectionKey.OP_READ);
}else if (key.isReadable()){
// 和服务端的read方法一样
// 服务器可读取消息 得到事件发生的Socket通道
SocketChannel channel= (SocketChannel) key.channel();
// 创建读取的缓冲区
ByteBuffer allocate = ByteBuffer.allocate(512);
int read = channel.read(allocate);
if (read!=-1){
System.out.println("客户端收取到信息:"+new String(allocate.array(),0,read
));
}
}
}
}
}
public void initClient(String ip,int port) throws IOException {
// 获取一个Socket通道
SocketChannel channel = SocketChannel.open();
// 设置通道为非阻塞的
channel.configureBlocking(false);
// 设置一个通道管理器
this.selector=Selector.open();
// 客户端连接服务器 其实方法执行并没有实现连接 需要在listen()方法中 调用
// channel.finishConnect() 才能完成连接
boolean connect = channel.connect(new InetSocketAddress(ip, port));
// 将通道管理器和该通道绑定 并为通道注册 SelectionKey.OP_CONNECT
// 注意这里注册和服务端事件不一样 服务端是ACCEPT
SelectionKey selectionKey = channel.register(selector, SelectionKey.OP_CONNECT);
}
NIO流程:
NIO流程说明:
Resdis就是典型的NIO线程模型,selector收集所有连接的事件并且转交给后端线程,线程连续执行所有事件命令并将结果回写客户端。
AIO (NIO 2.0)
异步非阻塞,由操作系统完成后回调通知服务端程序启动线程去处理,一般适用于连接数比较多且连接时间较长的引用。
应用场景:
AIO方式适用于连接数目多且连接比较长(重操作)的架构,从JDK1.7开始支持。AIO其实是对NIO进行的封装。虽然AIO是对NIO的封装,但是它的效率不一定比NIO的高,在工作用AIO的也不多。
异步和同步以及阻塞和非阻塞的区别:
网上举例最多的就是水壶烧水的例子:
同步和异步在我们的IO通信中就是,得到连接事件或读写事件是主动发现的还是被动告知的,如果是主动发现的,比如BIO,NIO中都有while循环,调用accept,select才能知道有什么事件到达。异步中如果有事件到达是主动通知调用方的。阻塞和非阻塞在IO通信中就是对于处理事件的线程的来说,接收事件的消息内容是不是阻塞的,如果是阻塞的就是一直等待事件消息的到来,如果是非阻塞的就是不用一直等待消息内容的到来,可以先去处理其他事情,等事件有消息内容到来的时候再去处理。
我们从上面的NIO的线程模型知道,在linux系统中内核版本2.6+,主要是靠epoll相关函数实现的,主要是epoll_create,epoll_ctl,epoll_wait。在redis中单线程也能处理高并发也是靠NIO的这个线程模型实现的。
Reactor 响应式编程,基于事件驱动的响应式编程。
其实上面的NIO模型就是典型的基于事件驱动的响应式编程模型,它这个事件是由操作系统中断程序触发的,进一步解释就是当有新的IO事件到达操作系统时候,操作系统的中断程序会判断这是那一个socket的连接,然后放到就绪事件列表里面,epoll_wait不再阻塞,select() 不再阻塞。
基础的响应式编程模型如下:
单线程模型:
这是最基本的单Reactor单线程模型。其中Reactor线程,负责多路分离套接字,有新连接到来触发connect 事件之后,交由Acceptor进行处理,有IO读写事件之后交给hanlder 处理。
Acceptor主要任务就是构建handler ,在获取到和client相关的SocketChannel之后 ,绑定到相应的hanlder上,对应的SocketChannel有读写事件之后,基于racotor 分发,hanlder就可以处理了(所有的IO事件都绑定到selector上,由Reactor分发)。
该模型 适用于处理器链中业务处理组件能快速完成的场景。不过,这种单线程模型不能充分利用多核资源,所以实际使用的不多。
单Reactor多线程模型
相对于第一种单线程的模式来说,在处理业务逻辑,也就是获取到IO的读写事件之后,交由线程池来处理,这样可以减小主reactor的性能开销,从而更专注的做事件分发工作了,从而提升整个应用的吞吐。
3:多Reactor多线程模型 ,主从模型
第三种模型比起第二种模型,是将Reactor分成两部分:
-
mainReactor负责监听server socket,用来处理新连接的建立,将建立的socketChannel指定注册给subReactor。
-
subReactor维护自己的selector, 基于mainReactor 注册的socketChannel多路分离IO读写事件,读写网 络数据,对业务处理的功能,另其扔给worker线程池来完成。
第三种模型中,我们可以看到,mainReactor 主要是用来处理网络IO 连接建立操作,通常一个线程就可以处理,而subReactor主要做和建立起来的socket做数据交互和事件业务处理操作,它的个数上一般是和CPU个数等同,每个subReactor一个线程来处理。
此种模型中,每个模块的工作更加专一,耦合度更低,性能和稳定性也大量的提升,支持的可并发客户端数量可达到上百万级别。
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· go语言实现终端里的倒计时
· 如何编写易于单元测试的代码
· 10年+ .NET Coder 心语,封装的思维:从隐藏、稳定开始理解其本质意义
· .NET Core 中如何实现缓存的预热?
· 从 HTTP 原因短语缺失研究 HTTP/2 和 HTTP/3 的设计差异
· 分享一个免费、快速、无限量使用的满血 DeepSeek R1 模型,支持深度思考和联网搜索!
· 使用C#创建一个MCP客户端
· 基于 Docker 搭建 FRP 内网穿透开源项目(很简单哒)
· ollama系列1:轻松3步本地部署deepseek,普通电脑可用
· 按钮权限的设计及实现