java BIO、NIO、AIO详细笔记

NIO以及AIO不适合基础不好的同学学习,需要熟练Java OOP编程,具有一定编程思维,熟悉java多线程编程,熟悉Java IO流编程,Java网络编程,还要熟悉常用Java设计模式
BIO大部分人已经接触,简单介绍一些,重点会放在NIO的讲解
源码:https://gitee.com/yin_zhipeng/learn_the_basics_of_java_io.git
通信架构的坎坷
  1. Java软件开发中,通信架构不可避免,不同系统或不同进程间数据交互,或者高并发下的通信场景下都需要网络通信技术,例如游戏
  2. 早期网络通信架构有一些缺陷,最让人恼火的就是基于性能低下的同步阻塞式I/O通信BIO(底层基于IO)
  3. 随着互联网发展,通信性能要求变高,java在2002年开始支持非阻塞式I/O通信技术NIO(底层基于缓冲区)
业务场景
  1. 局域网内的通信
  2. 多系统间的底层消息传递机制
  3. 高并发下,大数据量的通信场景
  4. 游戏行业,手游服务端,大型网络游戏
I/O模型
  1. 规定使用何种通道或通信模式和架构进行数据传输。Java共支持3中网络编程I/O模型BIO、NIO、AIO
  2. 不同业务场景和性能需求下,选择不同I/O模型
Java BIO(同步并阻塞):传统阻塞型
  1. 服务器实现模式为一个连接一个线程,当客户端有连接请求时,服务器就得启动一个线程处理。如果这个连接不做任何事,就会造成不必要线程开销。
    在这里插入图片描述
  2. 适用于连接数较小且固定的架构,对服务器资源要求较高,并发局限于应用中,JDK1.4前唯一的选择,程序简单
Java NIO(同步非阻塞)
  1. 一个线程处理多个请求(连接),客户端发送的连接请求都会注册到多路复用器(选择器)上,多路复用器轮询到连接,如果当前轮询的连接有I/O请求就进行处理(开一个线程处理),如果没有请求,它不会等待,而BIO会一直阻塞等着
    在这里插入图片描述
  2. 适用于连接数多且连接较短(轻操作)的架构,例如聊天服务器,弹幕系统,服务器间通讯等,编程较为复杂,JDK1.4开始支持
Java AIO(NIO.2,异步,异步非阻塞)
  1. 一个有效请求一个线程,客户端的I/O请求都由OS先完成了以后,再通知服务器应用去启动线程进行处理
  2. 适用于连接数多且连接较长(重操作)的架构,如相册服务器,充分调用OS参与并发操作,编程较为复杂,JDK7开始支持

零、Linux系统底层到JavaBIO/NIO

说白了,IO调用的都是底层Linux的方法,例如读方法read()需要传参fd(文件描述符),然后对文件进行读操作,如果socket是阻塞的,那么会有一个标识,我们调用read()时,需要人为帮他阻塞,如果是非阻塞的,我们调用read()时,不用帮他阻塞(当然还需要诸如select等内核帮助)。这些事JVM帮我们做了,它会通过BIO和NIO两种技术,帮我们完成阻塞和非阻塞的操作

1、IO和BIOLinux底层

1. 为什么IO是计算机的瓶颈

  1. 内存的时间单位是纳秒级别,而I/O(网卡,磁盘寻址,输入输出)是毫秒级别
  2. 1s = 1000ms =1000000vm= 1000000000ns(1毫秒=1000微秒=1000000纳秒)
  3. 所以差一点的内存比好一点的I/O设备快几十万倍,所以IO是计算机瓶颈

2. Socket是阻塞的么?每个accept连接都是新的文件对象么?

  1. Socket既可以阻塞,也可以非阻塞,每个Socket连接,都是一个新的文件对象,产生新的文件描述符
  2. 上面我虽然这么说,但是大家也不知道是不是真的,接下来看一下Linux源码,验证我上面的说法
  1. 通过Linux命令man 2 socket,可以看到,它给出了非阻塞的参数定义,也就是socket可以非阻塞
    在这里插入图片描述
    在这里插入图片描述
  2. 通过命令 man 2 bind,我们看example,实例程序,是C的代码,但大家应该能看懂
  3. 可见它main方法中,上来就调用了socket,返回值是int型的sfd,也就是文件描述符,然后bind绑定端口,listen开启监听(基于文件描述符),当开启监听后,需要accept开启连接,当开启连接后,生成全新文件描述符cfd
  4. 也就是说,每个新的连接,都生成一个新的文件对象
    在这里插入图片描述
    在这里插入图片描述

3. 什么是BIO呢?

  1. 把前面的东西理解了,就完全没问题了。Linux生成新的文件描述符,是因为Linux万物皆文件,而JAVA万物皆对象。
  2. 将Linux文件换成对象,就是一个Socket连接是一个对象,每个accept连接,又是一个新对象。所以每个accept连接互不冲突。
  3. BIO就是阻塞IO,也就是系统底层参数为默认的socket,上面我们说了,socket也可以非阻塞
  4. 也就是说,JAVA也不过是调用系统内核的东西,只不过JAVA还有JVM帮他,所以JAVA可以抽象为面向对象,实际还是JVM再调用Linux底层的东西。
  5. 所以你可以这样和面试官说,JAVA BIO就是通过JVM调用系统内核BIO的东西,首先,开启一个socket连接,对应一个文件描述符,有了socket连接后,进行bind绑定,绑定到端口,然后开启Listen监听,监听指定文件描述符,然后开启accept连接,开启一个accept连接,会生成一个新的文件描述符,把文件概念缓存对象,就是Java的BIO

2、Linux底层如何阻塞和非阻塞

4. Linux如何完成阻塞?

  1. 当我们没有指定socket不阻塞后,就是Linux中没有指定SOCK_NONBLOCK参数为非阻塞,也就默认socket是一个阻塞的连接
  2. JAVA中直接使用socket套接字,accept连接时,就是阻塞的。也就是默认的,并不是说Socket是阻塞的。
    3.Linux 内核底层的read读方法需要传参连接的文件描述符fd,缓存区大小,文件总大小,我们知道,socket可以用SOCK_NONBLOCK参数进行设置socket是阻塞的。规定生成阻塞的或非阻塞的socket连接文件描述符,如果是阻塞的read方法执行前,如果没有可以读取的内容,会帮他卡住,也就是阻塞住
    在这里插入图片描述

5. Linux如何完成非阻塞?

  1. 如何调用Linux的read()方法时,文件描述符fd是非阻塞的socket连接
  2. 那么它在read时,如果没有东西,立即返回结果,不进行阻塞
  3. 什么时候我们开心了想起来了,再去read()一下,如果此时有需要读的东西,就读。也就是我不等你,你来了,你等我一会。
  4. 非阻塞就是一种思想,假设它只开一个线程,进行监听,不断监听fd 6,当监听到后,有连接过来,它不断遍历,通过read()方法轮询fd 8 和 fd 9 ,有操作(读,写)过来,就进行处理,没有,就继续轮询,这样就不用阻塞了。
    在这里插入图片描述
  5. 上面这种结构有很大缺陷,假设1000个客户端连接,当有一个连接突然要发送数据,我们可能需要调用999次read()方法,才能去处理
  6. 而最好能够减少和内核交互(read())的次数,并且及时知道有操作过来,把1000次询问,变成1次,可以节省大量资源。如果可以把文件描述符拿到,然后及时知道想读想写就好了。
  7. 所以Linux提供了新的系统调用,select,通过man 2 select命令查看,JAVA模型当中,对应selector选择器对象
  1. 首先它的参数nfds,表示可以传很多个文件描述符,而指针fd_set *readfds表示想读的文件描述符,writefds表示想写的文件描述符,轮询时将相应的文件描述符放进去。我们调用方,通过指针可以获取到想读的,想写的
  2. 它会轮询这些文件描述符,判断它们的操作状态,然后返回文件描述符的个数
    在这里插入图片描述

2、JAVA 的 BIO和 NIO

javaBIO和NIO包括普通IO,都是对底层操作系统内核系统处理的封装,我们写的代码,都是一条条指令,调用操作系统指令,所以我们要先了解系统底层的逻辑和指令,然后看JAVA如何进行面向对象的封装

6. BIO一般搭配多线程使用,为什么?

  1. 假设服务端tomcat监听8080端口,文件描述符为6
  2. 当同时两个客户端,通过与服务端Linux内核三次握手,建立TCP连接后
  3. 双方开始分配资源,来处理连接,此时两个客户端各自分配好了资源
  4. 假设服务端只分配一个线程资源,但是连接不会建立一个,假设两个连接都建立完成,文件描述符分别为8和9
  5. 此时,tomcat只有一个线程,阻塞监听fd 8的读,但是此时fd 8 没有立即发送数据,或者发送的数据太大
  6. 那么fd 9就必须一直等着处理fd 8的线程执行结束,再来处理自己的请求。
    6
  7. 而有了多线程,第一个线程处理fd 8,第二个线程处理fd 9就好了,但是如果两个客户端都不发数据,两个线程依然会阻塞住
    在这里插入图片描述

7. JAVA NIO如何实现非阻塞?请结合系统底层原理阐述!

  1. BIO和NIO是阻塞IO和非阻塞IO的简写,是JAVA中或者说编程语言中的概念,底层调用的还是socket的read()方法
  2. 但是NIO和BIO不同之处在于,NIO使用了操作系统的select系统调用,它可以同时处理多个文件描述符轮询它们的操作状态,然后返回可以进行操作的文件描述符个数。此方法执行一次轮询一次,本身不阻塞
  1. 需要我们将多个文件描述符传入
  2. select的writefds和readfds两个指针,会保存想要读,写操作的文件描述符,当然还有其它操作
  3. 最终,还会返回需要处理操作的文件描述符的个数
  4. 但是select只是负责轮询,看看你给我的这些文件描述符,谁想写,谁想读,如果有新连接过来,不归我管
  5. 所以我们需要外部调用select的程序,自己实现这样功能,JAVA通过Channel通道来处理整个IO请求,而对select的调用和封装,通过Selector完成
  1. 所以,Java就是抽象了系统底层
  1. 首先启动socket,进行端口绑定,这里java抽象出了通道Channel的概念,我们所有的NIO操作都通过Channel进行,包括启动socket服务端监听
  2. 然后需要指定操作哪些文件描述符了java抽象了selector选择器,我们需要将连接注册到select,也就是文件描述符给select,并且java可以指定选择器轮询时,关注的操作有哪些,通过register()方法注册时可以指定,比如下图OP_ACCEPT就是关注监听连接事件。注意只是注册,并没有调用底层select方法
  3. 通俗点讲,bind相当于启动一个socket连接文件描述符,绑定到Channel通道,然后socket连接文件描述符就需要保存起来,通过ss.refister()保存
  4. 知道有哪些文件描述符,就可以调用select了,通过selector.select()调用操作系统select,然后可以得到select()方法返回值,也就是需要操作的文件描述符的个数,然后判断是否有需要操作的东西,没查到就一直去调用select(),直到有操作
  5. 如果有操作,获取操作集,看看是什么操作,然后执行相应的操作。假设是建立连接操作
  1. 我们知道建立一个socket连接,会生成新的文件描述符,而NIO都在通道中完成操作,所以通过key.channel()可以拿到新的socket通道,然后调用accept()生成socket连接获取SocketChannel
  2. 而我们前面也说过,socket可以指定阻塞或非阻塞java通过sc.configureBlocking(false)指定当前通道封装的socket连接,是非阻塞状态
  3. 然后我们可以选择把它放到selector中,sc.refister注册进去,关注读操作。
  4. 当前循环结束后,下一次while(true),又会进行selector.select()的调用,此时新加入的文件描述符,也在文件描述符集合中,作为select的参数,调用select,回到上面的第4步
    在这里插入图片描述
    在这里插入图片描述
    在这里插入图片描述

