Fork me on GitHub

NIO最全入门

1.NIO概述

在jdk4中引入了NIO ,可以最大限度的满足java程序I/O的需求

​ java.nio包,定义了各种与buffer相关的类

​ java.nio.channel包,包含了与Channel和Selector相关的类

​ java.nio.charset包,与字符集相关的类

NIO中有三大核心组件:Channel,Buffer,Selector

​ 传统的i/o面向流的,每次可以从流中读取一个或多个字节,只能向后读取,不能向前移动,NIO是面向缓冲区的,把数据读到缓冲区中,可以在缓冲区中向前向后移动,增加了程序的灵活性。

NIO模型

IO与NIO的区别

​ IO流是线程阻塞的,再调用read()和write()读写数据时,线程阻塞, 知道数据读取完毕或者数据完全写入,在读写过程中线程不能做其他任务。

​ NIO是非阻塞的,当线程从channel中读取数据时,如果通道中没有可用的数据,线程不会阻塞,可以做其他的任务。

2.Buffer

2.1Buffer的属性

​ buffer缓冲区实际上就是一个数组,把数组的内容与信息包装成一个Buffer对象,它提供了一组访问这些信息的方法。

​ 缓冲区的重要性:

​ capacity容量,是指缓冲区可以存多少数据,容量在创建buffer缓冲区是指 定大小,创建后不能再修改,如果缓冲区满了,需要清空后才能继续写数据

​ position表示当前位置,即缓冲区写入或读取的位置。刚刚创建Buffer对象后,position就向后移动一个单元,它的最大值是capacity-1,当Buffer从写模式切换到读模式,position会被重置为0,从buffer的开始位置读取数据,每读一个数据position就像后移动一个单元

​ limit上限,是指第一个不能被读出或写入的位置,limit上限后面的单元既不能读也不能写,在buffer缓冲区的写模式下,limit表示能够写入多少个数据,在读取模式下,limit表示最多可以读取多少个数据

​ mark标记,设置一个标记位置,可以调用mark()方法,把标记就设置在position位置,当调用reset方法时,就把postion设置为mark标记位

​ 0 <= mark <= position <= capa city

2.2Buffer的常用api

​ 在NIO中关键的buffer有:ByteBuffer,CharBuffer,DoubleBuffer,FloatBuffer,IntBuffer,LongBuffer,ShortBuffer.这些Buffer覆盖了能通过IO发送的所有基本类型:byte,char,int,double,float,long,short,s实际当中使用较多时ByteBuffer,CharBuffer.

​ 每个缓冲区类 都有一个静态方法allocate(capacity)可以创建一个指定容量的缓冲区;

​ 都有一个put()方法用于向缓冲区中存储数据,get()方法用于从缓冲区中读取数据;

​ 当缓冲区中,还有未读完的数据,可以调用compact()方法进行压缩,将所有未读取的数据复制到Buffer的起始位置,把position设置到最后一个未读取元素的后面,limit属性设置为capacity.

​ capacity()可以返回缓冲区的大小。

​ hasRemaining(),判断当前position后面是否还有可处理数据,即判断position和Limit之间是否还有数据可处理。

​ limit()返回limit上限的位置

​ mark()设置缓冲区的标志位置,这个值只能在0~position之间,可以通过reset()返回到这个位置。

​ position()可以返回position当前位置

​ remaining()返回当前position位置与limit之间的数据量

​ reset()方法可以将position设置为mark标志位

​ rewind()将position设置为0,取消mark标志位

​ clear()清空缓冲区,仅仅是修改position标志为0,设置limit为capacity,缓冲区中数据还是存在的

​ flip()可以把缓冲区由写模式切换到读模式,先把limit设置为position位置,在把position设置为0。

2.3Buffer中aip的使用

