Lesson29_NIO

36 篇文章 0 订阅
33 篇文章 0 订阅

Java NIO概述

  • 在早期计算机行业,制约效率的原因主要是CPU,随着科学发展,CPU的制约已大大减小,跟多的是文件IO操作的制约。各大系统相对的都对IO操作做了优化
  • Java为了保证JVM在不同的系统上执行效率的问题,于是屏蔽了系统对IO的优化,这一直影响着Java的效率,直到引入NIO
  • Java NIO(new IO或Non Blocking IO),是指新的IO流获取非阻塞IO操作,是从Java1.4版本开始引入的一个新的IO API,可以替代标准的Java IO API。NIO支持面向缓冲区的、基于通道的IO操作。NIO将以更加高效的方式进行文件的读写操作。

阻塞IO

  • 通常在进行同步IO操作时,如果读取数据,代码会阻塞直至有可供读取的数据。同样,写入调用将会阻塞直至数据能够写入。
  • 传统的Server/Client模式会基于TPR(Thread Per Request),服务器会为每个客户端请求建立一个线程,由该线程单独负责处理一个客户请求。
  • 这种模式带来的一个问题就是线程数量的剧增,大量的线程会增大服务器的开销。
  • 大多数的实现为了避免这个问题,都采用了线程池模型,并设置线程池的最大数量
  • 这又带来了新的问题,如果线程池有100个线程,而有100个用户都在进行 大文件下载 ,会导致第101个用户的请求无法及时处理,即便第101个用户只想请求一个几KB大小的页面
    在这里插入图片描述

非阻塞IO(NIO)

  • NIO中非阻塞I/O采用了基于Reactor模式的工作方式,I/O调用不会被阻塞,相反是注册感兴趣的特定I/O事件

  • 例如可读数据可达,新的套接字连接等等,在发生特定事件时,系统再通知我们。

  • NIO中实现非阻塞I/O的核心对象就是Selector,Selector就是注册各种I/O事件地方,而且当我们感兴趣的事件发生时,就是这个对象告诉我们所发生的事件
    在这里插入图片描述

  • 从图中可以看出,当有读或写等任何注册的事件发生时,可以从 Selector 中获得相应的 SelectionKey,同时从 SelectionKey 中可以找到发生的事件和该事件所发生的具体的 SelectableChannel,以获得客户端发送过来的数据。

    非阻塞指的是 IO 事件本身不阻塞,但是获取 IO 事件的 select()方法是需要阻塞等待的.
    区别是阻塞的 IO 会阻塞在 IO 操作上, NIO 阻塞在事件获取上,没有事件就没有 IO, 从高层次看 IO 就不阻塞了.
    也就是说只有 IO 已经发生那么我们才评估 IO 是否阻塞,但是select()阻塞的时候 IO 还没有发生,何谈 IO 的阻塞呢?
    NIO 的本质是延迟 IO 操作到真正发生 IO 的时候,而不是以前的只要 IO 流打开了就一直等待 IO 操作。

    在这里插入图片描述

NIO核心组成

  • Java NIO由以下几个核心部分组成:Channels、Buffers、Selectors
  • 虽然Java NIO中除此之外还有很多类和组件,但是Channel,Buffer和Selector构成了核心的API。
  • 其他组件,如Pipe和FileLock,只不过是与三个核心组件共同使用的工具类。
Channel
  • 首先说一下 Channel,可以翻译成“通道”。Channel 和 IO 中的 Stream(流)是差不多一个等级的。只不过 Stream 是单向的,譬如:InputStream, OutputStream.而Channel 是双向的,既可以用来进行读操作,又可以用来进行写操作。
  • NIO 中的 Channel 的主要实现有:FileChannel、DatagramChannel、SocketChannel 和 ServerSocketChannel,这里看名字就可以猜出个所以然来:分别可以对应文件 IO、UDP 和 TCP(Server 和 Client)。
Buffer

NIO 中的关键 Buffer 实现有:ByteBuffer, CharBuffer, DoubleBuffer, FloatBuffer, IntBuffer, LongBuffer, ShortBuffer,分别对应基本数据类型: byte, char, double, float, int, long, short。

Selector
  • Selector 运行单线程处理多个 Channel,如果你的应用打开了多个通道,但每个连接的流量都很低,使用 Selector 就会很方便。
  • 例如在一个聊天服务器中。要使用Selector, 得向 Selector 注册 Channel,然后调用它的 select()方法。这个方法会一直阻塞到某个注册的通道有事件就绪。
  • 一旦这个方法返回,线程就可以处理这些事件,事件的例子有如新的连接进来、数据接收等。

Channel

Channel概述

Channel概念

Channel 是一个通道,可以通过它读取和写入数据,它就像水管一样,网络数据通过Channel 读取和写入。通道与流的不同之处在于通道是双向的,流只是在一个方向上移动(一个流必须是 InputStream 或者 OutputStream 的子类),而且通道可以用于读、写或者同时用于读写。因为 Channel 是全双工的,所以它可以比流更好地映射底层操作系统的 API。
NIO 中通过 channel 封装了对数据源的操作,通过 channel 我们可以操作数据源,但又不必关心数据源的具体物理结构。这个数据源可能是多种的。比如,可以是文件,也可以是网络 socket。在大多数应用中,channel 与文件描述符或者 socket 是一一对应的。Channel 用于在字节缓冲区和位于通道另一侧的实体(通常是一个文件或套
接字)之间有效地传输数据。

Channel源码

在这里插入图片描述

与缓冲区不同,通道 API 主要由接口指定。不同的操作系统上通道实现(Channel Implementation)会有根本性的差异,所以通道 API 仅仅描述了可以做什么。因此很自然地,通道实现经常使用操作系统的本地代码。通道接口允许您以一种受控且可移植的方式来访问底层的 I/O 服务。
Channel 是一个对象,可以通过它读取和写入数据。拿 NIO 与原来的 I/O 做个比较,通道就像是流。所有数据都通过 Buffer 对象来处理。您永远不会将字节直接写入通道中,相反,您是将数据写入包含一个或者多个字节的缓冲区。同样,您不会直接从通道中读取字节,而是将数据从通道读入缓冲区,再从缓冲区获取这个字节。

