前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >java nio

java nio

作者头像
爱撒谎的男孩
发布2019-12-31 15:46:26
1.1K0
发布2019-12-31 15:46:26
举报
文章被收录于专栏:码猿技术专栏

文章目录

1. 缓冲区(Buffer)

1.1. 常用的方法

1.2. 核心属性

1.3. 直接缓冲区

1.4. 非直接缓冲区

2. 通道(Channel)

2.1. 获取通道

2.2. 实例

2.3. 通道之间指定进行数据传输

2.4. 分散读取

2.5. 聚集写入

2.6. NIO阻塞式

3. Selector(选择器)

3.1. SelectionKey

3.2. NIO非阻塞式

4. 参考文章

缓冲区(Buffer)

  • 负责数据的存取,实际上就是一个数组,用于存储不同的数据
  • 除了布尔类型之后,其他类型都有,最常用的就是ByteBuffer

常用的方法

  • allocate(int capacity): 分配指定大小的缓冲区(非直接缓冲区)
  • allocateDirect(int capacity): 分配指定大小的缓冲区(直接缓冲区)
  • put():向缓冲区中存储数据
  • get(byte[] dst):从缓冲区获取数据,这里的dst的容量必须和缓冲区的大小一致
  • get(byte[] dst,int offest,int length) :读取指定长度的内容到dst中,这里的dst容量没有要求
  • flip(): 缓冲区从写模式切换到读模式
  • clear():清空缓冲区,数据依然存在,只是处于一个“被遗忘”状态,改变的只是limitposition
  • array() :返回实现此缓冲区的 byte 数组
  • mark(): 标记当前位置(position)
  • reset():恢复到mark的位置

核心属性

  • capacity:容量,表示缓冲区的最大容量,一旦声明不能改变
  • limit: 界限,缓冲区中可以操作的数据的大小,实际存储数据的大小,limit之后的数据不能进行读写
  • position:位置,表示缓冲区中正在操作数据的位置
  • position<=limit<=capacity
代码语言:javascript
复制
@Test
public void test1(){
	String str="abcd";
	ByteBuffer buffer=ByteBuffer.allocate(1024);//分配1024个字节大小的缓冲区
	buffer.put(str.getBytes());  //写入数据
	System.out.println(buffer.capacity());  //容量 1024
	System.out.println(buffer.limit()); //界限,1024
	System.out.println(buffer.position());  //正在操作数据的位置  0
	
	buffer.flip();   //切换到读模式,读取数据的时候一定要切换,否则将会没有界限
	
	System.out.println(buffer.capacity());  //容量 1024
	System.out.println(buffer.limit()); //界限,4,允许读取的位置只能到4,因为就存储了这么多的数据
	System.out.println(buffer.position());  //正在操作数据的位置  0
	System.err.println(buffer.get(4));   //超出界限了,下标记从0开始,0<=index<limit
}
  • 实例
代码语言:javascript
复制
/**
 * 读取缓冲区中的数据到指定的字节数组中
 * 1、字节数组的大小一定要和buffer.limit()一样大小,否则会报错
 */
@Test
public void test2(){
	String str="abcdefg";
	ByteBuffer buffer=ByteBuffer.allocate(1024);  //申请空间大小
	buffer.put(str.getBytes());  //存入数据
	buffer.flip();   //切换到读模式
	//申请一个字节数组和实际数据一样大,这里必须和缓冲区的实际数据大小一样,否则将会报错
	byte[] dst=new byte[buffer.limit()]; 
	buffer.get(dst);   //读取缓冲区的数据到dst字节数组中
	System.out.println(new String(dst));
}


/**
 * 读取一个字节
 */
@Test
public void test3(){
	String str="abcdefg";
	ByteBuffer buffer=ByteBuffer.allocate(10);  //申请空间大小
	buffer.put(str.getBytes());  //存入数据
	buffer.flip();   //切换到读模式
	System.out.println((char)buffer.get());
}
  • 测试remark和reset