一、Java BIO

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

1. 何为同步阻塞

请参考我的另一篇文章中案例场景:https://blog.csdn.net/grd_java/article/details/109862876
  1. 案例中,服务端会一直等待客户端消息,如果客户端没有进行消息的发送,服务端会一直阻塞
  2. 服务端按照任何方式接收消息(例如按行接收),客户端也必须按照相同方式发送(也得按行),否则服务端可能无法正常收到消息,或者直接抛出异常
  3. 服务端可以反复接收消息,客户端也可以反复发送消息
  4. 当服务端accept()方法接收到连接后,将会一直和他建立连接,直到再次执行accept()方法阻塞,才会与下一个客户端建立连接
  5. 所以如果想要服务端处理多个客户端请求,就需要引入线程,每一个客户端请求,都由一个线程处理
关于需要使用多线程处理多个请求,可以到我另一篇文章(手写Tomcat服务器)看看,用多线程处理浏览器请求:https://blog.csdn.net/grd_java/article/details/122387297
  1. 伪异步IO,采用线程池,避免为每个请求都创建一个独立线程造成线程资源耗尽的问题,但由于底层依然使用同步阻塞BIO模型,无法根本上解决问题
  2. 如果单个消息处理缓慢,或者服务器线程池中的全部线程都被阻塞,后续socket的IO消息都将在队列中排队,新的Socket请求将被拒绝,客户端会发生大量连接超时。

2. 端口转发思想:群聊功能实现

源码:src/main/java/bio/instand_messaging包下,大家自己看下就行了,很简单,了解基本思路即可
实现一个客户端消息可以发送给所有客户端接收(群聊系统):

在这里插入图片描述

功能清单
  1. 客户端登录功能:启动客户端登录,需要输入用户名和服务端ip地址
  2. 在线人数实时更新:客户端登录后,同步更新所有客户端联系人信息栏
  3. 离线人数更新:检测客户端下线后,同步更新所有客户端联系人信息栏
  4. 群聊:任意一个客户端消息,推送给当前所有客户端接收
  5. 私聊:选择某个客户端,点击私聊,发出信息可以被该客户端单独接收
  6. @消息:发出消息@某客户端,其它人都能知道
  7. 消息用户和消息时间点:服务端可以实时记录用户的消息时间点,进行消息的多路转发或选择
功能演示
  1. 开启服务端
    在这里插入图片描述
  2. idea开启客户端并发执行
    在这里插入图片描述
    在这里插入图片描述
  3. 开启3个客户端,分别为kk,小刘,小红(ip地址全写127.0.0.1)
    在这里插入图片描述
  4. 随便使用一个客户端发送bbbb,所有人都能看见
    在这里插入图片描述
  5. 选择一个人,发送@消息,其它人都可以看见
    在这里插入图片描述
  6. 选中一个人,私聊
    在这里插入图片描述
实现思路
  1. 服务端,用容器保存已登录socket,根据容器,将消息推送给指定socket客户端

二、Java NIO

New IO(新IO)或Non Blocking IO(非阻塞IO),JDK1.4引入,支持面向缓冲区、基于通道的IO操作,NIO以更高效的方式进行文件读写操作
  1. BIO,阻塞住,等着客户端消息,一个线程与客户端连接,会一直等着它完成,无论它是不是有IO操作,没有任何操作,也会一直占着线程
  2. NIO,通过选择器,不断轮询调度,谁有IO请求,就找个线程处理谁,如果客户端没有操作,不会浪费资源再它身上
非阻塞
  1. IO事件本身不阻塞,但获取IO事件的select()方法需要阻塞
  2. BIO会阻塞在IO操作上,NIO阻塞在事件获取上,没有事件就没有IO,从高层的角度看IO就不阻塞了
  3. NIO本质是延迟IO操作到真正发生IO的时候,而不是BIO那种,只要IO流打开就一直等待IO操作
    在这里插入图片描述
NIO三大核心部分,NIO有很多类和组件,但3大核心构成了核心API,其它组件Pipe和FileLock,只是与三大核心组件共同使用的工具类
  1. Channels通道
  1. Channel和IO中Stream(流)差不多一个等级,不过Stream是单向的,例如InputStream或OutputStream。
  2. Channel是双向的,既可以进行读操作,又可以进行写操作
  3. NIO中channel主要实现有FileChannel(文件IO)、DatagramChannel(UDP)、SocketChannel(TCP,Client)和ServerSocketChannel(TCP,Server)
  1. Buffers缓冲区
  1. 关键Buffer实现有:ByteBuffer(byte)、CharBuffer(char)、DoubleBuffer(double)、FloatBuffer(float)、IntBuffer(int)、LongBuffer(long)、ShortBuffer(short)
  1. Selectors选择器
  1. 单线程处理多个Channel,当你的应用打开多个通道,但每个连接流量都很低,使用Selector就很方便
  2. 例如聊天服务器中,使用Selector,得向Selector注册Channel,然后调用它的select()方法,这个方法会一直阻塞,直到某个注册的通道有事件就绪。
  3. 一旦这个方法返回,线程就可以处理这些事件

1. File通道

1. 概述

Channel通道
  1. 可以通过Channel读取和写入数据,就像水管一样,网络数据通过Channel读取和写入
  2. 通道(Channel)与流(Stream)不同,通道是双向的,流只能一个方向(InputStream或OutputStream子类),通道可以读、写或同时读写,通道是全双工的,比流更好地映射底层操作系统API
  3. Channel封装了对数据源的操作,通过channel可以操作数据源,但不必关心数据源的具体物理结构。这个数据源可能是多种的(文件,网络socket)
  4. 大多数应用中,channel与文件描述符或socket是一一对应的。channel用于在字节缓冲区和位于通道另一侧的实体(通常是一个文件或套接字)之间有效传输数据。也就是说,它是缓冲区和实体的通道,缓冲区-------channel--------实体
Channel接口源码分析

在这里插入图片描述

  1. 两个方法,isOpen()返回通道是否打开,true表示打开,close()关闭通道
channel的特性
  1. 与缓冲区不同,channel的API主要由接口指定。不同操作系统上通道实现(Channel Implementation)会有根本性的差异,所以通道API仅仅描述了可以做什么。因此,通道实现经常使用操作系统的本地代码。channel接口允许以一种受控且可移植的方式来访问底层I/O服务。
  2. channel是一个对象,可以通过它读取和写入数据,就像流,所有数据都通过Buffer对象处理,永远不会将字节直接写到通道中,相反,数据将写入包含一个或多个字节的缓冲区中读取字节时,也是将数据从通道读入缓冲区,再从缓冲区获取字节
  3. channel可以异步地读写
channel的4个实现类
  1. FileChannel(文件IO):从文件中读写数据
  2. DatagramChannel(UDP):能通过UDP读写网络中的数据
  3. SocketChannel(TCP Server):能通过TCP读写网络中的数据
  4. ServerSocketChannel(TCP Client):可以监听新进来的TCP连接,像Web服务器那样。对每一个新进来的连接都会创建一个SocketChannel。

2. FileChannel(文件IO)

提供针对文件的常用读写操作和scatter/gather操作(设置/获取),同时提供很多专用于文件的新方法
描述(🍅:不常用/🏆:常用)方法
🏆从Channel中读取数据到ByteBufferint read(ByteBuffer dst)
🏆将Channel中的数据"分散"到ByteBuffer[]long read(ByteBuffer[] dsts)
🏆将 ByteBuffer 中的数据写入到Channelint write(ByteBuffer src)
🏆将ByteBuffer[] 中的数据"聚集"到Channellong write(ByteBuffer[] srcs)
🍅返回此通道的文件位置long position()
🍅设置此通道的文件位置FileChannel position(long p)
🍅返回此通道的文件的当前大小long size()
🍅将此通道的文件截取为给定大小FileChannel truncate(long s)
🍅强制将所有对此通道的文件更新写入到存储设备中void force(boolean metaData)
缓冲区后面将,但是现在必须用,通常操作如下
  1. 将数据写入缓冲区
  2. 调用buffer.flip()反转读写模式,上面都是将数据读入到缓冲区,这里转换,就可以直接写,而不用再建一个输出流
  3. 从缓冲区读取数据
  4. 调用buffer.clear()或buffer.compact()清除缓冲区内容
1. 入门案例
使用FileChannel步骤1:打开FileChannel
  1. FileChannel无法直接打开,需要通过一个InputStream、OutputStream或RandomAccessFile来获取一个FileChannel实例
使用FileChannel步骤2:读取数据,写同理
  1. channel只是通道,读数据需要缓冲区Buffer,读到Buffer中
  2. FileChannel.read()方法,将数据从channel读取到Buffer中
  3. read()方法返回int值,表示读取多少字节到Buffer中,返回-1表示读完了
ByteBuffer buf = ByteBuffer.allocate(1024);//字节缓冲区,大小为1024byte
int byteCount = channel.read(buf);//读取数据到缓冲区
案例,向E:/1.txt文件进行读写操作:src/main/java/nio/channel/FileChannelTest.java

在这里插入图片描述

import java.io.IOException;
import java.io.RandomAccessFile;
import java.nio.ByteBuffer;
import java.nio.channels.FileChannel;
import java.nio.charset.StandardCharsets;

public class FileChannelTest {
    public static void main(String[] args) {
        FileChannelTest fileChannelTest = new FileChannelTest();
        try {
            fileChannelTest.test();
            fileChannelTest.test2();
        } catch (IOException e) {
            e.printStackTrace();
        }
    }

    /**
     *  FileChannel将buffer中内容写到文件中
     */
    public void test() throws IOException{
        System.out.println("===========================================数据写入开始================================");
        //打开fileChannel
        RandomAccessFile aFile = new RandomAccessFile("E:/1.txt", "rw");//读写模式打开文件
        FileChannel channel = aFile.getChannel();//获取通道
        //创建Buffer
        ByteBuffer buf = ByteBuffer.allocate(1024);//用字节缓存区

        //要写入的内容
        String str = new String("贼爽,这是NIO中FileChannel要写入的内容1123444alsdkfjlaksdf");
        //清空一下,保险点,万一里面有东西呢
        buf.clear();
        //写入
        buf.put(str.getBytes(StandardCharsets.UTF_8));
        //读写转换
        buf.flip();
        System.out.println("buf.flip():切换读写模式,上面都是将数据读入到缓冲区,这里转换,就可以直接写,而不用再建一个输出流");
        //如果Buffer中有内容,就一直写
        while(buf.hasRemaining()){//如果有内容可读
            channel.write(buf);//读写由channel调用Buffer完成,不是channel完成
        }
        buf.clear();//清理缓冲区
        channel.close();//关通道
        aFile.close();//关流

        System.out.println("===========================================数据写入完成================================");
    }