Java NIO 的通道类似流,但又有些不同:
  • 既可以从通道中读取数据,又可以写数据到通道。但流的读写通常是单向的。
  • 通道可以异步地读写。
  • 通道中的数据总是要先读到一个 Buffer,或者总是要从一个 Buffer 中写入。
  • 正如上面所说,从通道读取数据到缓冲区,从缓冲区写入数据到通道。如下图所示:
    在这里插入图片描述

Channel实现

  1. FileChannel 从文件中读写数据。
  2. DatagramChannel 能通过 UDP 读写网络中的数据。
  3. SocketChannel 能通过 TCP 读写网络中的数据。
  4. ServerSocketChannel 可以监听新进来的 TCP 连接,像 Web 服务器那样。对每一个新进来的连接都会创建一个 SocketChannel。

    正如你所看到的,这些通道涵盖了 UDP 和 TCP 网络 IO,以及文件 IO

FileChannel 介绍和示例

FileChannel 类可以实现常用的 read,write 以及 scatter/gather 操作
同时它也提供了很多专用于文件的新方法。这些方法中的许多都是我们所熟悉的文件操作。

方法描述
int read(ByteBuffer dst)从Channel中读取数据到ByteBuffer
long read(ByteBuffer[] dsts)将Channel中数据“分散”到ByteBuffer[]
int write(ByteBuffer srs)将ByteBuffer中的数据写入到Channel
long write(ByteBuffer[] srcs)将ByteBuffer[] 中的数据“聚集”到Channel
long position()返回此通道的文件位置
FileChannel position(long p)设置此通道的文件位置
long size()返回此通道的文件的当前大小
FileChannel truncate(long s)将此通道的文件截取为给定大小
void force(boolean metaData)强制将所有对此通道的文件更新写入到存储设备中
Buffer通常的操作
  • 将数据写入缓冲区
  • 调用 buffer.flip() 反转读写模式
  • 从缓冲区读取数据
  • 调用 buffer.clear() 或 buffer.compact() 清除缓冲区内容
简单代码示例,将文件中的数据读取到Buffer中
/**
 * 通过FileChannel将文件数据读取到Buffer中
 * @author: 邪灵
 * @date: 2021/10/2 16:57
 * @version: 1.0
 */
public class FileChannelDemo01 {
    public static void main(String[] args) throws Exception {
        /*
         *  1. 创建FileChannel对象
         *   FileChannel无法直接创建,我们通过文件来获取
         *   而我们得到文件的一般通过文件流的方式,比如FileInputStream
         *   这里我们用另一种RandomAccessFile得到文件
         */
        // 参数1:文件路径   参数2:模式,r为读,rw为读写
        RandomAccessFile file = new RandomAccessFile("src/a.txt","rw");
        FileChannel channel = file.getChannel();
        /*
         *  2. 创建Buffer对象
         *   此处我们用字节类型的Buffer
         *   该类为抽象类,提供了allocate方法创建对象,参数为大小
         */
        ByteBuffer buffer = ByteBuffer.allocate(1024);
        /*
         *  3. 读取数据到Buffer中
         *   channel提供了read方法,接收一个Buffer对象
         *   返回值如果是-1,则结束了
         */
        int read = channel.read(buffer);
        while (read != -1) {
            System.out.println("读到长度"+read);
            // 转换buffer读写
            buffer.flip();
            // 该方法判断buffer中是否有剩余内容
            while (buffer.hasRemaining()) {
                // 通过 get方法取出数据,转为char后打印
                System.out.println((char) buffer.get());
            }
            // 从buffer中取出数据后,清空buffer
            buffer.clear();
            // 继续读取文件,直到读取结束
            read = channel.read(buffer);
        }
        // 整个结束后,关闭资源
        file.close();
        System.out.println("结束了");
    }
}
FileChannel操作详解
打开FileChannel
  • 在使用FileChannel之前,必须先打开它。
  • 但是我们无法直接打开一个FileChannel
  • 需要通过使用一个InputStream、OutputStream或RandomAccessFile来获取一个FileChannel实例
  • 下面是通过RandomAccessFile打开FileChannel的示例
  • 代码示例
    RandomAccessFile file = new RandomAccessFile("src/a.txt","rw");
    FileChannel channel = file.getChannel();
    
从FileChannel读取数据
  • 调用多个read()方法之一从FileChannel中读取数据。如:
    ByteBuffer buffer = ByteBuffer.allocate(1024);
    int read = channel.read(buffer);
    
  • 首先分配一个Buffer
  • 从FileChannel中读取的数据将被读取到Buffer中年。
  • 然后,调用FileChannel.read()方法。
  • 该方法将数据从FileChannel读取到Buffer中。
  • read()方法返回的int值表示了有多少个字节被读取到了Buffer中
  • 如果返回-1,表示了文件末尾。
向FileChannel写数据
  • 使用FileChannel.write()方法向FileChannel写数据
  • 该方法的参数是一个Buffer。如:
    /**
    * 通过FileChannel写数据
    *
    * @author: 邪灵
    * @date: 2021/10/2 18:14
    * @version: 1.0
    */
    public class FileChannelDemo02 {
    	public static void main(String[] args) throws Exception {
        	// 打开FileChannel
        	RandomAccessFile file = new RandomAccessFile("src/a.txt","rw");
        	FileChannel channel = file.getChannel();
        	// 创建Buffer对象
        	ByteBuffer buffer = ByteBuffer.allocate(1024);
        	// 先清空缓冲区
        	buffer.clear();
        	// 向buffer缓冲写入数据,通过put方法
        	buffer.put("你好啊!".getBytes(StandardCharsets.UTF_8));
        	// 转换buffer
        	buffer.flip();
        	// 循环判断,直到缓冲区中没有未写入内容退出
        	while (buffer.hasRemaining()) {
            	channel.write(buffer);
        	}
        	// 关闭资源
        	file.close();
    	}
    }
    
  • 注意FileChannel.write()是在while循环中调用的
  • 因为无法保证write()方法一次能向FileChannel写入多少字节,因此需要重复调用write()方法,直到Buffer中已经没有尚未写入通道的字节