`package com.xysf.channel;

import java.io.File;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.FileOutputStream;
import java.io.IOException;
import java.nio.Buffer;
import java.nio.CharBuffer;

public class FileChannelDemo1 {
public static void main(String[] args) {
//1.创建CharBuffer()缓冲区对象
CharBuffer charBuffer = CharBuffer.allocate(12);
//2.打印position,limit,Capacity
System.out.println("posistion:"+charBuffer.position()+" limit:"+charBuffer.limit()+" capacity:"+charBuffer.capacity());

    //向缓冲区中存储数据put()
	char [] arr ={'我','是','谁','我','在','哪','里'};
	charBuffer.put(arr);
	System.out.println("posistion:"+charBuffer.position()+"  limit:"+charBuffer.limit()+"  capacity:"+charBuffer.capacity());
	//4.调用flip(),把缓冲区切换为读模式
	charBuffer.flip();
	System.out.println("posistion:"+charBuffer.position()+"  limit:"+charBuffer.limit()+"  capacity:"+charBuffer.capacity());
	
	//5.调用get()方法读取缓冲区中的数据
	System.out.println(charBuffer.get());
	System.out.println("posistion:"+charBuffer.position()+"  limit:"+charBuffer.limit()+"  capacity:"+charBuffer.capacity());
	
	//再次存储数据,把数据存储到position位置
	
	charBuffer.put('神');
	System.out.println("posistion:"+charBuffer.position()+"  limit:"+charBuffer.limit()+"  capacity:"+charBuffer.capacity());

	//7.设置标志位mark();
    charBuffer.mark();//将position的位置设为标志位。位置为2
    
    //8.在读取一个数据
    		System.out.println(charBuffer.get());
    		System.out.println("posistion:"+charBuffer.position()+"  limit:"+charBuffer.limit()+"  capacity:"+charBuffer.capacity());
   
  //9.调用reset(),将position重置为mark()标记位
    		charBuffer.reset();
    		System.out.println("posistion:"+charBuffer.position()+"  limit:"+charBuffer.limit()+"  capacity:"+charBuffer.capacity());
    		
   //10.调用compact压缩,把Buffer中未读取的数据复制到position为0的位置
    		charBuffer.compact();
    		System.out.println("posistion:"+charBuffer.position()+"  limit:"+charBuffer.limit()+"  capacity:"+charBuffer.capacity());
   
   //调用clear()清空仅仅是修改position/limit的1值,
    		charBuffer.clear();
    		System.out.println("posistion:"+charBuffer.position()+"  limit:"+charBuffer.limit()+"  capacity:"+charBuffer.capacity());
	
    //通过循环可以将position到limit之间的所有值打印
    		
    	while(charBuffer.hasRemaining()){
    		System.out.println(charBuffer.get());
    	}
    	
}

}`

2.4创建缓冲区的两种方式

​ 分配操作创建缓冲区,allocate()方法分配一个私有的,指定容量大小的数组来存储元素;

​ 包装操作来创建缓冲区,它使用提供的数组作为存储空间来存储缓冲区中的数据,不在分配其他空间。

package com.xysf.channel;

import java.nio.CharBuffer;
import java.util.Arrays;

public class Test2 {
public static void main(String[] args) {
	//1>分配操作创建缓冲区
	CharBuffer allocate = CharBuffer.allocate(16);
	//使用包装操作创建缓冲区
	char [] arr=new char[16];
	CharBuffer charBuffer=CharBuffer.wrap(arr);
	//通过put向缓冲区中保存数据,也会直接影响到数组
	charBuffer.put("hello");
	charBuffer.flip();//反转切换到读模式
	System.out.println(charBuffer);
	System.out.println(Arrays.toString(arr));
	
	//数组的任何操作也会影响到缓冲区对象
	arr[0]='X';
	System.out.println(charBuffer);
	
	/*
	 * 不管是allocate()还是通过wrap()创建的缓冲区都是间接的,简介缓冲区会使用备份数组
	 * hasArray()方法可以判断是否有一个可存取的备份数组
	 * 如果hasArray()方法返回true,可以通过array()返回缓冲区对象使用的备份数组的引用
	 * */
	if (charBuffer.hasArray()) {
		char[] array = charBuffer.array();
			System.out.println(Arrays.toString(array));
		
	}
}
}

2.5缓冲区的复制与分割

​ 复制缓冲区,duplicate()复制的缓冲区和原来的缓冲区实际引用的同一个数组

​ 分割缓冲区,slice()方法根据【position,limit】区间创建一个新的缓冲区

package com.xysf.channel;

import java.nio.CharBuffer;

public class Test03 {
public static void main(String[] args) {
	//1.创建缓冲区
	CharBuffer allocate = CharBuffer.allocate(16);
	//2.存储数据
	CharBuffer put = allocate.put("hello");
	System.out.println("allocate.position="+allocate.position());
	
	//3,缓冲区的复制
	CharBuffer duplicate = allocate.duplicate();
	System.out.println("duplicate.capacity="+duplicate.capacity()+",duplicate.limit="+duplicate.limit()+",duplicate.position="+duplicate.position());
	//duplicate.capacity=16,duplicate.limit=16,duplicate.position=5
	
	duplicate.flip();//翻转
	System.out.println(duplicate);//hello
	
	//allocate与duplicate实际引用同一个数组
	duplicate.clear();
	duplicate.put("nioworld");
	//要输出allocate缓冲区中的内容
	allocate.flip(); //翻转,把limit设置为position的值5
	System.out.println(allocate);//niowo
	
	//分割缓冲区,slice()方法根据【position,limit】区间创建一个新的缓冲区
	duplicate.position(3);
	System.out.println("duplicate.capacity="+duplicate.capacity()+",duplicate.limit="+duplicate.limit()+",duplicate.position="+duplicate.position());
    CharBuffer slice = duplicate.slice();
	System.out.println("slice.capacity="+slice.capacity()+",slice.limit="+slice.limit()+",slice.position="+slice.position());
    //slice.capacity=13,slice.limit=13,slice.position=0
	System.out.println(slice);
	
}
}

