《Thinking in java》第18章--Java NIO

NIO (非阻塞式IO)

1、来看看IO、NIO区别?

        当用户线程发起一个IO请求操作(本文以读请求操作为例),内核会去查看要读取的数据是否就绪,对于阻塞IO来说,如果数据没有就绪,则会一直在那等待,直到数据就绪;对于非阻塞IO来说,如果数据没有就绪,则会返回一个标志信息告知用户线程当前要读的数据没有就绪。当数据就绪之后,便将数据拷贝到用户线程,这样才完成了一个完整的IO读请求操作,也就是说一个完整的IO读请求操作包括两个阶段:

  1)查看数据是否就绪;

  2)进行数据拷贝(内核将数据拷贝到用户线程)。

  那么阻塞(blocking IO)和非阻塞(non-blocking IO)的区别就在于第一个阶段,如果数据没有就绪,在查看数据是否就绪的过程中是一直等待,还是直接返回一个标志信息。

IO(面向流的,单向的,输入流、输出流是两种不同的流):
                                                      -----------------    
【磁盘、网络中的文件】》==  这里有字节的流动    ==》  【程序】
                                                      -----------------


NIO(面向缓冲区的,双向的,火车往前往后开):
                                                        通道
                                                   -----------------    
【磁盘、网络中的文件】《==     【缓冲区】    ==》  【程序】
                                                   -----------------

通道(看做铁路)负责传输,缓冲区(看做火车)负责存储数据


2、缓冲区负责数据的存取。缓冲区就是数组,用于存储不同数据类型的数组
    针对不同基本类型数据的缓冲区:ByteBuffer、CharBuffer、ShortBuffer、IntBuffer、LongBuffer、FloatBuffer、DoubleBuffer
    除了boolean类型没有

    2.1 缓冲区的核心方法

        put():存入数据到缓冲区中 
        get():获取缓冲区中的数据

    2.2 缓冲区的四个核心属性

        capacity:容量,表示缓冲区中的最大容量,一旦声明无法改变
        limit:界限,表示缓冲区中可以操作的数据大小(limit后的数据不能读写)
        position:位置,表示缓冲区中正在操作数据的位置
        mark:可以记录position的位置 ,通过buffer.mark()记录下当前position,之后position移动过了就可以通过buffer.reset()使position回到上一个位置
    
        0 <= mark <= position <= limit <= capacity

    2.3 Demo(读数据模式,写数据模式)
        //1.分配一个指定大小的缓冲区,capacity=5,position=0,limit=5
        ByteBuffer buffer = ByteBuffer.allocate(5);
        
        System.out.println("---allocate()---");
        System.out.println(buffer.position());//position=0
        System.out.println(buffer.limit());//limit=5
        System.out.println(buffer.capacity());//capacity=5

        //2、利用put存入数据到缓冲区,相当于写数据模式
        System.out.println("---put()---");
        String str = "abc";    buffer.put(str.getBytes());
        System.out.println(buffer.position());//3
        System.out.println(buffer.limit());//5
        System.out.println(buffer.capacity());//5

        //3、利用filp()方法切换读取数据模式
        System.out.println("---filp()---");
        buffer.flip();
        System.out.println(buffer.position());//0
        System.out.println(buffer.limit());//3
        System.out.println(buffer.capacity());//5

        //4、利用get()读取缓冲区的数据
        System.out.println("---get()---");
        byte[] dst = new byte[buffer.limit()];
        buffer.get(dst);//缓冲区中的数据读到字节数组中去
        System.out.println(new String(dst, 0, dst.length));
        System.out.println(buffer.position());//3
        System.out.println(buffer.limit());//3
        System.out.println(buffer.capacity());//5

        //5、rewind():可重复读数据
        System.out.println("---rewind()---");
        buffer.rewind();
        System.out.println(buffer.position());//0
        System.out.println(buffer.limit());//3
        System.out.println(buffer.capacity());//5

        //6、clear():清空缓冲区,但是缓冲区中的数据还在,但是处于“被遗忘”状态
        System.out.println("---clear()---");
        buffer.clear();
        System.out.println(buffer.position());//0
        System.out.println(buffer.limit());//5
        System.out.println(buffer.capacity());//5

     2.4 (NIO如何提升性能?)直接缓冲区与非直接缓冲
         * 非直接缓冲区:通过 allocate() 方法分配缓冲区,将缓冲区建立在 JVM 的内存中
         * 直接缓冲区:通过 allocateDirect() 方法分配直接缓冲区,将缓冲区建立在物理内存中。可以提高效率
        在JVM堆外分配内存,好处就是减少了Java堆和native堆中来回copy数据的消耗。坏处就是缓冲区建立在内存中,不可控,也受到物理内存空间的限制