    /**
     * FileChannel读取数据到buffer中
     */
    public void test2() throws IOException {
        System.out.println("===========================================数据读取开始================================");
        //打开FileChannel
        RandomAccessFile aFile = new RandomAccessFile("E:/1.txt", "rw");//读写模式打开文件
        FileChannel channel = aFile.getChannel();//获取通道

        //创建Buffer
        ByteBuffer buf = ByteBuffer.allocate(1024);//用字节缓存区

        //读取数据到buffer中,和基本IO基本一致
        int byteCount = 0;//读了几个字节
        while((byteCount = channel.read(buf))!=-1){
            System.out.print("读取内容:"+new String(buf.array(),0,byteCount));
            buf.flip();//切换读写模式------》上面都是将数据读入到缓冲区,这里转换,就可以直接写,而不用再建一个输出流
            System.out.println("buf.flip():切换读写模式,上面都是将数据读入到缓冲区,这里转换,就可以直接写,而不用再建一个输出流");
            while(buf.hasRemaining()){//如果有内容可读
                System.out.print(buf.get());
            }
            buf.clear();
        }
        aFile.close();
        System.out.println("========读取数据完成=======");
    }
}
2. 常用方法
1. position()方法:当需要在FileChannel的某个特定位置进行数据读写,可通过position()方法获取FileChannel的当前位置,也可以调用position(long pos)方法设置FileChannel的当前位置
  1. 如果将位置设置在文件结束符后面,然后试图从文件通道中读取数据,读方法将返回-1
  2. 如果是写数据,文件将撑大到当前位置并写入,可能导致文件空洞,导致磁盘上物理文件中写入的数据间有空隙
long pos = channel.position();//获取通道当前位置
channel.position(pos+123);//设置通道位置
2. size()方法:返回该实例所关联文件大小
long fileSize = channel.size();
3. truncate()方法:截取一个文件,截取时,文件中指定长度的后面的部分将被删除
channel.truncate(1024);//截取文件的前1024个字节
4. force()方法:通道里尚未写入磁盘的数据强制写到磁盘上
  1. 为了性能,操作系统会将数据缓存到内存,所以无法保证写入到FileChannel中的数据一定会即时写到磁盘上,只需要调用force()方法
  2. force()方法有一个boolean类型参数,指明是否同时将文件元数据(权限信息等)写到磁盘上。
5. transferTo()和transferFrom()方法:如果两个通道中,有一个是FileChannel,那就可以直接将数据从一个channel传输到另一个channel
3. transferFrom()方法和transferTo()方法
transferFrom():表示从…转移,将其它通道的东西,搞自己这里来
  1. 将数据从源通道传输到FileChannel中(JDK中解释:将字节从给定可读取字节通道传输到此通道的文件中)
transferTo():表示转移到…,将自己的东西,搞别人那

1.将数据从FileChannel传输到其它channel中

FileChannel完成文件间复制的例子

在这里插入图片描述

//使用transferFrom
public void test3()throws IOException{
    //fromChannel,被拷贝
    RandomAccessFile aFile = new RandomAccessFile("E:/1.txt", "rw");//读写模式打开文件
    FileChannel fromChannel = aFile.getChannel();//获取通道

    //toChannel,要拷贝东西过来
    RandomAccessFile bFile = new RandomAccessFile("E:/2.txt", "rw");//读写模式打开文件
    FileChannel toChannel = bFile.getChannel();//获取通道

    long position = 0;//起始位置
    long count = fromChannel.size();//实例所关联文件大小
    //a.transferFrom(b,...,...);将b的搞到a中,transferFrom就是将东西搞到自己这里
    toChannel.transferFrom(fromChannel,position,count);
    aFile.close();
    bFile.close();
    System.out.println("E:/1.txt文件内容到E:/2.txt拷贝完成!!!!");
}
//使用transferTo
public void test4()throws IOException{
    //fromChannel,被拷贝
    RandomAccessFile aFile = new RandomAccessFile("E:/1.txt", "rw");//读写模式打开文件
    FileChannel fromChannel = aFile.getChannel();//获取通道

    //toChannel,要拷贝东西过来
    RandomAccessFile bFile = new RandomAccessFile("E:/3.txt", "rw");//读写模式打开文件
    FileChannel toChannel = bFile.getChannel();//获取通道

    long position = 0;//起始位置
    long count = fromChannel.size();//实例所关联文件大小
    //transferTo
    fromChannel.transferTo(position,count,toChannel);
    aFile.close();
    bFile.close();
    System.out.println("E:/1.txt文件内容到E:/3.txt拷贝完成!!!!");
}

2. Socket通道

1. 概述

Socket通道类不同于传统BIO的Socket
  1. 可以运行非阻塞模式并且是可选择的,可以激活大程序(网络服务器和中间件组件)巨大的可升缩性和灵活性
  2. 再也没有为每个socket连接使用一个线程的必要了,避免管理大量线程所需的上下文交换开销。
  3. 借助新的NIO类,一个或多个线程可以管理成百上千的活动socket连接。并且只有很少甚至没有性能损失。
  4. 所有的socket通道类(DatagramChannel、SocketChannel和ServerSocketChannel)都继承了java.nio.channels.spi包中的AbstractSelectableChannel。也就是说我们可以用一个Selector对象执行socket通道的就绪选择
  5. DatagramChannel和SocketChannel实现定义读和写功能的接口而ServerSocketChannel不实现。ServerSocketChannel负责监听传入的连接和创建新的SocketChannel对象,本身不传输数据。
socket和socket通道之间的关系
  1. 通道:连接I/O服务,并提供与该服务交换的方法,对某个socket而言,它不会再次实现与之对应的socket通道类中的socket协议API,但是java.net中已存在的socket通道都可以被大多数协议操作重复使用
  2. socket通道类,实例化时都会创建一个对等socket对象。都是java.net中的类(DatagramChannel= = =>>>DatagramSocketSocketChannel = = =>>>SocketServerSocketChannel= = = >>>ServerSocket),它们已经被更新以识别通道,对等socket可以通过调用socket()方法从一个通道上获取。此外,这三个java.net类现有都有getChannel()方法。
socket置于非阻塞模式
  1. 依靠所有socket通道类的公有超级类SelectableChannel。就绪选择(readiness selection)是一种可以用来查询通道的机制,该查询可以判断通道是否准备好执行一个目标操作,如读或写。
  2. 非阻塞I/O和可选择性是紧密相连的,那也正是管理阻塞模式的API代码要在SelectableChannel超级类中定义的原因。
  3. 设置或重新设置一个通道的阻塞模式是很简单的,只要调用configureBlocking()方法即可,传递参数true则设为阻塞模式,参数值为false值设为非阻塞模式。可以通过调用isBlocking()方法来判断某个socket通道当前处于哪种模式。
  4. AbstractSelectableChannel继承于SelectableChannel,其中configureBlocking()方法源码如下:如果你传true,表示阻塞模式,如果和当前模式一样,不做改变返回this,否则将当前模式改为true(阻塞)
    在这里插入图片描述

2. ServerSocketChannel(TCP,Server)

ServerSocketChannel,对等socket对象是java.net.ServerSocket
  1. 基于通道的socket监听器(本身不传输数据,就是个监听器),和java.net.ServerSocket执行相同的任务,不过它增加了通道语义,因此能够在非阻塞模式下运行。
  2. ServerSocketChannel没有bind()方法必须取出对等的socket并使用它来绑定到一个端口开始监听连接,我们使用ServerSocket的API来根据需要设置其他的socket选项
  3. 和java.net.ServerSocket一样,ServerSocketChannel也有accept()方法,一旦创建一个ServerSocketChannel并用对等socket绑定,就可以在其中一个上调用accept
  4. 如果选择在ServerSocket上调用accept()方法,那么它会同任何其他的ServerSocket表现一样的行为:总是阻塞并返回一个java.net.Socket对象
  5. 如果选择在ServerSocketChannel上调用accept()方法则会返回SocketChannel类型对象,这个对象可以在非阻塞模式下运行。
  6. 其它Socket的accept()方法会阻塞返回一个Socket对象,如果ServerSocketChannel以非阻塞模式被调用,当没有传入连接在等待时,ServerSocketChannel.accept()会立即返回null。检查连接但不阻塞的机制,充分体现可伸缩性,可选择性,降低复杂性
  7. 我们还可以使用一个选择器注册ServerSocketChannel对象以实现新链接到达时自动通知功能。
打开/关闭ServerSocketChannel
//ServerSocketChannel.open()打开通道
ServerSocketChannel ssc = ServerSocketChannel.open();
//关闭
ssc.close()
阻塞模式和非阻塞模式的切换
//设置为非阻塞模式,configureBlocking(boolean b)方法,b是true表示阻塞,false表示非阻塞
ssc.configureBlocking(false);
监听新连接
  1. ServerSocketChannel上调用accept()方法监听新连接,当accept()方法返回时,会返回一个包含新连接的SocketChannel,
  2. 如果当前是阻塞模式,accept()方法会一直阻塞到有新连接到达,非阻塞模式,会直接自动向下执行
  3. 通常不仅仅只监听一个连接,一般while循环中调用accept()方法
SocketChannel sc = ssc.accept();//ServerSocketChannel上调用accept()方法则会返回
使用案例(非阻塞,阻塞的就是BIO了):src/main/java/nio/channel/ServerSocketChannelTest.java

在这里插入图片描述

import java.net.InetSocketAddress;
import java.nio.ByteBuffer;
import java.nio.channels.ServerSocketChannel;
import java.nio.channels.SocketChannel;
import java.nio.charset.StandardCharsets;

/**
 * ServerSocketChannel测试
 */
public class ServerSocketChannelTest {
    public static void main(String[] args) {
        ServerSocketChannelTest serverSocketChannelTest = new ServerSocketChannelTest();
        try {
            serverSocketChannelTest.test();
        } catch (Exception exception) {
            exception.printStackTrace();
        }
    }

    /**
     *  测试ServerSocketChannel
     */
    public void test() throws Exception {
        //定义端口号
        int port = 8080;
        //buffer,玩通道,必须要缓冲区
        ByteBuffer buffer = ByteBuffer.wrap("准备写的内容alkdsjfl234324k123sajdf".getBytes(StandardCharsets.UTF_8));
        //ServerSocketChannel.open()打开通道
        ServerSocketChannel ssc = ServerSocketChannel.open();
        //绑定ip和端口号,通道需要绑定对等socket对象,但通道本身没有绑定方法,需要借助对等socket对象的绑定方法,InetSocketAddress是一个提供本地ip+端口的工具类
        ssc.socket().bind(new InetSocketAddress(port));
        //设置为非阻塞模式,configureBlocking(boolean b)方法,b是true表示阻塞,false表示非阻塞
        ssc.configureBlocking(false);
        //开始监听连接
        while(true){
            System.out.println("Waiting for connections");
            SocketChannel sc = ssc.accept();//ServerSocketChannel上调用accept()方法则会返回SocketChannel类型对象,这个对象可以在非阻塞模式下运行
            if(sc == null){
                System.out.println("null,没有连接传入");
                Thread.sleep(2000);
            }else{//有链接
                System.out.println("Incoming connection from:"+sc.socket().getRemoteSocketAddress());
                buffer.rewind();//指针置为0
                sc.write(buffer);//通过通道将缓冲区内容写入
                sc.close();
            }
        }
    }
}

