织梦零基础做网站,重庆工程招标投标交易信息网,wordpress评论获取qq,汉中市建设工程信息价查询一、NIO基础
Java New IO是从Java1.4版本开始引入的一个新的IO api#xff0c;可以替代以往的标准IO#xff0c;NIO相比原来的IO有同样的作用和目的#xff0c;但是使用的方式完全不一样#xff0c;NIO是面向缓冲区的#xff0c;基于通道的IO操作#xff0c;这也让它比传…一、NIO基础
Java New IO是从Java1.4版本开始引入的一个新的IO api可以替代以往的标准IONIO相比原来的IO有同样的作用和目的但是使用的方式完全不一样NIO是面向缓冲区的基于通道的IO操作这也让它比传统IO有着更为高效的读写。
1.1 IO和NIO的主要区别
IONIO面向流面向缓冲区阻塞IO非阻塞IO无选择器
1.1.1 传统IO的流
以下用图来简单理解一下在传统IO中当App要对网络磁盘中的文件进行读写的时候它们必须建立一个连接流到底是一个什么样的概念呢我们可以先把它想象成自来水家里要用自来水需要有水管让水从水管过来到家里起到一个运输的作用。
所以当我们文件中的数据需要输入到App里面时它们就会建立一个输入的管道。而当我们的App有数据需要写入到文件系统的时候就会建立一个输出的管道这两条管道就是我们的输入流和输出流。那水从来没有逆流而上的呀所以它们都是单向管道。这么一讲是不是就很好懂了呢
1.1.2 NIO
也是同样的文件系统和App不过此时把流换成了一个channel现在我们可以先认为它就是一条铁道那我们知道铁道本身是不能传递货物的呀所以我们需要一个载具—火车也就是缓冲区App需要的数据就由这个名叫缓冲区的载具运输过来。那火车是可以开过来也可以开回去的所以NIO是双向传输的。
1.2 Buffer
NIO的核心在于通道channel和缓冲区buffer两个。通道是打开到IO设备的连接。使用时需要获取用于连接IO设备的通道以及用于容纳数据的缓冲区然后通过操作缓冲区对数据进行处理。其实就是上面那张图的事儿或者一句话就是一个负责传输一个负责存储。
缓冲区是Java.nio包定义好的所有缓冲区都是Buffer抽象类的子类。Buffer根据数据类型不同常用子类分别是基本数据类型除了Boolean外的xxxBufferIntBufferDoubleBuffer···等。不同的Buffer类它们的管理方式都是相同的获取对象的方法都是
// 创建一个容量为capacity的xxx类型的Buffer对象
static xxxBuffer allocate(int capacity)而且缓冲区提供了两个核心方法get()和put()put方法是将数据存入到缓冲区而get是获取缓冲区的数据。
此时我们用代码看一下
public class BufferTest {Testpublic void testBuffer(){// 创建缓冲区对象ByteBuffer byteBuffer ByteBuffer.allocate(1024);}
}点进去ByteBuffer会看到这个东西是继承了Buffer类的
public abstract class ByteBuffer extends Buffer implements ComparableByteBuffer此时继续点进去Buffer类第一眼看到的是有几个自带的属性
1.2.1 buffer的基本属性 ① capacity容量 表示Buffer的最大数据容量这个值不能为负。而且创建后是不能更改的。
② limit限制 第一个不能读取或写入的数据的索引位于此索引后的数据不可读写。这个数值不能为负且不能超过capacity如上图中第三个缓冲区在下标为5之后的数据块均不能读写那limit为5
③ position位置 下一个要读取或写入的数据的索引这个数值不能为负且不能超过capacity如图中第二个缓冲区前面5块写完成此时第6个数据块的下标为5所以position为5
④ mark标记/reset重置 mark是一个索引通过Buffer的mark()方法指定Buffer中一个特定的position后可以通过reset()方法重置到这个position这个通过代码来解释会比较好说明
1.2.2 code部分非常简单 1.首先我们创建一个缓冲区对象然后把它的属性打印出来 ByteBuffer byteBuffer ByteBuffer.allocate(10);
System.out.println(byteBuffer.position());
System.out.println(byteBuffer.capacity());
System.out.println(byteBuffer.limit());运行结果0,10,102.执行一个put()方法来把一个字符丢进去 String str abcde;
byteBuffer.put(str.getBytes());
System.out.println(byteBuffer.position());
System.out.println(byteBuffer.capacity());
System.out.println(byteBuffer.limit());运行结果5,10,10
abcde长度为5position已经变化其它不变3.使用flip()切换为读模式 byteBuffer.flip();
System.out.println(byteBuffer.position());
System.out.println(byteBuffer.capacity());
System.out.println(byteBuffer.limit());运行结果0,10,5此时position变成为0了因为一开始的5,是因为这时候要写的是下标为5的数据块而转换成读模式后第一个读的明显是下标为0的数据块呀。limit的数值也变成了5因为当前能读到的数据从下标为5开始就木有了所以limit为5
4.简单获取一下buffer中的数据
byte[] array new byte[byteBuffer.limit()];
byteBuffer.get(array);
System.out.println(new String(array,0,array.length));运行结果abcde5.mark() reset()
byte[] array new byte[byteBuffer.limit()];
byteBuffer.get(array,0,2);
System.out.println(new String(array,0,2));
System.out.println(byteBuffer.position());byteBuffer.mark();
byteBuffer.get(array,2,2);
System.out.println(new String(array,2,2));
System.out.println(byteBuffer.position());byteBuffer.reset();
System.out.println(byteBuffer.position());运行结果ab2cd42其实很简单就是第一次读取的时候只是读取了前面两个字符然后此时position的结果为2然后再读取后两个position为4可是因为我在读取前面2个的时候进行了一个mark操作它就自动回到我mark之前的那个读取位置而已就是这么简单
6.其他的一些方法 rewind()方法可重复读clear()清空缓冲区不过这个方法的清空缓冲区是一种被遗忘的状态就是说数据仍然还存于缓冲区中可是自动忽略掉了。此时再次读取数据是还是可以get()到的。hasRemaining()方法就是表示剩余可操作的数据量还有多少比如刚刚的mark的那个例子中我reset回去之后剩余的可操作数据就是3因为我只读了ab还有cde这三个。
1.2.3 直接缓冲区和非直接缓冲区
非直接缓冲区通过allocate()方法来分配缓冲区。将缓冲区建立在JVM的内存中。 直接缓冲区通过allocateDirect()方法分配缓冲区将缓冲区建立在物理内存中。效率更高。
① 非直接缓冲区 应用程序想要在磁盘中读取数据时首先它发起请求让物理磁盘先把它的数据读到内核地址空间当中之后这个内核空间再将这个数据copy一份到用户地址空间去。然后数据才能通过read()方法将数据返回个应用程序。而应用程序需要写数据进去也是同理先写到用户地址空间然后copy到内核地址空间再写入磁盘。此时不难发现这个copy的操作显得十分的多余所以非直接缓冲区的效率相对来说会低一些。
② 直接缓冲区 直接缓冲区就真的顾名思义非常直接了写入的时候写到物理内存映射文件中再由它写入物理磁盘读取也是磁盘把数据读到这个文件然后再由它读取到应用程序中即可。没有了copy的中间过程。
1.3 channel
1.3.1 扯一下概念背景
由java.nio.channels包定义表示IO源与目标打开的链接它本身不存在直接访问数据的能力只能和Buffer进行交互
传统的IO由cpu来全权负责此时这个设计在有大量文件读取操作时CPU的利用率会被拉的非常低因为IO操作把CPU的资源都抢占了。 在这种背景下进行了一些优化把对cpu的连接取消转为DMA(直接内存存取)的方式。当然DMA这个操作本身也是需要CPU进行调度的。不过这个损耗自然就会比大量的IO要小的多。 此时就出现了通道这个概念它是一个完全独立的处理器。专门用来负责文件的IO操作。 1.3.2 常用通道
Java为Channel接口提供的主要实现类
FileChannel用于读取写入映射和操作文件的通道
DatagramChannel通过UDP读写网络中的数据通道
SocketChannel通过TCP读写网络中的数据通道
ServerSocketChannel可以监听新进来的TCP连接对每一个新进来的连接都会创建一个SocketChannel 获取channel的一种方式是对支持通道的对象调用getChannel()方法支持类如下
FileInputStream
FileOutputStream
RandomAccessFile
DatagramSocket
Socket
ServerSocket获取的其他方式是使用Files类的静态方法newByteChannel()获取字节通道。再或者是通过通道的静态方法open()打开并返回指定通道。
1.3.3 常用方法和简单使用 ① 使用非直接缓冲区完成文件复制
// 创建输入输出流对象
FileInputStream fileInputStream new FileInputStream(testPic.jpg);
FileOutputStream fileOutputStream new FileOutputStream(testPic2.jpg);// 通过流对象获取通道channel
FileChannel inChannel fileInputStream.getChannel();
FileChannel outChannel fileOutputStream.getChannel();// 创建指定大小的缓冲区
ByteBuffer byteBuffer ByteBuffer.allocate(1024);// 将通道中的数据写入到缓冲区中
while (inChannel.read(byteBuffer) ! -1){// 切换成读取模式byteBuffer.flip();// 将缓冲区中的数据写到输出通道outChannel.write(byteBuffer);// 清空缓冲区byteBuffer.clear();}
//回收资源(这里为了省时间直接抛出去了反正这段不太重要)
outChannel.close();
inChannel.close();
fileInputStream.close();
fileOutputStream.close();运行结果就自然是复制了一个testPic2出来啦因为代码本身不难注释已经写得比较详细就不展开了
② 使用直接缓冲区来完成文件的复制 注意这里的StandardOpenOption是一个枚举表示模式很显然这里是要选择READ读取模式。
FileChannel inChannel FileChannel.open(Paths.get(testPic.jpg,StandardOpenOption.READ));
FileChannel outChannel FileChannel.open(Paths.get(testPic2.jpg),StandardOpenOption.WRITE,StandardOpenOption.READ,StandardOpenOption.CREATE);
// 进行内存映射
MappedByteBuffer inMappedBuffer inChannel.map(FileChannel.MapMode.READ_ONLY, 0, inChannel.size());
MappedByteBuffer outMapBuffer outChannel.map(FileChannel.MapMode.READ_WRITE, 0, inChannel.size());// 对缓冲区进行数据的读写操作
byte[] array new byte[inMappedBuffer.limit()];
inMappedBuffer.get(array);
outMapBuffer.put(array);// 回收资源
inChannel.close();
outChannel.close();如果需要看一下它们两个的时间差自己用最常规的系统时间来瞧瞧就好在这里就不再加上了。
二、NIO非阻塞式网络通信
传统的IO流都是阻塞式的当一个线程调用read或者write时该线程被阻塞直到数据被读取或者写入该线程在此期间都是不能执行其他任务的因此在完成网络通信进行IO操作时线程被阻塞所以服务器端必须为每个客户端提供一个独立线程进行处理当服务器端需要处理大量客户端时性能将会急剧下降。
NIO是非阻塞的当线程从某通道进行读写数据时若没有数据可用该线程可以进行其他任务。线程通常将非阻塞IO的空闲时间用于在其他通道上执行IO操作所以单独的线程可以管理多个输入和输出通道。因此NIO可以让服务器端使用一个或有限几个线程来同时处理连接到服务器端的所有客户端。
2.1 Selector
这个选择器其实就是在客户端和服务端之间引入一个通道的注册器比如现在我的客户端要像服务端传输数据了客户端会给选择器去发送一个channel的注册请求注册完成后Selector就会去监控这个channel的IO状态读写连接。只有当通道中的数据完全准备就绪Selector才会将数据分配到服务端的某个线程去处理。
这种非阻塞性的流程就可以更好地去使用CPU的资源。提高CPU的工作效率。这个可以用收快递来说明。如果你一开始就告诉我半小时后过来取快递而我在这时候已经到目的地了我有可能就原地不动站着等半个小时。这个期间啥地都去不了可是你是到了之后才打电话告诉我过来取那我就有了更多的自由时间。
2.2 code阻塞性IO的网络通信
现在我们来演示一下阻塞性IO的网络通信
2.2.1 client阻塞性IO
这个代码大家可以尝试这删除sChannel.shutdownOutput()此时会发现在启动好server运行client程序的时候程序也会阻塞这是因为这时服务端并无法确定你是否已经发送完成数据了所以client端也产生了阻塞双方就一直僵持。
还有一种方法是解阻塞之后进行阐述。
// 1.获取通道
SocketChannel sChannel SocketChannel.open(new InetSocketAddress(你的IP地址,9898));
// 2.创建文件通道
FileChannel inChannel FileChannel.open(Paths.get(C:/Users/Administrator/Desktop/testPic.jpg),StandardOpenOption.READ);
// 3.分配指定大小的缓冲区
ByteBuffer byteBuffer ByteBuffer.allocate(1024);// 4.发送数据,需要读取文件
while (inChannel.read(byteBuffer) ! -1){byteBuffer.flip();// 将buffer的数据写入到通道中sChannel.write(byteBuffer);byteBuffer.clear();
}// 主动告诉服务端数据已经发送完毕
sChannel.shutdownOutput();while (sChannel.read(byteBuffer) ! -1){byteBuffer.flip();System.out.println(接收服务端数据成功···);byteBuffer.clear();}// 5.关闭通道
inChannel.close();
sChannel.close();2.2.2 server阻塞性IO
// 1.获取通道
ServerSocketChannel ssChannel ServerSocketChannel.open();
// 创建一个输出通道将读取到的数据写入到输出通道中保存为testPic2
FileChannel outChannel FileChannel.open(Paths.get(testPic2.jpg),StandardOpenOption.WRITE,StandardOpenOption.CREATE);
// 2.绑定端口
ssChannel.bind(new InetSocketAddress(9898));
// 3.等待客户端连接连接成功时会得到一个通道
SocketChannel sChannel ssChannel.accept();
// 4.创建缓冲区
ByteBuffer byteBuffer ByteBuffer.allocate(1024);
// 5.接收客户端的数据存储到本地
while (sChannel.read(byteBuffer) ! -1){byteBuffer.flip();outChannel.write(byteBuffer);byteBuffer.clear();
}// 发送反馈给客户端// 向缓冲区中写入应答信息byteBuffer.put(服务端接收数据成功.getBytes());byteBuffer.flip();sChannel.write(byteBuffer);// 关闭通道
sChannel.close();
outChannel.close();
byteBuffer.clear();然后再当我们的客户端运行起来就会进行copy操作 2.3 Selector完成非阻塞IO
使用NIO完成网络通信需要三个核心对象 channeljava.nio.channels.Channel接口SocketChannelServerSocketChannelDatagramChannel 管道相关Pipe.SinkChannelPine.SourceChannel buffer负责存储数据 Selector其中Selector是SelectableChannel的多路复用器主要是用于监控SelectableChannel的IO状态 2.3.1 client非阻塞
// 1.获取通道默认是阻塞的
SocketChannel sChannel SocketChannel.open(new InetSocketAddress(192.168.80.1,9898));// 1.1 将阻塞的套接字变成非阻塞
sChannel.configureBlocking(false);// 2.创建指定大小的缓冲区
ByteBuffer byteBuffer ByteBuffer.allocate(1024);
// 3.发送数据给服务端直接将数据存储到缓冲区
byteBuffer.put(new Date().toString().getBytes());
// 4.将缓冲区的数据写入到sChannel
byteBuffer.flip();
sChannel.write(byteBuffer);
byteBuffer.clear();// 关闭
sChannel.close();2.3.2 server非阻塞
代码的注释中已经解释了整个过程的做法这里就不一一展开了。
// 1.获取通道
ServerSocketChannel ssChannel ServerSocketChannel.open();
// 2.将阻塞的套接字设置为非阻塞的
ssChannel.configureBlocking(false);
// 3.绑定端口号
ssChannel.bind(new InetSocketAddress(9898));// 4.创建选择器对象
Selector selector Selector.open();// 5.将通道注册到选择器上这里的第二个参数为selectionKey下面有解释
// 此时选择器就开始监听这个通道的接收时间此时接收工作准备就绪才开始下一步的操作
ssChannel.register(selector,SelectionKey.OP_ACCEPT);// 6.通过轮询的方式获取选择器上准备就绪的事件
// 如果大于0至少有一个SelectionKey准备就绪
while (selector.select() 0){// 7.获取当前选择器中所有注册的selectionKey已经准备就绪的监听事件IteratorSelectionKey selectionKeyIterator selector.selectedKeys().iterator();// 迭代获取已经准备就绪的选择键while (selectionKeyIterator.hasNext()){// 8.获取已经准备就绪的事件SelectionKey selectionKey selectionKeyIterator.next();if (selectionKey.isAcceptable()){// 9.调用accept方法SocketChannel sChannel ssChannel.accept();// 将sChannel设置为非阻塞// 再次强调整个过程不能有任何一条阻塞通道sChannel.configureBlocking(false);// 进行数据接收工作而且把sChannel也注册上选择器让选择器来监听sChannel.register(selector,SelectionKey.OP_READ);}else if (selectionKey.isReadable()){// 如果读状态已经准备就绪就开始读取数据// 10.获取当前选择器上读状态准备就绪的通道SocketChannel sChannel (SocketChannel) selectionKey.channel();// 11.读取客户端发送的数据需要先创建缓冲区ByteBuffer byteBuffer ByteBuffer.allocate(1024);// 12.读取缓冲区的数据while (sChannel.read(byteBuffer) 0){byteBuffer.flip();// 这里sChannel.read(byteBuffer)就是这个字节数组的长度System.out.println(new String(byteBuffer.array(),0,sChannel.read(byteBuffer)));// 清空缓冲区byteBuffer.clear();}}// 当selectionKey使用完毕需要移除否则会一直优先selectionKeyIterator.remove();}}当调用register方法将通道注册到选择器时选择器对通道的监听事件需要通过第二个参数ops决定
读SelectionKey.OP_READ(1)
写SelectionKey.OP_WRITE(4)
连接SelectionKey.OP_CONNECT(8)
接收SelectionKey.OP_ACCEPT(16)若注册时不仅仅只有一个监听事件则需要用位或操作符连接
int selectionKeySet SelectionKey.OP_READ|SelectionKey.OP_WRITE
而关于这个selectionKey它表示着SelectableChannel和Selectr之间的注册关系。它也有一系列对应的方法 2.3.3 客户端的改造
引入Scanner接收输入信息不过请注意在测试代码中输入IDEA需要进行一些设置具体做法是在Help-Edit Custom VM Option中加入一行 -Deditable.java.test.consoletrue
这样就可以输入了。
// 1.获取通道默认是阻塞的
SocketChannel sChannel SocketChannel.open(new InetSocketAddress(192.168.80.1,9898));// 1.1 将阻塞的套接字变成非阻塞
sChannel.configureBlocking(false);// 2.创建指定大小的缓冲区
ByteBuffer byteBuffer ByteBuffer.allocate(1024);Scanner scanner new Scanner(System.in);
while (scanner.hasNext()){String str scanner.next();// 3.发送数据给服务端直接将数据存储到缓冲区byteBuffer.put((new Date().toString()str).getBytes());// 4.将缓冲区的数据写入到sChannelbyteBuffer.flip();sChannel.write(byteBuffer);byteBuffer.clear();
}
// 关闭
sChannel.close();这样就完成了一个问答模式的网络通信。
2.4 Pipe管道
Java NIO中的管道是两个线程之间的单向数据连接Pipe有一个source管道和一个sink管道数据会被写到sink从source中获取 // 1.获取管道
Pipe pipe Pipe.open();// 2.创建缓冲区对象
ByteBuffer byteBuffer ByteBuffer.allocate(1024);
// 3.获取sink通道
Pipe.SinkChannel sinkChannel pipe.sink();
byteBuffer.put(通过单向管道传输数据.getBytes());// 4.将数据写入sinkChannel
byteBuffer.flip();
sinkChannel.write(byteBuffer);
// 5.读取缓冲区中的数据
Pipe.SourceChannel sourceChannel pipe.source();
// 6.读取sourceChannel中的数据放入到缓冲区
byteBuffer.flip();
sourceChannel.read(byteBuffer);
System.out.println(new String(byteBuffer.array(),0,sourceChannel.read(byteBuffer)));sourceChannel.close();
sinkChannel.close();运行结果就是打印了我们的那串字符通过单向管道传输数据