IO模型
BIO
同步并阻塞(传统型阻塞),服务器实现模式为 一个连接一个线程,即客户端 有链接请求时服务器就需要 启动一个线程进行处理,如果这个连接不做任何事情就会造成不要的线程开销。

NIO
同步非阻塞,服务器实现模式是 为一个 线程处理多个请求,即客户端发送的连接请求都会注册到 多路复用器上,多路复用器 轮询到连接有IO请求就进行处理

AIO
(又称为NIO2.0)异步非阻塞,服务器实现模式为 一个有效请求一个线程,客户端的IO请求都是由OS先完成了再通知服务器应用去启动线程进行出出力, 一般适用于连接数较多且连接时长较长的应用
使用场景分析
1、BlO方式适用于连接数目比较小且固定的架构,这种方式对服务器资源要求比较高,并发局限于应用中,JDK1.4以前的唯一选择,但程序简单易理解。
2、NIO方式适用于连接数目多且连接比较短(轻操作)的架构,比如聊天服务器,弹幕系统,服务器间通讯等。编程比较复杂,JDK1.4开始支持。
3、AlO方式使用于连接数目多且连接比较长(重操作)的架构,比如相册服务器,充分调用OS(OutputStream)参与并发操作,编程比较复杂,JDK7开始支持。
NIO
JAVA NIO的介绍
JAVA NIO(new IO)也有人称为java non-blocking IO 是从Java 1.4版本开始引入的一个新的IO API,可以替换标准的Java IO,但是使用方式完全不同,NIO支持面向 **缓存区、通道、选择器 **的IO操作。NIO **将以更加高效的方式进行文件的读写操作。NIO可以理解为非阻塞IO,传统的IOread和write只能阻塞进行,线程在读写IO的时候不能干其他的事情 **,比如socket.read()时,如果服务器一直没有数据传输过来,线程就一直阻塞。而nio可以配置为socket为非阻塞式。
NIO有三大核心类:Channel(通道)、Buffer(缓存区)、Selector(选择器)
Java NIO的非阻塞模式,使一个线程从某通道发送请求或者读取数据,但是它仅得到目前可用的数据,如果目前没有数据可用时,就什么都不做,而不是保持线程阻塞,所以直至数据变的可以读取之前,该线程可以继续做其他的事情。 非阻塞写也是如此,一个线程请求写入一些数据到某通道,但不需要等待它完全写入,这个线程同时可以去做别的事情。
通俗理解:NIO可以做到用一个线程来处理多个操作。假设有1000个请求,根据实际情况,可以分配20或者80个线程来处理。不像之前的阻塞IO,要分配1000个线程。
NIO和BIO的比较
BIO以流的方式处理数据,而NIO以数据块的方式处理数据,块IO比流IO快很多
BIO是阻塞的,NIO是非阻塞的
BIO基于字符和字节流进行操作,而NIO基于Channel(通道)和Buffer(缓存区)进行操作,数据总是从通道读取到缓存区,或者从缓存区写入到通道中。Selector(选择器)用来监听多个通道的事件
| NIO | BIO |
|---|---|
| 面向缓存流(Buffer) | 面向流(stream) |
| 非阻塞(Non Blocking IO) | 阻塞IO(Blocking IO) |
| 选择器(Selector) |
NIO三大核心原理示意图
NIO 有三大核心部分**:Channel( 通道) ,Buffer( 缓冲区), Selector( 选择器)**
Buffer缓存区
缓存区本质上 是一块可以写入数据,然后可以从中读取数据的内存。这块内存被包装成NIO Buffer对象,并提供了一组方法,用来方便的访问该内存。相比较直接对数据的操作,Buffer API更加容易操作和管理(这一块是直接申请到本地内存中的。)
Channel(通道)
Java NIO 的通道类类似流,但有些不同: **既可以从通道读取数据,又可以写数据到通道。**但流的(input或output)读写通常都是单向的。**通道可以非阻塞读写和写入通道,**通道可以支持读写或写入缓存区,也支持异步地读写。
Selector选择器
Selector是一个 Java NIO 的组件,可以能够检查一个或多个NIO通道,并确定哪些通道已经准备好进行读取或者写入。这样,一个单独的线程可以管理多个channel,从而管理多个网络连接,提高效率

NIO核心之一:缓存区
一个用于特定基本数据类型的容器。有java.nio包定义的,所有缓存区都是Buffer抽象类的子类。Java NIO中的Buffer主要用于NIO通道进行交互,数据是从通道读入缓存区,从缓存区写入通道。

