目录

java NIO概念

NIO和BIO的比较

NIO三大核心原理示意图

Buffer缓冲区

Channel(通道)

Selector选择器

NIO核心一:缓冲区(Buffer)

缓冲区(Buffer)

Buffer类及其子类

缓冲区的基本属性

Buffer常见方法

缓冲区的数据操作

常见API演示

直接与非直接缓冲区

NIO核心二:通道(Channel)

常用的Channel实现类

NIO下Selector选择器概述

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

NIO下通信入门案例-服务端代码实现


java NIO概念

Java NIO也有人称之为java non-blocking lO是从Java 1.4版本开始引入的一个新的IO API,可以替代标准的Java lO API。NIO与原来的IO有同样的作用和目的,但是使用的方式完全不同,NIO支持面向缓冲区的、基于通道的IO操作。NIO将以更加高效的方式进行文件的读写操作。NIO可以理解为非阻塞lO,传统的lO的read和write只能阻塞执行,线程在读写IO期间不能干其他事情,比如调用socket.read()时如果服务器一直没有数据传输过来,线程就一直阻塞,而NIO中可以配置socket为非阻塞模式。

NIO相关类都被放在java.nio包及子包下,并且对原java.io包中的很多类进行改写。

NIO有三大核心部分: Channel(通道),Buffer(缓冲区), Selector(选择器)
Java NlO的非阻塞模式,使一个线程从某通道发送请求或者读取数据,但是它仅能得到目前可用的数据,如果目前没有数据可用时,就什么都不会获取,而不是保持线程阻塞,所以直至数据变的可以读取之前,该线程可以继续做其他的事情。非阻塞写也是如此,一个线程请求写入一些数据到某通道,但不需要等待它完全写入,这个线程同时可以去做别的事情。

NIO和BIO的比较

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

NIO三大核心原理示意图

Buffer缓冲区

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

Channel(通道)

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

Selector选择器

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

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

NIO核心一:缓冲区(Buffer)

缓冲区(Buffer)

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

Buffer类及其子类

Buffer就像一个数组,可以保存多个相同类型的数据。根据数据类型不同,有以下Buffer常用子类:· ByteBuffer,CharBuffer,ShortBuffer,lntBuffer,LongBuffer,FloatBuffer,DoubleBuffer。
上述Buffer类他们都采用相似的方法进行管理数据,只是各自管理的数据类型不同而已。都是通过如下方法获取一个 Buffer对象:

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

Buffer常见方法
Buffer clear()清空缓冲区并返回对缓冲区的引用
Buffer flip()为将缓冲区的界限设置为当前位置,并将当前位置设值为0
int capacity()返回 Buffer 的capacity 大小
int limit()返回 Buffer 的界限(limit)的位置
boolean hasRemaining()判断缓冲区中是否还有元素
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)
  • put(byte b);将给定单个字节写入缓冲区的当前位置
  • put(byte[] src):将 src中的字节写入缓冲区的当前位置
  • put(int index,byte b):将指定字节写入缓冲区的索引位置(不会移动 position)

使用Buffer读写数据一般遵循以下四个步骤:·

  • 写入数据到Buffer
  • 调用flip()方法,转换为读取模式
  • 从Buffer中读取数据
  • 调用buffer.clear()方法或者buffer.compact()方法清除缓冲区
常见API演示
public class BufferTest {public static void main(String[] args) {//1、分配一个缓冲区,容量设置成10ByteBuffer buffer = ByteBuffer.allocate(10);//返回缓冲区的当前位置positionSystem.out.println(buffer.position());//0//返回 Buffer 的界限(limit)的位置System.out.println(buffer.limit());//10//返回 Buffer 的capacity 大小System.out.println(buffer.capacity());System.out.println("------------------");//10//2.put往缓冲区中添加数据String name = "zhangsan";//将给定单个字节写入缓冲区的当前位置buffer.put(name.getBytes());System.out.println(buffer.position());//8System.out.println(buffer.limit());//10System.out.println(buffer.capacity());//10System.out.println("------------------");//为将缓冲区的界限设置为当前位置,并将当前位置充值为0buffer.flip();System.out.println(buffer.position());//0System.out.println(buffer.limit());//8System.out.println(buffer.capacity());//10System.out.println("------------------");//读取单个字节char b = (char)buffer.get();System.out.println(b);//zSystem.out.println(buffer.position());//1System.out.println(buffer.limit());//8System.out.println(buffer.capacity());//10System.out.println("------------------");//clear清除缓冲区中的数据buffer.clear();System.out.println(buffer.position());//0System.out.println(buffer.limit());//10System.out.println(buffer.capacity());//10System.out.println((char)buffer.get());//zSystem.out.println("------------------");//定义一个新的缓冲区//1、分配一个缓冲区,容量设置成10ByteBuffer buf = ByteBuffer.allocate(10);String n = "wangwu";buf.put(n.getBytes());//为将缓冲区的界限设置为当前位置,并将当前位置设值为0buf.flip();//byte[] by = new byte[2];buf.get(by);String rs = new String(by);System.out.println(rs);//waSystem.out.println(buf.position());//2System.out.println(buf.limit());//6System.out.println(buf.capacity());//10System.out.println("------------------");//对缓冲区设置标记buf.mark();byte[] by1 = new byte[2];buf.get(by1);String rs1 = new String(by1);System.out.println(rs1);//ngSystem.out.println(buf.position());//4System.out.println(buf.limit());//6System.out.println(buf.capacity());//10System.out.println("------------------");//将位置 position转到以前设置的 mark所在的位置buf.reset();if (buf.hasRemaining()){System.out.println(buf.remaining());//4}}}
直接与非直接缓冲区

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

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

而直接内存是:

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

创建一个直接缓冲区

public class BufferTeat2 {public static void main(String[] args) {//创建一个直接内存的缓冲区ByteBuffer buffer = ByteBuffer.allocateDirect(1024);System.out.println(buffer.isDirect());}}

使用场景

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

NIO核心二:通道(Channel)

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

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