2.6直接字节缓冲区

​ 在在硬盘中和操作系统中处理的的数据都是01二进制,缓冲区中只有ByteBuffer字节缓冲区有资格参与I/O操作.

​ Channel通道只能使用ByteBuffer作为他的参数

​ 直接字节缓冲区通常是I/O操作的最好的选择,但是如果使用非直接字节缓冲区可能会导致性能损耗,如果向通道传递一个非直接字节缓冲区,通道可能会先创建一个临时的直接字节缓冲区,将非直接字节缓冲区的内容复制到临时的直接字节缓冲区中,使用临时直接直接字节缓冲区执行底层的I/O操作。

​ 直接字节缓冲区是I/O的最佳选择,但可能创捷直接字节缓冲区的成本比非直接字节缓冲区的成本要高,直接缓冲区使用内存是通过调用本地操作系统代码分配的,绕过了JVM的堆栈,现在jvm可能会执行缓冲区缓存的优化,入门的话不要考虑程序的优化问题,先保证程序的正确性。

​ ByteBuffer.allocateDirect()方法创建直接字节缓冲区

3.Channel

3.1 Channel概述

​ Channel是一种新的I/O的访问方式,用于在字节缓冲区与通道另一侧的实体(可以是文件也可以是socket)之间进行数据传输

​ Channel可以双向读取数据,也可以异步读写

​ 程序不能直接访问Channel ,Channel只能与Buffer缓冲区进行交互,即把通道中的数据读到Buffer缓冲区中,程序从缓冲区读取数据,在写操作时,程序把数据写入缓冲区中,再将缓冲区中的数据写入Channel通道中。

3.1.1常用的Channel

​ FileChannel,读写文件的Channel

​ SocketFannel/ServerSocketChannel,读写socket套接字中的的数据

​ DatagramChannel,通过UDP读写网络中的数据

3.2 Scatter/Gather

​ Scatter(发散) Gather(聚焦)是通道提供的一个重要功能(有时也称为矢量I/O) , Scatter/Gather 是指在多个缓冲区中实现一个简单的I/O操作

​ Scatter是指从Channel通道中读取数据,把这些数据按顺序分散写入到多个Buffer缓冲区中

Scatter

​ Gather是指在写操作时,将多个缓冲区当中的数据写入到同一个Channel通道中.

​ Scatte和Gather经常用于需要将传输的数据分开处理的场景

​ 如Scatter从一个Channel中读取数据存储到多个Buffer

​ ByteBuffer buf1=ByteBuffer.allocate(48);

​ ByteBuffer buf2=ByteBuffer.allocate(1024);

​ ByteBuffer [] bufferArray={buf1,buf2};

​ Channel.read(buferArray);

​ read()方法从Channel中读取数据,按照buffer在数组中的存储顺序依次存 储到缓冲区中,注意必须在buf1满后才能存储到buf2缓冲区中,使用Scatter和Gather处理的数据大小都是固定的。

3.3 FileChannel

​ FileChannel通过RandomAccessFile,FileInputStream,FileOutputStream对象调用getChannel()方法获取

​ FileChannel通道虽然是双向的,即可以读也可以写,但是从FileInputStream中获取的通道只能读不能写,如果进行写会抛出异常;从FileOutputStream流中获得的通道只能写不能读,如果访问的文件是只读的也不能进行写操作。

​ FileChannel是线程安全的,但是并不是所有的操作都是多线程的,如影响通道位置或文件大小的操作都是单线程的。

3.3.1.内存映射文件

​ FileChannel的map()方法可以将磁盘上的文件映射到虚拟内存中,把这块虚拟内存包装为一个MappedByteBuffer对象,(当一个文件映射到虚拟内存中,文件的内容通常不会从硬盘中读取到内存)

package com.xysf.channel;

import java.io.File;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.IOException;
import java.nio.CharBuffer;
import java.nio.MappedByteBuffer;
import java.nio.channels.FileChannel;
import java.nio.channels.FileChannel.MapMode;
import java.nio.charset.Charset;

public class Test04 {
public static void main(String[] args) {
	//把当前Test2.java文件以文件内存的方式读取到out.txt文件中
	//创建当前文件的File对象
	File file=new File("src/com/xysf/channel/Test2.java");
	try (
			FileChannel inChannel = new FileInputStream(file).getChannel();
			FileChannel outChannel=new FileOutputStream("out.txt").getChannel();
			){
		
		//将inChannel中的数据映射到虚拟内存当中
		MappedByteBuffer buffer = inChannel.map(MapMode.READ_ONLY, 0,file.length());
		outChannel.write(buffer);
		//也可以将Buffer中的内容打印出来
		buffer.flip();
		//创建字符集
		Charset defaultCharset = Charset.defaultCharset();
		//使用默认字符集把buffer中的字节转换为字符
		CharBuffer decode = defaultCharset.decode(buffer);
		System.out.println(decode);
		
	} catch (IOException e) {
		// TODO: handle exception
	}
	
	
}
}

