网络编程进阶学习笔记--JavaNIO核心讲解

前言

本笔记是我对尼恩高并发三部曲的学习笔记,定位为网络编程的进阶学习笔记,看这个笔记的人应当对netty有过基础的学习,没有任何网络里编程经验的同学建议看看我的nety基础系列的文章,
链接:https://www.yuque.com/u2196512/mgr9wm
再看本系列的文章学习起来可能回更加高效,链接奉上。本系列预计用两个月完成更新。

JavaNIO核心讲解

概述

NIO(Non-blocking I/O,在Java领域,也称为New I/O),是一种同步非阻塞的I/O模型,也是I/O多路复用的基础,已经被越来越多地应用到大型应用服务器,成为解决高并发与大量连接、I/O处理问题的有效方式。
高性能的Java通信,绝对离不开Java NIO组件,现在主流的技术框架或中间件服务 器,都使用了Java NIO组件,譬如Tomcat、Jetty、Netty。学习和掌握Java NIO组件,已经 不是一项加分技能,而是一项必备技能。

简介

在1.4版本之前,Java IO类库是阻塞IO;从1.4版本开始,引进了新的异步IO库,被称 为Java New IO类库,简称为JAVA NIO。New IO类库的目标,就是要让Java支持非阻塞IO, 基于这个原因,更多的人喜欢称Java NIO为非阻塞IO(Non-Block IO),称“老的”阻塞式 Java IO为OIO(Old IO)。总体上说,NIO弥补了原来面向流的OIO同步阻塞的不足,它为 标准Java代码提供了高速的、面向缓冲区的IO。
Java NIO类库包含以下三个核心组件:

⚫ Channel(通道)
⚫ Buffer(缓冲区)
⚫ Selector(选择器)

NIO与OIO的区别

在Java中,NIO和OIO的区别,主要体现在三个方面:

(1)OIO是面向流(Stream Oriented)的,NIO是面向缓冲区(Buffer Oriented)的。
Java IO面向流意味着每次从流中读一个或多个字节,直至读取所有字节,它们没有被缓存在任何地方。此外,它不能前后移动流中的数据。如果需要前后移动从流中读取的数据,需要先将它缓存到一个缓冲区。 Java NIO的缓冲导向方法略有不同。数据读取到一个它稍后处理的缓冲区,需要时可在缓冲区中前后移动。这就增加了处理过程中的灵活性。但是,还需要检查是否该缓冲区中包含所有您需要处理的数据。而且,需确保当更多的数据读入缓冲区时,不要覆盖缓冲区里尚未处理的数据。

(2)OIO的操作是阻塞的,而NIO的操作是非阻塞的。
NIO如何做到非阻塞的呢?大家都知道,OIO操作都是阻塞的,例如,我们调用一个 read方法读取一个文件的内容,那么调用read的线程会被阻塞住,直到read操作完成。 而在NIO的非阻塞模式中,当我们调用read方法时,如果此时有数据,则read读取数据 并返回;如果此时没有数据,则read也会直接返回,而不会阻塞当前线程。 NIO的非阻塞是如何做到的呢?其实在上一章,答案已经揭晓了,NIO使用了通道和
通道的多路复用技术。

(3)OIO没有选择器(Selector)概念,而NIO有选择器的概念。
NIO的实现是基于底层的选择器的系统调用,所以NIO的需要底层操作系统提供支 持。而OIO不需要用到选择器。

三大组件介绍

三大组件关系图

image.png

通道

在OIO中,同一个网络连接会关联到两个流:一个输入流(Input Stream),另一个输出 流(Output Stream),Java应用程序通过这两个流,不断地进行输入和输出的操作。
在NIO中,一个网络连接使用一个通道表示,所有的NIO的IO操作都是通过连接通道 完成的。一个通道类似于OIO中的两个流的结合体,既可以从通道读取数据,也可以向通
道写入数据

选择器

首先,回顾一个前面介绍的基础知识,什么是IO多路复用模型?IO多路复用指的是一 个进程/线程可以同时监视多个文件描述符(含socket连接),一旦其中的一个或者多个文件 描述符可读或者可写,该监听进程/线程能够进行IO就绪事件的查询。
在Java应用层面,如何实现对多个文件描述符的监视呢?需要用到一个非常重要的 Java NIO组件——Selector 选择器。Selector 选择器可以理解为一个IO事件的监听与查询 器。通过选择器,一个线程可以查询多个通道的IO事件的就绪状态。
从编程实现维度来说,IO多路复用编程的第一步,是把通道注册到选择器中,第二步 则是通过选择器所提供的事件查询(select)方法,这些注册的通道是否有已经就绪的IO事
件(例如可读、可写、网络连接完成等)。
由于一个选择器只需要一个线程进行监控,所以,我们可以很简单地使用一个线程, 通过选择器去管理多个连接通道。
与OIO相比,NIO使用选择器的最大优势:系统开销小,系统不必为每一个网络连接 (文件描述符)创建进程/线程,从而大大减小了系统的开销。总之,一个线程负责多个连
接通道的IO处理,是非常高效的,这种高效来自于Java的选择器组件Selector以及其底层的 操作系统IO多路复用技术的支持。

缓冲区

应用程序与通道(Channel)主要的交互,主要是进行数据的read读取和write写入。为 了完成NIO的非阻塞读写操作,NIO为大家准备了第三个重要的组件——NIO Buffer(NIO 缓冲区)。
所谓通道的读取,就是将数据从通道读取到缓冲区中;所谓通道的写入,就是将 数据从缓冲区中写入到通道中。缓冲区的使用,是面向流进行读写操作的OIO所没有的,
image.png
也是NIO非阻塞的重要前提和基础之一。
接下来笔者从缓冲区开始,为大家详细介绍NIO的Buffer(缓冲区)、Channel(通 道)、Selector(选择器)三大核心组件

缓冲区

NIO的Buffer(缓冲区)本质上是一个内存块,既可以写入数据,也可以从中读取数 据。Java NIO中代表缓冲区的Buffer类是一个抽象类,位于java.nio包中。
NIO的Buffer的内部是一个内存块(数组),此类与普通的内存块(Java数组)不同的 是:NIO Buffer对象,提供了一组比较有效的方法,用来进行写入和读取的交替访问。

Buffer 类是一个非线程安全类。
Buffer类

Buffer类是一个抽象类,对应于Java的主要数据类型,在NIO中有8种缓冲区类,分别 如下:ByteBuffer、CharBuffer、DoubleBuffer、FloatBuffer、IntBuffer、LongBuffer、
ShortBuffer、MappedByteBuffer。
前7种Buffer类型,覆盖了能在IO中传输的所有的Java基本数据类型。第8种类型 MappedByteBuffer是专门用于内存映射的一种ByteBuffer类型。不同的Buffer子类,其能操
作的数据类型能够通过名称进行判断,比如IntBuffer只能操作Integer类型的对象。
实际上,使用最多的还是ByteBuffer二进制字节缓冲区类型,后面会看到。

buffer类的重要属性

Buffer的子类会拥有一块内存,作为数据的读写缓冲区,但是读写缓冲区并没有定义 在Buffer基类,而是定义在具体的子类中。如ByteBuf子类就拥有一个byte[]类型的数组成员
final byte[] hb,作为自己的读写缓冲区,数组的元素类型与Buffer子类的操作类型相互对 应。

为了记录读写的状态和位置,Buffer类额外提供了一些重要的属性,其中有以下三个
重要的成员属性:
⚫ capacity(容量)
⚫ position(读写位置)
⚫ limit(读写的限制)

**1. capacity属性 **

Buffer类的capacity属性,表示内部容量的大小。一旦写入的对象数量超过了capacity容 量,缓冲区就满了,不能再写入了。
Buffer类的capacity属性一旦初始化,就不能再改变。原因是什么呢?Buffer类的对象在 初始化时,会按照capacity分配内部数组的内存,在数组内存分配好之后,它的大小当然就 不能改变了。
前面讲到,Buffer类是一个抽象类,Java不能直接用来新建对象。在具体使用的时候, 必须使用Buffer的某个子类,例如DoubleBuffer子类,该子类能写入的数据类型是double类 型,如果在创建实例时其capacity是100,那么我们最多可以写入100个double类型的数据。