  • 通道可以同时进行读写,而流只能读或者只能写
  • 通道可以实现异步读写数据
  • 通道可以从缓冲读数据,也可以写数据到缓冲

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

3、Channel在NIO中是一个接口

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

NIO下FileChannel写数据到文件中去

public class ChnnelTest {public static void main(String[] args) {//try {//字节输出流通向目标文件FileOutputStream fos = new FileOutputStream("data01.txt");//得到字节输出流对应的通道ChannelFileChannel channel = fos.getChannel();//分配缓冲区ByteBuffer buffer = ByteBuffer.allocate(1024);buffer.put("hello,channel".getBytes());//把缓徘区切换成写出模式buffer.flip();channel.write(buffer);channel.close();System.out.println("数据写入成功!");} catch (Exception e) {e.printStackTrace();}}}

NIO下FileChannel读文件数据显示

/** * 读数据 */public class ChannelTeat2 {public static void main(String[] args) {//try {//1、定义一个文件字节输入流与源文件接通FileInputStream is = new FileInputStream("data01.txt");//2、需要得到文件字节输入流的文件通道FileChannel channel = is.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 (Exception e) {e.printStackTrace();}}}

NIO下FileChannel完成文件复制

/** * NIO下FileChannel完成文件复制 */public class ChinnelTest3 {public static void main(String[] args) throws Exception {//源文件File readFile = new File("E:\\photo\\figure\\1.png");File writeFile = new File("E:\\photo\\figure\\new.png");//得到一个字节字节输入流FileInputStream fis = new FileInputStream(readFile);//得到一个字节输出流FileOutputStream fos = new FileOutputStream(writeFile);//得到的是文件通道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();}}

NIO下FileChannel完成分散和聚集操作数据

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

聚集写入(Gathering )是指将多个Buffer 中的数据”聚集”到Channel。

/** * 分散和聚集操作数据 */public class ChnnelTest4 {public static void main(String[] args) throws Exception {//字节输入流FileInputStream fis = new FileInputStream("data01.txt");FileChannel fisChannel = fis.getChannel();//字节输出流FileOutputStream fos = new FileOutputStream("data02.txt");FileChannel fosChannel = fos.getChannel();//3、定义多个缓冲区做数据分散ByteBuffer buffer1 = ByteBuffer.allocate(4);ByteBuffer buffer2 = ByteBuffer.allocate(1024);ByteBuffer[] buffers = {buffer1,buffer2};//4、从通道中读取数坼分散到各个缓冲区fisChannel.read(buffers);//5、从每个缓冲区中查询是否有数据读取到了for (ByteBuffer buffer : buffers) {buffer.flip();//切换到读数据模式System.out.println(new String(buffer.array(),0,buffer.remaining()));}//聚集写入到通道fosChannel.write(buffers);//关闭资源fosChannel.close();fisChannel.close();fos.close();fis.close();}}

NIO下FileChannel的transferFrom与transferTo方法

transferFrom():从目标通道中去复制原通道数据

transferTo():把原通道数据复制到目标通道

/** * 写数据 */public class ChnnelTest5 {public static void main(String[] args) throws Exception {//字节输入流FileInputStream fis = new FileInputStream("data01.txt");FileChannel fisChannel = fis.getChannel();//字节输出流FileOutputStream fos = new FileOutputStream("data04.txt");FileChannel fosChannel = fos.getChannel();//复制数据//fisChannel 起始通道//fisChannel.position() 起始位置//fisChannel.size() 通道大小//fosChannel.transferFrom(fisChannel,fisChannel.position(),fisChannel.size());fisChannel.transferTo(fisChannel.position(),fisChannel.size(),fosChannel);//关闭资源fosChannel.close();fisChannel.close();fos.close();fis.close();}}

NIO下Selector选择器概述

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

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

NIO下Selector选择器的获取与通道注

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

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

当调用register(Selector sel, int ops)将通道注册选择器时,选择器对通道的监听事件,需要通过第二个参数ops指定。可以监听的事件类型(用可使用SelectionKey的四个常量表示)︰

  • 读: SelectionKey.OP_READ(1)
  • 写: SelectionKey.@P_WRITE (4)
  • 连接:SelectionKey.OP_CONNECT (8)
  • 接收:SelectionKey.OP_ACCEPT (16)
  • 若注册时不止监听一个事件,则可以使用“位或”操作符连接。

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

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

服务端流程

1 当客户端连接服务端时,服务端会通过 ServerSocketChannel 得到 SocketChannel: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轮询式的获取选择器上已经“准备就绪”的事件
//轮询式的获取选择器上已经“准备就绪”的事件 while (selector.select() > 0) {System.out.println("轮一轮");//7. 获取当前选择器中所有注册的“选择键(已就绪的监听事件)”Iterator 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();}}}

客户端流程

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();
NIO下通信入门案例-服务端代码实现

需求:服务端接收客户端的连接请求,并接收多个客户端发送过来的事件。

/** 服务端 */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. 轮询式的获取选择器上已经“准备就绪”的事件while (selector.select() > 0) {System.out.println("轮一轮");//7. 获取当前选择器中所有注册的“选择键(已就绪的监听事件)”Iterator 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();}}}}

NIO下通信入门案例-客户端端代码实

/**客户端 */public class Client {public static void main(String[] args) throws Exception {//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();}}