3.3.2 FileChannel的双向传输

package com.xysf.channel;

import java.io.File;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.io.RandomAccessFile;
import java.nio.MappedByteBuffer;
import java.nio.channels.FileChannel;
import java.nio.channels.FileChannel.MapMode;

/*
 * 使用RandomAccessFile获得的通道是双向的
 * 把out.txt文件内容复制,追加到该文件的后面
 * */
public class Test05 {
public static void main(String[] args) {
	
	//创建File
	File file = new File("out.txt");
	try (RandomAccessFile randomAccessFile=new RandomAccessFile(file,"rw");
			FileChannel channel = randomAccessFile.getChannel();
			){
		//把channel中的数据映射到虚拟内存当中
		MappedByteBuffer buffer = channel.map(MapMode.READ_ONLY, 0, file.length());
		//设置channel的position属性
		channel.position(file.length());
		channel.write(buffer);//续写
		
	} catch (IOException e1) {
		// TODO Auto-generated catch block
		e1.printStackTrace();
	}
	

	
}
}

3.3.3设置缓冲区固定大小

package com.xysf.channel;

import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.FileOutputStream;
import java.io.IOException;
import java.nio.ByteBuffer;
import java.nio.channels.FileChannel;

/*
 * 读写文件时设置缓冲区为固定大小
 * */
public class Test06 {
	public static void main(String[] args) {
		try (
				FileChannel inchannel = new FileInputStream("out.txt").getChannel();
				FileChannel outChannel=new FileOutputStream("out2.txt").getChannel();
				){
			    ByteBuffer buffer = ByteBuffer.allocate(48);
			    int read=inchannel.read(buffer);
			    //当read不为-1时,没有读取到末尾文件
			    while(read!=-1){
			    	//将buffer中的内容写道OutChannel中
			    	  buffer.flip();//切换为
			    	  outChannel.write(buffer);
			    	  buffer.clear();
			    	  read=inchannel.read(buffer);
			    }
		} 
	catch (IOException e1) {
			// TODO Auto-generated catch block
			e1.printStackTrace();
		}
		
		
		
	}

}

3.3.4 Channel到Channel的传输

​ 经常需要把文件从一个位置批量传输到另一个位置,可以直接使用通道到通道之间的传输,不需要中间缓冲区传递数据

​ 注意,只有FileChannel支持通道到通道之间的传输

​ 通道到通道之间的传输十分迅捷,有的操作系统可以不使用用户空间直接传输数据。

package com.xysf.channel;

import java.io.File;
import java.io.FileOutputStream;
import java.io.RandomAccessFile;
import java.nio.channels.FileChannel;

/*
 * 通道到通道之间的传递
 * */
public class Test07 {
public static void main(String[] args) {
	//把文件out.txt复制到out3.txt中
	File file = new File("out.txt");
	try(
			    RandomAccessFile randomAccessFile = new RandomAccessFile(file, "rw");
		           FileChannel inChannel= randomAccessFile.getChannel();
			   FileChannel outChannel=new FileOutputStream("out3.txt").getChannel();
			){
		//把inChannel从0开始的所有字节传输到OutChannel中
		//inChannel.transferTo(0, file.length(), outChannel);
		
		//outChannel中的数据来自inChannel从0开始的所有字节
		outChannel.transferFrom(inChannel,0, file.length());
		
	}catch (Exception e) {
		// TODO: handle exception
	
	}
}
}

3.3.5 Gather

​ 把文件的属性和文件内容分别存储到不同的缓冲区当中,再写入到另外一个文件中

package com.xysf.channel;

import java.io.File;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.FileOutputStream;
import java.io.IOException;
import java.net.URLConnection;
import java.nio.ByteBuffer;
import java.nio.MappedByteBuffer;
import java.nio.channels.FileChannel;
import java.nio.channels.FileChannel.MapMode;
import java.text.SimpleDateFormat;
import java.util.Date;

/*
 * 把文件的属性和文件的内容,通过Gather形式写到另一个文件中
 * */