**2. position属性 **

Buffer类的position属性,表示当前的位置。position属性的值与缓冲区的读写模式有 关。在不同的模式下,position属性值的含义是不同的,在缓冲区进行读写的模式改变时, position值会进行相应的调整。
在写入模式下,position的值变化规则如下:
(1)在刚进入到写入模式时,position值为0,表示当前的写入位置为从头开始。
(2)每当一个数据写到缓冲区之后,position会向后移动到下一个可写的位置。
(3)初始的position值为0,最大可写值为limit–1。当position值达到limit时,缓冲区 就已经无空间可写了。
在读模式下,position的值变化规则如下:
(1)当缓冲区刚开始进入到读取模式时,position会被重置为0。
(2)当从缓冲区读取时,也是从position位置开始读。读取数据后,position向前移动
到下一个可读的位置。
(3)在读模式下,limit表示可以读上限。position的最大值,为最大可读上限limit,当 position达到limit时,表明缓冲区已经无数据可读。
Buffer的读写模式具体如何切换呢?当新建了一个缓冲区实例时,缓冲区处于写入模式,这时是可以写数据的。在数据写入完成后,如果要从缓冲区读取数据,这就要进行模 式的切换,可以使用(即调用)flip翻转方法,将缓冲区变成读取模式。
在从写入模式到读取模式的flip翻转过程中,position和limit属性值会进行调整,具体的
规则是:
(1)limit属性被设置成写入模式时的position值,表示可以读取的最大数据位置;
(2)position由原来的写入位置,变成新的可读位置,也就是0,表示可以从头开始 读。

3. limit属性

Buffer类的limit属性,表示可以写入或者读取的最大上限,其属性值的具体含义,也与 缓冲区的读写模式有关,在不同的模式下,limit的值的含义是不同的,具体分为以下两种情况:
(1)在写入模式下,limit属性值的含义为可以写入的数据最大上限。在刚进入到写入 模式时,limit的值会被设置成缓冲区的capacity容量值,表示可以一直将缓冲区的容量写满。
(2)在读取模式下,limit的值含义为最多能从缓冲区中读取到多少数据。 一般来说,在进行缓冲区操作时,是先写入然后再读取的。当缓冲区写入完成后,就可以开始从Buffer读取数据,可以使用flip翻转方法,这时,limit的值也会进行调整。具体 如何调整呢?将写入模式下的position值,设置成读取模式下的limit值,也就是说,将之前 写入的最大数量,作为可以读取的上限值。
Buffer在flip翻转时的属性值调整,主要涉及position、limit两个属性,但是这种调整比 较微妙,不是太好理解,下面是一个简单例子:
首先,创建缓冲区。新创建的缓冲区处于写入模式,其position值为0,limit值为最大 容量capacity。

然后,向缓冲区写数据。每写入一个数据,position向后面移动一个位置,也就是 position的值加1。这里假定写入了5个数,当写入完成后,position的值为5。
最后,使用flip方法将缓冲区切换到读模式。limit的值,先会被设置成写入模式时的 position值,所以新的limit值是5,表示可以读取的最大上限是5。之后调整position值,新的
position会被重置为0,表示可以从0开始读。 缓冲区切换到读模式后,就可以从缓冲区读取数据了,一直到缓冲区的数据读取完 毕。
除了以上capacity(容量)、position(读写位置)、limit(读写的限制)三个重要属性之 外,Buffer还有一个比较重要的标记属性:mark(标记)属性。该属性的大致作用为:在
缓冲区操作过程当中,可以将当前的position的值临时存入mark属性中;需要的时候,可以 再从mark中取出暂存的标记值,恢复到position属性中,重新从position位置开始处理。

image.png

allocate()
@Test
    public void allocate(){
        //定义一个buffer缓冲区
        IntBuffer buffer = IntBuffer.allocate(1024);
        System.out.println("capacity:"+buffer.capacity());
        System.out.println("limit:"+buffer.limit());
        System.out.println("position:"+buffer.position());
        System.out.println("mark:"+buffer.mark());
    }

image.png

例子中,IntBuffer是具体的Buffer子类,通过调用IntBuffer.allocate(20),创建了一个 Intbuffer实例对象,并且分配了1024 * 4个字节的内存空间。运行程序之后,通过程序的输出 结果,我们可以查看一个新建缓冲区实例对象的主要属性值,如下所示:

从上面的运行结果,可以看出:一个缓冲区在新建后,处于写入的模式,position属性 (代表写入位置)的值为0,缓冲区的capacity容量值也是初始化时allocate方法的参数值 (这里是20),而limit最大可写上限值也为的allocate方法的初始化参数值

put()
@Test
    public void put(){
        //定义一个buffer缓冲区
        IntBuffer buffer = IntBuffer.allocate(1024);
        for (int i = 0; i < 5; i++) {
            buffer.put(i);
        }
        System.out.println("capacity:"+buffer.capacity());
        System.out.println("limit:"+buffer.limit());
        System.out.println("position:"+buffer.position());
        System.out.println("mark:"+buffer.mark());
    }

image.png

从结果可以看到,写入了5个元素之后,缓冲区的position属性值变成了5,所以指向了 第6个(从0开始的)可以进行写入的元素位置。而limit最大可写上限、capacity最大容量两 个属性的值,都没有发生变化。

filp()翻转

向缓冲区写入数据之后,是否可以直接从缓冲区中读取数据呢?呵呵,不能。为什么 呢?这时缓冲区还处于写模式,如果需要读取数据,还需要将缓冲区转换成读模式。flip()
翻转方法是Buffer类提供的一个模式转变的重要方法,它的作用就是将写入模式翻转成读 取模式。
接着前面的例子,演示一下flip()方法的使用

 @Test
    public void filp(){
        //定义一个buffer缓冲区
        IntBuffer buffer = IntBuffer.allocate(1024);
        for (int i = 0; i < 5; i++) {
            buffer.put(i);
        }
        System.out.println("capacity:"+buffer.capacity());
        System.out.println("limit:"+buffer.limit());
        System.out.println("position:"+buffer.position());
        System.out.println("mark:"+buffer.mark());
        buffer.flip();
        System.out.println("===========flip==========");
        System.out.println("capacity:"+buffer.capacity());
        System.out.println("limit:"+buffer.limit());
        System.out.println("position:"+buffer.position());
        System.out.println("mark:"+buffer.mark());
    }

image.png

调用flip方法后,新模式下可读上限limit的值,变成了之前写入模式下的position属性 值,也就是5;而新的读取模式下的position值,简单粗暴地变成了0,表示从头开始读取。
对flip()方法的从写入到读取转换的规则,再一次详细的介绍如下:
(1)首先,设置可读上限limit的属性值。将写入模式下的缓冲区中内容的最后写入位 置position值,作为读取模式下的limit上限值。
(2)其次,把读的起始位置position的值设为0,表示从头开始读。
(3)最后,清除之前的mark标记,因为mark保存的是写入模式下的临时位置,发生 模式翻转后,如果继续使用旧的mark标记,会造成位置混乱。
有关上面的三步,其实可以查看Buffer.flip()方法的源代码,具体代码如下:
image.png

当然,新的问题来了:在读取完成后,如何再一次将缓冲区切换成写入模式呢?答案 是:可以调用Buffer.clear() 清空或者Buffer.compact()压缩方法,它们可以将缓冲区转换为
写模式。总体的Buffer模式转换,

image.png

get()

使用调用flip方法将缓冲区切换成读取模式之后,就可以开始从缓冲区中进行数据读取 了。读取数据的方法很简单,可以调用get方法每次从position的位置读取一个数据,并且进 行相应的缓冲区属性的调整。