代码语言:javascript
复制
/**
	 * 测试remark和rest
	 */
	@Test
	public void test1(){
		ByteBuffer buffer=ByteBuffer.allocate(1024);
		String str="abcdcdscdscds";
		buffer.put(str.getBytes());   //向缓冲区中写入数据
		buffer.flip();  //切换到读的模式
		byte[] dst=new byte[1024];   //创建byte数组
		System.out.println("---------------------读取两个字节的数据----------------------------");
		buffer.get(dst,0,2);   //读取两个字节长度的数据到dst中,此时的position的位置位2
		System.out.println(new String(dst));
		System.out.println("----------------------标记此时的位置------------------------------");
		buffer.mark();  //标记位置,此时的position的位置位2
		System.out.println("---------------------继续读取两个字节的数据----------------------------");
		buffer.get(dst,buffer.position(),2);  //继续从当前位置读取两个字节到dst中
		System.out.println(new String(dst));
		System.out.println(buffer.position());  //此时的position的位置为4
		
		System.out.println("---------------------重置缓冲区到remark的位置----------------------------");
		buffer.reset();  //重置缓冲区到rmark的位置
		System.out.println(buffer.position());   //此时的position为2
	}

直接缓冲区

  • 直接字节缓冲区可以通过调用此类的 allocateDirect()工厂方法 来创建。此方法返回的 缓冲区进行分配和取消分配所需成本通常高于非直接缓冲区 。直接缓冲区的内容可以驻留在常规的垃圾回收堆之外,因此,它们对应用程序的内存需求量造成的影响可能并不明显。所以,建议将直接缓冲区主要分配给那些易受基础系统的机 本机 I/O 操作影响的大型、持久的缓冲区。一般情况下,最好仅在直接缓冲区能在程序性能方面带来明显好处时分配它们。
直接缓冲区
直接缓冲区

非直接缓冲区

  • 在JVM中内存中创建,在每次调用基础操作系统的一个本机IO之前或者之后,虚拟机都会将缓冲区的内容复制到中间缓冲区(或者从中间缓冲区复制内容),缓冲区的内容驻留在JVM内,因此销毁容易,但是占用JVM内存开销,处理过程中有复制操作。
非直接缓冲区
非直接缓冲区
  • 写入步骤如下:
    1. 创建一个临时的直接ByteBuffer对象。
    2. 将非直接缓冲区的内容复制到临时缓冲中。
    3. 使用临时缓冲区执行低层次I/O操作。
    4. 临时缓冲区对象离开作用域,并最终成为被回收的无用数据。

通道(Channel)

  • 通道是双向的,流是单向的
  • 通道相当于输出和输入流
  • 主要的实现类如下:
    • FileChannel:文件的操作
    • SocketChannel:TCP
    • ServerSocketChannel:TCP
    • DatagramChannel:UDP

获取通道

  1. 本地IO,提供了getChannel()方法获取通道
    1. FileInputStream
    2. FileOutputStram
    3. RandomAccessFile
  2. 在JDK1.7中的NIO,针对各个通道提供了静态方法open()
  3. 在JDK1.7中的NIO的Files工具类的newByteChannel()

实例

  • 利用通道实现文件的复制(非直接缓冲区)
代码语言:javascript
复制
/**
 * 使用getChannel获取通道,实现文件的复制
 * @throws IOException 
 */
@Test
public void test1() throws IOException{
	FileInputStream inputStream=new FileInputStream(new File("C:/images/lifecrystal.png"));
	FileOutputStream outputStream=new FileOutputStream(new File("C:/images/2.png"));
	
	FileChannel inchannel = inputStream.getChannel();  //获取通道,用于读取
	FileChannel outchannel=outputStream.getChannel();  //获取通道,用于写入
	
	ByteBuffer buffer=ByteBuffer.allocate(1024);  //申请缓冲区
	//将通道中的数据写入缓冲区
	while (inchannel.read(buffer)!=-1) {
		buffer.flip(); //切换到读模式
		//将缓冲区中的数据写入通道
		outchannel.write(buffer);  
		buffer.clear();  //清空缓冲区,继续读取数据
	}
	
	//关闭通道
	inchannel.close();
	outchannel.close();
	inputStream.close();
	outchannel.close();
}
  • 使用直接缓冲区完成文件的复制,使用open()的方法获取通道