public class Test08 {
public static void main(String[] args) throws IOException {
	File file = new File("out.txt");//读取文件
	//获得文件属性
	String absolutePath = file.getAbsolutePath();//获得绝对路径
	long lastModified = file.lastModified();//最后一次修改时间
	SimpleDateFormat simpleDateFormat = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
	String format = simpleDateFormat.format(new Date(lastModified));
	String headerText="filename:"+absolutePath+"\r\n"+"last modified:"+format+"\r\n";
	//把文件属性存储在缓冲区中
	ByteBuffer header = ByteBuffer.wrap(headerText.getBytes());
	ByteBuffer contentBuf = ByteBuffer.allocate(128);//用来存储文件类型
	//创建一个缓冲区数组
	ByteBuffer[] gather ={header,contentBuf,null};
	
	String contentType="unknown";
	long contentLength=-1;
	try {
		FileInputStream fileInputStream = new FileInputStream(file);
		FileChannel fc = fileInputStream.getChannel();
		MappedByteBuffer byteBuffer = fc.map(MapMode.READ_ONLY,0,fc.size());
		//将文件内容保存到gather中
		 gather[2]=byteBuffer;
		 contentLength=fc.size();
		 contentType = URLConnection.guessContentTypeFromName(file.getAbsolutePath());
	} catch (Exception e) {
	      ByteBuffer byteBuffer = ByteBuffer.allocate(128);
	      String msg="文件访问异常"+e+"\n";
	      byteBuffer.put(msg.getBytes());
	      byteBuffer.flip();//切换为读模式
	      gather[2]=byteBuffer;//将文件的异常信息存储到数组中
	}
	StringBuilder sb=new StringBuilder();
	sb.append("contentLength:").append(contentLength).append("\r\n");
	sb.append("contentType:").append(contentType).append("\r\n");
	contentBuf.put(sb.toString().getBytes());
	contentBuf.flip();
	
	//把gather中的数组写到目标文件中
	FileOutputStream outputStream = new FileOutputStream("gather.txt");
	FileChannel channel = outputStream.getChannel();
	while (channel.write(gather)>0) {//用while是因为有可能一次性写不完。。
	}
}
}

3.4 SocketChannel与ServerSocketChannel

​ ServerSocketChannel可以监听新进来的TCP连接通道

​ SocketChannel是一个连接TCP网络套拉字的通道

package com.xysf.channel;

import java.io.IOException;
import java.net.InetSocketAddress;
import java.nio.ByteBuffer;
import java.nio.CharBuffer;
import java.nio.channels.ServerSocketChannel;
import java.nio.channels.SocketChannel;
import java.nio.charset.Charset;

/*
 * ServerSocketChannel
 服务器端
 * */

public class ServerSocketChannelTest09 {

	public static void main(String[] args) throws IOException, InterruptedException {
		int port = 1234;
		// 建立一个未绑定ServcerSocket服务器的通道
		ServerSocketChannel serverSocketChannel = ServerSocketChannel.open();
		// serverSocketChannel没有绑定方法,需要先通过socket()方法获得ServerSocket对象
		serverSocketChannel.socket().bind(new InetSocketAddress(port));
		// 设置通道为非阻塞模式,当没有传入连接时,accept()方法返回null
		serverSocketChannel.configureBlocking(false);
		while (true) {
			System.out.println("我是ServerSocket,已经准备好就等你来了");
			SocketChannel sc = serverSocketChannel.accept();
			if (sc == null) {
				Thread.sleep(1000);
			} else {
				// 先给客户端发送一个问候
				ByteBuffer byteBuffer = ByteBuffer.allocate(1024);
				byteBuffer.put("hello!I am from ServerSocket.".getBytes());
				byteBuffer.flip();
				sc.write(byteBuffer);

				// 在读取客户端中发送来的内容
				System.out.println("from Socket Client:" + sc.socket().getRemoteSocketAddress());
				byteBuffer.clear();
				sc.read(byteBuffer);// 读取客户端发送的数据,保存到buffer中
				byteBuffer.flip();
				CharBuffer decode = Charset.defaultCharset().decode(byteBuffer);
				System.out.println(decode);
				sc.close();
			}

		}

	}
}

package com.xysf.channel;

import java.io.FileInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.net.InetSocketAddress;
import java.nio.ByteBuffer;
import java.nio.CharBuffer;
import java.nio.channels.Channels;
import java.nio.channels.ReadableByteChannel;
import java.nio.channels.SocketChannel;
import java.nio.charset.Charset;
//客户端
//SocketChannel
public class SocketChannelTest10 {
public static void main(String[] args) throws IOException {
	String IP = "localhost";//Serversocket的IP地址
	int port=1234;
	InetSocketAddress inetSocketAddress = new InetSocketAddress(IP,port);
	 //创建一个未连接的SocketChannel()
	 SocketChannel sc = SocketChannel.open();
     //建立服务器的连接	 
     sc.connect(inetSocketAddress);
	 //TCP连接需要一定的时间,两个连接的建立需要进行包对话
	 //调用finishConnect()方法完成连接过程,如果没有连接成功返回false
	 
     	 while (!sc.finishConnect()) {
			System.out.println("等待连接过程中......");
		}
     	 System.out.println("连接成功!!");
     	 //向服务器发送消息
     	 ByteBuffer buffer = ByteBuffer.wrap("Im from client socket!".getBytes());
     	 while (buffer.hasRemaining()) {
			sc.write(buffer);
		}
     	 
     	 //获得服务器给Client发送的消息
     	 InputStream inputStream = sc.socket().getInputStream();
     	 ReadableByteChannel newChannel = Channels.newChannel(inputStream);
     	 buffer.clear();
     	 newChannel.read(buffer);
     	 buffer.flip();
     	 CharBuffer decode = Charset.defaultCharset().decode(buffer);
     	 System.out.println(decode);
     	 
     	 
	
}
}