3. SocketChannel(TCP,Client)

SocketChannel,对等socket对象是java.net.Socket
  1. 连接到TCP网络套接字的通道,一种面向流连接sockets套接字的可选择通道,SocketChannel是用来连接Socket套接字的
  2. 主要用来处理网络I/O的通道
  3. 基于TCP连接传输
  4. 实现可选通道,可以被多路复用
特征
  1. 已存在的socket不能创建SocketChannel
  2. 提供open接口创建的Channel没有网络级联,需要用connect接口连接到指定地址
  3. 未进行连接的SocketChannel执行I/O操作时,会抛NotYetConnectedException
  4. SocketChannel支持两种I/O模式:阻塞式和非阻塞式
  5. SocketChannel支持异步关闭,当SocketChannel在一个线程上read阻塞,另一个线程对该SocketChannel调用shutdownInput,则读阻塞的线程将直接返回-1,表示没有读取任何数据,如果SocketChannel在一个线程上write阻塞,另一个线程对SocketChannle调用shutdownWrite,则写阻塞线程叫抛出AsynchronousCloseException
  6. SocketChannel支持设定参数
  1. SO_SNDBUF 套接字发送缓冲区大小
  2. SO_RCVBUF 套接字接收缓冲区大小
  3. SO_KEEPALIVE 保活连接
  4. O_REUSEADDR 复用地址
  5. SO_LINGER 有数据传输时延缓关闭Channel(非阻塞模式下可用)
  6. TCP_NODELAY 禁用Nagle算法
创建SocketChannel两种方式(使用场景不同),一种直接建立TCP连接,另一种,不直接建立TCP连接,需要的时候调用connect进行连接
//方式一:通过SocketChannel.open创建SocketChannle,InetSocketAddress是工具类,指定ip和端口
SocketChannel sc = SocketChannel.open(new InetSocketAddress("www.baidu.com", 80));//直接TCP连接
//方式2:通过SocketChannel.open拿到SocketChannel,然后通过connect指定连接
SocketChannel sc2 = SocketChannel.open();//创建SocketChannel,但没直接TCP连接
sc2.connect(new InetSocketAddress("www.baidu.com", 80));//需要的时候,再进行TCP连接
连接校验
sc.isOpen();//测试SocketChannel是否为open状态
sc.isConnected();//测试SocketChannel是否被连接
sc.isConnectionPending();//测试SocketChannel是否正在进行连接
sc.finishConnect();//校验正在进行套接字连接的SocketChannel是否已经完成连接
切换阻塞/非阻塞模式
//3. 非阻塞模式
sc.configureBlocking(false);
设置和获取参数
sc
         .setOption(StandardSocketOptions.SO_KEEPALIVE,Boolean.TRUE)//设置保活连接参数
         .setOption(StandardSocketOptions.TCP_NODELAY,Boolean.TRUE);//套接字接收缓冲区大小参数设置

sc.getOption(StandardSocketOptions.SO_KEEPALIVE);
sc.getOption(StandardSocketOptions.SO_RCVBUF);

4. DatagramChannel(UDP)

DatagramChannel,对等socket对象是DatagramChannel
  1. SocketChannel模拟连接导向的流协议(例如TCP/IP),而DatagramChannel模拟包导向的无连接协议(例如UDP/IP)
  2. 无连接的,每个数据报(datagram)都是一个自包含的实体,拥有它自己的目的地址及不依赖其他数据报的数据负载。与面向流的socket不同,DatagramChannel可以发送单独的数据报给不同的目的地址。
  3. 也可以接收来自任意地址的数据包,每个到达的数据报都包含源地址(从哪来的)
打开DatagramChannel,下面例子,打开10086端口接收UDP数据包
DatagramChannel dc = DatagramChannel.open();
dc.socket().bind(new InetSocketAddress(10086));
接收数据:receive()接收UDP包,SocketAddress可以获得发包的ip、端口等信息,用toString查看(格式:/127.0.0.1:57126)
ByteBuffer buffer = ByteBuffer.allocate(64);//缓冲区
buffer.clear();//清空
SocketAddress receiveAddr = dc.receive(buffer);//接收UDP包
System.out.println(receiveAddr.toString());//打印信息
send()发送UDP包
DatagramChannel dc = DatagramChannel.open();//开启通道
ByteBuffer sendBuffer = ByteBuffer.wrap("client send".getBytes(StandardCharsets.UTF_8));//缓冲区
dc.send(sendBuffer,new InetSocketAddress("127.0.0.1",10086));//发送UDP包到指定端口
UDP不存在真正意义上的连接,这里的连接是向特定服务地址用read和write接收发送数据包
//read()和write()只有在connect()后才能使用,否则抛NotYetConnectionException异常,用read()接收时,如果没有接收到包,会抛
//PortUnreachableException
dc.connect(new InetSocketAddress("127.0.0.1",10086));
int readSize = dc.read(sendBuffer);
dc.write(sendBuffer);
案例,模拟服务端和客户端:src/main/java/nio/channel/DatagramChannelTest.java
  1. 让发送端,发
    在这里插入图片描述
  2. 接收端开启后,会一直接收
    在这里插入图片描述
import org.junit.Test;
import java.net.InetSocketAddress;
import java.net.SocketAddress;
import java.nio.ByteBuffer;
import java.nio.channels.DatagramChannel;
import java.nio.charset.Charset;
import java.nio.charset.StandardCharsets;
public class DatagramChannelTest {
    //发送报文
    @Test
    public void sendDatagram() throws Exception{
        //打开 DatagramChannel
        DatagramChannel sendChannel = DatagramChannel.open();
        //发送
        while(true){
            ByteBuffer buffer = ByteBuffer.wrap("我是要发送的UPD报文".getBytes(StandardCharsets.UTF_8));
            sendChannel.send(buffer,new InetSocketAddress("127.0.0.2",9999));
            System.out.println("发送完成");
            Thread.sleep(1000);
        }
    }
    //接收报文
    @Test
    public void receiveDatagram() throws Exception{
        //打开 DatagramChannel
        DatagramChannel receiveChannel = DatagramChannel.open();
        InetSocketAddress receiveAddress = new InetSocketAddress(9999);
        //绑定,和ServerSocketChannel不同,这个可以直接绑定
        receiveChannel.bind(receiveAddress);
        //buffer
        ByteBuffer receiveBuffer = ByteBuffer.allocate(1024);
        //接收
        while(true){
            receiveBuffer.clear();//先清除,免得缓冲区有莫名其妙的东西
            //receive()接收UDP包,SocketAddress可以获得发包的ip、端口等信息,用toString查看
            SocketAddress socketAddress = receiveChannel.receive(receiveBuffer);
            System.out.println(socketAddress.toString());

            receiveBuffer.flip();//读写转换
            //Charset.forName("UTF-8").decode(str),对str进行UTF-8的编码
            System.out.println(Charset.forName("UTF-8").decode(receiveBuffer));
        }
    }
}

3. 通道的Scantter(分散)/Gather(聚集)

Scatter(分散)/Gather(聚集)
  1. 用于描述从Channel中读取或写入到channel的操作
  2. 分散(Scatter):从channel中读取是指在读操作时将读取的数据写入多个buffer中。因此,channel将读取的数据“分散(scatter)”到多个Buffer中
  3. 聚集(gather):写入channel指写操作时将多个buffer的数据写入同一个channel,因此,channel将多个buffer中数据“聚集(gather)”后发送到channel
  4. scatter/gather经常用于需要将传输的数据分开处理的场合,例如传输一个由消息头和消息体组成的消息,你可能会将消息体和消息头分散到不同的buffer中,这样你可以方便的处理消息头和消息体
分散实例:Scattering Reads是指从一个channel读取到多个buffer中

在这里插入图片描述

ByteBuffer header = ByteBuffer.allocate(128);
ByteBuffer body = ByteBuffer.allocate(1024);

ByteBuffer[] bufferArray = {header,body};
channel.read(bufferArray);
/**
 * 注意两个buffer:header和body。首先都被插入到数组bufferArray,然后再将数组作为channel.read()的输入参数
 * read()方法按照buffer在数组中的顺序将从channel中读取的数据写入到buffer,当一个buffer被写满后,channel接着向另一个buffer中写
 * Scattering Reads在移动到下一个buffer前,必须先将当前buffer填满,意味着它不适合动态消息(消息大小不固定的)
 * 当然你可以设定一个保险值,比如消息头和消息体,你想分开处理,那么消息头最大128byte,header缓冲区正好128byte
 * 如果当前请求头不到128byte,就强行给他填充满,这样Scattering Reads就可以正常工作
 */
聚集实例:Gathering Writes是指数据从多个buffer写入到同一个channel

在这里插入图片描述

ByteBuffer header = ByteBuffer.allocate(128);
ByteBuffer body = ByteBuffer.allocate(1024);

ByteBuffer[] bufferArray = {header,body};
channel.write(bufferArray);
/**
 * 注意两个buffer:header和body。首先都被插入到数组bufferArray,然后再将数组作为channel.write()的输入参数
 * write()方法按照buffer在数组中的顺序将数据写入channel
 * 如果一个buffer的容量为128byte,但只包含58byte的数据,那么只会将这58byte的数据写到channel中
 * 与Scattering Reads相反,Gathering Writes能较好的处理动态消息
 */

4. Buffer

简介
  1. Buffer用于和NIO通道进行交互。数据是从通道读入缓冲区,从缓冲区写入通道中的
    在这里插入图片描述
  2. 缓冲区本质上是一块可以写入数据,然后可以从中读取数据的内存。这块内存被包装成NIO Buffer对象,提供一组方法方便访问内存块,实际就是个容器(数组)
  3. NIO库中,所有数据都是用缓冲区处理的
  4. 读取数据时,直接读到缓冲区,写入数据时,也是写入缓冲区,任何时候访问NIO中数据,都将它放到缓冲区中,而面向流I/O系统中,所有数据都是直接写入或者直接将数据读取到Stream对象中
  5. NIO中,所有缓冲区类型都继承抽象类Buffer,常用的就是ByteBuffer,对应byte数据类型,基本每一个基本数据类型都要一个具体Buffer类型与之对应(下面是不完全类图)
    在这里插入图片描述
Buffer读写数据,一般遵循4个步骤
  1. 写入数据到buffer
  2. 调用flip()方法切换读写转态
  3. 从buffer中读取数据
  4. 调用clear()方法(彻底清空)或compact()方法(只清除已读数据,未读的移到开头,新数据可以继续插入),清缓存区

1. Buffer的三个属性和类型