@Test
    public void get(){
        //定义一个buffer缓冲区
        IntBuffer buffer = IntBuffer.allocate(1024);
        for (int i = 0; i < 5; i++) {
            buffer.put(i);
        }
        System.out.println("capacity:"+buffer.capacity());
        System.out.println("limit:"+buffer.limit());
        System.out.println("position:"+buffer.position());
        System.out.println("mark:"+buffer.mark());
        buffer.flip();
        System.out.println("===========flip==========");
        System.out.println("capacity:"+buffer.capacity());
        System.out.println("limit:"+buffer.limit());
        System.out.println("position:"+buffer.position());
        System.out.println("mark:"+buffer.mark());
        System.out.println("======get()=======");
        for (int i = 0; i < 2; i++) {
            int i1 = buffer.get();
        }
        System.out.println("===========flip==========");
        System.out.println("capacity:"+buffer.capacity());
        System.out.println("limit:"+buffer.limit());
        System.out.println("position:"+buffer.position());
        System.out.println("mark:"+buffer.mark());
    }

image.png

从程序的输出结果,我们可以看到,读取操作会改变可读位置position的属性值,而 limit可读上限值并不会改变。在position值和limit的值相等时,表示所有数据读取完成,
position指向了一个没有数据的元素位置,已经不能再读了。此时再读,会抛出 BufferUnderflowException异常。
那么,在读完之后是否可以立即对缓冲区进行数据写入呢?答案是不能。现在还处于 读取模式,我们必须调用Buffer.clear()或Buffer.compact()方法,即清空或者压缩缓冲区,将
缓冲区切换成写入模式,让其重新可写。
此外还有一个问题:缓冲区是不是可以重复读呢?答案是可以的,既可以通过倒带方 法rewind()去完成,也可以通过mark( )和reset( )两个方法组合实现。

rwind()

已经读完的数据,如果需要再读一遍,可以调用rewind()方法。rewind()也叫倒带,就 像播放磁带一样倒回去,再重新播放。
接着前面的示例代码,继续rewind方法使用的演示,示例代码如下


    @Test
    public void rewind(){
        //定义一个buffer缓冲区
        IntBuffer buffer = IntBuffer.allocate(1024);
        for (int i = 0; i < 5; i++) {
            buffer.put(i);
        }
        System.out.println("capacity:"+buffer.capacity());
        System.out.println("limit:"+buffer.limit());
        System.out.println("position:"+buffer.position());
        System.out.println("mark:"+buffer.mark());
        buffer.rewind();
        System.out.println("===========rewind()==========");
        System.out.println("capacity:"+buffer.capacity());
        System.out.println("limit:"+buffer.limit());
        System.out.println("position:"+buffer.position());
        System.out.println("mark:"+buffer.mark());
    }

image.png

rewind ()方法,主要是调整了缓冲区的position属性与mark标记属性,具体的调整规则 如下

(1)position重置为0,所以可以重读缓冲区中的所有数据;
(2)limit保持不变,数据量还是一样的,仍然表示能从缓冲区中读取的元素数量;
(3)mark标记被清理,表示之前的临时位置不能再用了。

image.png

mark()和reset()

mark( )和reset( )两个方法是成套使用的:Buffer.mark()方法将当前position的值保存起来,放在mark属性中,让mark属性记住这个临时位置;之后,可以调用Buffer.reset()方法将 mark的值恢复到position中。

Buffer.mark()和 Buffer.reset()两个方法都涉及到 mark 属性的使用。mark()方法与 mark 属性,二者的名字虽然相同,但是一个是 Buffer 类的成员方法,另一个是 Buffer类的成员属性,不能混淆。

例如,可以在前面重复读取的示例代码中,在读到第3个元素(i为2时)时,可以调用 mark()方法,把当前位置position的值保存到mark属性中,这时mark属性的值为2。
然后,就可以调用reset( )方法,将mark属性的值恢复到position中,这样就可以从位置 2(第三个元素)开始重复读取。
继续接着前面重复读取的代码,进行mark( )方法和reset( )方法的示例演示,代码如 下

 @Test
    public void markAndreset(){
        //定义一个buffer缓冲区
        IntBuffer buffer = IntBuffer.allocate(1024);
        for (int i = 0; i < 5; i++) {
            buffer.put(i);
        }
        System.out.println("===========put()==========");
        System.out.println("capacity:"+buffer.capacity());
        System.out.println("limit:"+buffer.limit());
        System.out.println("position:"+buffer.position());
        buffer.flip();
        System.out.println("===========flip()==========");
        System.out.println("capacity:"+buffer.capacity());
        System.out.println("limit:"+buffer.limit());
        System.out.println("position:"+buffer.position());
        buffer.mark();
        System.out.println("===========mark()==========");
        System.out.println("capacity:"+buffer.capacity());
        System.out.println("limit:"+buffer.limit());
        System.out.println("position:"+buffer.position());
        for (int i = 0; i < 2; i++) {
            buffer.get();
        }
        System.out.println("===========get()==========");
        System.out.println("capacity:"+buffer.capacity());
        System.out.println("limit:"+buffer.limit());
        System.out.println("position:"+buffer.position());
        buffer.reset();
        System.out.println("===========reset()==========");
        System.out.println("capacity:"+buffer.capacity());
        System.out.println("limit:"+buffer.limit());
        System.out.println("position:"+buffer.position());
    }

image.png

clear()清除缓冲区

在读取模式下,调用clear()方法将缓冲区切换为写入模式。此方法的作用:
(1)会将position清零;
(2)limit设置为capacity最大容量值,可以一直写入,直到缓冲区写满。 接着上面的实例,演示一下clear( )方法的使用,大致的代码如下:

 @Test
    public void clear(){
        //定义一个buffer缓冲区
        IntBuffer buffer = IntBuffer.allocate(1024);
        for (int i = 0; i < 5; i++) {
            buffer.put(i);
        }
        System.out.println("===========rewind()==========");
        System.out.println("capacity:"+buffer.capacity());
        System.out.println("limit:"+buffer.limit());
        System.out.println("position:"+buffer.position());
        buffer.flip();
        System.out.println("===========flip()==========");
        System.out.println("capacity:"+buffer.capacity());
        System.out.println("limit:"+buffer.limit());
        System.out.println("position:"+buffer.position());
        buffer.clear();
        System.out.println("===========rewind()==========");
        System.out.println("capacity:"+buffer.capacity());
        System.out.println("limit:"+buffer.limit());
        System.out.println("position:"+buffer.position());

    }

image.png

在缓冲区处于读取模式时,调用clear(),缓冲区会被切换成写入模式。调用clear()之 后,我们可以看到清空了position(写入的起始位置)的值,其值被设置为0,并且limit值 (写入的上限)为最大容量。

基本使用步骤

总体来说,使用Java NIO Buffer类的基本步骤如下:
(1)使用创建子类实例对象的allocate( )方法,创建一个Buffer类的实例对象。
(2)调用put( )方法,将数据写入到缓冲区中。
(3)写入完成后,在开始读取数据前,调用Buffer.flip( )方法,将缓冲区转换为读模 式。
(4)调用get( )方法,可以从缓冲区中读取数据。
(5)读取完成后,调用Buffer.clear( )方法或Buffer.compact()方法,将缓冲区转换为写 入模式,可以继续写入。

Channel通道

前面提到,Java NIO中,一个socket连接使用一个Channel(通道)来表示。然而,从 更广泛的层面来说,一个通道可以表示一个底层的文件描述符,例如硬件设备、文件、网 络连接等。然而,远远不止如此,除了可以对应到底层文件描述符。所以,文件描述符相 对应,Java NIO的通道可以更加细化。例如,对应不同的网络传输协议类型,在Java中都 有不同的NIO Channel(通道)实现

Channel类似于流,但是Channel不能直接访问数据,只可以于buffer进行交互

类型

这里仅仅对我们经常使用到的通道进行介绍

(1)FileChannel文件通道,用于文件的数据读写;
(2)SocketChannel套接字通道,用于Socket套接字TCP连接的数据读写;
(3)ServerSocketChannel服务器套接字通道(或服务器监听通道),允许我们监听TCP 连接请求,为每个监听到的请求,创建一个SocketChannel套接字通道;
(4)DatagramChannel数据报通道,用于UDP协议的数据读写。 这个四种通道,涵盖了文件IO、TCP网络、UDP IO三类基础IO读写操作。下面从通道 的获取、读取、写入、关闭四个重要的操作入手,对四种通道进行简单的介绍。