3.5 DatagramChannel

​ DatagramChannel是对UDP无连接用户数据协议的通道

​ 发送端

package com.xysf.channel;

import java.io.IOException;
import java.net.InetSocketAddress;
import java.net.SocketAddress;
import java.nio.ByteBuffer;
import java.nio.channels.DatagramChannel;
import java.util.Scanner;

public class Test12DatagramChannelSender {
	public static void main(String[] args) throws IOException {
		// 创建未绑定的Channel
		DatagramChannel dc = DatagramChannel.open();
		dc.configureBlocking(false);
		ByteBuffer buffer = ByteBuffer.allocate(1024);
		Scanner scanner =new Scanner(System.in);
		while (scanner.hasNext()) {
			String inputString = scanner.next();
			buffer.clear();
			buffer.put(inputString.getBytes());
			buffer.flip();//切换读模式
			dc.send(buffer,new InetSocketAddress("localhost",8899));
			//接收数据
			buffer.clear();
			SocketAddress receive = dc.receive(buffer);
			while (receive==null) {
				receive = dc.receive(buffer);
			}
			buffer.flip();
			System.out.println(new String(buffer.array(),0,buffer.limit()));
			
		}

	}
}

​ 接收端

package com.xysf.channel;

import java.io.IOException;
import java.net.DatagramSocket;
import java.net.InetSocketAddress;
import java.net.SocketException;
import java.nio.ByteBuffer;
import java.nio.channels.DatagramChannel;
import java.util.Scanner;

public class Test11DataGramChannelReceiver {
	public static void main(String[] args) throws IOException {
		// 创建一个未绑定的通道
		DatagramChannel dc = DatagramChannel.open();
		dc.bind(new InetSocketAddress(8899));
		dc.configureBlocking(false);
		Scanner scanner = new Scanner(System.in);
		ByteBuffer buffer = ByteBuffer.allocate(1024);
		while (true) {
			// 先接收数据
			buffer.clear();
			InetSocketAddress isa = (InetSocketAddress)dc.receive(buffer);
			if (isa == null) {
				continue;
			}
            System.out.print("data from:"+isa);
            buffer.flip();
            String msg = new String(buffer.array(),0,buffer.limit());
            System.out.println(":"+isa.getPort()+"-->"+msg);
            //发送数据
            String next = scanner.next();
            dc.send(ByteBuffer.wrap(next.getBytes()), isa);
		}
	}
}

3.6 Pipe

​ Pipe管道用于两个线程之间的数据连接

​ Pipe有一个Source通道和Sink通道

Piepe

创建通道

​ Pipe pipe = new Pipe();

向管道中写数据,首先要访问Sink通道

​ Pipe.SinkChannel sc = new Pipe.SinkChannel();

​ sc.write(buffer);

读数据需要通过Source通道

​ Pipe.SourceChannel source = new Pipe.SourceChannel();

​ source.read(buffer);

package com.xysf.channel;

import java.io.IOException;
import java.io.PipedInputStream;
import java.io.PipedOutputStream;

/*
 * 演示在两个线程之间通过Pipe管道数据传输
 * 使用PipedOutputStream和PipedInputStream两个管道输出流和管道输入流类
 * 在管道通信时,线程A向PipeOutputStream中写入数据,这些数据会自动发送到对应的PipeInputStream
 * */
public class Test14Pipe {
	public static void main(String[] args) throws IOException {

		// 输入流管道
		PipedInputStream pipedInputStream = new PipedInputStream();
		// 输出流管道
		PipedOutputStream pipedOutputStream = new PipedOutputStream();
		// 在输入流与输出流管道之间建立连接
		pipedInputStream.connect(pipedOutputStream);
		// 创建线程
		Sender sender = new Sender(pipedOutputStream);
		Receiver receiver = new Receiver(pipedInputStream);

		new Thread(sender).start();
		new Thread(receiver).start();
	}
}

class Sender implements Runnable {
	PipedOutputStream out;

	public Sender() {
		super();
	}

	public Sender(PipedOutputStream out) {
		super();
		this.out = out;
	}

	@Override
	public void run() {
		// 模拟发送数据
		try {
			for (int i = 1; i < 100; i++) {
				String text = "hello,sender" + i + "\r\n";
				out.write(text.getBytes());
			}
		} catch (IOException e) {
			// TODO Auto-generated catch block
			e.printStackTrace();
		} finally {
			try {
				out.close();
			} catch (IOException e) {
				// TODO Auto-generated catch block
				e.printStackTrace();
			}
		}
	}
}

