NIO的整体认识

NIO、BIO、AIO的区别,及NIO的应用和框架选型-mikechen的互联网架构

AIO、BIO、NIO的区别

IO模型主要分类:

  •  同步(synchronous) IO和异步(asynchronous) IO
  •  阻塞(blocking) IO和非阻塞(non-blocking)IO
  •  同步阻塞(blocking-IO)简称BIO
  •  同步非阻塞(non-blocking-IO)简称NIO
  •  异步非阻塞(synchronous-non-blocking-IO)简称AIO

1.BIO (同步阻塞I/O模式)

数据的读取写入必须阻塞在一个线程内等待其完成。

这里使用那个经典的烧开水例子,这里假设一个烧开水的场景,有一排水壶在烧开水,BIO的工作模式就是, 叫一个线程停留在一个水壶那,直到这个水壶烧开,才去处理下一个水壶。但是实际上线程在等待水壶烧开的时间段什么都没有做。

2.NIO(同步非阻塞)

同时支持阻塞与非阻塞模式,但这里我们以其同步非阻塞I/O模式来说明,那么什么叫做同步非阻塞?如果还拿烧开水来说,NIO的做法是叫一个线程不断的轮询每个水壶的状态,看看是否有水壶的状态发生了改变,从而进行下一步的操作。

3.AIO (异步非阻塞I/O模型)

异步非阻塞与同步非阻塞的区别在哪里?异步非阻塞无需一个线程去轮询所有IO操作的状态改变,在相应的状态改变后,系统会通知对应的线程来处理。对应到烧开水中就是,为每个水壶上面装了一个开关,水烧开之后,水壶会自动通知我水烧开了。

4.IO与NIO区别

NIO、BIO、AIO的区别,及NIO的应用和框架选型-mikechen的互联网架构

5.同步与异步的区别

同步:发送一个请求,等待返回,再发送下一个请求,同步可以避免出现死锁,脏读的发生。

异步:发送一个请求,不等待返回,随时可以再发送下一个请求,可以提高效率,保证并发。

6.阻塞和非阻塞

阻塞:传统的IO流都是阻塞式的。也就是说,当一个线程调用read()或者write()方法时,该线程将被阻塞,直到有一些数据读读取或者被写入,在此期间,该线程不能执行其他任何任务。在完成网络通信进行IO操作时,由于线程会阻塞,所以服务器端必须为每个客户端都提供一个独立的线程进行处理,当服务器端需要处理大量的客户端时,性能急剧下降。

非阻塞:Java
NIO是非阻塞式的。当线程从某通道进行读写数据时,若没有数据可用时,该线程会去执行其他任务。线程通常将非阻塞IO的空闲时间用于在其他通道上执行IO操作,所以单独的线程可以管理多个输入和输出通道。因此NIO可以让服务器端使用一个或有限几个线程来同时处理连接到服务器端的所有客户端。

7.BIO、NIO、AIO适用场景

  •  BIO方式适用于连接数目比较小且固定的架构,这种方式对服务器资源要求比较高,并发局限于应用中,JDK1.4以前的唯一选择。
  •  NIO方式适用于连接数目多且连接比较短(轻操作)的架构,比如聊天服务器,并发局限于应用中,编程比较复杂。
  •  AIO方式使用于连接数目多且连接比较长(重操作)的架构,比如相册服务器,充分调用OS参与并发操作,编程比较复杂,JDK7开始支持。

NIO的3个核心概念

NIO重点是把Channel(通道),Buffer(缓冲区),Selector(选择器)三个类之间的关系弄清楚。

1.缓冲区Buffer

Buffer是一个对象。它包含一些要写入或者读出的数据。在面向流的I/O中,可以将数据写入或者将数据直接读到Stream对象中。

在NIO中,所有的数据都是用缓冲区处理。这也就本文上面谈到的IO是面向流的,NIO是面向缓冲区的。

缓冲区实质是一个数组,通常它是一个字节数组(ByteBuffer),也可以使用其他类的数组。但是一个缓冲区不仅仅是一个数组,缓冲区提供了对数据的结构化访问以及维护读写位置(limit)等信息。

最常用的缓冲区是ByteBuffer,一个ByteBuffer提供了一组功能于操作byte数组。除了ByteBuffer,还有其他的一些缓冲区,事实上,每一种Java基本类型(除了Boolean)都对应一种缓冲区,具体如下:

  •  ByteBuffer:字节缓冲区
  •  CharBuffer:字符缓冲区
  •  ShortBuffer:短整型缓冲区
  •  IntBuffer:整型缓冲区
  •  LongBuffer:长整型缓冲区
  •  FloatBuffer:浮点型缓冲区
  •  DoubleBuffer:双精度浮点型缓冲区

2.通道Channel