代码语言:javascript
复制
/**
	 * 使用直接缓冲区完成文件的复制
	 * 使用open()的方法获取通道
	 * @throws IOException
	 */
	@Test
	public void test2() throws IOException{
		//获取一个读取数据的通道,使用的读模式
		FileChannel inchannel=FileChannel.open(Paths.get("C:/images/2.png"), StandardOpenOption.READ);
		
		/**
		 * StandardOpenOption.CREATE : 如果文件不存在,那么就创建,如果存在将会覆盖,不报错
		 * StandardOpenOption.CREATE_NEW : 如果不存在就创建,如果存在,将会报错
		 */
		FileChannel outchannel=FileChannel.open(Paths.get("C:/images/3.png"), StandardOpenOption.CREATE,StandardOpenOption.WRITE,StandardOpenOption.READ);
		
		//创建一个内存映射文件,操作直接缓冲区,和allocatDirect()一样,MapMode.READ_ONLY表示只读的模式,用于读取
		MappedByteBuffer inMappedBuff = inchannel.map(MapMode.READ_ONLY, 0, inchannel.size());
		
		//创建一个内容映射文件,MapMode.READ_WRITE表示读写模式,可以读写
		MappedByteBuffer outMappedBuffer = outchannel.map(MapMode.READ_WRITE, 0, inchannel.size());
		
		byte[] dst=new byte[inMappedBuff.limit()];
		//将数据读入到dst中
		inMappedBuff.get(dst);
		
		//将数据从dst中读取到outMappedBuffer
		outMappedBuffer.put(dst);
	}

通道之间指定进行数据传输

  • transferTo(long position,long count,WritableByteChannel target):将数据从通道写入可写的通道target中
  • transferFrom(ReadableByteChannel from,long position,long count):将数据从通道from中读取到通道中
代码语言:javascript
复制
	/**
	 * 通道之间直接进行传输
	 * 	1、transferTo(long position,long count,WritableByteChannel target):将数据从通道写入可写的通道target中
	 * 	2、transferFrom(ReadableByteChannel from,long position,long count):将数据从通道from中读取到通道中
	 * @throws IOException
	 */
	@Test
	public void test3() throws IOException{
		//获取一个读取数据的通道,使用的读模式
		FileChannel inchannel = FileChannel.open(Paths.get("C:/images/2.png"),
				StandardOpenOption.READ);

		/**
		 * StandardOpenOption.CREATE : 如果文件不存在,那么就创建,如果存在将会覆盖,不报错
		 * StandardOpenOption.CREATE_NEW : 如果不存在就创建,如果存在,将会报错
		 */
		FileChannel outchannel=FileChannel.open(Paths.get("C:/images/4.png"), StandardOpenOption.CREATE,StandardOpenOption.WRITE,StandardOpenOption.READ);
		
		//将通道inchannel中的数据直接写入outchannel中
		inchannel.transferTo(0, inchannel.size(), outchannel);
		
		//和上面一样的效果
//		outchannel.transferFrom(inchannel, 0, inchannel.size());
		
		inchannel.close();
		outchannel.close();
	}

分散读取

  • 将通道中的数据分散到各个缓冲区中
代码语言:javascript
复制
/**
 * 分散读取:将通道中的数据写入各个缓冲区中,是按照顺序写入的,第一个缓冲区写满才会写入第二个缓冲区
 * @throws IOException 
 */
@Test
public void test4() throws IOException{
	//创建读写模式的RandomAccessFile
	RandomAccessFile accessFile=new RandomAccessFile(new File("C:/images/2.png"), "rw");
	FileChannel inchannel=accessFile.getChannel(); //读取
	
	ByteBuffer buffer1=ByteBuffer.allocate(10); //第一个缓冲区,10个字节大小
	ByteBuffer buffer2=ByteBuffer.allocate(1024);//第二个缓冲区
	
	ByteBuffer[] dst={buffer1,buffer2};
	
	//分散读取
	inchannel.read(dst);
	
	for (ByteBuffer byteBuffer : dst) {
		byteBuffer.flip();  //切换到读的模式
	}
	
	//输出第一个缓冲区的数据
	System.out.println(new String(buffer1.array()));
	
	//输出第二个缓冲区中的数据
	System.out.println(new String(buffer2.array()));
}

聚集写入

  • 将各个缓冲区的数据读入到通道中