// 接受端

class Receiver implements Runnable {
	PipedInputStream in;

	public Receiver() {
		super();
		// TODO Auto-generated constructor stub
	}

	public Receiver(PipedInputStream in) {
		super();
		this.in = in;
	}

	@Override
	public void run() {
		try {
			// 接收数据
			byte[] bytes = new byte[1024];
			int len = in.read(bytes);
			while (len > 0) {
				System.out.println(new String(bytes, 0, len));
				len = in.read(bytes);
			}
		} catch (IOException e) {
			// TODO Auto-generated catch block
			e.printStackTrace();
		} finally {
			try {
				in.close();
			} catch (IOException e) {
				// TODO Auto-generated catch block
				e.printStackTrace();
			}
		}
	}

}

4. Slector选择器

4.1 选择器基础

​ 选择器提供一种选择执行已经就绪的任务的能力

​ selector选择器允许单线程处理多个通道

​ 如果程序打开多个连接通道,每个连接都流量都特别的底,可以使用selector对通道进行管理。

selector

4.1.1 三个相关的类

​ Selector选择器管理着被注册的通道的集合的信息和他们的就绪状态

​ SelectableChannel可选择通道类,他是抽象类,是所有支持就绪检查

​ 的通道类的父类

​ (注意:FileChannel不是SelectorChannel的子类,即FileChannel不是可选择的,可选择通道可以注册到多个选择器上,但是同一个选择器只能注册一次)

4.1.2 如何建立选择器

​ 1)创建Selector

​ Selector selector = new Selector

​ 2)必须将通道设置为非阻塞模式才能注册到选择器上

​ 3)把注册器注册到选择器上,会返回一个选择键

​ SelectionKey key1 = selectableChannel.register

​ (selector,SelectionKey.OP_RRAD);

​ register()方法参数表示通道注册的选择器

​ 第二个参数表示关心通道的那个操作

​ SelectKey key2 =selectableChannel2.register(selector,Selection.

​ OP_READ|SelectionKey.OP_WRITE);//使用位运算将你关心的操

​ 作连接起来

​ SelectionKey的操作有:

​ SelectionKey.OP_CONNECT,指某个通道连接到服务器

​ SelectionKey.OP_ACCEPT,只有ServerSocketChannel有这个

​ 事件,查看是否有新的连接

​ SelectionKey.OP_READ 是否有可读的通道就绪

​ SelectionKey.OP_WRITE,写数据的通道是否就绪

​ 注册完成后,可以调用Select()方法轮询是否有就绪的通道

​ int readCount = selector.select();

​ select()方法,返回的是就绪通道的数量

4.2 选择键相关的方法

​ 向Selector注册一个Channel通道时,就会返回一个SelectionKey选择键对象,这个选择键表示一个通道与一个选择器之间的注册关系

​ SelectionKey相关的方法:

​ channel()方法,返回该键对应的通道

​ selector()方法,返回通道注册的选择器

​ cancel()方法,终结这种特定的注册关系

​ isValid()方法,判断注册器关系是否有效

​ interestOps()方法,返回你关心的操作,是以整数的形式进行编码的比特掩码,可以使用位运算检查所关心的操作,如:

​ Boolean isAccept = interestops & SelectionKey.OP_ACCEPT == SelectionKey.OP_ACCEPT

​ Boolean isConnect = interestops &SelectionKey.OP_CONNECT=interestops & SelectionKey.OP_CONNECT

​ readyOps()方法,返回通道已就绪的操作,返回值也是一个整数,也可以使用上面相同的位操作检测通道有那个事件或操作已就绪,如:

​ selectionKey.readyOps() & SelectionKey.OP_WRITE !=0 说明操作已就绪

​ 除了按位与操作之外,还可以使用isReadable(),isWritable(),isConnectable,

​ isAccetable()等方法检测这些比特值,上面一行检测write就绪的操作可以使用下面一行代码代替

​ if(selectionKey.isWritable){}

4.3 使用选择器

​ Selector选择器维护着注册过的通道集合,并且这些注册关系都封装在了SelectionKey对象中

​ 每个Selector对象都需要维护以下三个集合:

​ 已注册的键的集合,keys()方法返回这个已注册的键的集合,这个集合不能修改

​ 已选择的键的集合,selectKeys()方法返回,该集合中每个成员都是相关的通道被选择器判断已准备好的,并且包含了键的interest集合中的操作,键可以从集合中移除,不能添加

​ 已取消的建的集合,这个集合包含了调用过cancel()方法的键

开始刚刚初始化Selector对象,这三个集合都是空的。

​ Selector类的核心就是select()选择,该方法调用时执行一下步骤:

1)检查已取消键的集合,如果该集合非空,就把该集合的键从另外两个集合中移除,注销相关通道,这个步骤结束后,已取消的键的集合应该是空的