关闭FileChannel
  • 用完FileChannel后必须将其关闭,如:
    // 关闭资源
    channel.close();
    
FileChannel的position方法
  • 有时可能需要在FileChannel的某个特定文职进行数据的读/写操作
  • 可以通过调用position()方法获取FileChannel的当前位置
  • 也可以通过调用position(long pos)方法设置FileChannel的当前位置
  • 如下例子:
    long pos = channel.position();
    channel.position(pos+123);
    
  • 如果将为主子设置在文件结束符之后,然后试图从文件通道中国读取数据,读方法将返回-1(也就是文件结束标志)
  • 如果将位置设置在文件结束符之后,然后向通道中写数据,文件将撑大到当前位置并写入数据。这可能导致“文件空洞”,磁盘上物理文件中写入的数据间有空隙。
FileChannel的size方法
  • FileChannel实例的size()方法将返回该实例所关联文件的大小。如
    long fileSize = channel.size();
    
FileChannel的truncate方法
  • 可以使用FileChannel.truncate()方法截取一个文件。
  • 截取文件时,文件将参数中指定长度后面的部分将被删除,如:
    channel.truncate(1024);
    
  • 这个例子截取文件的前1024个字节
FileChannel的force方法
  • FileChannel.force()方法将通道里尚未写入磁盘的数据强制写到磁盘上。
  • 出于性能方面的考虑,操作系统会将数据缓存在内存中,所以无法保证写入到FileChannel里的数据一定会即时写到磁盘上
  • 要保证这一点,需要调用force()方法
  • force()方法有一个boolean类型的参数,指明是否同时将文件元数据(权限信息等)写到磁盘上。
FileChannel的transferTo和transferFrom方法

通道之间的数据传输:
如果两个通道中有一个是FileChannel,那你可以直接将数据从一个channel传输到另一个channel。
两个方法都可以实现。只是目标与调用者互换而已

  • transferFrom方法
    1. FileChannel的transferFrom()方法可以将数据从源通道传输到FileChannel中(jdk文档解释为将字节从给定的可读字节通道传输到此通道的文件中)
    2. 下面是一个FileChannel完成文件间的复制的例子:
      public class FileChannelDemo03 {
      	public static void main(String[] args) throws Exception {
      		// 先打开连个通道
      		RandomAccessFile file1 = new RandomAccessFile("src/a.txt","rw");
      		RandomAccessFile file2 = new RandomAccessFile("src/b.txt","rw");
      
      		FileChannel fromChannel = file1.getChannel();
      		FileChannel toChannel = file2.getChannel();
      
      		// 从fromChannel 中传输数据到toChannel
      		toChannel.transferFrom(fromChannel,0,fromChannel.size());
      
      		// 关闭资源
      		toChannel.close();
      		fromChannel.close();
      		file2.close();
      		file1.close();
      
      		System.out.println("复制结束");
      	}
      }
      
    3. 方法的输入参数position表示从position处开始向目标文件写入数据,count表示最多传输的字节数。
    4. 如果源通道的剩余空间小于count个字节,则所传输的字节数要小于请求的字节数
    5. 此处要注意,在SocketChannel的实现中,SocketChannel只会传输此刻准备好的数据(可能不足count字节)
    6. 因此,SocketChannel可能不会将请求的所有数据(count个字节)全部传输到FileChannel中。
  • transferTo方法
    1. transferTo()方法将数据从FileChannel传输到其他的channel中。
    2. 下面是一个transferTo()方法的例子:
      public class FileChannelDemo04 {
      	public static void main(String[] args) throws Exception {
      		// 先打开连个通道
      		RandomAccessFile file1 = new RandomAccessFile("src/a.txt","rw");
      		RandomAccessFile file2 = new RandomAccessFile("src/b.txt","rw");
      
      		FileChannel fromChannel = file1.getChannel();
      		FileChannel toChannel = file2.getChannel();
      
      		// 传输数据
      		fromChannel.transferTo(0, fromChannel.size(), toChannel);
      
      		toChannel.close();
      		fromChannel.close();
      		file2.close();
      		file1.close();
      
      		System.out.println("复制结束");
      	}
      }
      

Socket通道