代码语言:javascript
复制
@Test
public void test4() throws IOException{
	//创建读写模式的RandomAccessFile
	RandomAccessFile accessFile=new RandomAccessFile(new File("C:/images/2.png"), "rw");
	FileChannel inchannel=accessFile.getChannel(); //读取
	
	ByteBuffer buffer1=ByteBuffer.allocate(10); //第一个缓冲区,10个字节大小
	ByteBuffer buffer2=ByteBuffer.allocate(1024);//第二个缓冲区
	
	ByteBuffer[] dst={buffer1,buffer2};
	
	//分散读取
	inchannel.read(dst);
	
	for (ByteBuffer byteBuffer : dst) {
		byteBuffer.flip();  //切换到读的模式
	}
	
	//输出第一个缓冲区的数据
	System.out.println(new String(buffer1.array()));
	
	//输出第二个缓冲区中的数据
	System.out.println(new String(buffer2.array()));
	
	System.out.println("---------------------聚集写入-------------------");
	
	RandomAccessFile accessFile2=new RandomAccessFile(new File("C:/images/6.png"), "rw");
	FileChannel outChannel=accessFile2.getChannel();   //写入数据的通道
	//聚集写入,将数据从各个缓冲区中写入到通道中
	outChannel.write(dst); 
	
	inchannel.close();
	outChannel.close();
	
}

NIO阻塞式

  • 阻塞或者不阻塞是针对SocketChannelServerSocketChannel
  • NIO中的套接字可以轻松在阻塞和非阻塞之间切换,这里我们使用NIO实现阻塞式的TCP数据传输
代码语言:javascript
复制
/**
	 * 客户端使用SocketChannel
	 * 客户端使用SocketChannel中的write()方法向服务端发送数据,使用read()读取服务端返回的反馈
	 * 在数据发送完成之后如果不调用shutdownOutput告知服务端数据已传送完成,那么将会一直阻塞下去
	 * @throws Exception
	 */
	@Test
	public void testClient()throws Exception{
		//获取通道
		SocketChannel clientChannel = SocketChannel.open(new InetSocketAddress("127.0.0.1",9898));
		
		FileChannel inchannel = FileChannel.open(Paths.get("C:/images/2.png"), StandardOpenOption.READ);
		
		ByteBuffer buffer=ByteBuffer.allocate(1024);
		
		//循环读取本地图片,并且发送到服务端
		//1、先使用FileChannel将数据读取到缓冲区中
		//2、再使用SocketChannel的write方法将缓冲区的数据发送到服务端
		while(inchannel.read(buffer)!=-1){
			buffer.flip(); //切换读模式
			clientChannel.write(buffer);  //发送数据
			buffer.clear(); //清空缓冲区
		}
		
		//告诉服务端数据已经传送完成,否则将会一直阻塞
		clientChannel.shutdownOutput();  
		
		//接收服务端的反馈
		//使用read()方法接收服务端的反馈,将其读入到缓冲区中
		while(clientChannel.read(buffer)>0){
			buffer.flip();  //切换读模式
			System.out.println("服务端:"+new String(buffer.array()));
			buffer.clear();
		}
		
		//关闭通道
		inchannel.close();
		clientChannel.close();
	}
	
	/**
	 * 服务端使用ServerSocketChannel
	 * 服务端使用SocketChannel的read()方法读取客户端发送的数据,使用write()方法向客户端返回数据
	 * @throws Exception
	 */
	@Test
	public void testServer()throws Exception{
		//获取服务端的通道
		ServerSocketChannel serverChannel = ServerSocketChannel.open();
		
		//绑定连接
		serverChannel.bind(new InetSocketAddress(9898));
		
		//获取客户端的连接通道
		SocketChannel clientChannel = serverChannel.accept();
		
		//申请缓冲区
		ByteBuffer byteBuffer=ByteBuffer.allocate(1024);
		
		
		FileChannel outChannel = FileChannel.open(Paths.get("C:/images/12.png"), StandardOpenOption.WRITE,StandardOpenOption.CREATE);
		
		//循环接收客户端发送过来的数据,并且将其保存在本地
		while(clientChannel.read(byteBuffer)>0){
			byteBuffer.flip(); //切换读模式
			outChannel.write(byteBuffer);  //写入到本地
			byteBuffer.clear(); //清空缓冲区
		}
		
		//服务端发送反馈信息给客户端,使用的还是SocketChannel的write方法
		byteBuffer.put("服务端接收数据成功".getBytes());
		byteBuffer.flip(); //切换模式
		clientChannel.write(byteBuffer);
		clientChannel.shutdownOutput();  //告知客户端传输完成
		
		//关闭通道
		outChannel.close();
		clientChannel.close();
		serverChannel.close();
	}

Selector(选择器)

  • 总的来说,选择器是对通道进行监听,这样就会避免阻塞的发生,实现了多路复用