Channel是一个通道,可以通过它读取和写入数据,他就像自来水管一样,网络数据通过Channel读取和写入。

通道和流不同之处在于通道是双向的,流只是在一个方向移动,而且通道可以用于读,写或者同时用于读写。

因为Channel是全双工的,所以它比流更好地映射底层操作系统的API,特别是在UNIX网络编程中,底层操作系统的通道都是全双工的,同时支持读和写。

Channel有四种实现:

  •  FileChannel:是从文件中读取数据。
  •  DatagramChannel:从UDP网络中读取或者写入数据。
  •  SocketChannel:从TCP网络中读取或者写入数据。
  •  ServerSocketChannel:允许你监听来自TCP的连接,就像服务器一样。每一个连接都会有一个SocketChannel产生。

3.多路复用器Selector

Selector选择器可以监听多个Channel通道感兴趣的事情(read、write、accept(服务端接收)、connect,实现一个线程管理多个Channel,节省线程切换上下文的资源消耗。Selector只能管理非阻塞的通道,FileChannel是阻塞的,无法管理。

关键对象

  •  Selector:选择器对象,通道注册、通道监听对象和Selector相关。
  •  SelectorKey:通道监听关键字,通过它来监听通道状态。

监听注册

监听注册在Selector

socketChannel.register(selector, SelectionKey.OP_READ);

监听的事件有

  •  OP_ACCEPT: 接收就绪,serviceSocketChannel使用的
  •  OP_READ: 读取就绪,socketChannel使用
  •  OP_WRITE: 写入就绪,socketChannel使用
  •  OP_CONNECT: 连接就绪,socketChannel使用

NIO的应用和框架

1.NIO的应用

Java NIO成功的应用在了各种分布式、即时通信和中间件Java系统中,充分的证明了基于NIO构建的通信基础,是一种高效,且扩展性很强的通信架构。

例如:Dubbo(服务框架),就默认使用Netty作为基础通信组件,用于实现各进程节点之间的内部通信。

Jetty、Mina、Netty、Dubbo、ZooKeeper等都是基于NIO方式实现。

  •  Mina出身于开源界的大牛Apache组织
  •  Netty出身于商业开源大亨Jboss
  •  Dubbo阿里分布式服务框架

2.NIO框架

特别是Netty是目前最流行的一个Java开源框架NIO框架,Netty提供异步的、事件驱动的网络应用程序框架和工具,用以快速开发高性能、高可靠性的网络服务器和客户端程序。

相比JDK原生NIO,Netty提供了相对十分简单易用的API,非常适合网络编程。

Mina和Netty这两个NIO框架的创作者是同一个人Trustin Lee 。Netty从某种程度上讲是Mina的延伸和扩展,解决了一些Mina上的设计缺陷,也优化了一下Mina上面的设计理念。

另一方面Netty相比较Mina的优势:

  1.  更容易学习
  2.  API更简单
  3.  详细的范例源码和API文档
  4.  更活跃的论坛和社区
  5.  更高的代码更新维护速度

Netty无疑是NIO框架的首选,它的健壮性、功能、性能、可定制性和可扩展性在同类框架都是首屈一指的。

java socket通信的4种线程模型(包括netty和mina使用的)

下面是线程模型的演进

Thread per Connection

Thread per Connection: 在没有nio之前,这是传统的java网络编程方案所采用的线程模型。即有一个主循环,socket.accept阻塞等待,当建立连接后,创建新的线程/从线程池中取一个,把该socket连接交由新线程全权处理。这种方案优缺点都很明显,优点即实现简单,缺点则是方案的伸缩性受到线程数的限制。

Reactor in Single Thread

Reactor in Single Thread: 有了nio后,可以采用IO多路复用机制了。我们抽取出一个单线程版的reactor模型,时序图见下文,该方案只有一个线程,所有的socket连接均注册在了该reactor上,由一个线程全权负责所有的任务。它实现简单,且不受线程数的限制。这种方案受限于使用场景,仅适合于IO密集的应用,不太适合CPU密集的应用,且适合于CPU资源紧张的应用上。

http://my.oschina.net/xinxingegeya/blog/339027

 

技术分享

Reactor + Thread Pool

Reactor + Thread Pool: 方案2由于受限于使用场景,但为了可以更充分的使用CPU资源,抽取出一个逻辑处理线程池。reactor仅负责IO任务,线程池负责所有其它逻辑的处理。虽然该方案可以充分利用CPU资源,但是这个方案多了进出thread pool的两次上下文切换。

技术分享

Reactors in threads

Reactors in threads: 基于方案3缺点的考虑,将reactor分成两个部分。main reactor负责连接任务(accept、connect等),sub reactor负责IO、逻辑任务,即mina与netty的线程模型。该方案适应性十分强,可以调整sub reactor的数量适应CPU资源紧张的应用;同时CPU密集型任务时,又可以在业务处理逻辑中将任务交由线程池处理,如方案5。该方案有一个不太明显的缺点,即session没有分优先级,所有session平等对待均分到所有的线程中,这样可能会导致优先级低耗资源的session堵塞高优先级的session,但似乎netty与mina并没有针对这个做优化。

技术分享

Reactors in threads + Threads pool

Reactors in threads + Threads pool: 这也是我所在公司应用框架采用的模型,可以更为灵活的适应所有的应用场景:调整reactor数量、调整thread pool大小等。

技术分享

==========================END==========================

BIO与NIO的比较

1、线程数

  • BIO:一个客户端连接就要使用一个服务端线程来进行处理
    • 可能会有大量的想爱你成处于休眠状态,只是等待输入或输出(阻塞)
    • 为每个线程分配调用栈,大约1M,服务端内存吃紧
    • 即使服务端内存很大,线程数太大会导致线程上下文切换浪费大量时间
  • NIO:一个服务端线程操作一个Selector,就可以处理成千上万的客户端连接

2、阻塞情况

  • BIO:读、写、接受连接都会发生阻塞
  • NIO:只有Selector.select()会阻塞,其实是等待Channel上的“就绪事件”

3、面向对象

  • BIO:面向流
  • NIO:面向Buffer

4、适合点

  • BIO:如果你有少量的连接使用非常高的带宽,一次发送大量的数据,也许典型的IO服务器实现可能非常契合
  • NIO:如果需要管理同时打开的成千上万个连接,这些连接每次只是发送少量的数据,例如聊天服务器,实现NIO的服务器可能是一个优势。

 

四、Reactor模型

主从模型:

  • 主线程池:
    • 全部由NIO线程组成,使用线程池是因为担心性能问题
    • 接收客户端连接请求,可能包含认证
    • 接收到客户端的连接请求并处理完成(比如认证)后,将创建出来的SocketChannel(查看上边的NIOServer类)注册到次线程池的某一条NIO线程上,之后这条NIO线程进行IO操作。
  • 次线程池:
    • 全部由NIO线程组成
    • 进行IO操作(编解码、业务逻辑等)

1、Java NIO简介

Java NIO(非阻塞IO):NIO与原来的IO有同样的作用和目的,但是使用的方式完全不同。NIO支持面向缓冲区的、基于通道的IO操作。NIO将以更加高效的方式进行文件的读写操作。

2、java NIO和IO的主要区别

IONIO
面向流 面向缓冲区
单向传输 双向传输
阻塞IO 非阻塞IO
选择器Selectors

3、缓冲区buffer和通道channel

概括:channel负责传输,buffer负责存储

3.1、缓冲区buffer

(1)定义

在Java NIO中负责数据的存取,缓冲区就是个数组。用于存储不同类型的数据。

(2)类型

根据类型的不同(boolean除外),提供了相应类型的缓冲区:ByteBuffer、CharBuffer、ShortBuffer、IntBuffer、LongBuffer、FloatBuffer、DoubleBuffer

(3)创建缓冲区的方法

allocate(int capacity)

//创建一个大小为1024的Byte类型的缓冲区
ByteBuffer byteBuffer= ByteBuffer.allocate(1024);

(4)存取数据的方法

put存数据到缓冲区,get从缓冲区取数据,必须通过flip从读模式转换成写模式,才可以get

byteBuffer.put("abcde".getBytes());

byteBuffer.flip();//转换成写模式

byte[] dst = new byte[5];
byteBuffer.get(dst);
System.out.println(new String(dst,0,5));

<1>获取 Buffer 中的数据

get() :读取单个字节
get(byte[] dst):批量读取多个字节到 dst 中
get(int index):读取指定索引位置的字节(不会移动 position)

<2>放入数据到 Buffer 中

put(byte b):将给定单个字节写入缓冲区的当前位置
put(byte[] src):将 src 中的字节写入缓冲区的当前位置
put(int index, byte b):将指定字节写入缓冲区的索引位置(不会移动 position)

(5)缓冲区的四个核心属性

capacity:容量,表示缓冲区中最大存储数据的容量。一旦申明不能改变。
limit:界限,表示缓冲区中可以操作数据的大小。(limit后面的数据不能读写)
position:位置,表示缓冲区中正在操作数据的位置。
mark:标记,表示记录当前position的位置。可以通过rest()恢复到mark的位置

0=<mark<=position <= limit <= capacity

ByteBuffer byteBuffer = ByteBuffer.allocate(1024);

System.out.println("--------------初始创建缓冲区allocate()----------------");
System.out.println(byteBuffer.position());//输出0
System.out.println(byteBuffer.limit());//输出1024
System.out.println(byteBuffer.capacity());//输出1024

System.out.println("--------------写模式put()----------------");
byteBuffer.put("abcde".getBytes());
System.out.println(byteBuffer.position());//输出5
System.out.println(byteBuffer.limit());//输出1024
System.out.println(byteBuffer.capacity());//输出1024

System.out.println("--------------切换到读模式flip()----------------");
byteBuffer.flip();//将缓冲区的.limit设置为position,并将positions设为 0
System.out.println(byteBuffer.position());//输出0
System.out.println(byteBuffer.limit());//输出5
System.out.println(byteBuffer.capacity());//输出1024

System.out.println("--------------开始读数据get()----------------");
byte[] dst = new byte[5];
byteBuffer.get(dst);
System.out.println(new String(dst,0,5));
System.out.println(byteBuffer.position());//输出5
System.out.println(byteBuffer.limit());//输出5
System.out.println(byteBuffer.capacity());//输出1024

System.out.println("--------------重新读数据rewind()----------------");
byteBuffer.rewind();//将position设为 0
System.out.println(byteBuffer.position());//输出0
System.out.println(byteBuffer.limit());//输出5
System.out.println(byteBuffer.capacity());//输出1024

System.out.println("-------------清空缓冲区数据clear()----------------");
byteBuffer.clear();//清空缓冲区,但是缓冲区的数据依然存在,只是位置、界限变成最初状态,这个方法主要用来重新写数据到缓存
System.out.println(byteBuffer.position());//输出0
System.out.println(byteBuffer.limit());//输出1024
System.out.println(byteBuffer.capacity());//输出1024
byte[] dst1 = new byte[2];
byteBuffer.get(dst1);
System.out.println(new String(dst1));//输出ab

System.out.println("--------------标记位置mark(),恢复到标记的位置reset()----------------");
byteBuffer.mark(); //对缓冲区设置标记
byteBuffer.get(dst1);
System.out.println(new String(dst1));//输出cd
byteBuffer.reset();//将 position 回复到以前设置的 mark 所在的位置
byteBuffer.get(dst1);
System.out.println(new String(dst1));//又输出cd

(6)buffer的其他常用方法--hasRemaining和remaining

hasRemaining:是否还有数据可以读,返回true或false,代表limit-position是否大于0
remaining:还可以读多少个数据,代表limit-position的值

if(byteBuffer.hasRemaining()){
	System.out.println(byteBuffer.remaining() );
}

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

<1>非直接缓冲区:通过allocate()方法分配非直接缓冲区,这个缓存是建立在JVM的内容中的。

我们可以从图看到非直接缓冲区的缺点:中间有个copy过程,所以效率较低。

<2>直接缓冲区:通过allocateDirect()方法分配直接缓冲区,将缓冲区建立在物理内存中,可以提高效率。

我们可以从图看到多开辟了个物理内存,这导致分配和销毁数据耗费的资源很大,并且把数据写给映射文件以后,数据就不归我们管了,数据什么时候从映射文件存到磁盘完全由操作系统决定。应用程序和映射文件之间的连接断开是由垃圾回收机制释放的,可能导致应用程序要很久才断开连接。
使用场景:数据需要长时间在内存中进行操作,或者大数据

直接字节缓冲区可以通过调用此类的allocateDirect()工厂方法来创建。

ByteBuffer byteBuffer = ByteBuffer.allocateDirect(1024);
boolean isDirect = byteBuffer.isDirect();//isDirect=true;

还可以通过FileChannel的map方法将文件区域直接映射到内存中来创建,该方法返回MappedByteBuffer。(具体例子看4大节的例子2)

(8)两个buffer之间传送数据要注意字符集

字符集Charset的编码(字符串转换成字节数组)和解码(字节数据转换成字符串)

例子: 把字符串缓冲区编码转换成字节缓冲区,再解码转换成字符串缓冲区

@Test
public void NioTest5() {

    //获取编码的字符类型
    Charset gbkCharset = Charset.forName("GBK");

    //生成字符串缓冲区
    CharBuffer CharBuf1= CharBuffer.allocate(1024);
    CharBuf1.put("牛逼!!");
    CharBuf1.flip();

    //把charBuf编码成byteBuf,生成中间的字节缓冲区
    ByteBuffer byteBuf = gbkCharset.encode(CharBuf1);

    //获取解码的字符类型
    Charset utf8Charset = Charset.forName("GBK");//如果换成UTF-8等其他类型就会乱码

    //把byteBuf解码成charBuf,把中间的字节缓冲区转换成字符串缓冲区
    CharBuffer charBuf2 = utf8Charset.decode(byteBuf);
    System.out.println(charBuf2.toString());
}

3.2、channel

(1)定义

通道:用于源节点与目标节点的连接,在Java NIO中负责缓冲区中数据的传输。
channel是完全独立的处理器,附属于CPU,专门用于IO操作。有效提高CPU利用率。
注意:channel只能与buffer交互。

ByteBuffer buf=...;
//缓冲区写到channel
outChannel.write(buf);
//channel写到buf
inChannel.read(buf)

(2)Channel接口实现类

java.nio.channels.Channel接口主要实现类:

  • FileChannel(用于本地网络传输)
  • SocketChannel(用于Tcp网络传输)
  • ServerSocketChannel(用于Tcp网络传输)
  • DatagramChannel(用于Udp网络传输)

(3)获取通道的方法

<1>下面几个类可以通过getChannel()获得Channel

本地IO:

  • FileInputStream/FileOutputStream
  • RandomAccessFile

网络IO:

  • Socket
  • ServerSocket
  • DatagramSocket

<2>通道的静态方法open()
<3>File工具类的newByteChannel()

4、文件通道fileChannel的例子

例子1--文件流、FileChannel、非直接缓冲区

这里的文件流指的是FileInputStream/FileOutputStream。
通过文件流的getChannel()获得FileChannel,两个FileChannel之间进行传输数据,缓冲区为非直接缓冲区

eg1:复制1.jpg,生成2.jpg

@Test
public void NioTest1() {
    try(FileInputStream fis = new FileInputStream("1.jpg");//放在项目根目录下
        FileOutputStream fos = new FileOutputStream("2.jpg");//最后要生成的文件名
        //1、通过文件流获取通道
        FileChannel inChannel =fis.getChannel();
        FileChannel outChannel =fos.getChannel();){

        //2、通过ByteBuffer.allocate分配指定大小的非直接缓冲区
        ByteBuffer buf =ByteBuffer.allocate(1024);

        //3、将in通道中的数据存入缓存
        while(inChannel.read(buf)!=-1){
            buf.flip();//把缓冲区切换成读模式
            //4、将缓冲区的数据写入out通道
            outChannel.write(buf);
            buf.clear();//清空緩存
        }
    } catch (IOException e) {
        e.printStackTrace();
    }

}

例子2--文件流、FileChannel、直接缓冲区

通过FileChannel.open()获得FileChannel,两个FileChannel之间进行传输数据,缓冲区为直接缓冲区

eg2:复制1.jpg,生成2.jpg

@Test
public void NioTest2() {
    //1、通过FileChannel.open获取通道
    try(FileChannel inChannel=FileChannel.open(Paths.get("1.jpg"), StandardOpenOption.READ);
        FileChannel outChannel = FileChannel.open(Paths.get("2.jpg"),StandardOpenOption.READ,StandardOpenOption.WRITE,StandardOpenOption.CREATE)){
        //2、通过fileChannel的map方法获取直接缓冲区,即内存映射文件
        MappedByteBuffer inMappedBuf = inChannel.map(MapMode.READ_ONLY, 0, inChannel.size());
        MappedByteBuffer outMappedBuf = outChannel.map(MapMode.READ_WRITE, 0, inChannel.size());

        //3、直接对缓冲区进行操作,无需通过channel
        byte[] dst= new byte[inMappedBuf.limit()];// inChannel.size()和inMappedBuf.limit()一样的
        inMappedBuf.get(dst);
        outMappedBuf.put(dst);

    } catch (IOException e) {
        e.printStackTrace();
    }
}

注意:非直接缓冲区方式,有时候垃圾回收机制不能及时运行的话,导致资源一直连接着,没有断开。但是确实直接缓冲区比非直接缓冲区效率高很多。

例子3---文件流、FileChannel、直接缓冲区、通道之间的数据传输

eg2可以简化一下缓冲区操作步骤,直接利用通道之间的数据传输方法---transferFrom()、transferTo()

eg3:复制1.jpg,生成2.jpg

@Test
public void NioTest3() {
    //1、通过FileChannel.open获取通道
    try(FileChannel inChannel=FileChannel.open(Paths.get("1.jpg"), StandardOpenOption.READ);
        FileChannel outChannel = FileChannel.open(Paths.get("2.jpg"),StandardOpenOption.READ,StandardOpenOption.WRITE,StandardOpenOption.CREATE)){

        //inChannel.transferTo(0,inChannel.size(),outChannel);//transferTo底层和上面例子2差不多
        outChannel.transferFrom(inChannel,0,inChannel.size());//和transferTo一样,只是方向反一下而已
    } catch (IOException e) {
        e.printStackTrace();
    }
}

例子4--RandomAccessFile、FileChannel、直接缓冲区、分散与聚集

分散(Scatter):将通道中的数据分散读取到多个缓冲区中
聚集(Gather)将多个缓冲区的数据聚集到通道中

eg4:把1.txt通过channel读取到多个缓冲区中,然后把多个缓冲区的数据通过channel读到2.txt

@Test
public void NioTest4() throws IOException {
    //一、将通道中的数据分散读取到多个缓冲区中
    RandomAccessFile inRaf = new RandomAccessFile("1.txt", "rw");//1.txt为要读取的文件
    //1、通过RandomAccessFile获取通道
    FileChannel inChannel = inRaf.getChannel();

    //2、分配多个直接缓冲区,放到数组中
    ByteBuffer[] bufs = {ByteBuffer.allocate(102), ByteBuffer.allocate(1024)};

    //3、将通道中的数据分散读取到多个缓冲区中
    inChannel.read(bufs);

    //验证一下bufs的数据
    Arrays.stream(bufs).forEach(buf -> buf.flip());//先把每个缓冲区转换成读模式
    Arrays.stream(bufs).forEach(buf -> System.out.println(new String(buf.array()) + "\n----------------"));//可以看到,把1.txt的内容打印出来了

    //关闭流
    inRaf.close();
    inChannel.close();

    //二、将多个缓冲区的数据聚集到通道中
    RandomAccessFile outRaf = new RandomAccessFile("2.txt", "rw");//2.txt为要写入的文件,会自动创建
    //1、通过RandomAccessFile获取通道
    FileChannel outChannel = outRaf.getChannel();
    outChannel.write(bufs);

    //关闭流
    outRaf.close();
    outChannel.close();
}

5、阻塞式网络通信

传统的 IO 流都是阻塞式的。也就是说,当一个线程调用 read() 或 write()
时,该线程被阻塞,直到有一些数据被读取或写入,该线程在此阻塞期间不
能执行其他任务。

  使用NIO完成阻塞网络通信的两个核心:
     1、通道Channel:负责连接
         java.nio.channels.Channel接口
             | -- SelectableChannel类
                     | -- SocketChannel类
                     | -- SererSocketChannel类
                     | -- DatagramChannel类

                     | -- Pipe.SinkChannel类
                     | -- Pipe.SourceChannel类

    2、缓冲区Buffer:负责数据的存取

5.1、阻塞式网络通信+SocketChannel+ServerSocketChannel的例子

eg: 客户端发送一个图片给服务端,服务端保存到本地,并返回应答。这里还没有用到Selector(先运行服务端,再运行客户端)

@Test
public void client() throws IOException {

    //1、获取网络传输客户端通道,默认都是阻塞通讯
    SocketChannel socketChannel = SocketChannel.open(new InetSocketAddress("127.0.0.1", 9898));
    //2、分配指定大小的缓冲区
    ByteBuffer buf = ByteBuffer.allocate(1024);
    //3、读取本地文件到缓冲区
    FileChannel inChannel = FileChannel.open(Paths.get("1.jpg"), StandardOpenOption.READ);
    while (inChannel.read(buf) != -1) {
        buf.flip();
        //4、将缓冲区的数据写入socketChannel
        socketChannel.write(buf);
        buf.clear();
    }
    //5、告诉服务端我已经发完了,如果不写这个,服务端就一直监听客户端数据,导致阻塞
    socketChannel.shutdownOutput();
    //6、接收反馈
    int len=0;
    while((len=socketChannel.read(buf))!=-1){
        buf.flip();
        System.out.println(new String(buf.array(),0,len));
        buf.clear();
    }
    //5、关闭
    socketChannel.close();
    inChannel.close();
}

@Test
public void server() throws IOException {
    //1、获取网络传输服务端通道,并绑定本地连接的端口
    ServerSocketChannel serverSocketChannel = ServerSocketChannel.open();
    serverSocketChannel.bind(new InetSocketAddress(9898));

    //2、获取客户端连接的通道(阻塞监听)
    SocketChannel clientSocketChannel = serverSocketChannel.accept();

    //3、获取写数据到本地的文件通道
    FileChannel fileChannel = FileChannel.open(Paths.get("2.jpg"),StandardOpenOption.WRITE,StandardOpenOption.CREATE);

    //4、分配制定大小的非直接缓冲区
    ByteBuffer buf = ByteBuffer.allocate(1024);

    //5、接收客户端的数据到缓冲区
    while(clientSocketChannel.read(buf)!=-1){
        buf.flip();
        //把缓冲区的数据保存到本地
        fileChannel.write(buf);
        buf.clear();
    }
    //6、反馈给客户端
    buf.put("服务端接收数据成功".getBytes());
    buf.flip();
    clientSocketChannel.write(buf);

    //关闭
    serverSocketChannel.close();
    clientSocketChannel.close();
    fileChannel.close();
}

6、NIO的非阻塞式网络通信

NIO 可以让服务器端使用一个或有限几个线程来同时处理连接到服务器端的所有客户端。当线程从某通道进行读写数据时,若没有数据可用时,该线程可以进行其他任务。

  使用NIO完成非阻塞网络通信的三个核心:
     1、通道Channel:负责连接
         java.nio.channels.Channel接口
            | -- SelectableChannel
                 | -- SocketChannel
                 | -- SererSocketChannel
                 | -- DatagramChannel

                 | -- Pipe.SinkChannel
                 | -- Pipe.SourceChannel

    2、缓冲区Buffer:负责数据的存取
    3、选择器Selector:是SelectableChannel的多路复用器,用于监控SelectaleChannel的IO状况。Selector 可使一个单独的线程管理多个 Channel。Selector 是非阻塞 IO 的核心。

6.1、选择器selector

(1)创建selector和注册通道到selector

Selector selector= Selector.open();//创建选择器
selectableChannel.register(Selector selector,int ops);//selectableChannel注册到选择器

ops:就是选择器监听这个channel的事件类型,如果这个channel准备好了,就放行,否则不管

  • SelectionKey.OP_CONNECT:某个Channel成功连接到另一个服务器称为“ 连接就绪 ”
  • SelectionKey.OP_ACCEPT:一个Server Socket Channel准备好接收新进入的连接称为“ 接收就绪 ”
  • SelectionKey.OP_READ:一个有数据可读的通道可以说是“ 读就绪 ”
  • SelectionKey.OP_WRITE:等待写数据的通道可以说是“ 写就绪 ”

(2)selector常用的方法

方 法描 述
Set keys() 所有的 SelectionKey 集合。代表注册在该Selector上的Channel
selectedKeys() 被选择的 SelectionKey 集合。返回此Selector的已选择键集
int select() 监控所有注册的Channel,当它们中间有需要处理的 IO 操作时,该方法返回,并将对应得的 SelectionKey 加入被选择的
SelectionKey 集合中,该方法返回这些 Channel 的数量。
int select(long timeout) 可以设置超时时长的 select() 操作
int selectNow() 执行一个立即返回的 select() 操作,该方法不会阻塞线程
Selector wakeup() 使一个还未返回的 select() 方法立即返回
void close() 关闭该选择器

(3)SelectionKey常用的方法

方 法描 述
int interestOps() 获取感兴趣事件集合
int readyOps() 获取通道已经准备就绪的操作的集合
SelectableChannel channel() 获取注册通道
Selector selector() 返回选择器
boolean isReadable() 检测 Channal 中读事件是否就绪
boolean isWritable() 检测 Channal 中写事件是否就绪
boolean isConnectable() 检测 Channel 中连接是否就绪
boolean isAcceptable() 检测 Channel 中接收是否就绪

(4)非阻塞网络通讯(selector)+SocketChannel、ServerSocketChannel的例子

eg: 服务端一直监听客户端,可以同时多个客户端发送数据给服务端(先启动服务端,再启动客户端)

@Test
public void client() throws IOException {

    //1、获取网络传输客户端通道、并切换为非阻塞模式
    SocketChannel socketChannel = SocketChannel.open(new InetSocketAddress("127.0.0.1", 9898));
    socketChannel.configureBlocking(false);

    //2、分配指定大小的缓冲区,并写入数据
    ByteBuffer buf = ByteBuffer.allocate(1024);
    buf.put("客服端的数据".getBytes());

    //4、将缓冲区的数据写入socketChannel
    buf.flip();
    socketChannel.write(buf);

    //5、关闭
    socketChannel.close();
}

@Test
public void server() throws IOException {
    //1、获取网络传输服务端通道,并设置为非阻塞模式,并绑定本地连接的端口,
    ServerSocketChannel serverSocketChannel = ServerSocketChannel.open();
    serverSocketChannel.configureBlocking(false);
    serverSocketChannel.bind(new InetSocketAddress(9898));

    //2、获取选择器,并把服务端通道绑定到选择器
    Selector selector= Selector.open();
    serverSocketChannel.register(selector, SelectionKey.OP_ACCEPT);//指定服务端监听接收事件(此时还没有客服连接进来,所以是未就绪)

    //3、selector.select()已经就绪的channel,
    while(selector.select()>0){
        System.out.println("有就绪好的channel了,我进来了");
        //4、获取当前选择器中所有已经注册的“选择键”,也就是已经就绪的监听事件,步骤2已经注册了serverSocketChannel
        Iterator<SelectionKey> skIterator = selector.selectedKeys().iterator();

        while (skIterator.hasNext()){
            //5、获取准备就绪的事件
            SelectionKey sk = skIterator.next();

            //6、判断具体是什么事件准备就绪
            if(sk.isAcceptable()){
                //7、如果是接收就绪,获取客户端连接,并切换为非阻塞模式
                SocketChannel clientSocketChannel = serverSocketChannel.accept();
                clientSocketChannel.configureBlocking(false);

                //8、将该通道注册到选择器上
                clientSocketChannel.register(selector,SelectionKey.OP_READ);
            }else if(sk.isReadable()){
                //9、获取当前选择器上“读就绪”状态的通道
                SocketChannel clientSocketChannel = (SocketChannel) sk.channel();
                //10、读取数据
                ByteBuffer buf = ByteBuffer.allocate(1024);
                int len=0;
                while((len=clientSocketChannel.read(buf))!=-1){
                    buf.flip();
                    System.out.println(new String(buf.array(),0,len));
                    buf.clear();
                }
                //关闭
                clientSocketChannel.close();
            }
            //注意,要删除此次的选择键,不然步骤4又循环到这个选择键
            selector.selectedKeys().remove(sk);
        }
    }
}

6.2、DatagramChannel

(1)定义

Java NIO中的DatagramChannel是一个能收发UDP包的通道。

(2)非阻塞网络通讯(selector)+DatagramChannel的例子

eg: 服务端一直监听客户端,可以同时多个客户端发送数据给服务端(先启动服务端,再启动客户端)

@Test
public void client() throws IOException {

    //1、获取网络传输客户端通道、并切换为非阻塞模式
    DatagramChannel clientDatagramChannel = DatagramChannel.open();
    clientDatagramChannel.configureBlocking(false);

    //2、分配指定大小的缓冲区,并写入数据
    ByteBuffer buf = ByteBuffer.allocate(1024);
    buf.put("客服端的数据".getBytes());

    //4、将缓冲区的数据写入socketChannel
    buf.flip();
    clientDatagramChannel.send(buf,new InetSocketAddress("127.0.0.1",9898));

    //5、关闭
    clientDatagramChannel.close();
}

@Test
public void server() throws IOException {
    //1、获取网络传输服务端通道,并设置为非阻塞模式,并绑定本地连接的端口,
    DatagramChannel serverDatagramChannel = DatagramChannel.open();
    serverDatagramChannel.configureBlocking(false);
    serverDatagramChannel.bind(new InetSocketAddress(9898));

    //2、获取选择器,并把服务端通道绑定到选择器
    Selector selector= Selector.open();
    serverDatagramChannel.register(selector, SelectionKey.OP_READ);//指定服务端监听读事件(此时还没有客服连接进来,所以是未就绪)

    //3、selector.select()已经就绪的channel,
    while(selector.select()>0){
        System.out.println("有就绪好的channel了,我进来了");
        //4、获取当前选择器中所有已经注册的“选择键”,也就是已经就绪的监听事件,步骤2已经注册了serverSocketChannel
        Iterator<SelectionKey> skIterator = selector.selectedKeys().iterator();

        while (skIterator.hasNext()){
            //5、获取准备就绪的事件
            SelectionKey sk = skIterator.next();

            //6、判断具体是什么事件准备就绪
            if(sk.isReadable()){

                //7、读取数据到缓冲区
                ByteBuffer buf = ByteBuffer.allocate(1024);
                serverDatagramChannel.receive(buf);
                buf.flip();
                System.out.println(new String(buf.array(),0,buf.limit()));

            }
            //注意,要删除此次的选择键,不然步骤4又循环到这个选择键
            selector.selectedKeys().remove(sk);
        }
    }
}

6.3、管道Pipe

Java NIO 管道是2个线程之间的单向数据连接。
Pipe有一个source通道和一个sink通道。数据会被写到sink通道,从source通道读取。

eg: 写数据到sinkeChannel,从sourceChannel拿数据

@Test
public void pipeTest() throws IOException {
    //0、获取管道
    Pipe pipe = Pipe.open();

    //1、获取sink channel ,用来写数据
    Pipe.SinkChannel sinkChannel = pipe.sink();
    //2、创建缓冲区,并写数据,用来把数据存到channel
    ByteBuffer writeBuf = ByteBuffer.allocate(1024);
    writeBuf.put("我写数据进管道".getBytes());
    //3、把缓冲区的数据写到channel
    writeBuf.flip();
    sinkChannel.write(writeBuf);

    /**==================假装下面是另一个线程=========================**/

    //1、获取source channel,用来读数据
    Pipe.SourceChannel sourceChannel = pipe.source();
    //2、创建缓冲区,用来把channel的数据写到缓冲区
    ByteBuffer readBuf = ByteBuffer.allocate(1024);
    //3、把channel的数据写到buf
    sourceChannel.read(readBuf);
    //4、输出buf的数据
    readBuf.flip();
    System.out.println(new String(readBuf.array(),0,readBuf.limit()));

    //关闭
    sinkChannel.close();
    sourceChannel.close();
}
posted @ 2020-07-01 16:30  hanease  阅读(112)  评论(0)    收藏  举报