Socket通道特点
  • 特点一:

    新的socket通道类可以运行非阻塞模式并且是可选的,可以激活大程序(如网络服务器和中间件组件)巨大的可伸缩性和灵活性
    本节我们会看到,再也没有为每个socket连接使用一个线程的必要了,也避免了管理大量线程所需要的上下文交换开销。
    借助新的NIO类,一个或几个线程就可以管理成百上千的活动socket连接了,并且只有很少甚至没有性能损失。
    所有的socket通道类(DatagramChannel、SocketChannel和ServerSocketChannel)都继承了位于java.nio.channels.spi包中的AbstractSelectableChannel。这意味着我们可以用一个Selector对象来执行socket通道的就绪选择(readiness selection)

  • 特点二:

    请注意DatagramChannel和SocketChannel实现定义读和写的接口而ServerSocketChannel不实现
    ServerSocketChannel负责监听传入的连接和创建新的SocketChannel对象,它本身从不传输数据。

  • 特点三:

    在我们具体讨论每一种socket通道前,你应该了解socket和socket通道之间的关系。
    通道是一个连接I/O服务导管并提供与该服务交互的方法。
    就某个socket 而言,它不会再次实现与之对应的socket通道类中的socket协议API,而java.net中已经存在的socket通道都可以被大多数协议操作重复使用。
    全部socket通道类(DatagramChannel、SocketChannel和ServerSocketChannel)在被实例化时都会创建一个对等的socket对象
    这些是我们熟悉的来自java.net的类(Socket、ServerSocket和DatagramSocket),他们已经被更新以识别通道
    对等socket可以通过调用socket()方法从一个通道上获取。
    此外,这三个java.net类现在都有getChannel()方法

  • 特点四:

    要把每一个socket通道至于非阻塞模式,我们要依靠所有socket通道类的共有超级类:SelectableChannel。
    就绪选择(readiness selection)是一种可以用来查询通道的机制,该查询可以判断通道是否准备好执行一个目标操作,如读或写
    非阻塞I/O和可选择性是紧密相连的,那也正是管理阻塞模式的API代码要在SelectableChannel超级类中定义的原因。
    设置或重新设置一个通道的阻塞模式是很简单的,只要调用configureBlocking()方法即可,传递参数值为true则设为阻塞模式,参数值为false值设为非阻塞模式。可以通过调用isBlocking()方法来判断某个socket通道当前处于哪种模式

  • AbstractSelectableChannel.java中实现configureBlocking()方法如下:
    在这里插入图片描述

    非阻塞socket通常被认为是服务端使用的,因为它们使用同时管理很多socket通道变得更容易。
    但是,在客户端使用一个或几个非阻塞模式的socket通道也是有益处的,例如借助非阻塞socket通道,GUI程序可以专注与用户请求并且同时维护与一个或多个服务器的会话。在很多程序上,非阻塞模式都是有用的。
    偶尔的,我们也会需要放置socket通道的阻塞模式被更改。
    API中有一个blockingLock()方法,该方法会返回一个非透明的对象引用。
    返回的对象是通道实现修改阻塞模式时内部使用的,只有拥有此对象的锁的线程才能更改通道的阻塞模式。

ServerSocketChannel
基本概念
  • ServerSocketChannel是一个基于通道的socket监听器
  • 它同我们熟悉的java.net.ServerSocket执行相同的任务,不过它增加了通道语义,因此能够在非阻塞模式下运行
  • 由于ServerSocketChannel没有bind()方法,因此有必要取出对等的socket并使用它来绑定到一个端口以开始监听连接。
  • 我们也是使用对等的ServerSocket的API来根据需要设置其他的socket选项
  • 同java.net.ServerSocket一样,ServerSocketChannel也有accept()方法。一旦创建了一个ServerSocketChannel并用对等socket绑定了它,然后就可以在其中一个上调用accept()。如果选择在ServerSocket上调用accept()方法,那么它会同任何其他的ServerSocket表现一样的行为:总是阻塞并返回一个java.net.Socket对象。
  • 如果选择在ServerSocketChannel上调用accept()方法则会返回SocketChannel类型的对象,返回的对象能够在非阻塞模式下运行。
  • 换句话说。ServerSocketChannel的accept()方法会返回SocketChannel类型的对象,SocketChannel可以在非阻塞模式下运行
  • 其他的socket的accept()方法会阻塞返回一个Socket对象。如果ServerSocketChannel以非阻塞模式被调用,当没有传入连接在等待时,ServerSocketChannel.accept()会立即返回null
  • 正是这种检查连接而不阻塞的能力实现了可伸缩性并降低了复杂性。可选择性也因此得到实现
  • 我们可以使用一个选择器实例来注册ServerSocketChannel对象以实现新连接到达时自动通知的功能。