SelectionKey

  • 某个Channel成功连接到另一个服务器称为“ 连接就绪 ”。一个Server Socket Channel准备好接收新进入的连接称为“ 接收就绪 ”。一个有数据可读的通道可以说是“ 读就绪 ”。等待写数据的通道可以说是“ 写就绪 ”。
  • 选择器是用来轮询监听通道的状态,其中有四种状态如下:
    • SelectionKey.OP_CONNECT:连接就绪
    • SelectionKey.OP_ACCEPT:接收就绪
    • SelectionKey.OP_READ:读就绪
    • SelectionKey.OP_WRITE:写就绪

NIO非阻塞式

代码语言:javascript
复制
/**
 * 客户端需要使用configureBlocking(false)设置成非阻塞模式的
 * @throws Exception
 */
@Test
public void testClient()throws Exception{
	//获取通道
	SocketChannel client = SocketChannel.open(new InetSocketAddress("127.0.0.1",9898));
	//切换成非阻塞模式
	client.configureBlocking(false);
	
	ByteBuffer buf=ByteBuffer.allocate(1024);  //申请缓冲区
	
	Scanner scanner=new Scanner(System.in);
	while(scanner.hasNext()){
		String line=scanner.next();  //读取控制台输入的内容
		buf.put((new Date().toString()+"\n"+line).getBytes());  //向缓冲区写入数据
		buf.flip(); //切换到读模式
		client.write(buf);  //向服务端发送数据
		buf.clear();  //清空缓存区
	}
	
	scanner.close();
	client.close();
}


/**
 * 服务端
 * 1、将通道注册到选择器中,并且指定监听的事件
 * 2、程序每次都会轮询的从选择器中选择事件,可以选择不同状态的通道进行操作
 * @throws Exception
 */
@Test
public void testServer()throws Exception{
	//获取通道
	ServerSocketChannel server = ServerSocketChannel.open();
	
	//绑定端口
	server.bind(new InetSocketAddress(9898));
	
	//配置非阻塞
	server.configureBlocking(false);
	
	//获取选择器
	Selector selector = Selector.open();
	
	//将通道注册到选择器上,并且知道指定监听的事件为"接收就绪"
	server.register(selector, SelectionKey.OP_ACCEPT);
	
	//轮询式获取选择器上已经准备就绪的事件
	while(selector.select()>0){
		//获取当前选择器中所有的选择键(已经准备就绪的)
		Set<SelectionKey> keys = selector.selectedKeys();
		
		//获取迭代器
		Iterator<SelectionKey> iterator = keys.iterator();
		
		//迭代器遍历所有的选择键
		while(iterator.hasNext()){
			//获取当前选择键
			SelectionKey key = iterator.next();
			iterator.remove(); //删除选择键
			
			if (key.isAcceptable()) {  //如果接收就绪了 
				SocketChannel client = server.accept();  //获取SocketChannel
				client.configureBlocking(false);  //设置非阻塞模式
				client.register(selector, SelectionKey.OP_READ);  //将此通道注册到选择器中,指定监听的事件是读就绪
			}else if(key.isReadable()){  //如果读就绪
				SocketChannel client = (SocketChannel) key.channel();  //读就绪了,那么可以获取通道直接读取数据
				ByteBuffer buf=ByteBuffer.allocate(1024);  //声明一个缓冲区
				//循环接收客户端的到缓冲区中
				int len=0;
				while((len=client.read(buf))>0){
					buf.flip();
					System.out.println(new String(buf.array(),0,len));
					buf.clear();
				}
			}else if (key.isWritable()) {  //如果写就绪
				
			}else if (key.isConnectable()) {  //如果连接就绪
				
			}
			
		}
	}
	server.close();
}

参考文章

本文参与 腾讯云自媒体同步曝光计划,分享自作者个人站点/博客。
原始发表:2018-12-01,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 作者个人站点/博客 前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体同步曝光计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • 缓冲区(Buffer)
    • 常用的方法
      • 核心属性
        • 直接缓冲区
          • 非直接缓冲区
          • 通道(Channel)
            • 获取通道
              • 实例
                • 通道之间指定进行数据传输
                  • 分散读取
                    • 聚集写入
                      • NIO阻塞式
                      • Selector(选择器)
                        • SelectionKey
                          • NIO非阻塞式
                          • 参考文章
                          领券
                          问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档