BIO、NIO、AIO详解

article/2025/11/8 9:03:51

一、Java的I/O演进之路

Java共支持3种网络编程的I/O模型:BIO、NIO、AIO

BIO

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

NIO

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

AIO

异步非阻塞,服务器实现模式为一个有效请求一个线程,客户端的I/O请求都是由操作系统先完成了再通知服务器应用去启动线程进行处理,一般适用于连接数较多且连接时间较长的应用

二、BIO深入剖析

1、BIO概述

BIO(Blocking I/O)就是传统的Java IO编程,其相关的类和接口在java.io包下。BIO是同步阻塞的,服务器实现模式为一个连接一个线程,即客户端有连接请求时服务器端就需要启动一个线程进行处理,如果这个连接不做任何事情会造成不必要的线程开销,可以通过线程池机制改善(实现多个客户连接服务器)

BIO编程流程的梳理:

  1. 服务器端启动一个ServerSocket,注册端口,调用accpet方法监听客户端的Socket连接
  2. 客户端启动Socket对服务器进行通信,默认情况下服务器端需要对每个客户建立一个线程与之通讯

2、BIO案例

服务端

public class Server {public static void main(String[] args) throws IOException {System.out.println("===服务端启动===");// 1.定义一个ServerSocket对象进行服务端的端口注册ServerSocket ss = new ServerSocket(9999);// 2.监听客户端的Socket连接请求Socket socket = ss.accept();// 3.从Socket管道中得到一个字节输入流对象InputStream is = socket.getInputStream();// 4.把字节输入流包装成一个缓冲字符输入流BufferedReader br = new BufferedReader(new InputStreamReader(is));String msg;while ((msg = br.readLine()) != null) {System.out.println("服务端接收到:" + msg);}}
}

客户端

public class Client {public static void main(String[] args) throws IOException {System.out.println("===客户端启动===");// 1.创建Socket对象请求服务端的连接Socket socket = new Socket("127.0.0.1", 9999);// 2.从Socket对象中获取一个字节输出流OutputStream os = socket.getOutputStream();// 3.把字节输出流包装成一个打印流PrintStream ps = new PrintStream(os);ps.println("Hello World!服务端,你好!");ps.flush();}
}

小结

在以上通信中,服务端会一直等待客户端的消息,如果客户端没有进行消息的发送,服务端将一直进入阻塞状态

3、伪异步I/O

1)、概述

伪异步I/O采用线程池和任务队列实现,当客户端接入时,将客户端的Socket封装成一个Task(该任务实现java.lang.Runnable线程任务接口)交给后端的线程池中进行处理。JDK的线程池维护一个消息队列和N个活跃的线程,对消息队列中Socket任务进行处理,由于线程池可以设置消息队列的大小和最大线程数,因此,它的资源占用是可控的,无论多少个客户端并发访问,都不会导致资源的耗尽和宕机

在这里插入图片描述

2)、代码实现

客户端

public class Client {public static void main(String[] args) {try {Socket socket = new Socket("127.0.0.1", 9999);OutputStream os = socket.getOutputStream();PrintStream ps = new PrintStream(os);Scanner sc = new Scanner(System.in);while (true) {System.out.print("请说:");String msg = sc.nextLine();ps.println(msg);ps.flush();}} catch (IOException e) {e.printStackTrace();}}
}

线程池处理类

public class SocketServerPoolHandler {/*** 1.创建一个线程池的成员变量用于存储一个线程池对象*/private ExecutorService executorService;/*** 2.创建这个类的时候就需要初始化线程池对象*/public SocketServerPoolHandler(int maxThreadNum, int queueSize) {executorService = new ThreadPoolExecutor(3, maxThreadNum,120, TimeUnit.SECONDS, new ArrayBlockingQueue<>(queueSize));}/*** 3.提供一个方法来提交任务给线程池的任务队列来暂存,等着线程池来处理*/public void execute(Runnable target) {executorService.execute(target);}
}

服务端

public class Server {public static void main(String[] args) {try {// 1.注册端口ServerSocket ss = new ServerSocket(9999);// 2.定义一个循环接收客户端的Socket连接请求// 初始化一个线程池对象SocketServerPoolHandler poolHandler = new SocketServerPoolHandler(3, 10);while (true) {Socket socket = ss.accept();// 3.把Socket对象交给一个线程池进行处理// 把Socket封装成一个任务对象交给线程池处理Runnable target = new ServerRunnableTarget(socket);poolHandler.execute(target);}} catch (IOException e) {e.printStackTrace();}}
}
public class ServerRunnableTarget implements Runnable {private Socket socket;public ServerRunnableTarget(Socket socket) {this.socket = socket;}@Overridepublic void run() {// 处理接收的客户端Socket通信需求try {InputStream is = socket.getInputStream();BufferedReader br = new BufferedReader(new InputStreamReader(is));String msg;while ((msg = br.readLine()) != null) {System.out.println("服务端接收到:" + msg);}} catch (IOException e) {e.printStackTrace();}}
}

运行结果

启动服务端并启动多个客户端发送消息,由于核心线程数=最大线程数=3,当客户端数>3时,客户端的Socket任务会到线程池的阻塞队列中等待,关闭客户端,当客户端数<=3时,Socket任务将会被服务端处理

3)、小结