Buffer的三个属性
  1. Capacity:Buffer的固定大小值,Buffer是一个内存块,只能往里面写capacity个byte、long、char等类型数据。一旦Buffer满了,需要将其清空(读数据或调用clear()或compact())才能继续写数据
  2. Positon
  1. 写数据到Buffer中时:position表示写入数据的当前位置,初始为0,当一个byte、long…等数据写到Buffer后,position会后移到下一个可插入数据的Buffer单元,position最大可到capacity-1(因为初始为0)
  2. 读数据到Buffer时position表示读入数据的当前位置,如position = 2时表示已经开始读入3个byte,或从第三个byte开始读取。通过ByteBuffer.flip()切换到读模式时position会被重置为0,当Buffer从position读入数据后,position会下移到下一个可读入的数据Buffer单元
  1. limit
  1. 写数据时limit表示可对Buffer最多写入多少个数据。写模式下,limit等于Buffer的capacity
  2. 读数据时limit表示Buffer最多有多少可读数据(not null的数据)
  1. position 和 limit 的含义取决于Buffer处在读模式还是写模式,但是capacity无论读还是写模式,含义都是一样
    在这里插入图片描述
  1. 上图中,写模式,limit表示最多写多少,position,表示可写入的位置
  2. 当切换为读模式后,写模式position的值就是limit的值,表示这个地方就是可读区域的界限,而position会重新变为0,表示当前读数据的位置
Buffer的类型:这些Buffer类型代表不同的数据类型,就是可以通过char,short,int,long,float或double类型来操作缓冲区的字节
  1. ByteBuffer
  2. MappedByteBuffer:内存映射字节缓存,比普通的更快
  3. CharBuffer
  4. DoubleBuffer
  5. FloatBuffer
  6. IntBuffer
  7. LongBuffer
  8. ShortBuffer

2. Buffer分配和读写数据以及常用方法

Buffer分配
  1. Buffer对象的获取,需要先分配,每一个Buffer类都有一个allocate方法
  2. 假如分配48字节capacity的ByteBuffer
ByteBuffer buf = ByteBuffer.allocate(48);
  1. 下面是分配一个可存储1024字符的CharBuffer
CharBuffer cuf = CharBuffer.allocate(1024);
向Buffer中写数据,有两种方式
  1. 从Channel写到Buffer
//inChannel是一个通道,buf是ByteBuffer
int bytesRead = inChannel.read(buf);//从Channle中读数据到buf
  1. 通过Buffer的put()方法写到Buffer里,put方法有很多重载,例如写一个数据到指定位置,或直接把一个字节数组写到Buffer
//将127写到buf缓冲区
buf.put(127);
Buffer切换读写模式,调用flip()方法即可
  1. flip方法将Buffer从当前模式切换到另一种模式(读->写,写->读)
  2. 调用flip()方法会对position和limit进行相应操作变换,例如写切换到读后,position置为0,limit置为之前的position表示可读的界限位置
buf.flip();//读写切换
从Buffer中读取数据
  1. 从Buffer读取数据到Channel
int bytesWritten = inChannel.write(buf);
  1. 使用get()方法从Buffer中读取数据;get方法和put一样,有很多重载,可以指定positon读取,或从Buffer中读取数据到字节数组
byte aByte = buf.get();
Buffer常用方法
描述(🍅:不常用/🏆:常用)方法
🏆重置读写指针,将position设回0,可以重读或重写Buffer中数据。limit不变,依然表示读写的界限rewind()
🏆重置缓冲区,position置为0,limit设置为capacity,表面上Buffer被清空,实际上数据并没有清除clear()
🏆清除已读数据,将未读数据拷贝到Buffer起始处,position置为当前未读数据后面(可以插入数据的位置),limit置为capacity,适用于,数据没读完,但是想先写点compact()
🏆标记一个特定position位置,之后可通过Buffer.reset()方法恢复到这个positionmark()
🏆恢复到一个mark()方法标记的特定position位置reset()

3. 缓冲区操作

1. 缓冲区分片
NIO中,slice()方法创建一个子缓冲区,除了分配或包装一个缓冲区对象外,还能根据现有的缓冲区对象来创建一个子缓冲区,即在现有缓冲区上切出一片来作为一个新的缓冲区,但现有缓冲区与创建的子缓冲区在底层数组层面上是数据共享的,也就是说,子缓冲区相当于现有缓冲区的一个视图窗口
  1. 就是将一个缓冲区,分成多个部分,我们可以对每个单独的部分,进行操作
  2. 将一个缓冲区颗粒化,提高效率
下面是分区的例子:src/main/java/nio/buffer/BufferForExample.java

在这里插入图片描述

import org.junit.Test;

import java.nio.ByteBuffer;

/**
 * Buffer基本使用例子
 */
public class BufferForExample {
    /**
     * 缓冲区分片
     */
    @Test
    public void test1() throws Exception{
        //10个byte大小的byte缓冲区
        ByteBuffer buffer = ByteBuffer.allocate(10);
        //给这缓冲区中添加数据
        for(int i = 0;i<buffer.capacity();i++){
            buffer.put((byte)i);
        }
        System.out.println("===========================输出整个缓冲区数据=============================");
        //重置指针
        buffer.position(0);
        buffer.limit(buffer.capacity());
        while(buffer.remaining()>0){
            System.out.print(buffer.get()+" ");
        }
        System.out.println();
        //创建子缓冲区
        buffer.position(3);//起始下标3
        buffer.limit(7);//界限为7
        ByteBuffer slice = buffer.slice();//子缓冲区3~7(不含7)

        //改变子缓冲区内容,子缓冲区每个数据都*10
        System.out.println("===========================改变子缓冲区内容,子缓冲区每个数据都*10:(3~7(不含7))=============================");
        for(int i = 0;i < slice.capacity();i++){
            byte b = slice.get(i);
            b *= 10;
            slice.put(i,b);
        }

        //重置指针
        buffer.position(0);
        buffer.limit(buffer.capacity());

        //输出整个缓冲区数据,remaining()方法返回limit - position的值,也就是剩余可访问元素个数
        System.out.println("===========================输出整个缓冲区数据=============================");
        while(buffer.remaining()>0){
            System.out.print(buffer.get()+" ");
        }
    }
}
2. 只读缓冲区
将缓冲区搞成只读的,只能读取,不能写入。可以通过调用缓冲区asReadOnlyBuffer()方法,将任何常规缓冲区转换为只读缓冲区,这个方法返回一个与原缓冲区完全相同的缓冲区,并与原缓冲区共享数据,只不过它是只读的。如果原缓冲区的内容发生了变化,只读缓冲区的内容也随之发送变化
例子,只能读这个就不测试了,没有读的相关方法,测一测原缓冲区改变,只读缓冲区跟着变

在这里插入图片描述

    /**
     * 只读缓冲区
     */
    @Test
    public void test2() throws Exception{
        //10个byte大小的byte缓冲区
        ByteBuffer buffer = ByteBuffer.allocate(10);
        //给这缓冲区中添加数据
        for(int i = 0;i<buffer.capacity();i++){
            buffer.put((byte)i);
        }

        System.out.println("===========================输出整个缓冲区数据=============================");
        System.out.println(Arrays.toString(buffer.array()));

        //创建只读缓冲区
        ByteBuffer readOnlyBuffer = buffer.asReadOnlyBuffer();
        //改原缓冲区
        System.out.println("===========================原缓冲区下标为3位置的值改为7777=============================");
        buffer.put(3,(byte)7777);

        System.out.println("===========================输出只读缓冲区内容=============================");
        //给这缓冲区中添加数据
        for(int i = 0;i<buffer.capacity();i++){
            System.out.print(readOnlyBuffer.get(i)+" ");
        }
    }
3. 直接缓冲区
用来加速I/O速度,使用一种特殊方式为其分配内存的缓冲区,JDK文档中的描述为:给定一个直接字节缓冲区,Java虚拟机将尽最大努力直接对它执行本机I/O操作。它会在每一次调用底层操作系统本机I/O操作之前(或之后),尝试避免将缓冲区的内容拷贝到一个中间缓冲区中或从一个中间缓冲区中拷贝数据。要分配直接缓冲区,需要调用allocateDirect()方法而不是allocate()方法使用方法与普通缓冲区无区别
//分配直接缓冲区
ByteBuffer buffer = ByteBuffer.allocateDirect(1024);
//分配普通缓冲区
ByteBuffer buffer = ByteBuffer.allocate(1024);
//使用方法相同
4. 内存映射文件I/O
读和写文件数据的方法,可以比常规的基于流或基于通道的I/O块的多。并不是将整个文件读到内存,只有文件中实际读取或写入的部分才会映射到内存中

//内存映射字节缓存MappedByteBuffer
MappedByteBuffer mbb = fc.map(FileChannel.MapMode.READ_WRITE, start, size);

/**
 * 内存映射文件I/O
 */
static private final int start = 0;
static private final int size = 1024;
@Test
public void test3() throws Exception{
    RandomAccessFile raf = new RandomAccessFile("E:/1.txt","rw");
    FileChannel fc = raf.getChannel();
    //内存映射字节缓存MappedByteBuffer
    MappedByteBuffer mbb = fc.map(FileChannel.MapMode.READ_WRITE, start, size);
    mbb.put(0,(byte)97);
    mbb.put(1023,(byte)122);
    raf.close();
}

5. Selector

注意:与Selector一起使用,Channel必须处于非阻塞模式下,否则抛出IllegalBlockingModeException,而FileChannel不能切换到非阻塞模式,所以不能配合Selector使用,而套接字socket相关的通道都可以
Selector:选择器
  1. 又名多路复用器,Java NIO核心组件之一,用于检查一个或多个NIO Channel的状态是否处于可读、可写
  2. 实现单线程管理多个channels,也就是单线程可以管理多个网络链接
    在这里插入图片描述
  3. 如上图所示,使用更少的线程管理多个通道,对比使用多个线程的方式,避免了线程上下文切换带来的开销
SelectableChannel可选择通道(一个抽象类)
  1. 不是所有Channel都可以被Selector复用,比如FileChannel
  2. 判断一个Channel能被Selector复用,前提是,channel是否继承SelectableChannel抽象类,如果继承了,就可以被复用,否则不能。
  3. SelectableChannel提供实现通道可选择性所需的公共方法,所有支持就绪检查的通道类的父类,所有socket通道都继承了SelectableChannel类,都是可选择的。包括从管道(Pipe,后面讲)对象中获得的通道,但是FileChannel,没继承SelectableChannel,所以不可选
  4. 一个通道可以被注册到多个选择器上,但是每个选择器只能被注册一次,通道和选择器之间的关系,使用注册方式完成。SelectableChannel可以被注册到Selector对象上,注册时,需要指定通道的哪些操作,Selector感兴趣(只监听感兴趣的操作)
    在这里插入图片描述
Channel如何注册到Selector?
  1. 使用Channel.register(Selector sel,int ops)方法,将一个通道注册到一个选择器,第一个参数,指定通道注册的选择器,第二个参数,指定选择器感兴趣的操作
  2. 选择器感兴趣的操作
  1. SelectionKey.OP_READ:可读
  2. SelectionKey.OP_WRITE:可写
  3. SelectionKey.OP_CONNECT:连接
  4. SelectionKey.OP_ACCEPT:接收
  1. 如果Selector对通道的多个操作感兴趣,可以用"位或"操作符实现,例如:
//同时关注读写
int key = SelectionKey.OP_READ|SelectionKey.OP_WRITE;
  1. 配置如上两个关注点后,当通道已经准备读或者写时,选择器就会监听到,然后做出反应
  2. 选择器查询的不是通道的操作,而是通道的某种就绪状态(准备好读,或准备好写等等)
  1. OP_ACCEPT:接收就绪状态
  2. OP_READ:读就绪状态
  3. OP_WRITE:写就绪状态
  1. 一个通道,并不是一定支持所有4中操作,比如ServerSocketChannel支持Accept连接操作,SocketChannle却不支持
  2. 通道的validOps()方法,可以获取通道下所有支持的操作集合
选择键(SelectionKey)
  1. Channel注册之后,一旦通道处于某种就绪状态,就可以被选择器查询到,需要使用Selector的select()方法完成,select方法的作用是对感兴趣的通道操作,进行就绪状态查询
  2. Selector会轮询注册的Channel,并监听对每个通道感兴趣的状态,一旦有感兴趣的操作就绪,就会被Selector选中放入选择键集合中
  3. 一个选择键,首先是包含了注册在Selector的通道操作的类型,比如SelectionKey.OP_READ.也包含了特定的通道与特定的选择器之间的注册关系。
  4. NIO编程,就是根据对应选择键进行不同业务逻辑处理

1. 基本使用方法

1. Selector的创建
Selector selector = Selector.open();
2. 注册Selector到Channel
    @Test
    public void test()throws Exception{
        //1、获取Selector选择器
        Selector selector = Selector.open();
        //2、获取通道
        ServerSocketChannel serverSocketChannel = ServerSocketChannel.open();
        //3、设置为非阻塞
        serverSocketChannel.configureBlocking(false);
        //4、绑定连接
        serverSocketChannel.bind(new InetSocketAddress(9999));
        //5、将通道注册到选择器上,并制定监听事件为:"接收"事件
        serverSocketChannel.register(selector, SelectionKey.OP_ACCEPT);
    }

3. 轮询查询就绪操作
  1. Selector的select()方法,可以查询出已经就绪的通道操作,这些就绪状态集合,存储在一个元素是SelectionKey对象的Set集合中,select()方法有多个重载
  1. select():阻塞到至少有一个通道在你注册的事件上就绪了
  2. select(long timeout):和select相同,但是最长阻塞时间为timeout毫秒
  3. selectNow():非阻塞,只要有通道就绪就立刻返回
  1. select方法返回值类型为int类型,表示当前有多少通道已就绪(不包括以前select方法统计的,只统计上次select执行之后,到本次select执行之间的),只要返回值不是0,我们就可以通过Selector中的selectedKeys()方法,迭代选择键集合,根据就绪操作类型,完成对应操作
//6、查询已就绪通道操作,然后遍历集合,处理这些操作
Set<SelectionKey> selectionKeys = selector.selectedKeys();
//遍历集合
Iterator<SelectionKey> iterator = selectionKeys.iterator();
while(iterator.hasNext()){
    SelectionKey key = iterator.next();
    //判断key的状态
    if(key.isAcceptable()){//就绪新套接字连接
        //@TODO 就绪新套接字连接状态操作代码
    }else if(key.isConnectable()){
        //@TODO 就绪套接字连接完成或未完成状态操作代码
    }else if(key.isReadable()){
        //@TODO 就绪读状态操作代码
    }else if(key.isWritable()){
        //@TODO 就绪写状态操作代码
    }
    iterator.remove();//移除遍历的元素
}
停止选择方法:选择器执行选择的过程中,系统底层会依次询问每个通道是否已经就绪,这个过程可能造成调用线程进入阻塞状态,我们有以下方法唤醒在select()方法中阻塞的线程
  1. wakeup()方法:通过调用Selector对象的wakeup()方法让处在阻塞状态的select()方法立刻返回,强制选择器上的第一个还没返回的选择操作立即返回,如果当前没有进行中的选择操作,那么下一次对select()方法的一次调用将立即返回
  2. close()方法:通过close()方法关闭Selector,可以让任何一个在选择操作中阻塞的线程都被唤醒(类似wakeup()),同时使得注册到该Selector的所有Channel被注销,所有键都将被取消,但是Channel本身不会被关闭

2. 模拟服务端/客户端实例

在这里插入图片描述
在这里插入图片描述

    @Test
    public void server() throws Exception{
        ServerSocketChannel ssc = ServerSocketChannel.open();
        ssc.socket().bind(new InetSocketAddress("127.0.0.1",8000));
        ssc.configureBlocking(false);

        Selector selector = Selector.open();
        // 注册 channel,并且指定感兴趣的事件是 Accept
        ssc.register(selector,SelectionKey.OP_ACCEPT);
        //缓冲区
        ByteBuffer readBuffer = ByteBuffer.allocate(1024);
        ByteBuffer writeBuffer = ByteBuffer.allocate(128);
        //先读点内容,然后让它写
        writeBuffer.put("received".getBytes());
        //改变读写模式
        writeBuffer.flip();

        //开始轮询
        while(true){
            int nReady = selector.select();//阻塞轮询
            //当监听到操作就绪,获取操作列表
            Set<SelectionKey> keys = selector.selectedKeys();
            //迭代器迭代
            Iterator<SelectionKey> it = keys.iterator();

            while (it.hasNext()){
                SelectionKey key = it.next();
                it.remove();
                if(key.isAcceptable()){
                    // 创建新的连接,并且把连接注册到 selector 上,而且,
                    // 声明这个 channel 只对读操作感兴趣。
                    SocketChannel socketChannel = ssc.accept();
                    socketChannel.configureBlocking(false);
                    socketChannel.register(selector, SelectionKey.OP_READ);
                    System.out.println("isAcceptable,监听到连接就绪操作");
                }else if (key.isReadable()) {
                    SocketChannel socketChannel = (SocketChannel) key.channel();
                    readBuffer.clear();
                    socketChannel.read(readBuffer);
                    readBuffer.flip();
                    System.out.println("received 监听到读就绪操作,读到内容如下: " + new String(readBuffer.array()));
                    key.interestOps(SelectionKey.OP_WRITE);
                }else if (key.isWritable()) {
                    readBuffer.rewind();
                    SocketChannel socketChannel = (SocketChannel) key.channel();
                    socketChannel.write(readBuffer);
                    key.interestOps(SelectionKey.OP_READ);
                    System.out.println("isWritable 监听到写就绪操作,将读到内容写回去");

                }
            }
        }

    }
    @Test
    public void ClientDemo() throws Exception{
        SocketChannel socketChannel = SocketChannel.open();
        socketChannel.connect(new InetSocketAddress("127.0.0.1", 8000));
        //缓冲区
        ByteBuffer writeBuffer = ByteBuffer.allocate(32);
        ByteBuffer readBuffer = ByteBuffer.allocate(32);
        //读个hello
        writeBuffer.put("hello".getBytes());
        //切换读写模式
        writeBuffer.flip();
        //疯狂读写
        while (true) {
            //重置读写指针,将position设回0,可以重读或重写Buffer中数据。limit不变,依然表示读写的界限
            writeBuffer.rewind();
            //将缓冲区内容写入,服务端会进行读就绪
            socketChannel.write(writeBuffer);
            //重置缓冲区,position置为0,limit设置为capacity,表面上Buffer被清空,实际上数据并没有清除
            readBuffer.clear();
            //将通道内容读到readBuffer,服务端执行写就绪
            socketChannel.read(readBuffer);
            readBuffer.flip();
            System.out.println("服务端写过来的东西:"+new String(readBuffer.array(),0,readBuffer.limit()));
            Thread.sleep(500);
        }
    }

6. Pipe和sink、source通道

Pipe
  1. NIO管道是两个线程之间的单向数据连接,Pipe有一个source通道和一个sink通道。数据会被写到sink通道,从source通道读取。
    在这里插入图片描述
创建管道:Pipe.open()打开一个管道
//打开一个管道
Pipe pipe = Pipe.open();
写入管道:需要sink通道(SinkChannel)然后调用sink通道的write()方法写入
//访问sink通道,是Pipe的内部类
Pipe.SinkChannel sinkChannel = pipe.sink();
//要写的数据
String newData = "New String to write to file..."+System.currentTimeMillis();
//缓冲区
ByteBuffer buf = ByteBuffer.allocate(48);
//先清空,避免里面有莫名其妙的数据
buf.clear();
//将内容读入缓冲区
buf.put(newData.getBytes());
//切换缓冲区读写状态
buf.flip();
//缓冲区内容写入通道
while(buf.hasRemaining()){
	sinkChannel.write(buf);
}
sinkChannel.close();
从管道读取,需要source通道
//访问Source通道,是Pipe的内部类
Pipe.SourceChannel sourceChannel = pipe.source();
//缓冲区
ByteBuffer buf = ByteBuffer.allocate(48);
//通道内容读取到缓冲区
int bytesRead = sourceChannel.read(buf);
while(bytesRead != -1){
	System.out.print(new String(buf.array(),0,bytesRead));
	sourceChannel.read(buf);
}
sourceChannel.close();

7. FileLock

文件锁
  1. OS中很常见,多个程序同时访问、修改同一个文件,很容易因为文件数据不同步而出现问题,给文件加一个锁,同一时间,只能有一个程序修改此文件,或多个程序只能读此文件但不能修改,解决同步问题。
  2. 文件锁是进程级别的,不是线程级别。可以解决多个进程间并发访问,修改同一个文件问题,但不能解决一个进程内的多个线程并发同步问题。
  3. 文件锁是当前程序所属JVM实例持有,一旦获取文件锁(对文件加锁),需要调用release()或关闭对应的FileChannel对象,或JVM退出才会释放这个锁
  4. 一旦某个进程(例如JVM实例)对某个文件加锁,释放锁之前,此进程不能再对此文件加锁,就是锁不重叠(进程级别不能重复在同一个文件上获取锁)
文件锁分类
  1. 排它锁(独占锁):对文件加排它锁后,该进程可以对此文件进行读写,该进程独占此文件,其他进程不能读写此文件,直到该进程释放文件锁。
  2. 共享锁:某个进程对文件加共享锁后,其他进程也可以访问此文件,但这些进程(包括自己)只能读,不能写,线程是安全的。只要还有一个进程持有共享锁,此文件只能读,不能写
4种获取文件锁的方法
  1. lock():对整个文件加锁,默认排它锁,阻塞式的,如果没有获取文件锁,会一直阻塞当前线程,直到获取文件锁
  2. lock(long position,long size,boolean shared):自定义加锁,前两个参数指定要加锁部分(可以只对文件部分内容加锁),第三个参数指定是否使用共享锁
  3. tryLock():lock()的升级版,尝试对整个文件加锁,默认排它锁,非阻塞式的,尝试获取文件锁,成功就返回锁对象,不成功就返回null,不会阻塞当前线程
  4. tryLock(long position,long size,boolean shared):自定义加锁
  5. 共享锁时,其它线程如果试图写此文件,会抛异常
FileLock的两个方法,某些OS(操作系统)上,对某个文件加锁后,不能对此文件使用通道映射
  1. boolean isShared() :此文件是否是共享锁
  2. boolean isValid() :此文件锁是否还有效
FileChannel channel = new FileOutputStream("E:/1.txt").getChannel();
FileLock lock = channel.lock();//对下面文件操作的代码加锁
// @TODO 对文件的操作
lock.release();//释放锁
案例:src/main/java/nio/file_lock/FileLockTest.java