FileChannel文件通道

FileChannel是专门操作文件的通道。通过FileChannel,既可以从一个文件中读取数 据,也可以将数据写入到文件中。特别申明一下,FileChannel为阻塞模式,不能设置为非阻塞模式。
下面分别介绍:FileChannel的获取、读取、写入、关闭四个操作

常用方法
int  read(ByteBuffer dst) 		 从 Channel 中读取数据到 ByteBuffer
long read(ByteBuffer[] dsts) 	 将 Channel 中的数据“分散”到 ByteBuffer[]
int  write(ByteBuffer src) 	 	 将 ByteBuffer 中的数据写入到 Channel
long write(ByteBuffer[] srcs)    将 ByteBuffer[] 中的数据“聚集”到 Channel
long position() 				 返回此通道的文件位置
FileChannel position(long p) 	 设置此通道的文件位置
long size() 					 返回此通道的文件的当前大小
FileChannel truncate(long s) 	 将此通道的文件截取为给定大小
void force(boolean metaData) 	 强制将所有对此通道的文件更新写入到存储设备中
获取FileChannel通道的方式

可以通过文件的输入流、输出流获取FileChannel文件通道,示例如下:

@Test
    public void input1() throws FileNotFoundException {
        File file = new File("D:\\highconcurrency\\myhighconcurrency\\test.txt");
        /*创建一个文件输入流*/
        FileInputStream fileInputStream = new FileInputStream(file);
        /*获取文件输入流的通道*/
        FileChannel inputStreamChannel = fileInputStream.getChannel();
    }

也可以通过RandomAccessFile文件随机访问类,获取FileChannel文件通道实例,代码
如下:

 /*第2个获取方法*/
    @Test
    public void input2() throws FileNotFoundException {
        File file = new File("D:\\highconcurrency\\myhighconcurrency\\test.txt");
        RandomAccessFile accessFile = new RandomAccessFile(file, "rw");
        FileChannel channel = accessFile.getChannel();
    }
向本地文件写入数据
@Test
    public void getByBuffer() throws IOException {
        File file = new File("D:\\highconcurrency\\myhighconcurrency\\test.txt");
        /*获取输出流*/
        FileOutputStream fileOutputStream = new FileOutputStream(file);
        /*获取通道*/
        FileChannel channel = fileOutputStream.getChannel();
        /*向缓冲区中写入数据*/
        String message="Hello Word!!!";
        byte[] bytes = message.getBytes();
        ByteBuffer buffer = ByteBuffer.allocate(bytes.length);
        buffer.put(bytes);
        /*翻转缓冲区*/
        buffer.flip();
        channel.write(buffer);
        /*关闭通道*/
        channel.close();
        System.out.println("成功写入数据!!!");
    }

image.png

image.png

读取本地数据
 @Test
    public void test2(){
        try {
            /*1.创建一个输入流*/
            FileInputStream fileInputStream = new FileInputStream("D:\\highconcurrency\\myhighconcurrency\\test.txt");
            /*2.获取Channel*/
            FileChannel channel = fileInputStream.getChannel();
            /*3.创建一个缓冲区*/
            ByteBuffer buffer = ByteBuffer.allocate(1024);
            /*4.将通道中的数据输出到缓冲区中*/
            channel.read(buffer);
            channel.close();
            buffer.flip();
            /*5.输出缓冲区中的数据*/
            String s = new String(buffer.array(), 0, buffer.remaining());
            System.out.println("文件中的数据为:"+s);
        } catch (FileNotFoundException e) {
            e.printStackTrace();
        } catch (IOException e) {
            e.printStackTrace();
        }
    }

image.png

强制刷新到磁盘

在将缓冲区写入通道时,出于性能原因,操作系统不可能每次都实时将写入数据落地 (或刷新)到磁盘,完成最终的数据保存。
如果在将缓冲数据写入通道时,需要保证数据能落地写入到磁盘,可以在写入后调用 一下FileChannel的force()方法。
//强制刷新到磁盘
channel.force(true);

使用fileChannel完成文件的复制
 /*通过channel复制数据*/
    @Test
    public void copyFile() throws IOException {
        copyFile2();
    }
    public void copyFile2() throws IOException {
        String srcPath = "D:\\highconcurrency\\myhighconcurrency\\source.txt";
        String destPath = "D:\\highconcurrency\\myhighconcurrency\\destion.txt";
        File srcFile = new File(srcPath);
        File destFile = new File(destPath);
        try {
            //如果目标文件不存在,则新建
            if (!destFile.exists()) {
                destFile.createNewFile();
            }
        }catch (IOException ioException) {
            ioException.printStackTrace();
        }
        FileInputStream fis = null;
        FileOutputStream fos = null;
        FileChannel inChannel = null; //输入通道
        FileChannel outchannel = null; //输出通道
        try{
            fis = new FileInputStream(srcFile);
            fos = new FileOutputStream(destFile);
            inChannel = fis.getChannel();
            outchannel = fos.getChannel();
            int length = -1;
            //新建 buf,处于写入模式
            ByteBuffer buf = ByteBuffer.allocate(1024);
            while((length=inChannel.read(buf))!=-1){
                /*翻转缓冲区*/
                Buffer flip = buf.flip();
                int outlength = 0;
                /*将数据写入到输出通道*/
                while ((outlength = outchannel.write(buf)) != 0) {
                    System.out.println("写入的字节数:" + outlength);
                }
                /*清空缓存区,进行下一次的复制*/
                buf.clear();
            }
            /*强制刷新到磁盘*/
            outchannel.force(true);

        } catch (IOException e) {
            e.printStackTrace();
        }finally {
            inChannel.close();
            outchannel.close();
            fis.close();
            fos.close();
        }
    }

除了FileChannel的通道操作外,还需要注意代码执行过程中隐藏的ByteBuffer的模式切 换。由于新建的ByteBuffer是写入模式,才可作为inChannel.read(ByteBuffer)方法的参 数,inChannel.read(…)方法将从通道inChannel读到的数据写入到ByteBuffer。然后,需 要调用缓冲区的flip方法,将ByteBuffer从写入模式切换成读取模式,才能作为 outchannel.write(ByteBuffer)方法的参数,以便从ByteBuffer读取数据,最终写入到 outchannel输出通道。
完成一次复制之后,在进入下一次复制前,还要进行一次缓冲区的模式切换。此时, 需要将通过clear方法将Buffer切换成写入模式,才能进入下一次的复制。所以,在示例代码 中,每一轮外层的while循环,都需要两次ByteBuffer模式切换:第一次模式切换时,翻转 buf,变成读取模式;第二次模式切换时,清除buf,变成写入模式。
上面的示例代码,主要的目的在于:演示文件通道以及字节缓冲区的使用。然而,作 为文件复制的程序来说,以上实战代码的效率不是最高的。更高效的文件复制,可以调用 文件通道的transferFrom方法。具体的代码,可以参见源代码工程中的 FileNIOFastCopyDemo类,完整源文件的路径为:
com.crazymakercircle.iodemo.fileDemos.FileNIOFastCopyDemo
请大家在随书源码工程中自行运行和学习以上代码,这里不做赘述。

SocketChannel套接字通道

在NIO中,涉及网络连接的通道有两个:一个是SocketChannel负责连接的数据传输, 另一个是ServerSocketChannel负责连接的监听。其中,NIO中的SocketChannel传输通道,与 OIO中的Socket类对应;NIO中的ServerSocketChannel监听通道,对应于OIO中的 ServerSocket类。
ServerSocketChannel仅仅应用于服务器端,而SocketChannel则同时处于服务器端和客 户端,所以,对应于一个连接,两端都有一个负责传输的SocketChannel传输通道。
无论是ServerSocketChannel,还是SocketChannel,都支持阻塞和非阻塞两种模式。如何进行模式的设置呢?调用configureBlocking方法,具体如下:
(1)socketChannel.configureBlocking(false)设置为非阻塞模式。
(2)socketChannel.configureBlocking(true)设置为阻塞模式。
在阻塞模式下,SocketChannel通道的connect连接、read读、write写操作,都是同步的 和阻塞式的,在效率上与Java旧的OIO的面向流的阻塞式读写操作相同。因此,在这里不 介绍阻塞模式下的通道的具体操作。在非阻塞模式下,通道的操作是异步、高效率的,这 也是相对于传统的OIO的优势所在。下面仅仅详细介绍在非阻塞模式下通道的打开、读写和关闭操作等操作。