  • 伪异步I/O采用了线程池实现,因此避免了为每个请求创建一个独立线程造成线程资源耗尽的问题,但由于底层依然是采用的同步阻塞模型,因此无法从根本上解决问题
  • 如果单个消息处理的缓慢,或者服务器线程池中的全部线程都被阻塞,那么后续Socket的I/O消息都将在队列中排队。新的Socket请求将被拒绝,客户端会发生大量连接超时

多线程BIO通信模型图

三、NIO深入剖析

1、NIO概述

  • NIO(Non-Blocking IO)是从Java 1.4版本开始引入的一个新的IO API,可以替代标准的Java IO API。NIO与原来的IO有同样的作用和目的,但是使用的方式完全不同,NIO支持面向缓冲区的、基于通道的IO操作。NIO将以更加高效的方式进行文件的读写操作。NIO可以理解为非阻塞IO,传统的IO的read和write只能阻塞执行,线程在读写IO期间不能干其他事情,比如调用socket.read()时,如果服务器一直没有数据传输过来,线程就一直阻塞,而NIO中可以配置socket为非阻塞模式
  • NIO相关类都被放在java.nio包及子包下,并且对原java.io包中的很多类进行改写
  • NIO有三大核心部分:Channel(通道)、Buffer(缓冲区)、Selector(选择器)
  • Java NIO的非阻塞模式,使一个线程从某通道发送请求或者读取数据,但是它仅能得到目前可用的数据,如果目前没有数据可用时,就什么都不会获取,而不是保持线程阻塞,所以直至数据变的可以读取之前,该线程可以继续做其他的事情。非阻塞写也是如此,一个线程请求写入一些数据到某通道,但不需要等待它完全写入,这个线程同时可以去做别的事情
  • 通俗理解:NIO是可以做到用一个线程来处理多个操作的。假设有1000个请求过来,根据实际情况,可以分配20或者80个线程来处理。不像之前的阻塞IO那样,非得分配1000个

2、NIO和BIO的比较

  • BIO以流的方式处理数据,而NIO以块的方式处理数据,块I/O的效率比流I/O高很多
  • BIO是阻塞的,NIO则是非阻塞的
  • BIO基于字节流和字符流进行操作,而NIO基于Channel(通道)和Buffer(缓冲区)进行操作,数据总是从通道
    读取到缓冲区中,或者从缓冲区写入到通道中。Selector(选择器)用于监听多个通道的事件(比如:连接请求,数据到达等),因此使用单个线程就可以监听多个客户端通道
NIOBIO
面向缓冲区(Buffer)面向流(Stream)
非阻塞(Non Blocking IO)阻塞IO(Blocking IO)
选择器(Selector)

3、NIO三大核心原理示意图

NIO有三大核心部分:Channel(通道)、Buffer(缓冲区)、Selector(选择器)

Buffer缓冲区

缓冲区本质上是一块可以写入数据,然后可以从中读取数据的内存。这块内存被包装成NIO Buffer对象,并提供了一组方法,用来方便的访问该块内存。相比较直接对数组的操作,Buffer API更加容易操作和管理

Channel(通道)

Java NIO的通道类似流,但又有些不同:既可以从通道中读取数据,又可以写数据到通道。但流的(input或output)读写通常是单向的。通道可以非阻塞读取和写入通道,通道可以支持读取或写入缓冲区,也支持异步地读写

Selector选择器

Selector是一个Java NIO组件,可以能够检查一个或多个NIO通道,并确定哪些通道已经准备好进行读取或写入。这样,一个单独的线程可以管理多个channel,从而管理多个网络连接,提高效率

  • 每个channel都会对应一个Buffer
  • 一个线程对应Selector,一个Selector对应多个channel(连接)
  • 程序切换到哪个channel是由事件决定的
  • Selector会根据不同的事件,在各个通道上切换
  • Buffer就是一个内存块,底层是一个数组
  • 数据的读取写入是通过Buffer完成的,BIO中要么是输入流,或者是输出流,不能双向,但是NIO的Buffer是可以读也可以写
  • Java NIO系统的核心在于:通道(Channel)和缓冲区(Buffer)。通道表示打开到IO设备(例如:文件、套接字)的连接。若需要使用NIO系统,需要获取用于连接IO设备的通道以及用于容纳数据的缓冲区。然后操作缓冲区,对数据进行处理。简而言之,Channel负责传输,Buffer负责存取数据

4、NIO核心一:缓冲区(Buffer)

1)、Buffer概述

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

2)、Buffer类及其子类

Buffer就像一个数组,可以保存多个相同类型的数据。根据数据类型不同,有以下Buffer常用子类:

  • ByteBuffer
  • CharBuffer
  • ShortBuffer
  • IntBuffer
  • LongBuffer
  • FloatBuffer
  • DoubleBuffer

上述Buffer类都采用相似的方法进行管理数据,只是各自管理的数据类型不同而已。都是通过如下方法获取一个Buffer对象:

static XxxBuffer allocate(int capacity) //创建一个容量为capacity的XxxBuffer对象