在这里插入图片描述

@Test
public void test1() throws Exception{
    String input = "FileLockTest要写的内容";
    System.out.println("input=====>>>>"+input);

    //通过字节数组,搞缓冲区,allocate是指定大小
    ByteBuffer buffer = ByteBuffer.wrap(input.getBytes(StandardCharsets.UTF_8));

    //获取一个Path对象
    String filePath = "E:/1.txt";
    Path path = Paths.get(filePath);

    //打开通道,文件路径为path,写操作,追加模式
    FileChannel fileChannel = FileChannel.open(path, StandardOpenOption.WRITE, StandardOpenOption.APPEND);

    //定位到通道末尾可插入位置
    long size = fileChannel.size();//当前通道中,文件的大小
    fileChannel.position(size-1);//我们要追加内容到通道后面,正好是通道当前已有内容的后面

    //加锁
    FileLock lock = fileChannel.lock();
    System.out.println("是否是共享锁:"+lock.isShared());
    fileChannel.write(buffer);//将缓冲区内容写入
    fileChannel.close();//关闭通道
    System.out.println("写入完成!!!");
}

8. Path

Java Path接口是Java NIO更新的一部分,同Java NIO一起,包括在Java 6 和Java 7及以上版本中
  1. 但是Java Path接口是在Java 7中添加到Java NIO中的,Path接口位于java.nio.file包中
  2. Java Path实例表示文件系统中的路径,可以指向文件或目录,路径绝对路径,相对路径都可以
  3. java.nio.file.Path接口类似于java.io.File类,但还是有差别的,许多情况下,可以使用Path接口替换File类的使用
创建Path(绝对路径):Paths.get()创建路径实例,静态工厂设计模式
//注意引包是java.nio.file.下的
import java.nio.file.Path;
import java.nio.file.Paths;
//创建Path实例
Path path = Paths.get("E:/1.txt");
创建Path(相对路径):Paths.get(basePath,relativePath)创建相对路径实例
//指向目录:E:\projects
Path projects = Paths.get("E:/", "projects");
//指向文件:E:\projects\1.txt
Path path1 = Paths.get("E:/", "projects/1.txt");
路径标准化,Path.normalize()
  1. 标准化,意味着移除所有路径字符串中间的.和…代码,并解析路径字符串所有引用的路径
//Path.normalize()标准化路径
String originalPath = "E:/projects/../aa-project";
Path path2 = Paths.get(originalPath);
System.out.println(path2);//E:\projects\..\aa-project
Path normalize = path2.normalize();
System.out.println(normalize);//E:\aa-project

9. Files

Java NIO Files类(java.nio.file.Files)
  1. 提供几种操作文件系统中文件的方法,与java.nio.file.Path实例一起工作
Files.createDirectory():根据Path实例创建目录
Path path = Paths.get("d:\\sgg");//Path实例,理解为路径
try {
 Path newDir = Files.createDirectory(path);//根据路径创建目录
} catch(FileAlreadyExistsException e){//目录存在会抛出的异常
 // 目录已经存在
} catch (IOException e) {//父目录不存在等状况,会抛出IO异常
 // 其他发生的异常
 e.printStackTrace();
}
Files.copy():拷贝文件
Path sourcePath = Paths.get("E:\\test\\01.txt");//源文件路径
Path destinationPath = Paths.get("E:\\test\\002.txt");//拷贝到....
try {
 Files.copy(sourcePath, destinationPath);//复制文件过去,如果文件存在,抛出异常
 //复制文件,如果文件已存在,将覆盖现有文件,用参数 StandardCopyOption.REPLACE_EXISTING指定
 Files.copy(sourcePath, destinationPath, StandardCopyOption.REPLACE_EXISTING);
} catch(FileAlreadyExistsException e) {
 // 目录已经存在
} catch (IOException e) {
 // 其他发生的异常
 e.printStackTrace();
}
Files.move():移动文件,并且还会根据移动到的目标路径,改名
Path sourcePath = Paths.get("d:\\test\\01.txt");
Path destinationPath = Paths.get("d:\\test\\001.txt");
try {
 //移动并覆盖,会根据路径d:\\test\\001.txt,将名字改为001.txt
 Files.move(sourcePath, destinationPath,
 StandardCopyOption.REPLACE_EXISTING);
} catch (IOException e) {
 //移动文件失败
 e.printStackTrace();
}
Files.delete():删除文件或目录
Path path = Paths.get("d:\\test\\001.txt");
try {
 Files.delete(path);//删除指定路径文件,如果不存在,报异常
} catch (IOException e) {
 // 删除文件失败
 e.printStackTrace();
}
Files.walkFileTree():递归遍历目录树功能,Path实例(要遍历的目录)和FileVisitor(遍历期间被调用)为参数
  1. FileVisitor 是一个接口,必须自己实现 FileVisitor 接口,并将实现的实例传递给walkFileTree()方法。在目录遍历过程中,您的 FileVisitor 实现的每个方法都将被调用。如果不需要实现所有这些方法,那么可以扩展 SimpleFileVisitor 类,它包含FileVisitor 接口中所有方法的默认实现。
  2. FileVisitor 接口的方法中,每个都返回一个 FileVisitResult 枚举实例。FileVisitResult 枚举包含以下四个选项:
  1. CONTINUE 继续
  2. TERMINATE 终止
  3. SKIP_SIBLING 跳过同级
  4. SKIP_SUBTREE 跳过子级
Path rootPath = Paths.get("d:\\test");//根目录
String fileToFind = File.separator + "001.txt";//要查找的文件
try {
    Files.walkFileTree(rootPath, new SimpleFileVisitor<Path>() {
        @Override
        public FileVisitResult visitFile(Path file, BasicFileAttributes attrs) throws
                IOException {
            String fileString = file.toAbsolutePath().toString();
            //System.out.println("pathString = " + fileString);
            if(fileString.endsWith(fileToFind)){
                System.out.println("file found at path: " + file.toAbsolutePath());
                return FileVisitResult.TERMINATE;
            }
            return FileVisitResult.CONTINUE;
        }
    });
} catch(IOException e){
    e.printStackTrace();
}

三、JAVA AIO

Asynchronous I/O 异步IO
  1. Java 7中,Java NIO中加入AsynchronousFileChannel,可以异步地将数据写入文件
创建AsynchronousFileChannel:同样通过静态方法open()创建
    /**
     * 创建AsynchronousFileChannel
     */
    @Test
    public void test1() throws Exception{
        //创建Path路径
        Path path = Paths.get("E:/i.txt");
        //获取AIO通道,StandardOpenOption.READ,StandardOpenOption.WRITE表示对文件执行读和写操作
        AsynchronousFileChannel asynchronousFileChannel = AsynchronousFileChannel.open(path, StandardOpenOption.READ,StandardOpenOption.WRITE);

    }

1. 读取AsynchronousFileChannel数据

方法一:通过read()方法读取数据,返回Future对象,利用Future

在这里插入图片描述

    /**
     * 通过AIO异步获取1.txt文件中内容
     * @throws Exception
     */
    @Test
    public void test2() throws Exception{
        //创建路径
        Path path = Paths.get("E:/1.txt");
        //AIO通道,读操作
        AsynchronousFileChannel asynchronousFileChannel = AsynchronousFileChannel.open(path, StandardOpenOption.READ);
        //缓冲区,和位置0
        ByteBuffer buffer = ByteBuffer.allocate(1024);
        long position = 0;
        //调用AIO通道的read()方法,获取Future
        Future<Integer> operation = asynchronousFileChannel.read(buffer, position);
        //因为AIO是异步的,所以我这里要人为阻塞住它。isDone()方法,直到返回true后,才可以继续运行
        while(!operation.isDone());
        //切换读写模式
        buffer.flip();
        //因为我们创建的AIO只能读,所以我们人为遍历一下缓冲区
        byte[] data = new byte[buffer.limit()];//用来保存缓冲区内容
        buffer.get(data);
        //输出
        System.out.println(new String(data));
        //清空缓冲区
        buffer.clear();
        //关闭通道
        asynchronousFileChannel.close();
    }
方法二:通过read()方法配合CompletionHandler内部类

在这里插入图片描述

    /**
     * 通过AIO异步获取1.txt文件中内容,利用CompletionHandler
     */
    @Test
    public void test3() throws Exception{
        //路径
        Path path = Paths.get("E:/1.txt");
        //AIO通道
        AsynchronousFileChannel asynchronousFileChannel = AsynchronousFileChannel.open(path, StandardOpenOption.READ);
        //缓冲区和位置0
        ByteBuffer buffer = ByteBuffer.allocate(1024);
        long position = 0;
        //read(字节要传送到的缓冲区,开始传输的文件位置(>=0),要附加到I/O操作的对象(可以为空),用于消费结果的处理程序)
        //其中第三个参数是附加的对象,attachment,会作为completed()的参数
        asynchronousFileChannel.read(buffer, position, buffer,new CompletionHandler<Integer,ByteBuffer>() {
            /**
             * 执行操作
             * @param result 读取了多少字节
             * @param attachment read()方法的第三个参数,附加的I/O操作对象,如果我们没传,源码中会给我们一个它们自己创建的
             */
            @Override
            public void completed(Integer result, ByteBuffer attachment) {
                System.out.println("result = "+result);
                attachment.flip();
                //输出缓冲区中内容
                byte[] data = new byte[attachment.limit()];
                attachment.get(data);
                System.out.println(new String(data));

                attachment.clear();
            }

            /**
             * 执行操作失败调用
             */
            @Override
            public void failed(Throwable exc, ByteBuffer attachment) {

            }
        });
        asynchronousFileChannel.close();
    }

2. 写数据

方法一:通过 Future 写数据

在这里插入图片描述

    /**
     * AIO写数据到AIO通道,Future
     */
    @Test
    public void test4() throws Exception{
        //路径
        Path path = Paths.get("E:/1.txt");
        //AIO通道,写
        AsynchronousFileChannel asynchronousFileChannel = AsynchronousFileChannel.open(path, StandardOpenOption.WRITE);
        //缓冲区和位置0
        ByteBuffer buffer = ByteBuffer.allocate(1024);
        long position = 0;
        //放内容到缓冲区
        buffer.put("要写入的内容AIO写数据到AIO通道,Future".getBytes());
        //读写切换
        buffer.flip();
        //写,然后拿到Future
        Future<Integer> operation = asynchronousFileChannel.write(buffer, position);
        buffer.clear();
        //因为AIO是异步的,所以我这里要人为阻塞住它。isDone()方法,直到返回true后,才可以继续运行
        while(!operation.isDone());
        
        System.out.println("Write over");
    }
方法二:通过write()方法配合CompletionHandler内部类

在这里插入图片描述

    /**
     * AIO写数据到AIO通道,CompletionHandler
     */
    @Test
    public void test5() throws Exception{
        Path path = Paths.get("E:/1.txt");
        if(!Files.exists(path)){
            Files.createFile(path);
        }
        AsynchronousFileChannel asynchronousFileChannel = AsynchronousFileChannel.open(path, StandardOpenOption.WRITE);
        ByteBuffer buffer = ByteBuffer.allocate(1024);
        long position = 0;
        buffer.put("AIO通道通过CompletionHandler写".getBytes());
        buffer.flip();
        asynchronousFileChannel.write(buffer, position, buffer, new CompletionHandler<Integer, ByteBuffer>() {
            @Override
            public void completed(Integer result, ByteBuffer attachment) {
                System.out.println("bytes written:"+result);
            }

            @Override
            public void failed(Throwable exc, ByteBuffer attachment) {
                System.out.println("Write Failed");
                exc.printStackTrace();
            }
        });
        asynchronousFileChannel.close();
    }