3、通道

    3.1 是什么?
        用于源节点与目标节点的连接。在 Java NIO 中负责缓冲区中数据的传输。Channel 本身不存储数据,因此需要配合缓冲区进行传输。

    3.2 通道的主要实现类 以及 获取通道的api
       主要的实现类:

       java.nio.channels.Channel 接口:
              |--FileChannel        文件传输通道
              |--SocketChannel    TCP传输  
              |--ServerSocketChannel    TCP传输
              |--DatagramChannel    UDP传输

      获取通道的方式: 

       1)Java 针对支持通道的类提供了 getChannel() 方法
              本地 IO:
              FileInputStream/FileOutputStream
              RandomAccessFile
      
              网络IO:
              Socket
              ServerSocket
              DatagramSocket
        2)在 JDK 1.7 中的 NIO.2 针对各个通道提供了静态方法 open()
         3)在 JDK 1.7 中的 NIO.2 的 Files 工具类的 newByteChannel()
    
    3.3 几个Demo
        3.3.1 Demo1:利用通道进行本地文件复制
            ①获取输入输出流,并getChannel得到源通道与目标通道
            while(源通道中数据不为空)
                ②不断地将源通道中的数据存入缓冲区
                ③切换到读模式,并在目标通道中读取缓冲区中的数据
                ④缓冲区.clear(),下一次又可以到源通道拿数据
            ⑤关闭所有的通道,输入输出流
        
        3.3.2 Demo2:利用直接缓冲区完成本地文件的复制
            3.3.2.1 将缓冲区建立在内存中,由内存映射文件MappedByteBuffer来负责处理数据
            3.3.2.2 利用通道的transferTo或者transferFrom方法,在通道之间进行数据传输(简单)
    
    3.4 分散读取和聚集写入
        分散读取(Scattering Reads):将从通道中读取的数据分散到多个缓冲区中

FileChannel channel1 = raf1.getChannel();

ByteBuffer buf1 = ByteBuffer.allocate(100);
ByteBuffer buf2 = ByteBuffer.allocate(1024);

ByteBuffer[] bufs = {buf1, buf2};
channel1.read(bufs);

         聚集写入(Gathering Writes):将多个缓冲区中的数据聚集到通道中

RandomAccessFile raf2 = new RandomAccessFile("2.txt", "rw");
FileChannel channel2 = raf2.getChannel();
		
channel2.write(bufs);

4、字符集    
        字符集:Charset
        编码:Unicode字符串 -> 指定字符集的字节数组
        解码:指定字符集的字节数组  -> Unicode字符串

//获取指定的字符集
Charset cs1 = Charset.forName("GBK");
//获取编码器
CharsetEncoder ce = cs1.newEncoder();
//获取解码器
CharsetDecoder cd = cs1.newDecoder();

5、Selector是NIO中实现I/O多路复用的关键类,NIO在网络编程中的作用
    (一)Selector是非阻塞式IO的核心    
    选择器( Selector) 是 SelectableChannel 对象的多路复用器。另一个角度说,FileChannel是阻塞式的,而非阻塞式IO要求服务端、客户端通道都是处于非阻塞式模式下的。
    看一下SelectableChannel的结构
    SelectableChannel
        |--AbstractSelectableChannel
            |--SocketChannel
            |--ServerSocketChannel
            |--DatagramChannel
    这就说明了只有网络IO中才可以用到Selector选择器。一个Selector可以监控多个SelectableChannel对象的IO状况。利用Selector可使一个单独的线程或者几个(比如几个线程分别处理读、写、接收事件)来管理多个Channel!!!!
    
    (二)
    调用通道的register向某个选择器注册,可以监听四种“事件”类型:连接、接收、读、写。
    》分别表示,某个channel成功连接到另一个服务器称为”连接就绪“
    》一个ServerSocketChannel准备好接收新进入的连接称为”接收就绪“。
    》一个有数据可读的通道可以说是”读就绪“
    》等待写数据的通道可以说是”写就绪“
    
    register方法会返回一个SelectionKey:表示SelectableChannel和Selector之间的注册关系,并将这个SelectionKey加入到Selector的keys集合中。可以通过SelectionKey.cancel()移出该集合。
    调用Selector的select()返回一个int型的值,表示被Selector捕获到的就绪事件

使用NIO完成非阻塞式的网络通信(缓冲区+通道+选择器)(TCP方式)

import java.io.IOException;
import java.net.InetSocketAddress;
import java.net.ServerSocket;
import java.nio.ByteBuffer;
import java.nio.channels.SelectionKey;
import java.nio.channels.Selector;
import java.nio.channels.ServerSocketChannel;
import java.nio.channels.SocketChannel;
import java.util.Date;
import java.util.Iterator;
import java.util.Scanner;

import org.junit.Test;

/**
 * 模拟用NIO实现客户端向服务端发送数据,非阻塞式的(用到了通道+缓冲区+选择器Selector)
 * @author huzangyi
 *
 */