3)、缓冲区的基本属性

  • 容量(capacity):作为一个内存块,Buffer具有一定的固定大小,也称为容量,缓冲区容量不能为负,并且创建后不能更改
  • 限制(limit):表示缓冲区中可以操作数据的大小(limit后数据不能进行读写)。缓冲区的限制不能为负,并且不能大于其容量。写入模式,限制等于buffer的容量。读取模式下,limit等于写入的数据量
  • 位置(position):下一个要读取或写入的数据的索引。缓冲区的位置不能为负,并且不能大于其限制
  • 标记(mark)与重置(reset):标记是一个索引,通过Buffer中的mark()方法指定Buffer中一个特定的position,之后可以通过调用reset()方法恢复到这个position
  • 标记、位置、限制、容量遵守以下不变式:0 <= mark <= position <= limit <= capacity

在这里插入图片描述

4)、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

5)、缓冲区的数据操作

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()方法清除缓冲区

6)、Buffer案例

    @Testpublic void test01() {// 1.分配一个缓冲区,容量设置成10ByteBuffer buffer = ByteBuffer.allocate(10);System.out.println(buffer.position()); // 0System.out.println(buffer.limit()); // 10System.out.println(buffer.capacity()); // 10System.out.println("--------------------");// 2.put()往缓冲区中添加数据String name = "hello";buffer.put(name.getBytes());System.out.println(buffer.position()); // 5System.out.println(buffer.limit()); // 10System.out.println(buffer.capacity()); // 10System.out.println("--------------------");// 3.flip()为将缓冲区的界限设置为当前位置,并将当前位置重置为0 可读模式buffer.flip();System.out.println(buffer.position()); // 0System.out.println(buffer.limit()); // 5System.out.println(buffer.capacity()); // 10System.out.println("--------------------");// 4.get()数据的读取char ch = (char) buffer.get();System.out.println(ch);System.out.println(buffer.position()); // 1System.out.println(buffer.limit()); // 5System.out.println(buffer.capacity()); // 10System.out.println("--------------------");}
    @Testpublic void test02() {// 1.分配一个缓冲区,容量设置成10 put()往缓冲区中添加数据ByteBuffer buffer = ByteBuffer.allocate(10);String name = "hello";buffer.put(name.getBytes());System.out.println(buffer.position()); // 5System.out.println(buffer.limit()); // 10System.out.println(buffer.capacity()); // 10System.out.println("--------------------");// 2.clear()清除缓冲区中的数据 并没有真正清除数据,只是让position的位置恢复到初始位置,后续添加数据的时候才会覆盖每个位置的数据buffer.clear();System.out.println(buffer.position()); // 0System.out.println(buffer.limit()); // 10System.out.println(buffer.capacity()); // 10System.out.println((char) buffer.get()); // hSystem.out.println("--------------------");// 3.定义一个缓冲区ByteBuffer buf = ByteBuffer.allocate(10);String n = "hello";buf.put(n.getBytes());buf.flip();// 读取数据byte[] b = new byte[2];buf.get(b);System.out.println(new String(b));System.out.println(buf.position()); // 2System.out.println(buf.limit()); // 5System.out.println(buf.capacity()); // 10System.out.println("--------------------");buf.mark(); // 标记此刻这个位置 2byte[] b2 = new byte[3];buf.get(b2);System.out.println(new String(b2));System.out.println(buf.position()); // 5System.out.println(buf.limit()); // 5System.out.println(buf.capacity()); // 10System.out.println("--------------------");buf.reset(); // 回到标记位置if (buf.hasRemaining()) {System.out.println(buf.remaining()); // 3}}

7)、直接与非直接缓冲区

ByteBuffer可以是两种类型,一种是基于直接内存(也就是非堆内存);另一种是非直接内存(也就是堆内存)。对于直接内存来说,JVM将会在IO操作上具有更高的性能,因为它直接作用于本地系统的IO操作。而非直接内存,也就是堆内存中的数据,如果要作IO操作,会先从本进程内存复制到直接内存,再利用本地IO处理

从数据流的角度,非直接内存是下面这样的作用链:

本地IO-->直接内存-->非直接内存-->直接内存-->本地IO

而直接内存是:

本地IO-->直接内存-->本地IO

很明显,在做IO处理时,比如网络发送大量数据时,直接内存会具有更高的效率。直接内存使用allocateDirect()创建,但是它比申请普通的堆内存需要耗费更高的性能。不过,这部分的数据是在JVM之外的,因此它不会占用应用的内存。所以呢,当有很大的数据要缓存,并且它的生命周期又很长,那么就比较适合使用直接内存。只是一般来说,如果不是能带来很明显的性能提升,还是推荐直接使用堆内存。字节缓冲区是直接缓冲区还是非直接缓冲区可通过调用其isDirect()方法来确定

直接缓冲区使用场景

  • 有很大的数据需要存储,它的生命周期又很长
  • 适合频繁的IO操作,比如网络并发场景

5、NIO核心二:通道(Channel)

1)、Channel概述

通道(Channel):由java.nio.channels包定义的。Channel表示IO源与目标打开的连接。Channel类似于传统的流。只不过Channel本身不能直接访问数据,Channel只能与Buffer进行交互