2)检查已注册键的集合中所有键的interest集合,确定每个通道所关心的操作是否已经就绪

3)Select()方法返回的是从上一次调用Select()方法后进入就绪状态的通道的数量

通常使用以下方法来管理这些键:

1)在选择器上调用select()方法

2)便利selectKeys()方法返回键的集合

​ 检查每个键,查看相关通道的就绪信息,并进行处理

​ 将键从已选择集合中移除

​ 继续检查下一个键

4.4使用selector开发服务器端

package com.xysf.channel;

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

//服务器
public class Test15Serve {
	public static final int PORT = 8848;// 服务器端口号
	private static ByteBuffer buffer = ByteBuffer.allocate(1024);
	private static ServerSocketChannel ssc;

	public static void main(String[] args) throws IOException {
		// 打开一个未绑定的ServeSocketChannel
		ssc = ServerSocketChannel.open();
		// 获得serveSocketChannel关联的ServeSocket
		ServerSocket serverSocket = ssc.socket();
		// 绑定到指定端口
		serverSocket.bind(new InetSocketAddress(PORT));
		// 设置到ServerSocketChannel为非阻塞通道
		ssc.configureBlocking(false);

		// 创建选择器
		Selector selector = Selector.open();

		// 将ServerSocketChannel注册到选择器上
		ssc.register(selector, SelectionKey.OP_ACCEPT);

		while (true) {

			// 调用select()方法,会等待就绪的通道,会发生阻塞
			int n = selector.select();// 返回就绪通道的数量
			if (n == 0) {
				continue; // 没有就绪的通道,什么也不做
			}
			// 获得选择键集合的迭代器
			Iterator<SelectionKey> iterator = selector.selectedKeys().iterator();
			while (iterator.hasNext()) {
				SelectionKey selectionKey = iterator.next();
				// 判断通道上是否有接受的连接
				if (selectionKey.isAcceptable()) {
					// 有新的连接,获得ServerSocketChannel
					ServerSocketChannel sscTem = (ServerSocketChannel) selectionKey.channel();
					// 调用accept方法返回到达服务器的i你客户端连接
					SocketChannel socket = sscTem.accept();
					socket.configureBlocking(false);
					socket.register(selector, SelectionKey.OP_READ | selectionKey.OP_WRITE);
				}

				if (selectionKey.isReadable()) {
					// 就绪通道有数据可读
					readDataFromSocket(selectionKey);
				}
				if (selectionKey.isWritable()) {
					//写数据
					writeDateFrom(selectionKey);
				}
			}
		}
	}

	private static void writeDateFrom(SelectionKey selectionKey) {
		
		
	}

	// 读取指定通道上的数据
	private static void readDataFromSocket(SelectionKey selectionKey) throws IOException {
		// 获得SocketChannel
		SocketChannel socketChannel = (SocketChannel) selectionKey.channel();
		ArrayList<Byte> list = new ArrayList<Byte>();//
		buffer.clear();
		int count = socketChannel.read(buffer);
		while (count > 0) {
			buffer.flip();// 切换为读模式
			while (buffer.hasRemaining()) {
				list.add(buffer.get());
			}
			buffer.clear();
			count = socketChannel.read(buffer);
		}
		byte[] bytes = new byte[list.size()];
		for (int i = 0; i < list.size(); i++) {
                  bytes[i]=list.get(i);
		}
		//生成字符串
		String string = new String(bytes).trim();
		System.out.println(string);
		if (count<0) {
			socketChannel.close();
		}
	}
}

4.5开发客户端

package com.xysf.channel;

import java.io.IOException;
import java.net.InetSocketAddress;
import java.nio.ByteBuffer;
import java.nio.channels.SocketChannel;
import java.util.Scanner;

//客户端
public class Test16Client {
	public static void main(String[] args) throws IOException {
        //打开通道    
		SocketChannel sc = SocketChannel.open();
        
		//设置非阻塞
		sc.configureBlocking(false);
		//连接服务器
		sc.connect(new InetSocketAddress(8848));
		//调用finshConnect()方法完成连接
		while (!sc.finishConnect()) {
			System.out.println("正在建立连接,请稍等......");
		}
		System.out.println("连接成功");
		Scanner scanner = new Scanner(System.in);
		//向服务器发送数据
		while (true) {
			System.out.println("请输入要发送的内容");
			String inputval = scanner.next();
			ByteBuffer buffer = ByteBuffer.allocate(inputval.length());
			buffer.put(inputval.getBytes());
			buffer.flip();
			while (buffer.hasRemaining()) {
				sc.write(buffer);
			}
			buffer.clear();
			
		}
	}
}


4.5解决错误

客户端错误:

1639400779568

服务端错误:

1639400810194

原因:

posted @ 2021-12-13 21:10  风をした  阅读(138)  评论(0)    收藏  举报