io是指计算机的输入输出操作,广义的讲就是数据在的一种传输,可分为磁盘io(硬盘的读写)和网络io(socket的读写),这里的两种模式都是基于网络io的。
阻塞I/O:内核在检查数据未就绪时,会一直等待,直到数据就绪
非阻塞I/O:如果数据没有就绪,则会返回一个标志信息告知用户线程当前要读的数据没有就绪
它们的区别在于I/O的第一阶段,阻塞是选择等待,非阻塞是返回一个标志信息
事实上,同步IO和异步IO模型是针对用户线程和内核的交互来说的,即数据是否就绪的消息传递机制
同步IO:当用户发出IO请求操作之后,如果数据没有就绪,需要通过用户线程或者内核不断地去轮询数据是否
就绪,当数据就绪时,再将数据从内核拷贝到用户线程
异步IO:只有IO请求操作的发出是由用户线程来进行的,内核自动完成检查数据是否就绪和将数据拷贝
到用户空间的过程(不是用户线程自己做的),然后发送通知告知用户线程IO操作已经完成。
bio是同步阻塞IO模式,数据的读取写入必须阻塞在一个线程内等待其完成,这里使用那个经典的烧开水例子,这里假设一个烧开水的场景,有一排水壶在烧开水,BIO的工作模式就是, 叫一个线程停留在一个水壶那,直到这个水壶烧开,才去处理下一个水壶。但是实际上线程在等待水壶烧开的时间段什么都没有做。不知道io操作中什么时候有数据可读,所以一直是阻塞的模式。
nio是同步非阻塞IO模式,nio使用单线程或者只使用少量的多线程,多个连接共用一个线程,消耗的线程资源会大幅减小。并且当处于等待(没有事件)的时候线程资源可以释放出来处理别的请求,通过事件驱动模型当有accept/read/write等事件发生后通知(唤醒)主线程分配资源来处理相关事件。以buffer缓冲区的形式处理数据,处理更为方便。这里相当于用一个线程去轮询每一个茶壶的状态,有水浒烧开才去处理。
Channel(通道)
Channel可以理解为,互通的管道,和Java的IO中的各种Stream(InputStream、OutputStream等等)一个等级,只不过Channel是双向的,而Stream是单向的。通道的作用是将数据移入或移出道各种I/O源,即可读又可写。
在Java中Channel类的层次结构相当复杂,有多个接口和许多可选操作。不过,常用的也就几个。
FileChannel
DatagramChannel
SocketChannel
ServerSocketChannel
FileChannel可以对文件进行读和写,DatagramChannel可以以UDP的协议来进行数据读写,SocketChannel以TCP的协议来对网络两端进行读写,ServerSocketChanel能够监听客户端发起的TCP连接,并为每个TCP连接创建一个新的SocketChannel来进行数据读写。
Buffer是一个高效的数据容器,在NIO中所有的数据操作都必须经过缓冲区,这点是和BIO不同的,BIO是直接将数据写到Stream对象中的。因为Stream对象的设计是按顺序一个字节一个字节的传送数据。虽然出于性能考虑,也可以传递字节数组,但是基本概念都是一个字节一个字节的传递数据。通道与之不同之处在于,通道会传送缓冲区的数据块,而且通道的基本概念就是按照一个数据块一个数据块的去读和写。所以也可以将缓冲区理解为一个字节数组,专门用来存储以及准备好出入通道的字节。
如上图所示,无论是客户端发送和接收数据,还是服务端接收和相应数据,都是从缓冲区中进行数据操作的。
Selector是Java NIO中最重要的一部分,Selector的作用就是用单线程来轮询处理注册的Channel,一旦哪个Channel的数据准备就绪了,就可以进行处理了。
import java.io.IOException;
import java.net.InetSocketAddress;
import java.nio.ByteBuffer;
import java.nio.channels.*;
import java.util.Iterator;
/**
* 阻塞点this.selector.select();
* 轮询器虽然是一个线程内部也是线程池
*/
public class NioSocket {
private Selector selector; //通道管理器(管理器)
/**
* 初始化Channel并绑定端口
* @param port
* @throws IOException
*/
public void initServer(int port) throws IOException {
ServerSocketChannel serverChannel = ServerSocketChannel.open();
serverChannel.configureBlocking(false); //非阻塞
serverChannel.socket().bind(new InetSocketAddress(port));
this.selector = Selector.open();
serverChannel.register(selector, SelectionKey.OP_ACCEPT);
System.out.println("服务器已启动...");
}
/**
* 监听轮询器
* @throws IOException
*/
public void listenSelector() throws IOException {
//轮询监听selector
while (true){
//等待客户连接
//select模型,多路复用
//this.selector.select(); //在这里会阻塞,无论是连接还是客户端发送数据还是客户端关闭,这里都会触发
this.selector.selectNow(); //这里不阻塞会立即执行
Iterator<SelectionKey> iteKey = this.selector.selectedKeys().iterator();
while (iteKey.hasNext()){
SelectionKey key = iteKey.next();
iteKey.remove(); //移除,防止重复处理
//处理请求
handler(key);
}
}
}
/**
* 处理客户端请求
* @param key
*/
private void handler(SelectionKey key) throws IOException {
if (key.isAcceptable()){ //处理连接请求
//处理客户端连接请求事件
ServerSocketChannel serverChannel = (ServerSocketChannel) key.channel();
SocketChannel socketChannel = serverChannel.accept();
//接受客户端发送的信息时,需要给通道设置读权限
socketChannel.configureBlocking(false);
socketChannel.register(selector,SelectionKey.OP_READ);
}else if(key.isReadable()){ //处理读请求
//处理读事件
SocketChannel socketChannel = (SocketChannel) key.channel();
ByteBuffer buffer = ByteBuffer.allocate(1024);
int readData = socketChannel.read(buffer);
if (readData>0){
String info = new String(buffer.array(),"GBK").trim();
System.out.println("服务端收到数据: "+Thread.currentThread()+info);
}else {
System.out.println("客户端关闭了...");
key.cancel();
}
}
}
public static void main(String[] args) throws IOException {
NioSocket nio = new NioSocket();
nio.initServer(8888);
nio.listenSelector();
}
}
客户端建立连接代码
```java
package com.test;
import java.net.InetSocketAddress;
import java.nio.channels.SelectionKey;
import java.nio.channels.Selector;
import java.nio.channels.SocketChannel;
/**
* @author hcc
* @date 2020-10-17 12:39
* @description
*/
public class NioSocketClient {
public static void main(String[] args) {
SocketChannel channel = null;
Selector selector = null;
try {
channel = SocketChannel.open();
channel.configureBlocking(false);
//请求连接
channel.connect(new InetSocketAddress("localhost", 8888));
selector = Selector.open();
channel.register(selector, SelectionKey.OP_CONNECT);
} catch (Exception e) {
e.printStackTr
ace();
}
}
}
同步非阻塞式IO,关键是采用了事件驱动的思想来实现了一个多路转换器。
NIO与BIO最大的区别就是只需要开启一个线程就可以处理来自多个客户端的IO事件,实现原理就是多路复用器,可以监听来自多个客户端的IO事件:
A. 若服务端监听到客户端连接请求,便为其建立通信套接字(java中就是通道),然后返回继续监听,若同时有多个客户端连接请求到来也可以全部收到,依次为它们都建立通信套接字。
B. 若服务端监听到来自已经创建了通信套接字的客户端发送来的数据,就会调用对应接口处理接收到的数据,若同时有多个客户端发来数据也可以依次进行处理。
C. 监听多个客户端的连接请求和接收数据请求同时还能监听自己是否有数据要发送。
总之就是在一个线程中就可以调用多路复用接口(java中是select)阻塞同时监听来自多个客户端的IO请求,一旦有收到IO请求就调用对应函数处理。
一旦有请求到来(不管是几个同时到还是只有一个到),都会调用对应IO处理函数处理,所以:
(1)NIO适合处理连接数目特别多,但是连接比较短(轻操作)的场景,Jetty,Mina,ZooKeeper等都是基于java nio实现。
(2)BIO方式适用于连接数目比较小且固定的场景,这种方式对服务器资源要求比较高,并发局限于应用中。
文章参考:
https://www.cnblogs.com/aeolian/p/10773786.html
https://www.jianshu.com/p/8b3af5bf4ce1
https://www.cnblogs.com/jimoer/p/11575610.html