1)NIO的通道类似于流,但有些区别如下:

  • 通道可以同时进行读写,而流只能读或者只能写

  • 通道可以实现异步读写数据

  • 通道可以从缓冲读数据,也可以写数据到缓冲

2)BIO中的stream是单向的,例如FileInputStream对象只能进行读取数据的操作,而NIO中的Channel是双向的,可以读操作,也可以写操作

3)Channel在NIO中是一个接口

public interface Channel extends Closeable

2)、常用的Channel实现类

  • FileChannel:用于读取、写入、映射和操作文件的通道
  • DatagramChannel:通过UDP读写网络中的数据通道
  • SocketChannel:通过TCP读写网络中的数据
  • ServerSocketChannel:可以监听新进来的TCP连接,对每一个新进来的连接都会创建一个SocketChannel(ServerSocketChannel类似ServerSocket,SocketChannel类似Socket)

3)、FileChannel类

获取通道的一种方式是对支持通道的对象调用getChannel()方法。支持通道的类如下:

  • FileInputStream
  • FileOutputStream
  • RandomAccessFile
  • DatagramSocket
  • Socket
  • ServerSocket

获取通道的其他方式是使用Files类的静态方法newByteChannel()获取字节通道,或者通过通道的静态方法open()打开并返回指定通道

4)、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) // 强制将所有对此通道的文件更新写入到存储设备中

5)、FileChannel案例

1)本地文件写数据

    @Testpublic void write() {try {// 1.字节输出流通向目标文件FileOutputStream fos = new FileOutputStream("data01.txt");// 2.得到字节输出流对应的通道ChannelFileChannel channel = fos.getChannel();// 3.分配缓冲区ByteBuffer buffer = ByteBuffer.allocate(1024);buffer.put("Hello World!".getBytes());// 4.把缓冲区切换成写出模式buffer.flip();channel.write(buffer);channel.close();System.out.println("写数据到文件中!");} catch (IOException e) {e.printStackTrace();}}

2)本地文件读数据

    @Testpublic void read() {try {// 1.定义一个文件字节输入流与源文件接通FileInputStream fis = new FileInputStream("data01.txt");// 2.需要得到文件输入流的文件通道FileChannel channel = fis.getChannel();// 3.定义一个缓冲区ByteBuffer buffer = ByteBuffer.allocate(1024);// 4.读取数据到缓冲区channel.read(buffer);buffer.flip();// 5.读取出缓冲区中的数据并输出String rs = new String(buffer.array(), 0, buffer.remaining());System.out.println(rs);} catch (IOException e) {e.printStackTrace();}}

3)使用Buffer完成文件复制

    @Testpublic void copy() {try {// 源文件File srcFile = new File("data01.txt");// 目标文件File destFile = new File("data02.txt");// 得到字节输入流FileInputStream fis = new FileInputStream(srcFile);// 得到字节输出流FileOutputStream fos = new FileOutputStream(destFile);// 得到文件通道FileChannel isChannel = fis.getChannel();FileChannel osChannel = fos.getChannel();// 分配缓冲区ByteBuffer buffer = ByteBuffer.allocate(1024);while (true) {// 必须先清空缓冲区再写入数据到缓冲区buffer.clear();// 开始读取一次数据int flag = isChannel.read(buffer);if (flag == -1) {break;}// 已经读取了数据,把缓冲区的模式切换成可读模式buffer.flip();// 把数据写出osChannel.write(buffer);}isChannel.close();osChannel.close();System.out.println("复制完成!");} catch (IOException e) {e.printStackTrace();}}

4)分散(Scatter)和聚集(Gather)

分散读取(Scatter):是指把Channel通道的数据读入到多个缓冲区中去

聚集写入(Gather):是指将多个Buffer中的数据聚集到Channel

    @Testpublic void test() {try {// 1.字节输入管道FileInputStream fis = new FileInputStream("data01.txt");FileChannel isChannel = fis.getChannel();// 2.字节输出管道FileOutputStream fos = new FileOutputStream("data03.txt");FileChannel osChannel = fos.getChannel();// 3.定义多个缓冲区做数据分散ByteBuffer buffer1 = ByteBuffer.allocate(6);ByteBuffer buffer2 = ByteBuffer.allocate(1024);ByteBuffer[] buffers = {buffer1, buffer2};// 4.从通道中读取数据分散到各个缓冲区isChannel.read(buffers);// 5.从每个缓冲区中查询是否有数据读取到for (ByteBuffer buffer : buffers) {// 切换到读数据模式buffer.flip();System.out.println(new String(buffer.array(), 0, buffer.remaining()));}// 6.聚集写入到通道osChannel.write(buffers);isChannel.close();osChannel.close();System.out.println("复制完成!");} catch (IOException e) {e.printStackTrace();}}

5)transferFrom()

从目标通道中去复制原通道数据

    @Testpublic void test02() {try {// 1.字节输入管道FileInputStream fis = new FileInputStream("data01.txt");FileChannel isChannel = fis.getChannel();// 2.字节输出管道FileOutputStream fos = new FileOutputStream("data04.txt");FileChannel osChannel = fos.getChannel();// 3.复制数据osChannel.transferFrom(isChannel, isChannel.position(), isChannel.size());isChannel.close();osChannel.close();System.out.println("复制完成!");} catch (IOException e) {e.printStackTrace();}}

6)transferTo()

把原通道数据复制到目标通道

    @Testpublic void test03() {try {// 1.字节输入管道FileInputStream fis = new FileInputStream("data01.txt");FileChannel isChannel = fis.getChannel();// 2.字节输出管道FileOutputStream fos = new FileOutputStream("data05.txt");FileChannel osChannel = fos.getChannel();// 3.复制数据isChannel.transferTo(isChannel.position(), isChannel.size(), osChannel);isChannel.close();osChannel.close();System.out.println("复制完成!");} catch (IOException e) {e.printStackTrace();}}

6、NIO核心三:选择器(Selector)

1)、Selector概述

选择器(Selector)是非阻塞IO的核心,是SelectableChannel对象的多路复用器,Selector可以同时监控多个SelectableChannel的IO状况,也就是说,利用Selector可使一个单独的线程管理多个Channel

在这里插入图片描述

  • NIO用非阻塞的IO方式。可以用一个线程,处理多个的客户端连接,就会使用到Selector
  • Selector能够检测多个注册的通道上是否有事件发生(多个Channel以事件的方式可以注册到同一个Selector),如果有事件发生,便获取事件然后针对每个事件进行相应的处理。这样就可以只用一个单线程去管理多个通道,也就是管理多个连接和请求
  • 只有在连接或通道有读写事件发生时,才会进行读写,就大大地减少了系统开销,并且不必为每个连接都创建一个线程,不用去维护多个线程
  • 避免了多线程之间的上下文切换导致的开销

2)、Selector的应用

创建Selector:通过调用Selector.open()方法创建一个Selector

Selector selector = Selector.open();

向选择器注册通道:SelectableChannel.register(Selector sel, int ops)

// 1.获取通道
ServerSocketChannel ssChannel = ServerSocketChannel.open();
// 2.切换非阻塞模式
ssChannel.configureBlocking(false);
// 3.绑定连接
ssChannel.bind(new InetSocketAddress(9898));
// 4.获取选择器
Selector selector = Selector.open();
// 5.将通道注册到选择器上,并且指定监听接收事件
ssChannel.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 interestSet = SelectionKey.OP_READ | SelectionKey.OP_WRITE

7、NIO非阻塞式网络通信原理分析

1)、Selector特点说明

Selector可以实现:一个I/O线程可以并发处理N个客户端连接和读写操作,这从根本上解决了传统同步阻塞I/O一连接一线程模型,架构的性能、弹性伸缩能力和可靠性都得到了极大的提升

2)、服务端流程

1)当客户端连接服务端时,服务端会通过ServerSocketChannel得到SocketChannel:获取通道

        ServerSocketChannel ssChannel = ServerSocketChannel.open();

2)切换非阻塞模式

        ssChannel.configureBlocking(false);

3)绑定连接

        ssChannel.bind(new InetSocketAddress(9999));

