NIO的概念
Java NIO是JDK1.4以后引入的新型IO,即New IO或者Non Blocking IO(非阻塞式IO),它主要是为了高并发场景而诞生的,功能强大,NIO具备传统IO的所有功能,但是使用方式完全不同,NIO支持面向缓冲区的、基于通道的IO操作,NIO将以更加高效的方式进行文件的读写操作。下面是IO和NIO的主要区别:
IO | NIO |
---|---|
面向流(Stream Oriented) | 面向缓冲区(Buffer Oriented) |
阻塞IO(Blocking IO) | 非阻塞IO(Non Blocking IO) |
无 | 选择器(Selectors) |
上表描述了传统IO和NIO之间的主要区别,前一个主要是传输方式的一种区别,IO是通过流直接通过流完成了文件到程序之间的传输,并且输入有输入流、输出有输出流,具备单向传输的特性:
在输入、输出流上直接携带信息进行传输;而NIO是通过一条通道,然后通过通道来传输携带信息的缓冲区,这个缓冲区是可以双向传输的:
在NIO中,Channel
负责传输,就是上图中的通道,Buffer
负责存储,就是上图中的缓冲区。在NIO中主要学习就是上面提及的三点:缓冲区、通道和选择器。下面一一来看。
1. 缓冲区
缓冲区(Buffer)在NIO中负责数据的存取,缓冲区本质上就是数组,用于存取不同数据类型的数据,它为除boolean
外所有的基本数据类提供了各自对应的缓冲区:ByteBuffer
、CharBuffer
、ShortBuffer
、IntBuffer
、LongBuffer
、FloatBuffer
、DoubleBuffer
。上面几种缓冲区的管理方式几乎一致:
- 通过
XXXBuffer.allocate()
获取指定大小的缓冲区; - 通过
put()
将数据存入缓冲区; - 通过
get()
将数据从缓冲区取出;
对于缓冲区有如下的四个特别重要的属性:
capacity
表示缓冲区的最大存储数据的容量;limit
表示缓冲区可以操作数据的大小,即limit
后面的数据不能进行读写;position
表示缓冲区中正在操作的数据位置,所以它必定不大于limit
;mark
是标记,用于记录position
的位置,通过reset()
方法可以恢复到mark
的位置。
具体的操作过程如下:
@Test
public void testByteBuffer() {
//分配一个1024字节的缓冲区
ByteBuffer byteBuffer = ByteBuffer.allocate(1024);
System.out.println("--------init------------");
System.out.println("position: " + byteBuffer.position());
System.out.println("limit: " + byteBuffer.limit());
System.out.println("capacity: " + byteBuffer.capacity());
//向缓冲区中存放东西后,再次观察
byteBuffer.put("wer".getBytes());
System.out.println("--------put------------");
System.out.println("position: " + byteBuffer.position());
System.out.println("limit: " + byteBuffer.limit());
System.out.println("capacity: " + byteBuffer.capacity());
//缓冲区切换为可读的模式
byteBuffer.flip();
System.out.println("--------change to read mode------------");
//再次查看变化
System.out.println("position: " + byteBuffer.position());
System.out.println("limit: " + byteBuffer.limit());
System.out.println("capacity: " + byteBuffer.capacity());
//初始化一个存放读取缓冲区内容的数组,大小即为byteBuffer.limit()
byte[] dst = new byte[byteBuffer.limit()];
byteBuffer.get(dst);
System.out.println("get from buffer: " + new String(dst, 0, dst.length));
System.out.println("--------after get------------");
System.out.println("position: " + byteBuffer.position());
System.out.println("limit: " + byteBuffer.limit());
System.out.println("capacity: " + byteBuffer.capacity());
//可重复度模式,带操作位置回到0
byteBuffer.rewind();
System.out.println("--------after rewind------------");
System.out.println("position: " + byteBuffer.position());
System.out.println("limit: " + byteBuffer.limit());
System.out.println("capacity: " + byteBuffer.capacity());
//清空缓存
byteBuffer.clear();
System.out.println("--------after clear------------");
System.out.println("position: " + byteBuffer.position());
System.out.println("limit: " + byteBuffer.limit());
System.out.println("capacity: " + byteBuffer.capacity());
System.out.println((char) byteBuffer.get());
}
在缓冲区中主要理解是位置的变化,上述代码的过程如下:
上面的4个都是一些数字,具体看一下是如何这4个属性是如何变化的,以初始化一个1024大小的缓冲区为例
缓冲区初始化时:
- 缓冲区正在操作的数据位置
position
为0,其实我觉着理解为缓冲区待操作的位置更加合适,因为刚初始化时0号位无数据,所以才为0; - 缓存区的容量大小
capacity
为1024; - 缓存区可以操作的大小
limit
为1024,即可操作的最大索引号(不包括本身);
存放一些内容,这里存放“wer”为例,到缓冲区中:
- 此时,缓冲区待操作的位置
position
为3; - 缓存区的容量
capacity
和可操作的大小limit
还是为1024;
缓冲区通过flip()
方法可以切换到读的状态:
- 此时的缓冲区待操作位置
position
为0; - 缓存区可以操作(读操作)的大小
limit
为3,索引号大于等于3的都不可以操作; - 缓存区的容量
capacity
为1024,其实这一点在初始化后这个就一直不会再变;
获取缓冲区中的内容后的状态:
因为每获取一次,缓冲区中的待操作位置position
都要后移一位,直到移至limit
位置,表示读操作已经获取了缓冲区中所有内容,所以这里position
和limit
都为3,容量capacity
为1024不变;
获取缓冲区后还想再次获取一次,调用rewind()
方法后
- 此时的缓冲区待操作位置
position
重新回到0; - 缓存区可以操作(读操作)的大小
limit
为3; - 缓存区的容量
capacity
为1024;
还有一点是缓冲区调用clear()
清空后:
直接回到初始化的状态,很好理解
- 此时的缓冲区待操作位置
position
为0; - 缓存区可以操作(读操作)的大小
limit
为1024; - 缓存区的容量
capacity
为1024;
【注意】这里需要注意的是,虽然调用了clear()
方法,但是缓冲区中的内容并没有真正的清空,也就是说”wer”还是在缓冲区中的,但是这些数据处于“被遗忘”的状态,所以最后还是有输出为“w”。
下面看一下mark
的用法,用于记录position
的位置,下面是关于mark
的简单用法:
ByteBuffer byteBuffer = ByteBuffer.allocate(1024);
byteBuffer.put("jklghsaf".getBytes());
//切换成读模式
byteBuffer.flip();
byte[] dst = new byte[byteBuffer.limit()];
//从缓冲区的0号位置开始读,读取2个
byteBuffer.get(dst, 0, 2);
//注意String的构造方法
System.out.println(new String(dst, 0, 2));
System.out.println(byteBuffer.position());
byteBuffer.mark();
byteBuffer.get(dst, 2, 2);
System.out.println(new String(dst, 2, 2));
System.out.println(byteBuffer.position());
//调用reset()方法后,position回到原来mark标记时的position位置2
byteBuffer.reset();
System.out.println(byteBuffer.position());
1.1 直接缓冲区和非直接缓冲区
在前面已经介绍过了缓冲区的几个重要概念,其中缓冲区还分为直接缓冲区和非直接缓冲区:
- 直接缓冲区:通过
allocateDirect()
方法直接分配缓冲区到本地系统物理内存中,可以提高效率; - 非直接缓冲区:通过
allocate()
方法在JVM内存中建立缓冲区。
2. 通道(Channel)
通道是NIO中较为重要的一个概念,这传统的“流”有点相似,表示IO源和目标打开的连接,负责缓冲区中数据的传输,注意Channel
本身不是数据,只是通道,它只能与Buffer
进行交互,即配合缓冲区进行工作。在处理类大型文件的读写时,通道的方式要比“流”的方式更快,因为它的CPU的利用率更高。
在java中,java.nio.channels.Channel
接口的主要实现类有:
FileChannel
:本地文件通道;SocketChannel
:用于TCP传输通道;ServerSocketChannel
:用于TCP传输通道;DatagramChannel
:用于UDP传输通道;
通道的获取方式,在JDK1.7以后只要支持通道的类都提供了三种方式:
1. getChannel()
方法,支持通道的类主要分为:
本地IO:
FileInputStream/FileOutputStream
;RandomAccessFile
;
网络IO:
Socket
;ServerSocket
;DatagramSocket
;
2.在JDK1.7中的NIO2(对NIO改进后的版本)针对通道提供了open()
方法;
3. 在JDK1.7中的NIO2中的File工具类的newByteChannel()
。
下面来看一些具体的代码操作,常规的文件复制在NIO之前我们一般用IO这么去搞:
@Test
public void testIOCopy() throws IOException {
//定义目标文件并获取输入流
File file = new File("C:\\Users\\weiguo Liu\\Desktop\\课表.txt");
InputStream is = new FileInputStream(file);
BufferedReader br = new BufferedReader(new FileReader(file));
String in = br.readLine();
while(in!=null){
System.out.println(in);
in = br.readLine();
}
is.close();
br.close();
}
在NIO中,步骤相较于上述IO的读取和传输有些繁琐,主要在于NIO不是直接传输数据,而是先将数据写入缓冲区,然后再将缓冲区写入通道,虽然略微繁琐,但是它在处理大型文件时比较高效,下面是简单的文件复制的过程(利用非直接缓冲区):
@Test
public void testChannel() {
FileInputStream fis = null;
FileOutputStream fos = null;
FileChannel fisChannel = null;
FileChannel fosChannel = null;
try {
fis = new FileInputStream("C:\\Users\\weiguo Liu\\Desktop\\课表.txt");
fos = new FileOutputStream("C:\\Users\\weiguo Liu\\Desktop\\课表-复制.txt");
//从支持通道的类中打开通道
fisChannel = fis.getChannel();
fosChannel = fos.getChannel();
//创建缓冲区,非直接缓冲区
ByteBuffer buffer = ByteBuffer.allocate(1024);
//先将数据从输入文件写入缓冲区,然后再将缓冲区写入输出通道即可
while (fisChannel.read(buffer) != -1) {
//缓冲区切换到读模式
buffer.flip();
//将缓冲区写入输出通道
fosChannel.write(buffer);
//清空缓冲区,循环读写
buffer.clear();
}
} catch (IOException e) {
e.printStackTrace();
} finally {
//最后关闭通道
if (fisChannel!=null) {
try {
fisChannel.close();
} catch (IOException e) {
e.printStackTrace();
}
}
if (fosChannel != null) {
try {
fosChannel.close();
} catch (IOException e) {
e.printStackTrace();
}
}
if (fis != null) {
try {
fis.close();
} catch (IOException e) {
e.printStackTrace();
}
}
if (fos != null) {
try {
fos.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
}
那利用直接缓冲区如何高效完成文件的复制呢,下面利用内存映射文件来实现文件的,首先看一下程序完成文件复制的流程:
在程序操作文件的复制并不是直接想象中的程序直接可以操作电脑中物理磁盘中的文件,两者之间是通过内核地址空和用户地址空间之间的交互完成操作的,而且中间不断的有文件的Copy操作,这中间的过程会耗费大量的时间,那么这里引入物理内存映射文件直接可以跨过中间的步骤更快速的完成文件的操作,虽然没有使用allocateDirect()
方法,但是原理是一样的,下面看具体代码的实现:
/*使用直接缓冲区完成文件的复制(采用内存映射文件的方式MappedBuffer)
* 效率高(很明显)但是不太稳定,有时垃圾回收机制不能及时运行会一直占用资源,导致程序长时间不能
* 彻底结束,就会导致一个现象,文件已经复制完成,但是程序还在跑,所以选择的时候需要做一定的测试
* 耗费时间 118009s 2635s 2533s
*/
@Test
public void testObjectCopy() throws IOException {
long strat = System.currentTimeMillis();
//注意open()方法中的两类参数,第一个参数是文件路径,后面的几个参数表示要对该路径下的文件做什么操作
// 这里的Paths.get()括号里面的信息操作文件的路径,它也提供了字符串的拼接功能,多个字符串之间用逗号隔开
//StandardOpenOption.READ表示对上述路径下的文件进行读操作
FileChannel inChannel = FileChannel.open(Paths.get("E:\\电影\\终极硬.mkv"), StandardOpenOption.READ);
//StandardOpenOption.CREATE_NEW表示对应的位置上存在相应的文件就会报异常,不存在就创建
//StandardOpenOption.CREATE表示对应的位置上是否存在相应的文件都会创建
//StandardOpenOption.WRITE表示要对对应路径上的文件进行写操作
FileChannel outChannel = FileChannel.open(Paths.get("E:\\电影\\终极硬_复制.mkv"), StandardOpenOption.WRITE, StandardOpenOption.READ, StandardOpenOption.CREATE);
/*内存映射文件MappedByteBuffer只有Byte支持,采用直接缓冲方式(将缓冲区建立在内存中),
它的道理和allocateDirect()方法原理是一样的,现在的缓冲区在物理缓冲区。
注意参数:第一个参数表示对内存映射文件的何种操作
第二个参数表示操作的第一个位置的标号
第三个参数表示操作多少位
这里有在创建内存映射文件时,特别注意要和通道定义时的文件操作行为一致,否则报错;
inChannel和inMappedBuf内存映射文件对应:
前者是StandardOpenOption.READ支持读,那么对应内存映射文件(即后者)是MapMode.READ_ONLY
outChannel和outMappedBuf内存映射文件对应:
前者是StandardOpenOption.WRITE和StandardOpenOption.READ,后者对应的是MapMode.READ_WRITE
其实对于输出内存映射文件我们只需要它的写功能即可,但是他没有MapMode.WRITE_ONLY,所以
只能用READ_WRITE模式
*/
MappedByteBuffer inMappedBuf = inChannel.map(FileChannel.MapMode.READ_ONLY, 0, inChannel.size());
MappedByteBuffer outMappedBuf = outChannel.map(FileChannel.MapMode.READ_WRITE, 0, inChannel.size());
//此时不再需要通道来进行传输,直接放到缓冲区就是放到了物理内存中,也就是写到了文件中
byte[] dst = new byte[inMappedBuf.limit()];
inMappedBuf.get(dst);
outMappedBuf.put(dst);
inChannel.close();
outChannel.close();
long end = System.currentTimeMillis();
System.out.println("time long: " + (end- strat) + "s");
}
除了上述的方式,还可以直接利用管道直接进行数据的传输:
//通道之间的传输(直接缓冲区):transferTo和transferFrom
@Test
public void testCopyWithChannel() throws IOException {
long start = System.currentTimeMillis();
//这里的Paths.get()括号里面的信息提供了字符串的拼接功能,多个字符串之间用逗号隔开
FileChannel inChannel = FileChannel.open(Paths.get("E:\\电影\\终极硬.mkv"));
//StandardOpenOption.CREATE_NEW表示对应的位置上有相应的文件就会报异常
FileChannel outChannel = FileChannel.open(Paths.get("E:\\电影\\终极硬_复制.mkv"), StandardOpenOption.WRITE, StandardOpenOption.READ, StandardOpenOption.CREATE);
//方法1:表示数据从inChannel的0号位置到末位传输到outChannel通道
inChannel.transferTo(0, inChannel.size(), outChannel);
//方法2:有to就有from,道理是一样的,下面的作用和上面的一样
//outChannel.transferFrom(inChannel, 0, inChannel.size());
inChannel.close();
outChannel.close();
long end = System.currentTimeMillis();
System.out.println("time long: " + (end-start) + "s");
}
上面两种代码都是通过直接缓冲区的方式进行数据的传输,在复制大型文件时可以更快完成操作。
【注意】上面已经测试过八九百兆大小电影文件复制的时间,采用内存映射文件的方式可以更快的完成文件的复制,但是有时不太稳定,主要是由于java本身的来及回收机制导致的,所以上第一个代码片测试时出现了“118009s 2635s 2533s”这样的情况,第一个数字很突兀,就是因为垃回收机制没有及时的完成,导致通过无法关闭,出现了磁盘上文件已经复制完成,但是程序莫名奇妙的还没有结束运行。
3. 分散(Scatter)读入和聚集(Gather)写入
这两个概念是见名知意,具体的定义如下:
- 分散读取(Scattering Reads):指从通道Channel中读取的数据“分散”到多个缓冲区中(按顺序);
- 聚集写入(Gathering Write):将多个缓冲区聚集到通道中(按顺序);
主要示例代码如下:
@Test
public void testScatterandGather() throws IOException {
//第一个参数为文件路径,第二个为操作行为模式
RandomAccessFile raf = new RandomAccessFile("C:\\Users\\weiguo Liu\\Desktop\\课表.txt", "rw");
FileChannel channel = raf.getChannel();
//分为两个缓冲区进行操作,一个100,一个1024
ByteBuffer buffer1 = ByteBuffer.allocate(100);
ByteBuffer buffer2 = ByteBuffer.allocate(1024);
ByteBuffer[] bufs = {buffer1, buffer2};
channel.read(bufs);
for (ByteBuffer bf: bufs) {
bf.flip();
}
//这里的构造参数是byte[]数组,所以先转成数组
System.out.println(new String(bufs[0].array(), 0, bufs[0].limit()));
System.out.println("=====================================");
System.out.println(new String(bufs[1].array(), 0, bufs[1].limit()));
//聚集写入
RandomAccessFile raf2 = new RandomAccessFile("C:\\Users\\weiguo Liu\\Desktop\\课表_复制.txt", "rw");
FileChannel channel2 = raf2.getChannel();
channel2.write(bufs);
}
4. 字符集(Charset)
这里经常出现乱码的问题,Charset
就是用来解决这个问题的,首先两个概念:
- 编码:字符串–>字节数组
- 解码:字节数组–>字符串
看一下主要的Charset
支持的有哪些:
@Test
public void testCharset() {
SortedMap<String, Charset> availableCharsets = Charset.availableCharsets();
Set<Map.Entry<String, Charset>> entries = availableCharsets.entrySet();
//注意lambda表示式的写法
entries.forEach((Map.Entry<String, Charset> abc)-> System.out.println(abc.getKey() +
"=" + abc.getValue()));
}
具体的编码解码方式如下:
@Test
public void testEndeCode() throws CharacterCodingException {
Charset gbk = Charset.forName("GBK");
//获取编码器
CharsetEncoder charsetEncoder = gbk.newEncoder();
//获取解码器
CharsetDecoder charsetDecoder = gbk.newDecoder();
//分配缓冲区
CharBuffer buffer = CharBuffer.allocate(1024);
//将数据放入缓冲区中
buffer.put("河海大学");
buffer.flip();
//编码
ByteBuffer encodeBuffer = charsetEncoder.encode(buffer);
//一个中文两个字节,所以4个中文8个字节
for (int i = 0; i < 8; i++) {
System.out.println(encodeBuffer.get());
}
//解码前首先将缓冲区切换成读模式,否则不能读取
encodeBuffer.flip();
CharBuffer decodeBuffer = charsetDecoder.decode(encodeBuffer);
System.out.println(decodeBuffer.toString());
//如果是由其他编码方式解码器必定乱码,这里使用UTF-8的方式进行解码将会出现乱码
Charset utf_8 = Charset.forName("UTF-8");
//这里每次读都必须调用flip()方法
encodeBuffer.flip();
CharBuffer utf8Con = utf_8.decode(encodeBuffer);
System.out.println(utf8Con.toString());
}
5. 非阻塞式通信(相较于网络通信而言)
前面已经已经提过NIO和IO有一个主要区别是NIO是非阻塞式通信,首先来看下传统的阻塞式通信
第一条线,在Client端向服务端发送读写请求时(也是先到内核地址空间,然后再到用户地址空间),当Server端无法确认Client端发送的数据是否有效时(即内核地址空间中是否存在有效数据)就会发生阻塞,用户一般会有多个读写请求,一旦前面的请求发生阻塞,那么后面的紧跟的请求将无法进行,此时Server端的CPU也无法做任何事情,效率极低。然后为了解决这个问题,使用了多线程来处理,CPU为客户端每个读写请求分配了一个专用线程,就算有出现某些线程通信阻塞,也不会影响用户其他线程的通信,这样确实使得效率得到一部分和提升,但是有一点需要注意的是,对于高并发场景下,我们实际开发的时候不会让它无限制的创建,一直使用线程池来进行管理,对与超出容量部分的线程会有不同的管理策略(一般采用排队的方式),就是说用户多个读写请求是分批完成的(一批有多个线程),所以在此时,如果出现阻塞的话,那么必定还是会有正在等待的线程无法进行正常通信,所以采用多线程在一定程度上提高了效率,但还不是很完美。下面看一NIO是如何解决这个问题的。
在NIO通信过程中引入选择器,过程如下:
选择器将所有网络通信通道全部注册到选择器里面,选择器将对通道状态进行监控,只有在数据准备好才会将为此次通信安排一个线程进行传输,这样就保证了不会出现为无效数据分配线程造成阻塞的情况,极大的提高了传输效率。
使用NIO完成网络通信主要有三个核心点:
- 通道(Channel):负责连接,用于网络通信的
Channel
接口实现类主要是SelectableChannel
,子类有:
SocketChannel
,TCPServerSocketChannel
,TCPDatagramChannel
,UDPPipe.SinkChannel
,管道Pipe.SourceChannel
,管道
- 缓冲区(Buffer):负责存放数据;
- 选择器(Selector):是
SelectableChannel
的多路复用器,用于监控SelectableChannel
(指的是上面的SelectableChannel
子类)的IO状态。
在看NIO的非阻塞式的通信内容时先看下NIO的阻塞式的通信方式(注意启动顺序,网络通信永远是先启动Server端,然后再启动客户端,记住一点,服务器一般情况下都是被动响应):
//后启动
@Test
public void client() throws IOException {
//获取通道
SocketChannel socketChannel = SocketChannel.open(new InetSocketAddress("127.0.0.1", 9898));
//2.分配缓冲区
ByteBuffer buffer = ByteBuffer.allocate(1024);
//3.读取本地文件并发送到服务端
FileChannel fileChannel = FileChannel.open(Paths.get("C:\\Users\\weiguo Liu\\Desktop\\课表.txt"), StandardOpenOption.READ);
//循环读写
while (fileChannel.read(buffer)!=-1) {
buffer.flip();
socketChannel.write(buffer);
buffer.clear();
}
//关闭资源
fileChannel.close();
socketChannel.close();
}
//先启动
@Test
public void server() throws IOException {
//1. 获取通道
ServerSocketChannel serverSocketChannel = ServerSocketChannel.open();
//2.绑定连接
serverSocketChannel.bind(new InetSocketAddress(9898));
//3. 获取客户端的连接通道
SocketChannel socketChannel = serverSocketChannel.accept();
//4. 接受客户端的数据并存到本地,注意操作行为,WRITE和CREATE_NEW,一个都不能少
FileChannel fileOutChannel = FileChannel.open(Paths.get("C:\\Users\\weiguo Liu\\Desktop\\课表_复制.txt"), StandardOpenOption.WRITE, StandardOpenOption.CREATE_NEW);
ByteBuffer buffer = ByteBuffer.allocate(1024);
while (socketChannel.read(buffer)!=-1) {
buffer.flip();
fileOutChannel.write(buffer);
buffer.clear();
}
//5. 关闭通道
socketChannel.close();
fileOutChannel.close();
serverSocketChannel.close();
}
上述实现的还是传统意义上的阻塞式网络通信,只不过是使用NIO实现的,还没有涉及到选择器的使用,所以可以理解它还是个非阻塞式,下面看一下非阻塞式通信代码的具体实现。
非阻塞式通信方式非常重要,予以重视!!!,下面是TCP非阻塞式通信,步骤几乎一样,主要核心点也一样:通道、缓冲区、选择器
/*
非阻塞式的Server和Client
*/
public static void main(String[] args) throws IOException {
//1. 打开Socket通道
SocketChannel sChannel = SocketChannel.open(new InetSocketAddress("127.0.0.1", 8888));
//切换成非阻塞的通道
sChannel.configureBlocking(false);
//2. 分配缓冲区
ByteBuffer buffer = ByteBuffer.allocate(1024);
Scanner in = new Scanner(System.in);
while (in.hasNext()) {
System.out.println("开始读取键盘的输入");
String str = in.next();
buffer.put((new Date().toString() + "\n" + str).getBytes());
buffer.flip();
sChannel.write(buffer);
buffer.clear();
}
sChannel.close();
}
@Test
public void Server2() throws IOException {
//获取通道
ServerSocketChannel ssChannel = ServerSocketChannel.open();
//将其切换成非阻塞通道
ssChannel.configureBlocking(false);
//绑定端口
ssChannel.bind(new InetSocketAddress(8888));
//获取选择器
Selector selector = Selector.open();
/*将通道注册到选择器里面,两个参数(第一个是选择器,第二个表示监听事件)
监听事件有:OP_READ(读),OP_WRITE(写),OP_CONNECT(连接),OP_ACCEPT(接受)
这里是监听的接收事件
*/
ssChannel.register(selector, SelectionKey.OP_ACCEPT);
//轮询式的获取选择器上“已经准备就绪”的事件
while (selector.select()>0) {
//获取当前选择器中所有注册的“选择键(已经就绪的监听事件)”
Iterator<SelectionKey> selectKeys = selector.selectedKeys().iterator();
//
while (selectKeys.hasNext()) {
//获取准备就绪的事件
SelectionKey sk = selectKeys.next();
//判断具体是什么事件就绪
if(sk.isAcceptable()) {
System.out.println("存在客户端正在连接服务器。。。");
//若事件是接受就绪,则获取客户端连接
SocketChannel sChannel = ssChannel.accept();
//切换成非阻塞式
sChannel.configureBlocking(false);
//将通道注册到选择器上,监听读就绪状态
sChannel.register(selector, SelectionKey.OP_READ);
} else if(sk.isReadable()) {//若读就绪
//获取当前选择器上“读就绪”状态的通道
SocketChannel sChannel = (SocketChannel) sk.channel();
//读数据
ByteBuffer buffer = ByteBuffer.allocate(1024);
int len = 0;
while ((len=sChannel.read(buffer))>0) {
buffer.flip();
System.out.println(new String(buffer.array(), 0, len));
buffer.clear();
}
}
//选择器用完后,需要手动取消
selectKeys.remove();
}
}
}
【注意】这里使用的IDEA,在客户端使用JUinst测试的时候不能测试Scanner
读取键盘的输入,只能使用main()
函数作为客户端的测试输入。
上面讲述的是TCP的非阻塞通信,谈及TCP,必然也少不了UDP的通信,看一下UDP的非阻塞式的通信方式。在NIO中,DatagramChannel
是UDP通信的通道,主要操作步骤:打开DatagramChannel
,然后收发数据即可。
public static void main(String[] args) throws IOException {
//1. 打开UDP通道
DatagramChannel dc = DatagramChannel.open();
//2. 将通道设置为非阻塞式
dc.configureBlocking(false);
//3. 分配缓冲区
ByteBuffer buffer = ByteBuffer.allocate(1024);
//4. 将数据(这里是键盘的输入)装配到缓冲区准备发送
Scanner scanner = new Scanner(System.in);
while (scanner.hasNext()) {
String str = scanner.next();
buffer.put((new Date().toString() + ":\n" + str).getBytes());
//切换成读模式,发送的时候肯定先要将数据读取到管道中啊
buffer.flip();
dc.send(buffer, new InetSocketAddress("127.0.0.1", 8888));
//清空缓存
buffer.clear();
}
//5. 关闭通道
dc.close();
}
@Test
public void testUDPReceive() throws IOException {
//1. 打开通道
DatagramChannel dc = DatagramChannel.open();
//2. 将通道设置为非阻塞式
dc.configureBlocking(false);
//3. 绑定端口号
dc.bind(new InetSocketAddress(8888));
//4. 创建选择器
Selector selector = Selector.open();
//5. 将通道注册到选择器上,监听通道的读状态
dc.register(selector, SelectionKey.OP_READ);
//6. 轮询选择器,获取各个通道监听事件的状态
while (selector.select()>0) {
//获取选择器上选择键的迭代器
Iterator<SelectionKey> iterator = selector.selectedKeys().iterator();
while (iterator.hasNext()) {
SelectionKey sk = iterator.next();
if (sk.isReadable()) {
ByteBuffer buffer = ByteBuffer.allocate(1024);
dc.receive(buffer);
buffer.flip();
System.out.println(new String(buffer.array(), 0, buffer.limit()));
buffer.clear();
}
}
//移除选择器
iterator.remove();
}
}
6. 管道(Pie)
NIO中的管道是2个线程之间的单向数据连接,Pipe有一个source通道和一个sink通道,数据一般是被写入到sink通道,而读取则是在source通道中进行,如下图
下面看具体的代码流程:
@Test
public void test1() throws IOException {
//1. 获取管道
Pipe pipe = Pipe.open();
//2. 创建缓冲区
ByteBuffer buffer = ByteBuffer.allocate(1024);
//3. 获取管道的写入端Sink
Pipe.SinkChannel sinkChannel = pipe.sink();
//4. 通过缓冲区向Sink端管道写入数据
buffer.put("hjkjashjd".getBytes());
//先读才能写
buffer.flip();
sinkChannel.write(buffer);
//5. 读取缓冲区的数据
Pipe.SourceChannel sourceChannel = pipe.source();
buffer.flip();
int len = sourceChannel.read(buffer);
System.out.println(new String(buffer.array(), 0, len));
//6. 关闭资源
sourceChannel.close();
sinkChannel.close();
}
【注意】管道用作网络通信,既然是单向的传输连接,那么就可以将Source和Sink看作是管道的两端,Sink是发送的端口,Source是接受的端口,就很好理解,一般将这两个端口的操作分布在两个不同的线程中来做通信(一个线程用作发送(Sink端口操作),一个线程用作接口(Source端口)),即将上述代码的中的4、5两个步骤分开。