Buffer类以及其子类
Buffer就像一个数组,可以保存多个相同类型的数据。根据数据类型不同,有了如下子类
//初始化
IntBuffer inBuffer = IntBuffer.allocate(1);
- ByteBuff
- CharByte
- DoubleBuffer
- FloatBuffer
- IntBuffer
- LongBuffer
- ShortBuffer
缓存区基本属性
Buffer中的重要概念:
- **容器(capacity):**作为一个内存块,Buffer具有一定的固定大小,也称为 “容量”,缓存区容量不能为负,并且创建后不能更改。
- 限制(limit):表示缓存区可以操作数据的大小(limit后数据不能进行读写)。缓存区的限制不能为负,并且不能大于容量。 写入模式,限制等于buffer的容量。读取模式,limit等于写入的数据量。
- 位置(position):下一个要读取或写入数据的索引。缓存区的位置不能为负,并且不能大于其限制
- **标志(mark)与重置:(reset):**标记是一个索引,通过Buffer的mark()方法指定Buffer的一个特定的position,之后可以通过调用reset()方法恢复到这个position。
- 标记、位置、限制、容量遵守以下原则:0<=mark<=position<=limit<=capacity

Buffer常用API
Buffer常见方法
Buffer clear() 清空缓冲区并返回对缓冲区的引用
Buffer flip() 为 将缓冲区的界限设置为当前位置,并将当前位置充值为 0
int capacity() 返回 Buffer 的 capacity 大小
boolean hasRemaining() 判断缓冲区中是否还有元素
int limit() 返回 Buffer 的界限(limit) 的位置
Buffer limit(int n) 将设置缓冲区界限为 n, 并返回一个具有新 limit 的缓冲区对象
Buffer mark() 对缓冲区设置标记
int position() 返回缓冲区的当前位置 position
Buffer position(int n) 将设置缓冲区的当前位置为 n , 并返回修改后的 Buffer 对象
int remaining() 返回 position 和 limit 之间的元素个数
Buffer reset() 将位置 position 转到以前设置的 mark 所在的位置
Buffer rewind() 将位置设为为 0, 取消设置的 mark
缓冲区的数据操作
Buffer 所有子类提供了两个用于数据操作的方法:get()put() 方法
取获取 Buffer中的数据
get() :读取单个字节
get(byte[] dst):批量读取多个字节到 dst 中
get(int index):读取指定索引位置的字节(不会移动 position)
放到 入数据到 Buffer 中 中
put(byte b):将给定单个字节写入缓冲区的当前位置
put(byte[] src):将 src 中的字节写入缓冲区的当前位置
put(int index, byte b):将指定字节写入缓冲区的索引位置(不会移动 position)
**使用步骤: **
使用Buffer读写数据一般遵循以下四个步骤:
1 写入数据到Buffer
2 调用flip()方法,转换为读取模式
3 从Buffer中读取数据
4 调用buffer.clear()方法或者buffer.compact()方法清除缓冲区
代码演示:
public class BufferTest {
@Test
public void test02(){
ByteBuffer buffer = ByteBuffer.allocate(10);
String name ="HIllCheung";
buffer.put(name.getBytes());
buffer.flip();//切换成读取模式
byte [] bt=new byte[buffer.limit()];
buffer.get(bt,0,2);
System.out.println(new String(bt));
System.out.println(buffer.position());
//标记
buffer.mark();
buffer.get(bt,2,2);
System.out.println(new String(bt));
System.out.println(buffer.position());
//reset恢复到标记的位置
buffer.reset();
System.out.println(buffer.position());
if(buffer.hasRemaining()){
System.out.println(buffer.remaining());
}
}
@Test
public void test01(){
//1.分配一个缓存区,容量设置为10
ByteBuffer allocate = ByteBuffer.allocate(10);
System.out.println(allocate.position());
System.out.println(allocate.limit());
System.out.println(allocate.capacity());
System.out.println("-------------------");
//2.put缓冲区中添加数据
String name ="Cheung";
allocate.put(name.getBytes());
System.out.println(allocate.position());
System.out.println(allocate.limit());
System.out.println(allocate.capacity());
System.out.println("-------------------");
//3.Buffer filp()为缓存区的界限设置为当前的值,并将当前位置设置为0,切换成:可读模式
allocate.flip();
System.out.println(allocate.position());
System.out.println(allocate.limit());
System.out.println(allocate.capacity());
System.out.println("-------------------");
char c = (char) allocate.get();
System.out.println(c);
System.out.println(allocate.position());
System.out.println(allocate.limit());
System.out.println(allocate.capacity());
}
}
直接与非直接缓存区
什么是直接内存与非直接内存
根据官方文档的描述:
byte buffer可以是两种类型,**一种是基于直接内存(也就是非堆内存),另一种是非直接内存(也就是堆内存)。**对于直接内存来说,JVM会将会在IO操作上具有更高的性能,因为它直接作用于本地系统的IO操作。而非直接内存,也就是堆内存中的数据,如果要操作IO操作,会先从本进程内存复制到直接内存,再利用本地IO处理
从数据流的角度,非直接内存是下面这样的作业链:
本地IO -->直接内存–>非直接内存–>直接内存–>本地IO
而直接内存是:
本地IO–>直接内存–>本地IO
很明显,在做IO处理时,比如网络发送大量数据时, 直接内存会具有更高的效率。直接内存使用allocateDirect创建,但是它比申请普通的堆内存需要 耗费更高的性能,不过,这部分的数据是在JVM之外的,因此它不会占用应用的内存。所以,当有大量的数据需要缓存,并且它的生命周青很长,那么就比较适合使用直接内存。只是一般来说,如果不是能带来很明显的性能替身,还是推荐直接使用堆内存。
字节缓存区是直接缓存区还是非直接缓冲区可以通过其isDirect()方法来确定。
使用场景
- 有很大的数据需要存储,它的生命周期又很长。
- 适合频繁的IO操作,比如网络并发场景。
//使用方法
ByteBuffer byteBuffer = ByteBuffer.allocateDirect(10);
NIO核心之二:通道(Channel)
通道Channel概述
通道(Channel):由java.nio.channels包定义的。Channel表示IO源于目标打开的联机。Channel类似于传统的 “流”。只不过Channel 本身不能直接访问数据,Channel只能与Buffer进行交互。
- NIO的通道类似于流,但有 区别如下:
- 通道可以同时进行读写,而流只能读或者只能写
- 通道可以实现异步读写数据
- 通道可以缓冲读数据,也可以写到数据到缓冲;
- BIO中的stream是单向的,例如FileInputStream对象只能进行读取数据的操作,而NIO的通道(Channel)是双向的,可以读数据,也可以写数据
- Channel在NIO是一个接口
public interface Channel extends Closeable
常见的Channel实现类
- FileChannel: 用于读取、写入、映射和操作文件的通道
- DatagramChannel:通过UDP读写网络中的数据通道
- SocketChannel:通过TCP读写网络中的数据
- ServerSocketChannel:可以监听新进来的TCP连接,对每一个新进来的连接都会创建一个SocketChannel。【ServerSocketChannel类似ServerSocket,SocketChannel类似于Socket】
FileChannel类
获取通道的一种方式是对支持通道的对象调用getChannel()方法。支持通道的类如下:
- FileInputStream
- FileOutputStream
- RandomAccessFile
- DatagramSocket
- Socket
- ServerSocket
获取通道的其他方式是使用Files类的静态方法 **newByteChannel()获取直接通道。**或者通过通道的静态方法open()打开并返回指定的通道
FileChannel常用方法
int read(ByteBuffer dst) 从 从 Channel 到 中读取数据到 ByteBuffer
long read(ByteBuffer[] dsts) 将 将 Channel 到 中的数据“分散”到 ByteBuffer[]
int write(ByteBuffer src) 将 将 ByteBuffer 到 中的数据写入到 Channel
long write(ByteBuffer[] srcs) 将 将 ByteBuffer[] 到 中的数据“聚集”到 Channel
long position() 返回此通道的文件位置
FileChannel position(long p) 设置此通道的文件位置
long size() 返回此通道的文件的当前大小
FileChannel truncate(long s) 将此通道的文件截取为给定大小
void force(boolean metaData) 强制将所有对此通道的文件更新写入到存储设备中
实例测试
本地写入数据
@Test
public void write() throws Exception {
FileOutputStream out = new FileOutputStream("data.txt");
FileChannel channel = out.getChannel(); //根据文件输出流获取通道
ByteBuffer allocate = ByteBuffer.allocate(1024);//因为通道不能直接写入要接入缓存区
allocate.put("你好,HIllCheung".getBytes()); //将内容放入缓存区
allocate.flip(); //切换到写入模式
channel.write(allocate);
}
本地读取数据
@Test public void read() throws Exception{ FileInputStream in = new FileInputStream("data.txt"); FileChannel channel = in.getChannel(); ByteBuffer allocate = ByteBuffer.allocate(1024); channel.read(allocate); String s = new String(allocate.array(), 0, allocate.remaining()); System.out.println(s); }
本地文件复制
@Testpublic void copy()throws Exception{ FileInputStream in = new FileInputStream("data.txt"); FileOutputStream out = new FileOutputStream("copy_data.txt"); //获取响应的通道 FileChannel inChannel = in.getChannel(); FileChannel outChannel = out.getChannel(); //分配缓存区 ByteBuffer buffer =ByteBuffer.allocate(1024); //读取数据 while (true){ //必须先把缓存区清空再写入缓存区 buffer.clear(); if(inChannel.read(buffer)==-1){ //等于-1说明读取完了 break; } //数据读取完后,下标落实到最后一位,此时要需要一个复位 buffer.flip(); outChannel.write(buffer); } inChannel.close(); outChannel.close(); System.out.println("ok");}
分散(Scatter)和聚焦(Gather)
分散读取(Scatter):是指把Channel通道的数据读入到多个缓存区中去
聚集写入(Gather):是指将多个Buffer中的数据"聚集"到Channel。


transferFrom()
从目标通道中去复制原通道数据
@Testpublic void transferTo() throws Exception { //1.直接输入管道 FileInputStream in = new FileInputStream("1.txt"); FileChannel channel = in.getChannel(); //2.直接输出流管道 FileOutputStream fos = new FileOutputStream("data04.txt"); FileChannel fosChannel = fos.getChannel(); //3.复制 channel.transferTo(channel.position(),channel.size(),fosChannel); channel.close(); fosChannel.close();}
有点像文件复制


总结

NIO选择三:选择器(Selector)
选择器(Selector)是SelectableChannel对象的多路复用器,Selector可以同时健康多个SelecttableChannel的IO状况,也就是说,利用Selector可用Selector可使一个单独的线程管理多个Channel。Selector是非阻塞IO的核心
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-UpbjpTIH-1627542348311)(C:\Users\dell\AppData\Roaming\Typora\typora-user-images\image-20210514102712873.png)]
- Java的NIO,用非阻塞的IO方式。可以用一个线程,处理多个的客户端连接,就会使用到Selector(选择器)
- Selector能够坚持多个注册的通道上是否 事件发生(注意:多个Channel以事件的方式可以注册到同一个Selector),如果有时间发生,便获取事件然后针对每个时间进行相应的处理。这样就可以志勇一个单线程去管理多个通道,也就是管理多个连接和请求。
- 只有在 连接/通道 真正有读写时间发生时,才会进行读写,就大大地减少了系统开销,并且不必为每哥连接都创建一个线程,不用去维护多个线程
- 避免了多线程之间的上下文切换导致的开销。
选择器(Selector)的应用
创建Selector:通过调用Selector.open()方法创建一个Selector
Selector selector =Selector.open();
**向选择器注册通道:**selectableChannel.register(Selector sel ,int ops)
@Test public void test1() throws Exception { //1.获取通道 ServerSocketChannel ssChanel = ServerSocketChannel.open(); //2.切换成非阻塞模式 ssChanel.configureBlocking(false); //3.绑定连接 ssChanel.bind(new InetSocketAddress(9898)); //4.获取选择器 Selector selector = Selector.open();// 5.将通道注册到选择器上,并且指定 监听事件 ssChanel.register(selector, SelectionKey.OP_ACCEPT); }
当调用register(Selector sel,int ops)将通道注册选择器时,选择器对通道的监听事件,需要通过第二个参数ops指定。可以监听的时间类型(可使用SelectionKey的四个常量表示):
- 读: SelectionKey.OP_READ(1)
- 写:SelectionKey.OP_WRITE(4)
- 连接:SelectionKey.OP_CONNECT(8)
- 接收:SeLectionKey.OP_ACCEPT(16)
- 若注册时不止一个监听事件,则可以使用“位或”操作符连接。
int key=SelectionKey.OP_READ | SelectionKey.OP_WRITE;
NIO非阻塞式网络通信原理分析
Selector示意图和特点说明
**Selector可以实现:**一个I/O线程可以并发处理N个客户端连接和读写操作,这从根本上解决了传统同步阻塞I/O一连接一线线程模型,架构的性能、弹性伸缩能力和可靠性都得到了极大的替身。
![[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-ySkdhZ6w-1614494690634)(F:/typora/tupian/1614438987804.png)]](https://i-blog.csdnimg.cn/blog_migrate/2d4c57da45d3387bfec5596d5274cc9c.png)
服务端流程(*)
- 1. 当客户端连接服务端时,服务端会通过ServerSocketChannel得到SocketChannel:1.获取通道
ServerSocketChannel ssChanel = ServerSocketChannel.open();
-
**2.**切换为非阻塞模式
//2.切换成非阻塞模式ssChanel.configureBlocking(false); -
**3.**绑定连接
//3.绑定连接ssChanel.bind(new InetSocketAddress(9898)); -
**4.**获取选择器
//4.获取选择器Selector selector = Selector.open(); -
**5.**将通道注册到选择器上,并且指定"监听接收事件"
ssChanel.register(selector, SelectionKey.OP_ACCEPT);
- **6.**轮询式的获取选择器上已经"准备就绪"的事件
while (selector.select()>0){ System.out.println("轮一轮");// 获取当前选择器所有注册的“选择键” Iterator<SelectionKey> iterator = selector.selectedKeys().iterator(); while (iterator.hasNext()){ SelectionKey sk = iterator.next(); if(sk.isAcceptable()){ //若接受就绪,获取客户端连接 SocketChannel sChannel = socketChannel.accept(); sChannel.configureBlocking(false); sChannel.register(selector,SelectionKey.OP_READ); }else if(sk.isReadable()){ SocketChannel channel = (SocketChannel) sk.channel(); //14.读取数据 ByteBuffer buf = ByteBuffer.allocate(1024); int len =0; while ( (len=channel.read(buf))>0){ buf.flip(); System.out.println(new String(buf.array(),0,len)); buf.clear(); } } iterator.remove(); } }
例子
需求:服务端接收客户端的连接请求,并接收多个客户端发送过来的事件。
public class NIO_Client { public static void main(String[] args) throws Exception { //获取通道 SocketChannel sChannel = SocketChannel.open(new InetSocketAddress("127.0.0.1", 9999)); //切换到非阻塞模式 sChannel.configureBlocking(false); //3.分配指定大小的缓冲区 ByteBuffer buf = ByteBuffer.allocate(1024); //4.发送数据给服务端 Scanner sc =new Scanner(System.in); while (sc.hasNext()){ //要发送的内容 System.out.println(1); String str = sc.nextLine(); buf.put( ((new SimpleDateFormat("yyyy/MM/dd HH:mm:ss").format(System.currentTimeMillis())) +"\n"+str).getBytes() ); buf.flip(); sChannel.write(buf); buf.clear(); } sChannel.close(); sc.close(); }}
public class NIO_Server {
public static void main(String[] args) throws Exception {
//获取通道
ServerSocketChannel socketChannel = ServerSocketChannel.open();
//切换成非阻塞式
socketChannel.configureBlocking(false);
//绑定连接
socketChannel.bind(new InetSocketAddress(9999));
//获取选择器
Selector selector =Selector.open();
// 将通道注册到选择器上,并且“监听接收事件”
socketChannel.register(selector, SelectionKey.OP_ACCEPT);
//轮询式的获取选择器上已经 “准备就绪”的时间
while (selector.select()>0){
System.out.println("轮一轮");
// 获取当前选择器所有注册的“选择键”
Iterator<SelectionKey> iterator = selector.selectedKeys().iterator();
while (iterator.hasNext()){
SelectionKey sk = iterator.next();
if(sk.isAcceptable()){
//若接受就绪,获取客户端连接
SocketChannel sChannel = socketChannel.accept();
sChannel.configureBlocking(false);
sChannel.register(selector,SelectionKey.OP_READ);
}else if(sk.isReadable()){
SocketChannel channel = (SocketChannel) sk.channel();
//14.读取数据
ByteBuffer buf = ByteBuffer.allocate(1024);
int len =0;
while ( (len=channel.read(buf))>0){
buf.flip();
System.out.println(new String(buf.array(),0,len));
buf.clear();
}
}
iterator.remove();
}
}
}
}
群聊系统
目标
需求:进一步理解 NIO 非阻塞网络编程机制,实现多人群聊
- 编写一个 NIO 群聊系统,实现客户端与客户端的通信需求(非阻塞)
- 服务器端:可以监测用户上线,离线,并实现消息转发功能
- 客户端:通过 channel 可以无阻塞发送消息给其它所有客户端用户,同时可以接受其它客户端用户通过服务端转发来的消息

本文详细介绍了Java NIO(非阻塞I/O)的核心概念,包括BIO、NIO和AIO的区别、NIO的三大核心组件——通道(Channel)、缓存区(Buffer)和选择器(Selector)。通过实例展示了如何使用Buffer进行数据读写,以及如何通过Selector实现单线程管理多个通道的非阻塞IO操作。NIO的优势在于提高了并发性能,尤其适合处理连接数众多且连接时长较长的场景。

2040

被折叠的 条评论
为什么被折叠?