4)获取选择器

        Selector selector = Selector.open();

5)将通道注册到选择器上,并且指定监听接收事件

        ssChannel.register(selector, SelectionKey.OP_ACCEPT);

6)轮询式的获取选择器上已经准备就绪的事件

        // 轮询式的获取选择器上已经准备就绪的事件while (selector.select() > 0) {// 7)获取当前选择器中所有注册的选择键(已就绪的监听事件)Iterator<SelectionKey> it = selector.selectedKeys().iterator();while (it.hasNext()) {// 8)获取准备就绪的是事件SelectionKey sk = it.next();// 9)判断具体是什么事件准备就绪if (sk.isAcceptable()) {// 10)若接收就绪,获取客户端连接SocketChannel sChannel = ssChannel.accept();//11)切换非阻塞模式sChannel.configureBlocking(false);// 12)将该通道注册到选择器上sChannel.register(selector, SelectionKey.OP_READ);} else if (sk.isReadable()) {// 13)获取当前选择器上读就绪状态的通道SocketChannel sChannel = (SocketChannel) sk.channel();// 14)读取数据ByteBuffer buf = ByteBuffer.allocate(1024);int len = 0;while ((len = sChannel.read(buf)) > 0) {buf.flip();System.out.println(new String(buf.array(), 0, len));buf.clear();}}// 15)取消选择键SelectionKeyit.remove();}}

3)、客户端流程

1)获取通道

        SocketChannel sChannel = SocketChannel.open(new InetSocketAddress("127.0.0.1", 9999));

2)切换非阻塞模式

        sChannel.configureBlocking(false);

3)分配指定大小的缓冲区

        ByteBuffer buf = ByteBuffer.allocate(1024);

4)发送数据给服务端

        Scanner scan = new Scanner(System.in);while (scan.hasNext()) {String str = scan.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();

8、NIO非阻塞式网络通信入门案例

1)、服务端实现