public class TestNonBlockingIO {
	@Test
	public void client() throws IOException{
		//1.开启客户端通道
		SocketChannel clientChannel = SocketChannel.open(new InetSocketAddress("127.0.0.1", 9000));
		//2.设置为非阻塞式的
		clientChannel.configureBlocking(false);
		//3.分配一个缓冲区
		ByteBuffer buffer = ByteBuffer.allocate(1024);
		//4.发送一条时间数据给服务端
//		buffer.put(new Date().toString().getBytes());
//		buffer.flip();
//		clientChannel.write(buffer);
//		buffer.clear();
		Scanner scan = new Scanner(System.in);
		
		while(scan.hasNext()){
			String str = scan.next();
			buffer.put(str.getBytes());
			buffer.flip();
			clientChannel.write(buffer);
			buffer.clear();
		}
		//5.关闭通道
		clientChannel.close();
	}
	
	@Test
	public void server() throws IOException{
		//1.开启服务端通道
		ServerSocketChannel serverSocketChannel = ServerSocketChannel.open();
		//2.切换到非阻塞式的
		serverSocketChannel.configureBlocking(false);
		//3.绑定指定端口
		serverSocketChannel.bind(new InetSocketAddress(9000));
		//4.获取Selector并向Selector注册,指定监听“接收”事件
		Selector selector = Selector.open();
		serverSocketChannel.register(selector, SelectionKey.OP_ACCEPT);
		//5.轮询式的获取选择器上已经“准备就绪”的事件
		while(selector.select() > 0){
			//5.1.获取当前选择器中所有的已经注册的“选择键”
			Iterator<SelectionKey> it = selector.selectedKeys().iterator();//所有被选择器捕获的key
			while (it.hasNext()) {
				SelectionKey selectionKey = it.next();
				//5.2.根据不同的事件作出不同的处理
				if (selectionKey.isAcceptable()) {
					System.out.println("--有个通道连接就绪");
					//若“接收就绪”,获取客户端连接通道
					SocketChannel socketChannel = serverSocketChannel.accept();
					//切换非阻塞
					socketChannel.configureBlocking(false);
					//将该通道注册到选择器上
					socketChannel.register(selector, SelectionKey.OP_READ);//把“读就绪”选择键加入到了所有key列表l中
				}else if(selectionKey.isReadable()){
					System.out.println("--有个通道有数据可读");
					//获取“读就绪”状态的通道
					SocketChannel socketChannel = (SocketChannel) selectionKey.channel();
					//分配buffer
					ByteBuffer buffer = ByteBuffer.allocate(1024);
					//读取数据
					int len = 0;
					while((len = socketChannel.read(buffer))>0){
						buffer.flip();
						System.out.println(new String(buffer.array(),0,len));
						buffer.clear();
					}
					//若cancel(),从列表l中移除这个Key
					//selectionKey.cancel();
				}
				//5.3 取消这个选择键SelectionKey
				it.remove();
			}
		}
	}
}

转载知乎上一个很形象的比喻

作者:levin
链接:https://www.zhihu.com/question/32163005/answer/255238636
来源:知乎
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。
 

IO 多路复用是5种I/O模型中的第3种,对各种模型讲个故事,描述下区别:

故事情节为:老李去买火车票,三天后买到一张退票。参演人员(老李,黄牛,售票员,快递员),往返车站耗费1小时。

1.阻塞I/O模型

老李去火车站买票,排队三天买到一张退票。

耗费:在车站吃喝拉撒睡 3天,其他事一件没干。

 

2.非阻塞I/O模型

老李去火车站买票,隔12小时去火车站问有没有退票,三天后买到一张票。

耗费:往返车站6次,路上6小时,其他时间做了好多事。

 

3.I/O复用模型

1.select/poll

老李去火车站买票,委托黄牛,然后每隔6小时电话黄牛询问,黄牛三天内买到票,然后老李去火车站交钱领票。

耗费:往返车站2次,路上2小时,黄牛手续费100元,打电话17次

 

2.epoll

老李去火车站买票,委托黄牛,黄牛买到后即通知老李去领,然后老李去火车站交钱领票。

耗费:往返车站2次,路上2小时,黄牛手续费100元,无需打电话

 

4.信号驱动I/O模型

老李去火车站买票,给售票员留下电话,有票后,售票员电话通知老李,然后老李去火车站交钱领票。

耗费:往返车站2次,路上2小时,免黄牛费100元,无需打电话

 

5.异步I/O模型

老李去火车站买票,给售票员留下电话,有票后,售票员电话通知老李并快递送票上门。

耗费:往返车站1次,路上1小时,免黄牛费100元,无需打电话

 

1同2的区别是:自己轮询

2同3的区别是:委托黄牛

3同4的区别是:电话代替黄牛

4同5的区别是:电话通知是自取还是送票上门

 

 

 

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值