获取SocketChannel传输通道

在客户端,先通过SocketChannel静态方法open()获得一个套接字传输通道;然后,将socket套接字设置为非阻塞模式;最后,通过connect()实例方法,对服务器的IP和端口发起连接。

public class ClientSocket {
    public static void main(String[] args) throws IOException {
        /*获取一个套接字传输通道*/
        SocketChannel socketChannel = SocketChannel.open();
        socketChannel.bind(new InetSocketAddress("127.0.0.1",9999));
        System.out.println("socketChannel:"+socketChannel.toString());
        /*将通道设置未非阻塞模式*/
        socketChannel.configureBlocking(false);
        /*对服务器的IP与端口发起连接*/
        socketChannel.connect(new InetSocketAddress("127.0.0.1",80));
        /*非阻塞情况下,与服务器的连接可能还没有真正建立,socketChannel.connect方法就返
        回了,因此需要不断地自旋,检查当前是否是连接到了主机*/
        while (!socketChannel.finishConnect()){
            System.out.println("=====自旋======");
        }
    }
}

在服务器端,如何获取与客户端对应的传输套接字呢? 在连接建立的事件到来时,服务器端的ServerSocketChannel能成功地查询出这个新连 接事件,并且通过调用服务器端ServerSocketChannel监听套接字的accept()方法,来获取新 连接的套接字通道:

public class ServerSocket {
    public static void main(String[] args) throws IOException {
        /*通过serverSocket来获取连接*/
        ServerSocketChannel serverSocketChannel = ServerSocketChannel.open();
        /*绑定端口*/
        serverSocketChannel.bind(new InetSocketAddress("127.0.0.1",80));
        System.out.println("serverSocketChannel:"+serverSocketChannel.toString());
        /*获取连接,这里会一直阻塞直到有连接到来*/
        SocketChannel accept = serverSocketChannel.accept();
        System.out.println("accept:"+accept.toString());
        /*设置为非阻塞模式*/
        accept.configureBlocking(false);
    }
}

使用SocketChannel发送文件的案例

下面的实践案例是使用FileChannel文件通道读取本地文件内容,然后在客户端使用 SocketChannel套接字通道,把文件信息和文件内容发送到服务器。客户端的完整代码如下

ServerFileSocket

public class ServerFileSocket {
    public static void main(String[] args) throws IOException {
        /*创建一个serverSocketChannel*/
        ServerSocketChannel serverSocketChannel = ServerSocketChannel.open();
        /*绑定端口*/
        serverSocketChannel.bind(new InetSocketAddress("127.0.0.1", 9001));
        /*调用accept方法等待连接的到来*/
        SocketChannel socketChannel = serverSocketChannel.accept();
        /*创建缓冲区*/
        ByteBuffer buffer = ByteBuffer.allocate(1024);
        /*创建输出数据的通道*/
        File file = new File("D:\\highconcurrency\\myhighconcurrency\\destion.txt");
        RandomAccessFile accessFile = new RandomAccessFile(file,"rw");
        FileChannel fileChannel = accessFile.getChannel();
        int length=0;
        while((length= socketChannel.read(buffer))!=-1){
            /*翻转缓冲区*/
            buffer.flip();
            String string = new String(buffer.array(),buffer.position(),buffer.remaining());
            System.out.println(string);
            /*调用fileChannel输出数据*/
            fileChannel.write(buffer);
            /*清空缓冲区*/
            buffer.clear();
        }
        /*关闭通道*/
        socketChannel.close();
        serverSocketChannel.close();
    }
}

ClientSocket

public class ClientSocket {
    public static void main(String[] args) throws IOException {
        /*创建客户端连接对象*/
        SocketChannel socketChannel = SocketChannel.open();
        /*绑定端口*/
        socketChannel.bind(new InetSocketAddress("127.0.0.1", 9000));
        /*向服务器端发起连接*/
        socketChannel.connect(new InetSocketAddress("127.0.0.1", 9001));
        /*设置为非阻塞模式*/
        socketChannel.configureBlocking(false);
        /*从本地文件中读取数据并发送给远程的客户端*/
        File file = new File("D:\\highconcurrency\\myhighconcurrency\\source.txt");
        RandomAccessFile accessFile = new RandomAccessFile(file,"rw");
        FileChannel fileChannel = accessFile.getChannel();
        /*创建缓冲区*/
        ByteBuffer buffer = ByteBuffer.allocate(1024);
        int length=0;
        while((length=fileChannel.read(buffer))!=0){
            /*使用socketChannel发送消息*/
            /*翻转buffer*/
            buffer.flip();
            socketChannel.write(buffer);
            /*清空缓冲区进行下一次的发送*/
            buffer.clear();
        }
        /*关闭通道*/
        fileChannel.close();
        socketChannel.close();
    }
}

DatagramChannel数据报通道

在Java中使用UDP协议传输数据,比TCP协议更加简单。和Socket套接字的TCP传输协 议不同,UDP协议不是面向连接的协议。使用UDP协议时,只要知道服务器的IP和端口, 就可以直接向对方发送数据。在Java NIO中,使用DatagramChannel数据报通道来处理UDP 协议的数据传输。

1. 获取DatagramChannel数据报通道

获取数据报通道的方式很简单,调用DatagramChannel类的open静态方法即可。然后调用configureBlocking(false)方法,设置成非阻塞模式。
//获取 DatagramChannel 数据报通道
DatagramChannel channel = DatagramChannel.open();
//设置为非阻塞模式
datagramChannel.configureBlocking(false);
如果需要接收数据,还需要调用bind方法绑定一个数据报的监听端口,具体如下:
//调用 bind 方法绑定一个数据报的监听端口
channel.socket().bind(new InetSocketAddress(18080));

2. 读取DatagramChannel数据报通道数据

当DatagramChannel通道可读时,可以从DatagramChannel读取数据。和前面的 SocketChannel读取方式不同,这里不调用read方法,而是调用receive(ByteBufferbuf)方法 将数据从DatagramChannel读入,再写入到ByteBuffer缓冲区中。

//创建缓冲区 
ByteBuffer buf = ByteBuffer.allocate(1024); 
//从 DatagramChannel 读入,再写入到 ByteBuffer 缓冲区 
SocketAddress clientAddr= datagramChannel.receive(buf); 

通道读取receive(ByteBufferbuf)方法虽然读取了数据到buf缓冲区,但是其返回值是 SocketAddress类型,表示返回发送端的连接地址(包括IP和端口)。通过receive方法读取数 据非常简单,但是,在非阻塞模式下,如何知道DatagramChannel通道何时是可读的呢?和 SocketChannel一样,同样需要用到NIO的新组件—Selector通道选择器,稍后介绍。

3. 写入DatagramChannel数据报通道 向DatagramChannel发送数据,和向SocketChannel通道发送数据的方法也是不同的。这 里不是调用write方法,而是调用send方法。示例代码如下:
//把缓冲区翻转到读取模式 
buffer.flip(); 
//调用 send 方法,把数据发送到目标 IP+端口 
dChannel.send(buffer, new InetSocketAddress("127.0.0.1",18899)); 
//清空缓冲区,切换到写入模式 
buffer.clear(); 
由于UDP是面向非连接的协议,因此,在调用send方法发送数据的时候,需要指定接 收方的地址(IP和端口)。
4. 关闭DatagramChannel数据报通道 这个比较简单,直接调用close()方法,即可关闭数据报通道。
//简单关闭即可 
dChannel.close();
5.使用DatagramChannel数据包通道发送数据的实践案例