public class Server {public static void main(String[] args) throws IOException {// 1.获取通道ServerSocketChannel ssChannel = ServerSocketChannel.open();// 2.切换非阻塞模式ssChannel.configureBlocking(false);// 3.绑定连接ssChannel.bind(new InetSocketAddress(9999));// 4.获取选择器Selector selector = Selector.open();// 5.将通道注册到选择器上,并且指定监听接收事件ssChannel.register(selector, SelectionKey.OP_ACCEPT);// 6.使用Selector选择器轮询已经就绪好的事件while (selector.select() > 0) {// 7.获取选择器中的所有注册的通道中已经就绪好的事件Iterator<SelectionKey> it = selector.selectedKeys().iterator();// 8.开始遍历这些准备好的事件while (it.hasNext()) {SelectionKey sk = it.next();// 9.判断这个事件具体是什么if (sk.isAcceptable()) {// 10.直接获取当前接入的客户端通道SocketChannel channel = ssChannel.accept();// 11.切换非阻塞模式channel.configureBlocking(false);// 12.将该通道注册到选择器上channel.register(selector, SelectionKey.OP_READ);} else if (sk.isReadable()) {// 13.获取当前选择器上读就绪状态的通道SocketChannel sChannel = (SocketChannel) sk.channel();// 14.读取数据ByteBuffer buf = ByteBuffer.allocate(1024);int len = 0;while ((len = sChannel.read(buf)) > 0) {buf.flip();System.out.println(new String(buf.array(), 0, buf.remaining()));buf.clear();}}// 15.取消选择键SelectionKeyit.remove();}}}
}

2)、客户端实现

public class Client {public static void main(String[] args) throws IOException {// 1.获取通道SocketChannel sChannel = SocketChannel.open(new InetSocketAddress("127.0.0.1", 9999));// 2.切换非阻塞模式sChannel.configureBlocking(false);// 3.分配指定大小的缓冲区ByteBuffer buf = ByteBuffer.allocate(1024);// 4.发送数据给服务端Scanner scan = new Scanner(System.in);while (scan.hasNext()) {String str = scan.nextLine();buf.put((new SimpleDateFormat("yyyy/MM/dd HH:mm:ss").format(System.currentTimeMillis())+ "\n" + str).getBytes());buf.flip();sChannel.write(buf);buf.clear();}// 5.关闭通道sChannel.close();}
}

NIO非阻塞通信模型图

在这里插入图片描述

9、NIO网络编程应用实例-群聊系统

1)、需求

编写一个NIO群聊系统,实现客户端与客户端的通信需求(非阻塞)

服务器端:可以监测用户上线、离线,并实现消息转发功能

客户端:通过Channel可以无阻塞发送消息给其它所有客户端用户,同时可以接受其它客户端用户通过服务端转发来的消息

2)、服务端实现

public class Server {private Selector selector;private ServerSocketChannel ssChannel;private static final int PORT = 9999;public Server() {try {selector = Selector.open();ssChannel = ServerSocketChannel.open();ssChannel.bind(new InetSocketAddress(PORT));ssChannel.configureBlocking(false);ssChannel.register(selector, SelectionKey.OP_ACCEPT);} catch (Exception e) {e.printStackTrace();}}/*** 监听*/public void listen() {try {while (selector.select() > 0) {Iterator<SelectionKey> it = selector.selectedKeys().iterator();while (it.hasNext()) {SelectionKey sk = it.next();if (sk.isAcceptable()) {SocketChannel sChannel = ssChannel.accept();sChannel.configureBlocking(false);System.out.println(sChannel.getRemoteAddress() + "上线");sChannel.register(selector, SelectionKey.OP_READ);} else if (sk.isReadable()) {readClientData(sk);}it.remove();}}} catch (Exception e) {e.printStackTrace();}}/*** 读取客户端消息** @param sk*/private void readClientData(SelectionKey sk) {SocketChannel sChannel = null;try {sChannel = (SocketChannel) sk.channel();ByteBuffer buffer = ByteBuffer.allocate(1024);int count = sChannel.read(buffer);if (count > 0) {buffer.flip();String msg = new String(buffer.array(), 0, buffer.remaining());System.out.println("接收到了客户端消息:" + msg);// 向其它的客户端转发消息(去掉自己)sendInfoToOtherClients(msg, sChannel);}} catch (IOException e) {e.printStackTrace();try {System.out.println(sChannel.getRemoteAddress() + "离线了..");sk.cancel();sChannel.close();} catch (IOException e2) {e2.printStackTrace();}}}/*** 转发消息给其它客户端** @param msg* @param sChannel*/private void sendInfoToOtherClients(String msg, SocketChannel sChannel) throws IOException {System.out.println("服务器转发消息中...");// 遍历所有注册到selector上的SocketChannel并排除sChannelfor (SelectionKey key : selector.keys()) {Channel targetChannel = key.channel();// 排除自己if (targetChannel instanceof SocketChannel && targetChannel != sChannel) {SocketChannel dest = (SocketChannel) targetChannel;ByteBuffer buffer = ByteBuffer.wrap(msg.getBytes());dest.write(buffer);}}}public static void main(String[] args) {Server server = new Server();server.listen();}
}

3)、客户端实现

public class Client {private Selector selector;private SocketChannel socketChannel;private String username;private static final String IP = "127.0.0.1";private static final int PORT = 9999;public Client() {try {selector = Selector.open();socketChannel = SocketChannel.open(new InetSocketAddress(IP, PORT));socketChannel.configureBlocking(false);socketChannel.register(selector, SelectionKey.OP_READ);username = socketChannel.getLocalAddress().toString().substring(1);System.out.println("当前客户端准备完成...");} catch (IOException e) {e.printStackTrace();}}/*** 读取从服务器端回复的消息** @throws IOException*/private void readInfo() throws IOException {while (selector.select() > 0) {Iterator<SelectionKey> iterator = selector.selectedKeys().iterator();while (iterator.hasNext()) {SelectionKey key = iterator.next();if (key.isReadable()) {SocketChannel sc = (SocketChannel) key.channel();ByteBuffer buffer = ByteBuffer.allocate(1024);sc.read(buffer);System.out.println(new String(buffer.array()).trim());}iterator.remove();}}}/*** 向服务器发送消息** @param s*/private void sendToServer(String s) {s = username + "说:" + s;try {socketChannel.write(ByteBuffer.wrap(s.getBytes()));} catch (IOException e) {e.printStackTrace();}}public static void main(String[] args) {Client client = new Client();new Thread(() -> {try {client.readInfo();} catch (IOException e) {e.printStackTrace();}}).start();Scanner sc = new Scanner(System.in);while (sc.hasNextLine()) {String s = sc.nextLine();client.sendToServer(s);}}
}

四、AIO简介

Java AIO(NIO 2.0)异步非阻塞,服务器实现模式为一个有效请求一个线程,客户端的I/O请求都是由操作系统先完成了再通知服务器应用去启动线程进行处理

BIONIOAIO
SocketSocketChannelAsynchronousSocketChannel
ServerSocketServerSocketChannelAsynchronousServerSocketChannel

与NIO不同,当进行读写操作时,只须直接调用API的read或write方法即可,这两种方法均为异步的,对于读操作而言,当有流可读取时,操作系统会将可读的流传入read方法的缓冲区,对于写操作而言,当操作系统将write方法传递的流写入完毕时,操作系统主动通知应用程序

即可以理解为,read/write方法都是异步的,完成后会主动调用回调函数。在JDK1.7中,这部分内容被称作NIO 2.0,主要在Java.nio.channels包下增加了下面四个异步通道:

