NIO介绍
- 全称java non-blocking IO,是从JDK 1.4开始提供的新API。提供了一系列改进的输入、输出特性,被统称为NIO,即同步非阻塞
- NIO相关类放到了java.nio下,并且对原java.io包中的很多了进行了改写
- NIO三大组件:该JDK提供了 Channel,Buffer,Selector来实现NIO编程
- NIO是面向缓冲区,或者面向块的编程。增加了处理的灵活性,实现了同步通信的非阻塞
NIO和BIO的比较
- BIO以流的方式处理数据,NIO以块的方式处理数据,块IO的效率高很多
- BIO是阻塞的,NIO是非阻塞的,但都是同步的
- BIO基于字节流和字符流操作,而NIO基于Channel和Buffer操作,数据总是从通道读取到缓冲区,或从缓冲区写入到通道。Selector用来监听多个通道的事件(连接请求、数据到达等),因此使用单个线程就可以监听多用户通道
【注:】
除了BIO、NIO之外,还有一种IO叫AIO,即异步IO(同样分为阻塞和飞阻塞两种),我个人的理解是BIO和NIO属于server端的IO处理方式,而AIO则是client端的IO处理方式(比如Rocket MQ的client端可以注册异步监听器来处理broker的响应)
NIO三大核心原理示意图
- 每个channel对应一个buffer
- selector对应一个线程,一个线程对应多个channel,channel注册到selector
- 程序切换到哪个channel是事件决定
- selector会根据不同事件,在各个channel上切换
- buffer就是一个内存块,底层是数组
- NIO数据的读取写入通过buffer,读取的切换通过flip方法,而BIO是直接操作输入、输出两种流
- channel是双向的,也就是支持读写,这点有别于bio,比如bio读用input,写用output。
Buffer
Buffer介绍
缓冲区本质是一个可以读写数据的内存块,可以理解为一个数组容器对象,该对象提供了方法来管理缓冲区,缓冲区对象内置了一些机制,能够跟踪和记录缓冲区的状态变化。Channel提供从文件、网络读取数据的渠道,但是读取或写入的数据必须经由BufferBuffer视频讲解推荐https://www.bilibili.com/video/BV1DJ411m7NR?p=11&vd_source=8be621c052fd9f705308579363b67881
Buffer类
常用的buffer实现
Buffer类定义了所有缓冲区具有的四个属性提供关于其包含的数据元素
属性 | 描述 |
---|---|
capacity | 私有,表示当前Buffer的容量,创建后不可变 |
limit | 私有,表示读写的最大值,<=capacity |
position | 私有,索引位,初始值0 |
mark | 私有,标记,初始值-1 |
Buffer读写的属性变化图示
上手验证
debug代码观察上述属性的变化
public class BasicBuffer {
public static void main(String[] args) {
// 创建一个大小为5个int值的buffer
IntBuffer intBuffer = IntBuffer.allocate(5);
for(int i=0;i<intBuffer.capacity();i++){
intBuffer.put(i*2);
}
// 如何从buffer读取数据
// 将buffer转换,读写切换
intBuffer.flip();
while(intBuffer.hasRemaining()){
System.out.println(intBuffer.get());
}
}
}
相关方法
方法 | 功能 |
---|---|
public final int capacity() | 获取buffer容量 |
public final int position() | 获取索引位置 |
public final Buffer position(int newPosition) | 设置缓冲区的位置 |
public final int limit() | 返回此buffer的限制 |
public final Buffer limit(int newLimit) | 设置缓冲区的限制 |
public final Buffer mark() | 在缓冲区的位置设置标记 |
public final Buffer reset() | 将缓冲区的位置重置为mark的值 |
public final Buffer clear() | 清除缓冲区,将各个标记恢复到初始态,数据并未清除 |
public final Buffer flip() | 读写切换 |
public final Buffer rewind() | 重绕此缓冲区 |
public final int remaining() | 返回当前位置和限制之间的元素数 |
public final boolean hasRemaining() | 告知缓冲区当前位置和限制之间是否有元素 |
public abstract boolean isReadOnly() | 是否为只读缓冲区 |
public abstract boolean hasArray() | 告知缓冲区是否具有可访问的底层实现数组 |
public abstract Object array() | 返回此缓冲区的底层实现数组 |
public abstract int arrayOffset() | 返回此缓冲区的底层实现数组中第一个缓冲区元素的偏移量 |
public abstract boolean isDirect() | 告知此缓冲区是否为直接缓冲区 |
selector
1、当serverSocketChannel连接上客户端后生成socketChannel;
2、当socketChannel注册到selecor上时同时也会注册一个selectorKey,register(Selector sel, int ops)#SelectorKey
BIO代码
传统的网络io模式面向流,一个线程对接一个会话,因此高并发会因线程阻塞而性能低效
public class BIO implements Connector {
private Integer port = 8080;
@Override
public void start() throws IOException {
ServerSocket serverSocket = new ServerSocket(port);
LogUtil.getLogger().debug("端口"+port+"启动");
while (true){
final Socket socket = serverSocket.accept();
ThreadUtil.threadPool.execute(new Runnable() {
@Override
public void run() {
handler(socket);
}
});
}
}
public static void handler(Socket socket){
try {
byte[] bytes = new byte[1024];
InputStream inputStream = socket.getInputStream();
while (true){
int status = inputStream.read(bytes);
if(status!=-1){
LogUtil.getLogger().debug(new String(bytes,0,status));
}else {
break;
}
}
} catch (IOException e) {
throw new RuntimeException(e);
} finally {
if(Objects.nonNull(socket)){
try {
socket.close();
LogUtil.getLogger().debug("关闭链接");
} catch (IOException e) {
throw new RuntimeException(e);
}
}
}
}
}
代码逻辑
1、实现客户端tcp连接:Socket socket = serverSocket.accept(port)
2、调用额外线程实现socket监听和读写:如socket.read();
【注:】
accept方法的原理
1、(调用os的接口)告诉操作系统,当前程序的socket指定了8888端口,当端口收到连接信息之后,会把消息通过DMA的方式放到一个缓存区中,并唤醒注册在os中的阻塞线程,让这个线程到缓存区处理连接消息。(或者缓存区由socket程序指定)
2、端口有连接进来时,给cpu发中断,然后由该方法所在线程处理连接