四、字符集Charset

java中使用Charset表示字符集编码对象,常用静态方法如下:
描述(🍅:普通/🏆:静态)方法
🏆通过编码类型获得 Charset 对象public static Charset forName(String charsetName)
🏆获得系统支持的所有编码方式public static SortedMap<String,Charset> availableCharsets()
🏆获得虚拟机默认的编码方式public static Charset defaultCharset()
🏆判断是否支持该编码类型public static boolean isSupported(String charsetName)
🍅获得 Charset 对象的编码类型(String)public final String name()
🍅获得编码器对象public abstract CharsetEncoder newEncoder()
🍅获得解码器对象public abstract CharsetDecoder newDecoder()
    @Test
    public void charSetEncoderAndDecoder() throws CharacterCodingException {
        //通过UTF-8编码类型获得 Charset 对象
        Charset charset=Charset.forName("UTF-8");
        //1.获取UTF-8编码器
        CharsetEncoder charsetEncoder=charset.newEncoder();
        //2.获取UTF-8解码器
        CharsetDecoder charsetDecoder=charset.newDecoder();
        //3.获取需要解码编码的数据
        CharBuffer charBuffer = CharBuffer.allocate(1024);
        charBuffer.put("字符集编码解码");
        charBuffer.flip();
        //4.编码
        ByteBuffer byteBuffer = charsetEncoder.encode(charBuffer);
        System.out.println("编码后:");
        for (int i = 0;i < byteBuffer.limit();i++) {
            System.out.println(byteBuffer.get());
        }
        //5.解码
        byteBuffer.flip();
        CharBuffer charBuffer1=charsetDecoder.decode(byteBuffer);
        System.out.println("解码后:");
        System.out.println(charBuffer1.toString());
        
        //其它操作
        System.out.println("指定其他格式解码:");
        Charset charset1=Charset.forName("GBK");
        byteBuffer.flip();
        CharBuffer charBuffer2 =charset1.decode(byteBuffer);
        System.out.println(charBuffer2.toString());
        //6.获取 Charset 所支持的字符编码
        Map<String ,Charset> map = Charset.availableCharsets();
        Set<Map.Entry<String,Charset>> set = map.entrySet();
        for (Map.Entry<String,Charset> entry: set) {
            System.out.println(entry.getKey() + "="+entry.getValue().toString());
        }
    }

五、Java NIO 聊天室综合案例

源码位置:src/main/java/nio_chat包下

在这里插入图片描述
在这里插入图片描述
在这里插入图片描述

1. 服务端

思路
  1. 通过选择器管理通道,当有客户端连接就绪,我们处理连接,给客户端输出“欢迎进入聊天室,请注意隐私安全”
  2. 当处理读就绪时(客户端发送消息),我们将其读出,然后广播给其它通道
    在这里插入图片描述
import java.io.IOException;
import java.net.InetSocketAddress;
import java.nio.ByteBuffer;
import java.nio.channels.*;
import java.nio.charset.Charset;
import java.util.Iterator;
import java.util.Set;

//服务器端
public class ChatServer {
    //服务器端启动的方法
    public void startServer() throws IOException {
        //1 创建 Selector 选择器
        Selector selector = Selector.open();
        //2 创建 ServerSocketChannel 通道
        ServerSocketChannel serverSocketChannel =ServerSocketChannel.open();
        //3 为 channel 通道绑定监听端口
        serverSocketChannel.bind(new InetSocketAddress(8000));
        //设置非阻塞模式
        serverSocketChannel.configureBlocking(false);
        //4 把 channel 通道注册到 selector 选择器上
        serverSocketChannel.register(selector, SelectionKey.OP_ACCEPT);
        System.out.println("服务器已经启动成功了");
        //5 循环,等待有新链接接入
        //while(true)
        for(;;) {
            //获取 channel 数量
            int readChannels = selector.select();
            if(readChannels == 0) {
                continue;
            }
            //获取可用的 channel
            Set<SelectionKey> selectionKeys = selector.selectedKeys();
            //遍历集合
            Iterator<SelectionKey> iterator = selectionKeys.iterator();
            while (iterator.hasNext()) {
                SelectionKey selectionKey = iterator.next();
                //移除 set 集合当前 selectionKey
                iterator.remove();
                //6 根据就绪状态,调用对应方法实现具体业务操作
                //6.1 如果 accept 状态
                if(selectionKey.isAcceptable()) {
                    acceptOperator(serverSocketChannel,selector);
                }
                //6.2 如果可读状态
                if(selectionKey.isReadable()) {
                    readOperator(selector,selectionKey);
                }
            }
        }
    }
    //处理可读状态操作
    private void readOperator(Selector selector, SelectionKey selectionKey)
            throws IOException {
        //1 从 SelectionKey 获取到已经就绪的通道
        SocketChannel socketChannel =
                (SocketChannel)selectionKey.channel();
        //2 创建 buffer
        ByteBuffer byteBuffer = ByteBuffer.allocate(1024);
        //3 循环读取客户端消息
        int readLength = socketChannel.read(byteBuffer);
        String message = "";
        if(readLength >0) {
            //切换读模式
            byteBuffer.flip();
            //读取内容
            message += Charset.forName("UTF-8").decode(byteBuffer);
        }
        //4 将 channel 再次注册到选择器上,监听可读状态
        socketChannel.register(selector,SelectionKey.OP_READ);
        //5 把客户端发送消息,广播到其他客户端
        if(message.length()>0) {
            //广播给其他客户端
            System.out.println(message);
            castOtherClient(message,selector,socketChannel);
        }
    }
    //广播到其他客户端
    private void castOtherClient(String message, Selector selector, SocketChannel socketChannel) throws IOException {
        //1 获取所有已经接入 channel
        Set<SelectionKey> selectionKeySet = selector.keys();
        //2 循环想所有 channel 广播消息
        for(SelectionKey selectionKey : selectionKeySet) {
            //获取每个 channel
            Channel tarChannel = selectionKey.channel();
            //不需要给自己发送
            if(tarChannel instanceof SocketChannel && tarChannel != socketChannel) {
                ((SocketChannel)tarChannel).write(Charset.forName("UTF-8").encode(message));
            }
        }
    }
    //处理接入状态操作
    private void acceptOperator(ServerSocketChannel serverSocketChannel, Selector selector) throws IOException {
        //1 接入状态,创建 socketChannel
        SocketChannel socketChannel = serverSocketChannel.accept();
        //2 把 socketChannel 设置非阻塞模式
        socketChannel.configureBlocking(false);
        //3 把 channel 注册到 selector 选择器上,监听可读状态
        socketChannel.register(selector,SelectionKey.OP_READ);
        //4 给客户端回复信息
        socketChannel.write(Charset.forName("UTF-8").encode("欢迎进入聊天室,请注意隐私安全"));
    }
    //启动主方法
    public static void main(String[] args) {
        try {
            new ChatServer().startServer();
        } catch (IOException e) {
            e.printStackTrace();
        }
    }
}

2. 客户端

思路
  1. 选择器监听读就绪
  2. 写的时候,直接给服务端写就好,服务端的选择器,监听读就绪后会处理
线程类:每个客户端需要一个线程处理,注意,客户端不是在我们服务器上运行的,而是用户电脑上,我们这里用一台电脑模拟

在这里插入图片描述

import java.io.IOException;
import java.nio.ByteBuffer;
import java.nio.channels.SelectionKey;
import java.nio.channels.Selector;
import java.nio.channels.SocketChannel;
import java.nio.charset.Charset;
import java.util.Iterator;
import java.util.Set;

public class ClientThread implements Runnable {
    private Selector selector;
    public ClientThread(Selector selector) {
        this.selector = selector;
    }
    @Override
    public void run() {
        try {
            for(;;) {
                //获取 channel 数量
                int readChannels = selector.select();
                if(readChannels == 0) {
                    continue;
                }
                //获取可用的 channel
                Set<SelectionKey> selectionKeys = selector.selectedKeys();
                //遍历集合
                Iterator<SelectionKey> iterator = selectionKeys.iterator();
                while (iterator.hasNext()) {
                    SelectionKey selectionKey = iterator.next();
                    //移除 set 集合当前 selectionKey
                    iterator.remove();
                    //如果可读状态
                    if(selectionKey.isReadable()) {
                        readOperator(selector,selectionKey);
                    }
                }
            }
        }catch(Exception e) {
        }
    }
    //处理可读状态操作
    private void readOperator(Selector selector, SelectionKey selectionKey) throws IOException {
        //1 从 SelectionKey 获取到已经就绪的通道
        SocketChannel socketChannel =
                (SocketChannel)selectionKey.channel();
        //2 创建 buffer
        ByteBuffer byteBuffer = ByteBuffer.allocate(1024);
        //3 循环读取客户端消息
        int readLength = socketChannel.read(byteBuffer);
        String message = "";
        if(readLength >0) {
            //切换读模式
            byteBuffer.flip();
            //读取内容
            message += Charset.forName("UTF-8").decode(byteBuffer);
        }
        //4 将 channel 再次注册到选择器上,监听可读状态
        socketChannel.register(selector, SelectionKey.OP_READ);
        //5 把客户端发送消息,广播到其他客户端
        if(message.length()>0) {
            //广播给其他客户端
            System.out.println(message);
        }
    }
}
客户端

在这里插入图片描述

import java.io.IOException;
import java.net.InetSocketAddress;
import java.nio.channels.SelectionKey;
import java.nio.channels.Selector;
import java.nio.channels.SocketChannel;
import java.nio.charset.Charset;
import java.util.Scanner;

//客户端
public class ChatClient {
    //启动客户端方法
    public void startClient(String name) throws IOException {
        //连接服务端
        SocketChannel socketChannel = SocketChannel.open(new InetSocketAddress("127.0.0.1",8000));
        //接收服务端响应数据
        Selector selector = Selector.open();
        socketChannel.configureBlocking(false);
        socketChannel.register(selector, SelectionKey.OP_READ);
        //创建线程
        new Thread(new ClientThread(selector)).start();
        //向服务器端发送消息
        Scanner scanner = new Scanner(System.in);
        while(scanner.hasNextLine()) {
            String msg = scanner.nextLine();
            if(msg.length()>0) {
                socketChannel.write(Charset.forName("UTF-8").encode(name +" : " +msg));
            }
        }
    }

    public static void main(String[] args) throws IOException {
        //传入你的用户名,需要开启idea并发运行
        new ChatClient().startClient("爱国者");
    }
}

3. 总结

  1. BIO每次连接,就需要启动一个线程,全程对其进行处理,尽管这个客户端连上后,什么也不干
  2. 但是NIO只需要一个选择器,和相关通道
  3. 当监听到感兴趣的就绪操作,才会对其进行处理,而不是一个线程全程连接客户端
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

ydenergy_殷志鹏

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值