下面是一个使用DatagramChannel数据包通到发送数据的客户端示例程序代码。其功能 是:获取用户的输入数据,通过DatagramChannel数据报通道,将数据发送到远程的服务 器。客户端的完整程序代码如下:

public class ClientChannel {
    public static void main(String[] args) throws IOException {
        /*创建客户端连接对象*/
        DatagramChannel datagramChannel = DatagramChannel.open();
        /*绑定端口*/
       datagramChannel.bind(new InetSocketAddress("127.0.0.1", 9000));
        /*向服务器端发起连接*/
        /*设置为非阻塞模式*/
        datagramChannel.configureBlocking(false);
        /*从本地文件中读取数据并发送给远程的客户端*/
        File file = new File("D:\\highconcurrency\\myhighconcurrency\\source.txt");
        RandomAccessFile accessFile = new RandomAccessFile(file,"rw");
        FileChannel fileChannel = accessFile.getChannel();
        /*创建缓冲区*/
        ByteBuffer buffer = ByteBuffer.allocate(1024);
        int length=0;
        while((length=fileChannel.read(buffer))!=0){
            /*翻转buffer*/
            buffer.flip();
            /*使用socketChannel发送消息*/
            datagramChannel.send(buffer,new InetSocketAddress("127.0.0.1", 9001));
            /*清空缓冲区进行下一次的发送*/
            buffer.clear();
        }
        /*关闭通道*/
        fileChannel.close();
        datagramChannel.close();
    }
}

在服务器端,首先调用了bind方法绑定datagramChannel的监听端口。当数据到来后, 调用了receive方法,从datagramChannel数据包通道接收数据,再写入到ByteBuffer缓冲区中。
在服务器端代码中,为了监控数据的到来,使用了Selector选择器。什么是选择器?如 何使用选择器呢?欲知后事如何,请听下节分解。

public class ServerSocketChannel {
    public static void main(String[] args) throws IOException {
        /*创建一个serverSocketChannel*/
        DatagramChannel datagramChannel = DatagramChannel.open();
        /*设置为非阻塞模式否则会报错*/
        datagramChannel.configureBlocking(false);
        /*绑定端口*/
        datagramChannel.bind(new InetSocketAddress("127.0.0.1", 9001));
        /*创建缓冲区*/
        ByteBuffer buffer = ByteBuffer.allocate(1024);
        /*创建输出数据的通道*/
        File file = new File("D:\\highconcurrency\\myhighconcurrency\\destion2.txt");
        RandomAccessFile accessFile = new RandomAccessFile(file,"rw");
        FileChannel fileChannel = accessFile.getChannel();
        int length=0;
        /*创建一个选择器*/
        Selector selector = Selector.open();
        /*将通道注册到选择器中*/
        /*在构造方法中注明选择器与对应的读写事件*/
         datagramChannel.register(selector, SelectionKey.OP_READ);
        /*该方法会一直阻塞直到有对应通道的就绪事件发送*/
        while(selector.select()>0){
            /*获取全部的的就绪事件*/
            Iterator<SelectionKey> iterator = selector.selectedKeys().iterator();
            while (iterator.hasNext()){
                SelectionKey selectionKey = iterator.next();
                /*判断是否有可读的事件发生*/
                if (selectionKey.isReadable()){
                    System.out.println("====可读事件发生=====");
                    datagramChannel.receive(buffer);
                    /*翻转缓冲区*/
                    buffer.flip();
                    String string = new String(buffer.array(),buffer.position(),buffer.remaining());
                    System.out.println(string);
                    /*调用fileChannel输出数据*/
                    fileChannel.write(buffer);
                    /*清空缓冲区*/
                    buffer.clear();
                }
            }
        }
        /*关闭通道*/
        datagramChannel.close();
       fileChannel.close();
    }
}

选择器

概述

选择器用于监听多个通道,一般是一个线程处理一个选择器,即一个选择器监听的多个通道,,通过选择器,一个单线程可以处理数百、数千、数万、甚至更多的通道。在极端情况下(数万个连接),只用一个线程就可以处理所有的通道,这样会大量地减少线程之间上 下文切换的开销。
选择器与通道直接的关系可以说是一种监控与被监控的关系。
通道以事件的方式注册到选择器,事件包括以下四种
(1)可读:SelectionKey.OP_READ
(2)可写:SelectionKey.OP_WRITE
(3)连接:SelectionKey.OP_CONNECT
(4)接收:SelectionKey.OP_ACCEPT

以上的事件类型常量定义在SelectionKey类中。如果选择器要监控通道的多种事件,可 以用“按位或”运算符来实现。例如,同时监控可读和可写IO事件:
//监控通道的多种事件,用“按位或”运算符来实现
int key = SelectionKey.OP_READ | SelectionKey.OP_WRITE ;

何为IO事件(通道处于某种IO操作的就绪状态,即IO操作已经完成的状态)

这个概念容易混淆,这里特别说明一下。这里的IO事件不是对通道的IO操作,而是通道处于某个IO操作的就绪状态,表示通道具备执行某个IO操作的条件。比方说某个 SocketChannel传输通道,如果完成了和对端的三次握手过程,则会发生“连接就绪” (OP_CONNECT)的事件。再比方说某个ServerSocketChannel服务器连接监听通道,在监 听到一个新连接的到来时,则会发生“接收就绪”(OP_ACCEPT)的事件。还比方说,一 个SocketChannel通道有数据可读,则会发生“读就绪”(OP_READ)事件;一个等待写入 数据的SocketChannel通道,会发生写就绪(OP_WRITE)事件。

可选择通道

只有允许非阻塞的通道才可以注册到选择器中。

并不是所有的通道,都是可以被选择器监控或选择的。比方说,FileChannel文件通道 就不能被选择器复用。判断一个通道能否被选择器监控或选择,有一个前提:判断它是否 继承了抽象类SelectableChannel(可选择通道),如果是则可以被选择,否则不能。
简单地说,一条通道若能被选择,必须继承SelectableChannel类。
SelectableChannel类,是何方神圣呢?它提供了实现通道的可选择性所需要的公共方 法。Java NIO中所有网络链接Socket套接字通道,都继承了SelectableChannel类,都是可选 择的。而FileChannel文件通道,并没有继承SelectableChannel,因此不是可选择通道。

为什么FileChannel不支持非阻塞

以下是我个人的一些观点,大家看看就好。

没必要。因为FileChannel都是在本地进行数据的操作,速度很快,不像SocketChannel是一种主机与主机之间的远程通信,速度比较慢,而cpu又是计算机中极其珍贵的资源,如果线程占用cpu资源却什么也不干,这是一种十分浪费的资源的行为,因而需要非主色。如果FileChannel专门提供一个非阻塞的API可能还会显得很鸡肋。

某网友观点:

即使是非阻塞,在IO的过程时,也是阻塞的,那么对于文件类的IO,你要设计成非阻塞,无非就是在拿文件的描述符时,但这样的非阻塞是没有意义的,因为拿到一个文件描述符(读写时需要判断文件可读、可写)的过程是很快的,如果判断改文件可读,就可以进行IO操作了,如果你前面有一个非常慢的IO,那么你就得排队,因为IO操作的过程是阻塞的,不可能同时读两个文件。

SelectionKey选择键

通道和选择器的监控关系注册成功后,就可以选择就绪事件。具体的选择工作,和调 用选择器Selector的select( )方法来完成。通过select方法,选择器可以不断地选择通道中所 发生操作的就绪状态,返回注册过的感兴趣的那些IO事件。换句话说,一旦在通道中发生 了某些IO事件(就绪状态达成),并且是在选择器中注册过的IO事件,就会被选择器选中,并放入SelectionKey选择键的集合中
这里出现一个新的概念——SelectionKey选择键。SelectionKey选择键是什么呢?简单 地说,SelectionKey选择键就是那些被选择器选中的IO事件。前面讲到,一个IO事件发生 (就绪状态达成)后,如果之前在选择器中注册过,就会被选择器选中,并放入 SelectionKey选择键集合中;如果之前没有注册过,即使发生了IO事件,也不会被选择器选 中。SelectionKey选择键和IO的关系,可以简单地理解为:选择键,就是被选中了的IO事 件。
在实际编程时,选择键的功能是很强大的。通过SelectionKey选择键,不仅仅可以获得 通道的IO事件类型,比方说SelectionKey.OP_READ;还可以获得发生IO事件所在的通道; 另外,也可以获得选出选择键的选择器实例。