代码实现
public class ServerSocketChannelDemo {
    public static void main(String[] args) throws Exception {
        // 设置一个端口号
        int port = 10086;
        // 打开一个ServerSocketChannel对象
        ServerSocketChannel serverSocketChannel = ServerSocketChannel.open();
        // 绑定端口
        serverSocketChannel.socket().bind(new InetSocketAddress(port));
        // 设置非阻塞模式
        serverSocketChannel.configureBlocking(false);
        // 创建一个缓冲
        ByteBuffer buffer = ByteBuffer.wrap("你好,java".getBytes(StandardCharsets.UTF_8));
        // 循环进行监听
        while (true) {
            System.out.println("waiting for connections");
            SocketChannel accept = serverSocketChannel.accept();
            if (accept==null) {
                System.out.println("没有新的连接传入");
                Thread.sleep(2000);
            } else {
                System.out.println("Incoming connection from" + accept.socket().getRemoteSocketAddress());
                // buffer中指针指向0
                buffer.rewind();
                accept.write(buffer);
                accept.close();
            }
        }
    }
}
步骤详解
  • 打开ServerSocketChannel

    通过调用ServerSocketChannel.open()方法来打开ServerSocketChannel

    ServerSocketChannel serverSocketChannel = ServerSocketChannel.open();
    
  • 关闭ServerSocketChannel

    通过调用ServerSocketChannel.close()方法来关闭ServerSocketChannel

    serverSocketChannel.close();
    
  • 监听新的连接

    通过ServerSocketChannel.accept()方法监听新进来的连接。
    当accept()方法返回时,它返回一个包含新进来的连接的SocketChannel。
    因此,accetp()方法会一直阻塞到有新连接到达
    通常不会仅仅只监听一个连接,在while循环中调用accept()方法,如下的例子:

    while(true) {
    	System.out.println("Waiting for connections");
    	SocketChannel sc = ssc.accept();
    }
    
  • 阻塞模式

    会在SocketChannel sc = ssc.accept();这里阻塞住进程
    控制台只打印等待连接,会一直等待

    Waiting for connections
    
  • 非阻塞模式

    ServerSocketChannel可以设置成非阻塞模式。
    在非阻塞模式下,accept()方法会立刻返回,如果还没有新进来的连接,返回的将是null
    因此,需要检查返回的SocketChannel是否是null,如下:

    ServerSocketChannel ssc = ServerSocketChannel.open();
    ssc.socket().bind(new InetSocketAddress(8888));
    ssc.configureBlocking(false);
    while(true) {
    	System.out.println("Waiting for connections");
    	SocketChannel sc = ssc.accept();
    	if (sc == null) {
    		System.out.println("null");
    
SocketChannel
基本概念

Java NIO 中的SocketChannel是一个连接到TCP网络套接字的通道
A selectable channel for stream-oriented connecting sockets.
以上是Java docs中对于SocketChannel的描述:SocketChannel是一种面向流连接sockets套接字的可选通道。从这里可看出:

  • SocketChannel是用来连接Socket套接字
  • SocketChannel主要用途是用来处理网络I/O的通道
  • SocketChannel是基于TCP连接传输
  • SocketChannel实现了可选通道,可以被多路复用的
特点特征
  1. 对于已经存在的socket不能创建SocketChannel
  2. SocketChannel中提供的open接口创建的Channel并没有进行网络连接,需要使用connect接口连接到指定网址
  3. 未进行连接的SocketChannel执行I/O操作时,会抛出NotYetConnectedException异常
  4. SocketChannel支持两种I/O模式:阻塞和非阻塞
  5. SocketChannel支持异步关闭。如果SocketChannel在一个线程上read阻塞,另一个线程对该SocketChannel调用shutdownInput,则读阻塞的线程将返回-1表示没有读取任何数据;如果SocketChannel在一个线程上write阻塞,另一个线程对该SocketChannel调用shutdownWrite,则写阻塞将抛出AsynchronousCloseException异常
  6. SocketChannel支持设定参数:
    SO_SNDUBF:套接字发送缓冲区大小
    SO_RCVBUF:套接字接收缓冲区大小
    SO_KEEPALIVE:保活连接
    O_REUSEADDR:复用地址
    SO_LINGER:有数据传输时延缓关闭Channel(只有非阻塞模式下有用)
    TCP_NODELAY:禁用Nagle算法
实现过程
  • 创建SocketChannel
    1. 方式一:
      SocketChannel socketChannel = SocketChannel.open(new InetSocketAddress("www.baidu.com",80));
      
    2. 方式二:
      SocketChannel socketChannel = SocketChannel.open();
      socketChannel.connect(new InetSocketAddress("www.baidu.com",80));
      

    直接通过有参的openAPI或者使用无参openAPI,但是无参open只是创建了一个SocketChannel对象,并没有进行实质的tcp连接

  • 连接校验
    // 测试SocketChannel是否为open状态
    sokcetChannel.isOpen();
    // 测试SocketChannel是否已经被连接
    socketChannel.isConnected();
    // 测试SocketChannel是否正在进行连接
    socketChannel.isConnectionPending();
    // 校验正在进行套接字连接的SocketChannel是否已经完成连接
    socketChannel.finishConnect();
    
  • 读写模式
    前面提到SocketChannel支持阻塞和非阻塞两种模式
    socketChannel.configureBlocking(false);
    
    通过以上方法设置SocketChannel的读写模式。false表示非阻塞,true表示阻塞
  • 读写
    SocketChannel socketChannel = SocketChannel.open(new InetSocketAddress("www.baidu.com",80));
    ByteBuffer byteBuffer = ByteBuffer.allocate(16);
    socketChannel.read(byteBuffer);
    socketChannel.close();
    System.out.println("read over");
    
    以上为阻塞式读,当执行到read处,线程将阻塞,控制台将无法打印read over
    SocketChannel socketChannel = SocketChannel.open(new InetSocketAddress("www.baidu.com",80));
    socketChannel.configureBlocking(false);
    ByteBuffer byteBuffer = ByteBuffer.allocate(16);
    socketChannel.read(byteBuffer);
    socketChannel.close();
    System.out.println("read over");
    
    以上为非阻塞读,控制台将打印read over
    读写都是面向缓冲区,这个读写方式与前文中的FileChannel相同。
  • 设置和获取参数
    socketChannel.setOption(StandardSocketOptions.SO_KEEPALIVE,Boolean.TRUE).setOption(StandardSocketOptions.TCP_NODELAY,Boolean.TRUE);
    
    通过setOption方法可以设置socket套接字的相关参数。
    socketChannel.getOption(StandardSocketOptions.SO_KEEPALIVE);
    socketChannel.getOption(StandardSocketOptions.SO_RCVBUF);
    
    可以通过getOption方法获取相关参数的值,如默认的连接缓冲区大小是8192byte。
    SocketChannel还支持多路复用,但是多路复用在后续内容中会介绍到。
  • 读操作代码示例
    public class SocketChannelDemo {
    	public static void main(String[] args) throws Exception {
        	// 先创建SocketChannel
        	SocketChannel socketChannel = SocketChannel.open(new InetSocketAddress("localhost",10086));
        	// 设置读写模式为非阻塞模式
        	socketChannel.configureBlocking(false);
        	// 创建缓冲区
        	ByteBuffer buffer = ByteBuffer.allocate(16);
        	// 进行读操作
        	socketChannel.read(buffer);
        	// 关闭通道
        	socketChannel.close();
        	System.out.println("read over");
    	}
    }
    
DatagramChannel
基本概念
  • 正如SocketChannel对应Socket,ServerSocketChannel对应ServerSocket,每一个DatagramChannel对应也有一个关联的DatagramSocket对象
  • 正如SocketChannel模拟连接导向的流协议(如TCP/IP),DatagramChannel则模拟导向的无连接协议(如UDP/IP)。
  • DatagramChannel是无连接的,每个数据报(datagram)都是一个自包含的实体,拥有它自己的目的地址及不依赖其他数据报的数据负载
  • 与面向流的socket不同,DatagramChannel可以发送单独的数据报给不同的目的地址
  • 这样,DatagramChannel对象也可以接收来自任意地址的数据包
  • 每个到达的数据报都含有关于它来自何处的信息(源地址)
实现过程
  1. 打开DatagramChannel
    DatagramChannel server = DatagramChannel.open();
    server.socket().bind(new InetSocketAddress(10086));
    
    此例子是打开10086端口接收UDP数据包
  2. 接收数据
    通过receive()接收UDP包
    ByteBuffer buffer = ByteBuffer.allocate(64);
    buffer.clear();
    SocketAddress receiveAddr = server.receive(buffer);
    
    你SocketAddress可以获得发包的ip、端口等信息,用toString查看,格式为/127.0.0.1:57126
  3. 发送数据
    通过send()发送UDP包
    DatagramChannel server = DatagramChannel.open();
    ByteBuffer sendBuffer = ByteBuffer.wrap("client send".getBytes());
    server.send(sendBuffer,new InetSocketAddress("localhost",10086));
    
  4. 连接
    UDP不存在真正意义上的连接,这里的连接是向特定服务地址用read和write接收发送数据包。
    client.connection(new InettSocketAddress("localhost",10086));
    int readSize = client.read(sendBuffer);
    server.write(sendBuffer);
    
    read()和write()只有在connect()后才能使用,不然会抛出NotYetConnectedException异常。用read()接收时,荣辱观没有接收到包,会抛出PortUnreachableException异常。
  5. DatagramChannel示例
    发送端代码
    public class SendDatagramChannel {
    	public static void main(String[] args) throws Exception {
        	// 打开通道
        	DatagramChannel sendChannel = DatagramChannel.open();
        	// 设置ip和端口
        	InetSocketAddress inetSocketAddress = new InetSocketAddress("localhost",10086);
        	// 创建buffer区
        	ByteBuffer buffer = ByteBuffer.wrap("你好,我是发送端数据".getBytes(StandardCharsets.UTF_8));
        	// 开始发送,循环发送
        	while (true) {
            	sendChannel.send(buffer,inetSocketAddress);
            	System.out.println("发送完成");
            	// 指针指向0,或者需要将创建buffer的代码放到循环中,负责第一次发送后面的就没有信息了
            	buffer.rewind();
            	Thread.sleep(3000);
        	}
    	}
    ]
    
    接收端代码
    public class ReceiveDatagramChannel {
    	public static void main(String[] args) throws Exception {
        	// 打开通道
        	DatagramChannel receiveChannel = DatagramChannel.open();
        	// 绑定接收端端口
        	receiveChannel.bind(new InetSocketAddress(10086));
        	// 创建用于接收的buffer
        	ByteBuffer buffer = ByteBuffer.allocate(1024);
        	// 开始循环接收
        	while (true) {
            	// 先清空缓冲
            	buffer.clear();
            	// 接收
            	SocketAddress receive = receiveChannel.receive(buffer);
            	// 接收到后翻转buffer
            	buffer.flip();
            	// 打印发送端信息
            	System.out.println(receive);
            	// 打印接收到的数据
            	System.out.println(Charset.forName("utf-8").decode(buffer));
        	}
    	}
    }
    
    连接代码(对应第四步操作)
    public class DatagramChannelConnect {
    	public static void main(String[] args) throws Exception{
        	// 打开通道
        	DatagramChannel channel = DatagramChannel.open();
        	// 绑定端口
        	channel.bind(new InetSocketAddress(10086));
        	// 进行连接
        	channel.connect(new InetSocketAddress("localhost",10086));
        	// write方法发送数据
        	channel.write(ByteBuffer.wrap("我是发送的数据".getBytes(StandardCharsets.UTF_8)));
        	// 创建buffer用于接收数据
        	ByteBuffer buffer = ByteBuffer.allocate(1024);
        	while (true) {
            	// 先清空缓冲
            	buffer.clear();
            	// read 方法接收数据
            	channel.read(buffer);
            	// 翻转buffer
            	buffer.flip();
            	// 打印里面的值
            	System.out.println(Charset.forName("utf-8").decode(buffer));
        	}
    	}
    }
    

scatter和gather

Java NIO开始支持scatter/gather,scatter/gather用于描述从Channel中读取或写入到Channel的操作

概念

scatter/gather经常用于需要将传输的数据分开处理的场合
例如传输由消息头和消息体组成的消息
你可以会将消息头和消息体分散到不同的buffer中,这样你可以方便的处理消息头和消息体

scatter(分散)

scatter从Channel中读取是指在读操作时将读取的数据写入多个buffer中。
因此,Channel将从Channel中读取的数据“分散(scatter)”到多个buffer中

gather(聚集)

gather写入Channel是指在写操作时,将多个buffer的数据写入同一个Channel
因此,Channel将多个buffer中的数据“聚集(gather )”后发送到Channel

Scattering Reads

Scattering Reads是指数据从一个Channel读取到多个buffer中。如下
在这里插入图片描述

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

ByteBuffer[] bufferArray = {header,body};

channel.read(buufferArray);

注意buffer首先被插入到数组中,然后再将数组作为channel.read()的参数传入
read()方法按照buffer在数组中的顺序将从channel中读取的数据写入到buffer,当一个buffer被写满后,channel紧接着向另一个buffer中写
Scattering Reads在移动到下一个buffer前,必须填满当前的buffer
这也意味着它不适用于动态消息(消息大小不固定)
换句话说,如果存在消息头和消息体,消息头必须完成填充(如128byte),Scattering Reads才能正常工作

Gather Writes

Gathering Writes是指数据从多个buffer写入到同一个channel。如下:
在这里插入图片描述

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

ByteBuffer[] bufferArray = {header,body};

channel.write(bufferArray);

buffers数组是write()方法的入参
write()方法会按照buffer在数组中的顺序,将数据写入到channel
注意只有position和limit之间的数据才会被写入
因此,如果一个buffer的容量为128byte,但是仅仅包含了60byte的数据,那么这60byte的数据将被写入到channel中。
因此与Scattering Reads相反,Gathering Writers能较好的处理动态消息

Buffer

简介

Java NIO 中的Buffer用于和 NIO 通道进行交互。数据是从通道读入缓冲区,从缓冲
区写入到通道中的。
在这里插入图片描述
缓冲区本质上是一块可以写入数据,然后可以从中读取数据的内存。
这块内存被包装成 NIO Buffer 对象,并提供了一组方法,用来方便的访问该块内存。
缓冲区实际上是一个容器对象,更直接的说,其实就是一个数组,在 NIO 库中,所有数据都是用缓冲区处理的。
在读取数据时,它是直接读到缓冲区中的; 在写入数据时,它也是写入到缓冲区中的;任何时候访问 NIO 中的数据,都是将它放到缓冲区中。
而在面向流 I/O系统中,所有数据都是直接写入或者直接将数据读取到 Stream 对象中。

在 NIO 中,所有的缓冲区类型都继承于抽象类 Buffer,最常用的就是 ByteBuffer
对于 Java 中的基本类型,基本都有一个具体 Buffer 类型与之相对应,它们之间的继承关系如下图所示
在这里插入图片描述

基本用法

使用步骤
  • 写入数据到buffer
  • 调用flip()方法
  • 从buffer中读取数据
  • 调用clear()方法获取compact()方法

当向 buffer 写入数据时,buffer 会记录下写了多少数据。
一旦要读取数据,需要通过flip()方法将 Buffer 从写模式切换到读模式。
在读模式下,可以读取之前写入到 buffer的所有数据。
一旦读完了所有的数据,就需要清空缓冲区,让它可以再次被写入。
有两种方式能清空缓冲区:调用 clear()或 compact()方法。
clear()方法会清空整个缓冲区。compact()方法只会清除已经读过的数据。
任何未读的数据都被移到缓冲区的起始处,新写入的数据将放到缓冲区未读数据的后面。

使用案例
Buffer使用
public class BufferRead {
    public static void main(String[] args)throws Exception {
        RandomAccessFile file = new RandomAccessFile("路径","rw");
        FileChannel channel = file.getChannel();
        ByteBuffer buffer = ByteBuffer.allocate(1024);
        int read = channel.read(buffer);
        while (read != -1) {
            buffer.flip();
            while (buffer.hasRemaining()) {
                System.out.println((char)buffer.get());
            }
            buffer.clear();
            read = channel.read(buffer);
        }
    }
}
IntBuffer使用
public class BufferRead {
    public static void main(String[] args)throws Exception {
        // 创建一个buffer
        IntBuffer buffer = IntBuffer.allocate(8);
        // 向buffer中放入数据
        for (int i = 0; i < buffer.capacity(); i++) {
            buffer.put(2*(i+1));
        }
        // 翻转模式
        buffer.flip();
        // 取出buffer中的值
        while (buffer.hasRemaining()) {
            System.out.print(buffer.get()+" ");
        }
    }
}

capacity、position和limit

为了理解 Buffer 的工作原理,需要熟悉它的三个属性:Capacity、Position、limit
position和limit的含义取决于Buffer处在读模式还是写模式。不管Buffer处在什么模式,capacity的含义总是一样的
关于capacity、position和limit在读写模式中的说明如下:
在这里插入图片描述

capacity
  • 作为一个内存块,Buffer有一个固定的大小值,也叫capacity,你只能往里面写capacity个byte、long、char等类型。
  • 一旦Buffer人满了,需要将其清空(通过读数据或者清除数据)才能继续往里面写数据
position
写数据到Buffer中时
  • position表示写入数据的当前位置,position的初始值为0.
  • 当一个byte、loong等数据写到Buffer后,position会向下移动到下一个可插入数据的Buffer单元
  • position最大可为capacity-1(因为position的初始值为0)
从Buffer读数据时
  • position表示读入数据的当前位置,如position=2时表示已开始读入了3个byte,或从第三个byte开始读取
  • 通过ByteBuffer.flip()切换到读模式时position会被重置为0,当Buffer从position读入数据后,position会下移到下一个可读入的数据Buffer单元
limit
写数据时
  • limit表示可对Buffer最多写入多少个数据
  • 写模式下,limit等于Buffer的capacity
读数据时
  • limit表示Buffer里面有多少个可读数据(not null的数据)
  • 因此能读到之前写入的所有数据(limit被设置成已写数据的数量,这个值在写模式下就是position)。

类型

Java NIO有以下Buffer类型:
ByteBuffer、MappedByteBuffer、CharBuffer、DoubleBuffer
FloatBuffer、IntBuffer、LongBuffer、ShortBuffer
这些Buffer类型代表了不同的数据类型。换句话说就是可以通过char、short、int、long、float或double类型来操作缓冲区中的字节。

分配和写数据

Buffer分配

想要获得一个Buffer对象首先要进行分配。每一个Buffer类都有一个allocate方法

  • 下面是一个分配48字节capacity的ByteBuffer的示例:
    ByteBuffer buffer = ByteBuffer.allocate(48);
    
  • 这是分配一个可存储1024个字符的CharBuffer:
    CharBuffer buffer = CharBuffer.allocate(1024);
    
向Buffer中写数据
写数据到Buffer两种方式
  • 从Channel写到Buffer
  • 通过Buffer的put()方法写到Buffer里
从Channel写到Buffer
// read into buffer
int read = inChannel.read(buffer);
通过put方法写入Buffer
buffer.put(127);
  • put方法有很多版本,允许你以不同的方式把数据写入到Buffer中
  • 例如,写到一个指定的位置,或者把一个字节数组写入到Buffer
flip()方法

flip方法将Buffer从写模式切换到读模式
调用flip()方法会将position设回0,并将limit设置成之前position的值
换句话说,position现在用于标记读到的位置,limit表示之前写进了多少个byte、char等(现在能读取到多少个byte、char等)

读取数据

从Buffer读取数据的两种方式:
  • 从Buffered读取数据到Channel。
  • 使用get()方法从Buffer中读取数据。
从Buffer读取数据到Channel
// read from buffe into channel
int write = inChannel.write(buffer);
使用get()方法从Buffer读取数据
byte bytes = buffer.get();
  • get方法有很多版本,允许你以不同的方式从Buffer中读取数据
  • 例如从指定position读取,或者从Buffer中读取数据到字节数组

buffer几个方法

rewind()方法

Buffer.rewind()将position设回0,所以你可以重读Buffer中的所有数据
limit保持不变,任然表示能从Buffer中读取多少个元素(byte、char等)

clear()与compact()方法
  • 一旦读完Buffer中的数据,需要让Buffer准备再次被写入。可以通过clear()或compact()方法来完成
  • 如果调用的是clear()方法,position将被设回0,limit被设置成capacity的值。换句话说,Buffer被清空了。Buffer中的数据并未清除,只是这些标记告诉我们可以从哪里开始往Buffer里写数据
  • 如果Buffer中有一些未读的数据,调用clear()方法,数据将“被遗忘”,意味着不再有任何标记会告诉你哪些数据被读过,哪些还没有。
  • 如果Buffer中仍有未读的数据,且后续还需要这些数据,但是此时想要先写些数据,那么使用compact()方法
  • compact()方法将所有未读的数据拷贝到Buffer起始处。然后将position设到最后一个未读元素正后面。limit属性依然像clear()方法一样,设置成capacity。现在Buffer准备好写数据了,但是不会覆盖未读的数据。
mark()与reset()方法

通过调用Buffer.mark()方法,可以标记Buffer中的一个特定position。之后可以通过调用Buffer.reset()方法恢复到这个position。例如

buffer.mark();
// call buffer.get() a couple of times,e.g.during parsing.
buffer.reset();// set position back to mark

缓冲区操作

缓冲区分片

在 NIO 中,除了可以分配或者包装一个缓冲区对象外,还可以根据现有的缓冲区对象来创建一个子缓冲区
即在现有缓冲区上切出一片来作为一个新的缓冲区,但现有的缓冲区与创建的子缓冲区在底层数组层面上是数据共享的
也就是说,子缓冲区相当于是现有缓冲区的一个视图窗口。
调用 slice()方法可以创建一个子缓冲区。

ByteBuffer buffer = ByteBuffer.allocate(10);

for (int i = 0; i < buffer.capacity(); i++) {
	buffer.put((byte)i);
}
// 创建子缓冲区
buffer.position(3);
buffer.limit(7);
ByteBuffer slice = buffer.slice();

// 改变子缓冲区的内容
for (int i = 0; i < slice.capacity(); i++) {
	byte b = slice.get(i);
	b *= 10;
	slice.put(i,b);
}
// 打印buffer的值
buffer.position(0);
buffer.limit(buffer.capacity());

// remaining()类似于hasremaining()方法
while(buffer.remaining() > 0) {
	// 打印结果为:0 1 2 30 40 50 60 7 8 9
	System.out.println(buffer.get());
}
只读缓冲区

只读缓冲区非常简单,可以读取它们,但是不能向它们写入数据。
可以通过调用缓冲区的 asReadOnlyBuffer()方法,将任何常规缓冲区转 换为只读缓冲区
这个方法返回一个与原缓冲区完全相同的缓冲区,并与原缓冲区共享数据,只不过它是只读的。
如果原缓冲区的内容发生了变化,只读缓冲区的内容也随之发生变化:

ByteBuffer buffer = ByteBuffer.allocate(10);

for (int i = 0; i < buffer.capacity(); i++) {
	buffer.put((byte)i);
}

// 创建只读缓冲区
ByteBuffer readonly = buffer.asReadOnlyBuffer();

// 改变原缓冲区内容
for (int i = 0; i < buffer.capacity(); i++) {
	byte b = buffer.get(i);
	b *= 10;
	buffer.put(i,b);
}

// 取出只读缓冲区内容
readonly.position(0);
readonly.limit(buffer.capacity());

while(readonly.remaining() > 0) {
	System.out.println(readonly.get());
}

如果尝试修改只读缓冲区的内容,则会报 ReadOnlyBufferException 异常。
只读缓冲区对于保护数据很有用。在将缓冲区传递给某个对象的方法时,无法知道这个方法是否会修改缓冲区中的数据。
创建一个只读的缓冲区可以保证该缓冲区不会被修改。只可以把常规缓冲区转换为只读缓冲区,而不能将只读的缓冲区转换为可写的缓冲区。

直接缓冲区

直接缓冲区是为加快 I/O 速度,使用一种特殊方式为其分配内存的缓冲区
JDK 文档中的描述为:给定一个直接字节缓冲区,Java 虚拟机将尽最大努力直接对它执行本机I/O 操作。
也就是说,它会在每一次调用底层操作系统的本机 I/O 操作之前(或之后),尝试避免将缓冲区的内容拷贝到一个中间缓冲区中 或者从一个中间缓冲区中拷贝数据。
要分配直接缓冲区,需要调用 allocateDirect()方法,而不是 allocate()方法,使用方式与普通缓冲区并无区别。

// 创建一个源文件channel通道
String infile = "d:\\test\\01.txt";
FileInputStream fin = new FileInputStream(infile);
FileChannel finChannel = fin.getChannel();
// 创建一个目标文件channel通道
String outfile = "d:\\test\\02.txt";
FileOutputStream fout = new FileOutputStream(outfile);
FileChannel foutChannel = fout.getChannel();

// 创建直接缓冲区
ByteBuffer buffer = ByteBuffer.allocateDirect(1024);

// 进行文件复制
while(true) {
	buffer.clear();
	int r = finChannel.read(buffer);
	if (r == 1) {
		break;
	}
	buffer.flip();
	foutChannel.write(buffer);
}
内存映射文件I/O

内存映射文件 I/O 是一种读和写文件数据的方法,它可以比常规的基于流或者基于通道的 I/O 快的多。
内存映射文件 I/O 是通过使文件中的数据出现为 内存数组的内容来完成的,这其初听起来似乎不过就是将整个文件读到内存中,但是事实上并不是这样。
一般来说,只有文件中实际读取或者写入的部分才会映射到内存中。

public class Demo {
	static private final int start = 0;
	static private final int size = 1024;
	
	static public void main(String args[]) throws Exception {
 		RandomAccessFile raf = new RandomAccessFile("d:\\atguigu\\01.txt", "rw");
 		FileChannel fc = raf.getChannel();
 		MappedByteBuffer mbb = fc.map(FileChannel.MapMode.READ_WRITE, start, size);
 		mbb.put(0, (byte) 97);
 		mbb.put(1023, (byte) 122);
 		raf.close();
	}
}

Selector

Pipe和FileLock

其他

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值