  • AsynchronousSocketChannel
  • AsynchronousServerSocketChannel
  • AsynchronousFileChannel
  • AsynchronousDatagramChannel

推荐资料

IO模式讲解(AIO&BIO&NIO)


http://chatgpt.dhexx.cn/article/q12xVov1.shtml

相关文章

OpenSSL BIO源码简析

文章目录 1. BIO简介BIO chainBIO数据结构BIO_METHOD数据结构 2. Base64示例分析初始化构造BIO链写数据free 1. BIO简介 相关文档 /html/man7/bio.html /html/man3/BIO_*.htmlbio - Basic I/O abstraction&#xff0c;即IO抽象层。 BIO有两种: source/sink BIO&#xff0c;…

二、JAVA BIO

NIO 目录 文章目录 二、JAVA BIO1、 Java BIO基本介绍2、 java BIO工作机制3、传统的BIO编程实例回顾3.1、客户端案例如下3.2、服务端案例如下3.3、输出3.4、小结 4、BIO模式下多发和多收消息4.1、客户端代码如下4.2、服务端代码如下4.3、输出 5、BIO模式下接收多个客户端5.1、…

BIO和NIO的区别

1.BIO基本介绍 BIO是传统的Java IO编程&#xff0c;其基本的类和接口在java.io包中BIO(blocking I/O)&#xff1a;同步阻塞&#xff0c;服务器实现模式为一个连接一个线程&#xff0c;即客户端有连接请求时服务器端就需要启动一个线程进行处理&#xff0c;如果这个连接不做任何…

网络编程之中篇——BIO模型详述

1、BIO介绍 1.1、BIO的概念 BIO&#xff08;Blocking IO&#xff09;同步阻塞IO模型&#xff0c;在JDK 1.4之前&#xff0c;建立网络链接采用的只有BIO的模型 需要服务端首先启动建立一个ServerSocket实例&#xff0c;然后客户端启动Socket实例对服务端进行连接通信&#xf…

BIO,NIO,AIO分别是什么?他们有什么区别?

1、BIO 概念&#xff1a; BIO是一种同步阻塞I/O模式&#xff0c;服务实现模式为一个连接对应一个线程&#xff0c;即客户端发送一个连接&#xff0c;服务端要有一个线程来处理。 存在的问题&#xff1a; 一旦有高并发的大量请求,就会有如下问题&#xff1a; 1&#xff09;线程…

JAVA BIO与NIO、AIO的区别(这个容易理解)

IO的方式通常分为几种&#xff0c;同步阻塞的BIO、同步非阻塞的NIO、异步非阻塞的AIO。 一、BIO 在JDK1.4出来之前&#xff0c;我们建立网络连接的时候采用BIO模式&#xff0c;需要先在服务端启动一个ServerSocket&#xff0c;然后在客户端启动Socket来对服务端进行通信&#…

Quartus II 上手攻略

第一次接触EDA实验&#xff0c;对这方面的相关操作并不熟悉。本篇文章结合上课内容和B站Quartus进行整理&#xff0c;总结一下Quartus 这款软件的基本使用。 参考的B站教学链接&#xff1a;《Quartus II 软件安装与入门教程》 Quartus 软件简介 Quartus II 是Altera公司为其FP…

完全卸载quartus ii 9.0

即将毕业了&#xff0c;把电脑一些不用的软件清清&#xff0c;发现quartus软件贼占空间&#xff0c;删除又貌似找不到卸载的exe&#xff0c;百度了好多都不靠谱 下面介绍一种方法&#xff0c;可以很好的卸载掉quartus&#xff0c;原先我的quartus是安装在D盘下&#xff0c;结果…

Quartus II与Modelsim软件安装教程

Quartus II与Modelsim软件安装教程 一、Quartus II软件安装1、Quartus II安装2、器件安装3、Quartus 破解4、USB Blaster 驱动安装 二、Modelsim软件安装1、modelsim安装2、modelsim注册 三、参考资料 一、Quartus II软件安装 本节主要讲述Quartus II13.1软件的安装使用&#x…

Quartus II13.1安装教程

安装前先关闭杀毒软件和360卫士&#xff0c;注意安装路径不能有中文&#xff0c;安装包路径也不要有中文。 1.鼠标右击【Quartus II 13.1】压缩包选择【解压到Quartus II 13.1】。 2.双击打开解压后的【Quartus II 13.1】文件夹。 3.双击打开【Quartus】文件夹。 4.鼠标右击【Q…

Quartus II下载器件库

Quartus II下载器件库 1、在浏览器中输入网址 https://fpgasoftware.intel.com/18.1/?editionstandard&platformwindows&#xff0c; 或https://fpgasoftware.intel.com/ 进入如下图所示界面。 2、在版本类型和版本中输入Quartus II所对应的版本 3、输入完版本后&#…

Quartus II软件的使用

在这里&#xff0c;我们只是简单的介绍了一下上述的流程图&#xff0c;让大家有个大致的了解&#xff0c;接下来我们就以流水灯实验的工程为例&#xff0c;对每个流程进行详细的操作演示&#xff0c;一步步、手把手带领大家学习使用Quartus II软件。 在创建工程之前&#xff0c…

QuartusII中LPM_COUNTER的使用

ALTERA建议&#xff0c;在设计时时序允许的情况下尽量使用Megafunction的资源&#xff0c;因为在多数情况下Megafunction的综合和实现结果更为优化。现在&#xff0c;就LPM_COUNTER的使用&#xff0c;浅谈一下。 Megafunction中LPM_COUNTER的参数设定主要是以下三部分&#xf…

quartus II 18.1 下载

quartus II 18.1 下载链接 以及解析 链接:https://pan.baidu.com/s/1warS-Vvv1maDmOKu8RsteQ 提取码&#xff1a;awxd 这个链接是已经下好的安装包 链接:https://pan.baidu.com/s/13HuyxUZvZ19vdYUmlLJujQ 提取码&#xff1a;gudn 第二个链接解压密码&#xff1a; wqlx.13542…

Quartus II14.1安装教程

安装前先关闭杀毒软件和360卫士&#xff0c;注意安装路径不能有中文&#xff0c;安装包路径也不要有中文。 1.鼠标右击【Quartus II 14.1】压缩包选择【解压到Quartus II 14.1】。 2.双击打开解压后的【Quartus II 14.1】文件夹。 3.双击打开【Quartus】文件夹。 4.鼠标右击【Q…

quartus ii matlab,基於Quartus II和MATLAB的FIR濾波器設計與仿真(二)

接上文 基於Quartus II和MATLAB的FIR濾波器設計與仿真(一)&#xff1a; 3 QuartusII 調用 IP 核生成 FIR 濾波器模塊 在 Quartus II 中&#xff0c; Altera 提供了一系列可供用戶免費使用的 IP 核&#xff0c; FIR濾波器就包含其中&#xff0c;所以只需要在 Quartus II 中調用…

安装Quartus II教程

下载Quartus安装包&#xff0c;给大家一个11.3版本的安装包 链接&#xff1a;https://pan.baidu.com/s/1eXtjL2JZVGV1RBC0VozqVQ?pwdhmnv 提取码&#xff1a;hmnv 1.打开安装程序&#xff0c;点击next 2.点击接受&#xff0c;下一步 3.选择安装路径&#xff0c;这里最好选择…

Quartus II 仿真

Quartus II 使用university program VWF仿真 1.File->new->university program VWF->OK打开仿真页面 2.edit->insert->insert node or bus或者直接双击左边空白地方弹出insert node or bus对话框。 3.node finder->list-> >> ->OK->OK 4.设…

QuartusII9.0--项目文件的新建

第一步&#xff1a;打开QuartusII软件&#xff0c;界面如下&#xff1a; 第二步&#xff1a;选择File->New Project Wizard菜单项&#xff0c;则弹出New Project Wizard:Indroduction对话框&#xff0c;如下图所示&#xff1a; 单击Next按钮&#xff0c;则进入项目工程的目…

Quartus II报错

使用如下电路语句创建异步时序实现D触发器时 一直报错 Error (10200): Verilog HDL Conditional Statement error at flip_flop.v(9): cannot match operand(s) in the condition to the corresponding edges in the enclosing event control of the always construct 在网上…