选择器的使用流程

使用选择器,主要有以下三步:
(1)获取选择器实例;
(2)将通道注册到选择器中;
(3)轮询感兴趣的IO就绪事件(选择键集合)。
第一步:获取选择器实例。选择器实例是通过调用静态工厂方法open()来获取的,具
体如下:

//调用静态工厂方法 open()来获取 Selector 实例
Selector selector = Selector.open();

Selector选择器的类方法open( )的内部,是向选择器SPI(SelectorProvider)发出请求, 通过默认的SelectorProvider(选择器提供者)对象,获取一个新的选择器实例。Java中SPI 全称为(Service Provider Interface,服务提供者接口),是JDK的一种可以扩展的服务提供 和发现机制。Java通过SPI的方式,提供选择器的默认实现版本。也就是说,其他的服务提 供商可以通过SPI的方式,提供定制化版本的选择器的动态替换或者扩展。
第二步:将通道注册到选择器实例。要实现选择器管理通道,需要将通道注册到相应 的选择器上,简单的示例代码如下:

// 2.获取通道
ServerSocketChannelserverSocketChannel = ServerSocketChannel.open();
// 3.设置为非阻塞
serverSocketChannel.configureBlocking(false);
// 4.绑定连接
serverSocketChannel.bind(new InetSocketAddress(18899));
// 5.将通道注册到选择器上,并制定监听事件为:“接收连接”事件
serverSocketChannel.register(selector,SelectionKey.OP_ACCEPT);

上面通过调用通道的register(…)方法,将ServerSocketChannel通道注册到了一个选择器 上。当然,在注册之前,首先需要准备好通道。
这里需要注意:注册到选择器的通道,必须处于非阻塞模式下,否则将抛出 IllegalBlockingModeException异常。这意味着,FileChannel文件通道不能与选择器一起使 用,因为FileChannel文件通道只有阻塞模式,不能切换到非阻塞模式;而Socket套接字相 关的所有通道都可以。
其次,还需要注意:一个通道,并不一定要支持所有的四种IO事件。例如服务器监听 通道ServerSocketChannel,仅仅支持Accept(接收到新连接)IO事件;而传输通道 SocketChannel则不同,该类型通道不支持Accept类型的IO事件。
如何判断通道支持哪些事件呢?可以在注册之前,可以通过通道的validOps()方法,来 获取该通道所有支持的IO事件集合。
第三步:选出感兴趣的IO就绪事件(选择键集合)。通过Selector选择器的select()方 法,选出已经注册的、已经就绪的IO事件,并且保存到SelectionKey选择键集合中。SelectionKey集合保存在选择器实例内部,其元素为SelectionKey类型实例。调用选择器的 selectedKeys()方法,可以取得选择键集合。
接下来,需要迭代集合的每一个选择键,根据具体IO事件类型,执行对应的业务操 作。大致的处理流程如下:

//轮询,选择感兴趣的 IO 就绪事件(选择键集合)
while (selector.select() > 0) {
Set selectedKeys = selector.selectedKeys();
Iterator keyIterator = selectedKeys.iterator();
while(keyIterator.hasNext()) {
SelectionKey key = keyIterator.next();
//根据具体的 IO 事件类型,执行对应的业务操作
if(key.isAcceptable()) {
 // IO 事件:ServerSocketChannel 服务器监听通道有新连接
} else if (key.isConnectable()) {
 // IO 事件:传输通道连接成功
} else if (key.isReadable()) {
 // IO 事件:传输通道可读
} else if (key.isWritable()) {
 // IO 事件:传输通道可写
}
//处理完成后,移除选择键
keyIterator.remove();
}

处理完成后,需要将选择键从这个SelectionKey集合中移除,防止下一次循环的时候, 被重复的处理。SelectionKey集合不能添加元素,如果试图向SelectionKey选择键集合中添 加元素,则将抛出java.lang.UnsupportedOperationException异常。
用于选择就绪的IO事件的select()方法,有多个重载的实现版本,具体如下:
(1)select():阻塞调用,一直到至少有一个通道发生了注册的IO事件。
(2)select(long timeout):和select()一样,但最长阻塞时间为timeout指定的毫秒数。
(3)selectNow():非阻塞,不管有没有IO事件,都会立刻返回。
select()方法的返回值的是整数类型(int),表示发生了IO事件的数量。更准确地说, 是从上一次select到这一次select之间,有多少通道发生了IO事件,更加准确地说,是指发
生了选择器感兴趣(注册过)的IO事件数。

使用NIO实现Discard服务器示例

Discard服务器的功能很简单:仅仅读取客户端通道的输入数据,读取完成后直接关闭 客户端通道;并且读取到的数据直接抛弃掉(Discard)。Discard服务器足够简单明了,作 为第一个学习NIO的通信实例,较有参考价值。
下面的Discard服务器代码,其中将选择器使用流程中的步骤进行了进一步细化:

public class ServerSocketChannel {
    public static void main(String[] args) throws IOException {
        /*创建一个serverSocketChannel*/
        java.nio.channels.ServerSocketChannel serverSocketChannel = java.nio.channels.ServerSocketChannel.open();
        /*绑定对应的端口*/
        serverSocketChannel.bind(new InetSocketAddress("127.0.0.1", 9001));
        /*获取连接*/
        SocketChannel socketChannel = serverSocketChannel.accept();
        /*设置为非阻塞模式否则会报错*/
        socketChannel.configureBlocking(false);
        System.out.println("socketChannel的值为:"+socketChannel.toString());
        /*创建缓冲区*/
        ByteBuffer buffer = ByteBuffer.allocate(1024);
        /*创建一个选择器*/
        Selector selector = Selector.open();
        /*将通道注册到选择器中*/
        /*在构造方法中注明选择器与对应的读写事件*/
        socketChannel.register(selector, SelectionKey.OP_READ);
        /*该方法会一直阻塞直到有对应通道的就绪事件发送*/
        while(selector.select()>0){
            /*获取全部的的就绪事件*/
            Iterator<SelectionKey> iterator = selector.selectedKeys().iterator();
            while (iterator.hasNext()){
                SelectionKey selectionKey = iterator.next();
                /*判断是否有可读的事件发生*/
                if (selectionKey.isReadable()){
                    System.out.println("====可读事件发生=====");
                    socketChannel.read(buffer);
                    /*翻转缓冲区*/
                    buffer.flip();
                    String string = new String(buffer.array(),buffer.position(),buffer.remaining());
                    System.out.println(string);
                    /*清空缓冲区*/
                    buffer.clear();
                }
                iterator.remove();
            }
        }
        /*关闭通道*/
        socketChannel.close();
    }
}

实现DiscardServer丢弃服务一共分为16步,其中第7到第15步是循环执行的,不断查询 选择感兴趣的IO事件到选择键集合中,然后通过selector.selectedKeys()获取该选择键集合, 并且进行迭代处理。在事件处理过程中,对于新建立的socketChannel客户端传输通道,也 要注册到同一个选择器上,这样就能使用同一个选择线程,不断地对所有的注册通道进行 选择键的查询。
在DiscardServer程序中,涉及到两次选择器注册:一次是注册serverChannel服务器通 道;另一次,注册接收到的socketChannel客户端传输通道。前者serverChannel服务器通道 所注册的,是新连接的IO事件SelectionKey.OP_ACCEPT;后者客户端传输通道socketChannel所注册的,是可读IO事件SelectionKey.OP_READ。

注册完成后如果有事件发生,也就是DiscardServer在对选择键进行处理时,通过对类 型进行判断,然后进行相应的处理:
(1)如果是SelectionKey.OP_ACCEPT新连接事件类型,代表serverChannel服务器通道 接收到新的客户端连接,发生了新连接事件,则通过服务器通道的accept方法,获取新的 socketChannel传输通道,并且将新通道注册到选择器;
(2)如果是SelectionKey.OP_READ可读事件类型,代表某个客户端通道有数据可 读,则读取选择键中socketChannel传输通道的数据,进行业务处理,这里是直接丢弃数据。
客户端的DiscardClient代码,则更为简单。客户端首先建立到服务器的连接,发送一 些简单的数据,然后直接关闭连接。代码如下:

使用SocketChannel在服务器端接收文件的实践案例

本示例演示文件的接收,是服务器端的程序。和前面介绍的文件发送的SocketChannel 客户端程序是相互配合使用的。由于在服务器端,需要用到选择器,所以,一直到此处完 成了选择器的知识介绍之后,才姗姗来迟开始介绍NIO文件传输的Socket服务器端程序。服 务器端接收文件的示例代码如下所示:

public class Server {
    /*文件路径*/
    private  static String Receive_FilePath="D:\\highconcurrency\\myhighconcurrency\\source.txt";
    /*编码格式*/
    private static Charset charset = Charset.forName("UTF-8");
    /*服务器端保存的客户端对象*/
    static  class ClientEntity{
        //文件名称
        String fileName;
        //长度
        long fileLength;
        //开始传输时间
        long startTime;
        //客户端的地址
        InetSocketAddress remoteSocketAddress;
        //输出的文件通道
        FileChannel outChannel;
        //接收文件的长度
        long receiveLength;
        /*判断文件是否接收完成*/
        public boolean isFinished(){
            return receiveLength>=fileLength;
        }
    }
    //定义一个缓冲区
    private ByteBuffer buffer = ByteBuffer.allocate(NioDemoConfig.SERVER_BUFFER_SIZE);
    //使用 Map 保存每个客户端传输//当 OP_READ 通道可读时,根据 channel 找到对应的对象
    Map<SelectableChannel, ClientEntity> clientMap = new HashMap<SelectableChannel,ClientEntity>();
    /*服务器启动方法*/
    public void startServer() throws IOException {
        /*获取选择器*/
        Selector selector = Selector.open();
        /*获取一个ServerSocketChannel*/
        ServerSocketChannel serverSocketChannel = ServerSocketChannel.open();
        /*设置为非阻塞*/
        serverSocketChannel.configureBlocking(false);
        /*绑定端口*/
        serverSocketChannel.bind(new InetSocketAddress(9001));
        /*将serverSocket注册到选择器上*/
        serverSocketChannel.register(selector,SelectionKey.OP_ACCEPT);
        System.out.println("ServerSocket正在监听=====");
        /*轮询事件*/
        while(selector.select()>0){
            /*获取selectionkey的集合*/
            Iterator<SelectionKey> iterator = selector.selectedKeys().iterator();
            while(iterator.hasNext()){
                SelectionKey key = iterator.next();
                /*判断事件的类型*/
                if (key.isAcceptable()){
                    ServerSocketChannel channel = (ServerSocketChannel) key.channel();
                    SocketChannel socketChannel = channel.accept();
                    /*将新的channel注册到selector*/
                    socketChannel.configureBlocking(false);
                    socketChannel.register(selector,SelectionKey.OP_READ);
                    //余下为业务处理
                    ClientEntity client = new ClientEntity();
                    client.remoteSocketAddress=(InetSocketAddress) socketChannel.getRemoteAddress();
                    clientMap.put(socketChannel,client);
                    System.out.println("连接建立完成!!!");
                }else if(key.isReadable()){
                    /*调用读事件处理函数*/
                    processData(key);
                }
                /*移除key*/
                iterator.remove();
            }
        }
    }

    private void processData(SelectionKey key) {
        /*处理客户端传输过来的消息*/
        ClientEntity client = clientMap.get(key.channel());
        SocketChannel socketChannel = (SocketChannel) key.channel();
        int num=0;
        try{
            buffer.clear();
            while((num=socketChannel.read(buffer))>0){
                /*翻转缓冲区*/
                buffer.flip();
                if(null==client.fileName){
                    if (buffer.capacity() < 4)
                    {
                        continue;
                    }
                    /*取出缓冲区中的内容*/
                    int fileNameLen = buffer.getInt();
                    byte[] fileNameBytes = new byte[fileNameLen];
                    buffer.get(fileNameBytes);
                    /*文件名*/
                    String fileName = new String(fileNameBytes, charset);
                    File directory = new File(Receive_FilePath);
                    if(directory.exists()){
                        directory.mkdir();
                    }
                    System.out.println("NIO 传输目标 dir:"+directory.toString());
                    client.fileName=fileName;
                    String fullName = directory.getAbsolutePath() + File.separatorChar + fileName;
                    System.out.println("传输目标文件:"+fullName);
                    File file = new File(fullName.trim());
                    if (!file.exists())
                    {
                        file.createNewFile();
                    }
                    FileChannel fileChannel =
                            new FileOutputStream(file).getChannel();
                    client.outChannel = fileChannel;
                    if (buffer.capacity() < 8)
                    {
                        continue;
                    }
                    // 文件长度
                    long fileLength = buffer.getLong();
                    client.fileLength = fileLength;
                    client.startTime = System.currentTimeMillis();
                    System.out.println("NIO 传输开始:");

                    client.receiveLength += buffer.capacity();
                    if (buffer.capacity() > 0)
                    {
                        // 写入文件
                        client.outChannel.write(buffer);
                    }
                    if (client.isFinished())
                    {
                        finished(key, client);
                    }
                    buffer.clear();
                }else {
                    client.receiveLength += buffer.capacity();
                    // 写入文件
                    client.outChannel.write(buffer);
                    if (client.isFinished())
                    {
                        finished(key, client);
                    }
                    buffer.clear();
                }
                key.cancel();
            }
            //客户端发送过来的,最后是文件内容
            } catch (IOException e) {
            e.printStackTrace();
        }
        if (num == -1)
        {
            finished(key, client);
            buffer.clear();
        }
    }

    private void finished(SelectionKey key, ClientEntity client) {
        try {
            client.outChannel.close();
            System.out.println("上传完毕");
            key.cancel();
            System.out.println("文件接收成功,File Name:" + client.fileName);
            System.out.println(" Size:" + client.fileLength);
            long endTime = System.currentTimeMillis();
            System.out.println("NIO IO 传输毫秒数:" +
                    (endTime - client.startTime));
        } catch (IOException e) {
            e.printStackTrace();
        }
    }

    public static void main(String[] args) throws IOException {
        Server server = new Server();
        server.startServer();
    }
}

由于客户端每次传输文件,都会分为多次传输:
(1)首先传入文件名称;
(2)其次是文件大小;
(3)然后是文件内容。
对应于每一个客户端socketChannel,创建一个Client客户端对象,用于保存客户端状 态,分别保存文件名、文件大小和写入的目标文件通道outChannel。
socketChannel和Client对象之间是一对一的对应关系:建立连接的时候,以键值对的形 式保存Client实例在map中,其中socketChannel作为键(Key),Client对象作为值(Value)。 当socketChannel传输通道有数据可读时,通过选择键key.channel()方法,取出IO事件所在 socketChannel通道。然后通过socketChannel通道,从map中取到对应的Client对象。
接收到数据时,如果文件名为空,先处理文件名称,并把文件名保存到Client对象,同 时创建服务器上的目标文件;接下来再读到数据,说明接收到了文件大小,把文件大小保 存到Client对象;接下来再接到数据,说明是文件内容了,则写入Client对象的outChannel文 件通道中,直到数据读取完毕。
运行方式:启动这个NioReceiveServer服务器程序后,再启动前面介绍的客户端程序 NioSendClient,即可以完成文件的传输。
由于NIO传输是非阻塞的、异步的,所以,在传输过程中会出现“粘包”和“半包” 问题。正因为这个原因,无论是前面NIO文件传输实例、还是Discard服务器程序,都会在 传输过程中的出现异常现象(偶现)。由于以上的实例,在生产过程中不会使用,仅仅是为 了大家学习NIO的知识,所以,没有为了解决“粘包”和“半包”问题而将代码编写得很 复杂。

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值