https://blog.csdn.net/qq_37049496/article/details/81267367 原地址
双向 nio https://www.jianshu.com/p/e6ff812a4877
1 NIO概述
Java NIO(New IO)是一个可替代Java IO API(从Java1.4开始),JAVA NIO提供了与标准IO不同的工作方式。
Java NIO:Channels and Buffers(通道和缓冲区)
标准的IO基于字节流或者字符流进行操作,而NIO基于通道和缓冲区进行操作,数据是总是从通道读取的缓冲区中,或者从缓冲区写入到通道中。
Java NIO:Non-blocking IO(非阻塞IO):Java NIO可以让你非阻塞的使用IO,例如:当线程从通道读取数据到缓冲区时,线程还是可以进行其他事情,当数据被写入缓冲区时,线程可以继续处理它,从缓冲区写入通道也类似。
Java NIO:Selectors(选择器):Java NIO引入了选择器的概念,选择器用于监听多个通道的事件(比如:连接打开,数据到达)。因此,单个线程可以监听多个数据通道。
选择KEY
1、SelectionKey.OP_CONNECT(如果选择器检测到相应的通道已经准备好完成他的连接序列或者有个错误即将发生,那么就添加OP_CONNECT到选择key的就绪集合并且将该键添加到选择器的可选择键集合If the selector detects that the corresponding socket channel is ready to complete its connection sequence, or has an error pending, then it will add OP_CONNECT to the key's ready set and add the key to its selected-key set.)
2、SelectionKey.OP_ACCEPT(If the selector detects that the corresponding channel is ready to accept another connection, or has an error pending, then it will add OP_ACCEPT to the key's ready set and add the key to its selected-key set.)
3、SelectionKey.OP_READ(If the selector detects that the corresponding channel is ready for reading, has reached end-of-stream, has been remotely shut down for further reading, or has an error pending, then it will add OP_READ to the key's ready set and add the key to its selected-key set.)
4、SelectionKey.OP_WRITE(If the selector detects that the corresponding channel is ready for writing, has been remotely shut down for further writing, or has an error pending, then it will add OP_WRITE to the key's ready set and add the key to its selected-key set.)
如果你对不止一种事件感兴趣,那么可以用“位或”操作符将常量连接起来,如下:int interestSet = SelectionKey.OP_READ | SelectionKey.OP_WRITE;
传统IO是单向,NIO是双向
如图,当程序读取文件时,通过输入流进行文件字节流或者字符流读取类似水管一样,写入文件类似。
而NIO则是利用缓冲区(传输的数据就存放在这里)进行双向的数据传输,通过通道将缓冲区传输到文件或者读取到程序。(面向缓冲区),传输的是缓冲区而不再是字节或者字符流
与传统IO区别:
IO
NIO
面向流
面向缓冲区
阻塞IO
非阻塞IO
无
选择器
2 Buffer的数据存取
是NIO提供给数据传输和通道一起配合使用,存储数据的容器
1)容量(capacity):表示Buffer最大数据容量,缓冲区容量不能为负,并且建立后不能修改。
2)限制(limit):第一个不应该读取或者写入的数据的索引,即位于limit后的数据不可以读写。缓冲区的限制不能为负,并且不能大于其容量(capacity)。
3)位置(position):下一个要读取或写入的数据的索引。缓冲区的位置不能为负,并且不能大于其限制(limit)。
4)标记(mark)与重置(reset):标记是一个索引,通过Buffer中的mark()方法指定Buffer中一个特定的position,之后可以通过调用reset()方法恢复到这个position。
@Test
public void test001(){
ByteBuffer allocate = ByteBuffer.allocate(1024);
System.out.println(allocate.limit());//1024
System.out.println(allocate.position());//0
System.out.println(allocate.capacity());//1024
System.out.println("---------王buffer存放数据------------");
allocate.put("abcd1".getBytes());
allocate.put("abcd1".getBytes());
System.out.println(allocate.limit());//1024
System.out.println(allocate.position());//10
System.out.println(allocate.capacity());//1024
// System.out.println("----------错误读取值--------------------");
// byte[] bytes=new byte[allocate.limit()];
// allocate.get(bytes);
// System.out.println(new String(bytes,0,bytes.length));
System.out.println("----------正确读取值--------------------");
allocate.flip();//翻转 limit = position;position = 0;mark = -1;
System.out.println(allocate.limit());//10
System.out.println(allocate.position());//0
System.out.println(allocate.capacity());//1024
byte[] bytes2=new byte[allocate.limit()];
/**
* public ByteBuffer get(byte[] dst) {
* return get(dst, 0, dst.length);
* }
* public ByteBuffer get(byte[] dst, int offset, int length) {
* checkBounds(offset, length, dst.length);
* if (length > remaining())
* throw new BufferUnderflowException();
* int end = offset + length;
* for (int i = offset; i < end; i++)
* dst[i] = get();
* return this;
* }
* public final int remaining() {
* return limit - position;
* }
* */
allocate.get(bytes2);//
System.out.println(new String(bytes2,0,bytes2.length));//abcd1abcd1
System.out.println(allocate.limit());//10
System.out.println(allocate.position());//************10*****************代表不能重复读取
System.out.println(allocate.capacity());//1024;
allocate.rewind();// position = 0;mark = -1;与flip有区别哟
System.out.println("-----------清空缓冲区-----------------");
allocate.clear();// position = 0;limit = capacity;mark = -1;
System.out.println(allocate.limit());//1024
System.out.println(allocate.position());//0
System.out.println(allocate.capacity());//1024
allocate.get(bytes2);
System.out.println(new String(bytes2,0,bytes2.length));//abcd1abcd1 可以看到数据并没有被清空
System.out.println("---------------mark 和 reset---------------------");
allocate.reset();// int m = mark;if (m < 0)throw new InvalidMarkException();position = m;
allocate.mark();// mark = position;用于重置,起标记作用
}
3 直接缓冲区和间接缓冲区
非直接缓冲区:通过allocate()方法分配缓冲区,将缓冲区建立在JVM的内存中,当从物理磁盘读取数据的时候,先读取到物理空间(其实还是在设备上的),再copy到jvm空间,然后才从jvm空间里进行读取,由于这里涉及到一个copy的过程,所以效率相比直接缓冲区较低。IO缓冲区属于非直接,大多数缓冲区都是非直接缓冲区
直接缓冲区:通过allocateDirect()方法分配直接缓冲区,将缓冲区建立在物理内存中,可以提高效率。非常占内存,安全性相比非直接缓冲区较低
Java 虚拟机会尽最大努力直接在此缓冲区上执行本机 I/O 操作。也就是说,在每次调用基础操作系统的一个本机 I/O 操作之前(或之后),虚拟机都会尽量避免将缓冲区的内容复制到中间缓冲区中(或从中间缓冲区中复制内容)。
直接字节缓冲区可以通过调用此类的 allocateDirect() 工厂方法来创建。此方法返回的缓冲区进行分配和取消分配所需成本通常高于非直接缓冲区。直接缓冲区的内容可以驻留在常规的垃圾回收堆之外,因此,它们对应用程序的内存需求量造成的影响可能并不明显。所以,建议将直接缓冲区主要分配给那些易受基础系统的本机 I/O 操作影响的大型、持久的缓冲区。一般情况下,最好仅在直接缓冲区能在程序性能方面带来明显好处时分配它们。
直接字节缓冲区还可以通过 FileChannel 的 map() 方法 将文件区域直接映射到内存中来创建。该方法返回MappedByteBuffer 。 Java 平台的实现有助于通过 JNI 从本机代码创建直接字节缓冲区。如果以上这些缓冲区中的某个缓冲区实例指的是不可访问的内
3.1 样例比较
//菲直接缓冲区 读写操作
@Test
public void test002() throws IOException {
long start = System.currentTimeMillis();
//读入流
FileInputStream fileInputStream = new FileInputStream("F:\\someTools\\test\\1.mp4");
//写入流
FileOutputStream fileOutputStream = new FileOutputStream("F:\\someTools\\test\\2.mp4");
//创建管道
FileChannel channel = fileInputStream.getChannel();
FileChannel channel1 = fileOutputStream.getChannel();
//分配指定大小缓冲区
ByteBuffer buf=ByteBuffer.allocate(1024);
while(channel.read(buf)!=-1){
//开启读取模式
buf.flip();
//将数据写入到通道中
channel1.write(buf);
buf.clear();
}
channel.close();
channel1.close();
fileInputStream.close();
fileOutputStream.close();
long end = System.currentTimeMillis();
System.out.println("非直接缓冲区用时:"+(end-start)+"ms");
}
//直接缓冲区 读写操作
@Test
public void test003() throws IOException {
long start = System.currentTimeMillis();
//创建管道
FileChannel inChannel=FileChannel.open(Paths.get("F:\\someTools\\test\\1.mp4"),StandardOpenOption.READ);
FileChannel outChanel = FileChannel.open(Paths.get("F:\\someTools\\test\\2.mp4"), StandardOpenOption.READ,StandardOpenOption.WRITE, StandardOpenOption.CREATE);
//定义映射文件
MappedByteBuffer inMappedByteBuffer = inChannel.map(FileChannel.MapMode.READ_ONLY, 0, inChannel.size());
MappedByteBuffer outMappedByteBuffer = outChanel.map(FileChannel.MapMode.READ_WRITE, 0, inChannel.size());
//直接对缓冲区操作
byte[] buf=new byte[inMappedByteBuffer.limit()];
inMappedByteBuffer.get(buf);
outMappedByteBuffer.put(buf);
inChannel.close();
outChanel.close();
long end = System.currentTimeMillis();
System.out.println("直接缓冲区用时:"+(end-start)+"ms");
}
使用一个207 MB视频测试,发现直接缓冲区平均用时300多ms而非直接缓冲区用时3000多ms,可以看到性能相差很大。。
4 通道(Channel)的原理获取
通道表示打开到 IO 设备(例如:文件、套接字)的连接。若需要使用 NIO 系统,需要获取用于连接 IO 设备的通道以及用于容纳数据的缓冲区。然后操作缓冲区,对数据进行处理。Channel 负责传输, Buffer 负责存储。通道是由 java.nio.channels 包定义的。 Channel 表示 IO 源与目标打开的连接。Channel 类似于传统的“流”。只不过 Channel本身不能直接访问数据, Channel 只能与Buffer 进行交互。
java.nio.channels.Channel 接口:
|--FileChannel
|--SocketChannel
|--ServerSocketChannel
|--DatagramChannel
获取通道
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()
5 分散读取,聚集写入
分散读取(scattering Reads):将通道中的数据分散到多个缓冲区中
聚集写入(gathering Writes):将多个缓冲区的数据聚集到通道中
//分散读取,聚集写入
@Test
public void test004() throws IOException {
RandomAccessFile raf=new RandomAccessFile("test.txt","rw");
//获取通道
FileChannel channel = raf.getChannel();
//分配指定大小的缓冲区
ByteBuffer allocate = ByteBuffer.allocate(100);
ByteBuffer allocate1 = ByteBuffer.allocate(1024);
//分散读取
ByteBuffer[] bufs={allocate,allocate1};
channel.read(bufs);
for (ByteBuffer byteBuffer:bufs) {
//切换模式
byteBuffer.flip();
}
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()));
System.out.println("------------------聚集写入------------------------");
RandomAccessFile raf2=new RandomAccessFile("test.txt","rw");
//获取通道
FileChannel channel2 = raf2.getChannel();
channel2.write(bufs);
raf.close();
raf2.close();
}
6 阻塞IO和非阻塞IO的
模型关系:
同步阻塞IO(BIO)
伪异步IO
非阻塞IO(NIO)
异步IO(AIO)
客户端个数:IO线程
1:1
M:N(其中M可以大于N)
M:1(1个IO线程处理多个客户端连接)
M:0(不需要启动额外的IO线程,被动回调)
IO类型(是否阻塞)
是
是
否
否
阻塞概念:应用程序在获取网络数据的时候,如果网络数据传输很慢,那么程序就会一直等待,直到传输完毕。
非阻塞:应用程序直接获取已经准备好的数据,无须等待。JDK1.7之前NIO为同步非阻塞IO,JDK1.7之后升级了NIO,支持异步非阻塞通讯模型NIO2.0(AIO),传统IO为同步阻塞IO
同步阻塞IO:当服务器accept时(应用程序获取网络程序时,网络传输数据很慢),他会一直等待客户端发送数据过来,如果客户端没有发送数据过来,那么他就会一直等待(阻塞)直到客户端发送数据后,继续往下执行。我是这样理解的,因为客户端与IO线程是1:1的关系也就是一个客户端发送请求数据,对应一个服务端线程,所以在服务端进行IO操作时(比如等待客户端的数据),服务端不会做其他事(毕竟单线程嘛)只能阻塞等待(比如上面的服务端while循环一直就卡在accept是吧?????)数据准备完成后,才继续执行。
//tcp服务器端...
class TcpServer {
public static void main(String[] args) throws IOException {
System.out.println("socket tcp服务器端启动....");
ServerSocket serverSocket = new ServerSocket(8080);
// 等待客户端请求
try {
while (true) {
System.out.println("服务器等待新连接。。。。。。。。。。");
Socket accept = serverSocket.accept();
new Thread(new Runnable() {
@Override
public void run() {
try {
InputStream inputStream = accept.getInputStream();
// 转换成string类型
byte[] buf = new byte[1024];
int len = inputStream.read(buf);
String str = new String(buf, 0, len);
System.out.println("服务器接受客户端内容:" + str);
} catch (Exception e) {
// TODO: handle exception
}
}
}).start();
}
} catch (Exception e) {
e.printStackTrace();
} finally {
serverSocket.close();
}
}
}
public class TcpClient {
public static void main(String[] args) throws UnknownHostException, IOException {
System.out.println("socket tcp 客户端启动....");
Socket socket = new Socket("127.0.0.1", 8080);
OutputStream outputStream = socket.getOutputStream();
outputStream.write("我是客户端".getBytes());
socket.close();
}
}
伪异步IO:服务端使用线程池的方式,当客户端一次发送多个请求(或者多个客户端同时请求时),服务端线程池调度一个线程进行处理,虽然在服务端看来某个线程在处理IO时,其他线程依然可以继续运行,但是没有实质性解决同步阻塞问题。毕竟没有连接到达时,你还不是一直在accept????????
//tcp服务器端...
class TcpServer {
public static void main(String[] args) throws IOException {
ExecutorService newCachedThreadPool = Executors.newCachedThreadPool();
System.out.println("socket tcp服务器端启动....");
ServerSocket serverSocket = new ServerSocket(8080);
// 等待客户端请求
try {
while (true) {
System.out.println("服务器等待新连接。。。。。。。。。。");
Socket accept = serverSocket.accept();
//使用线程
newCachedThreadPool.execute(new Runnable() {
@Override
public void run() {
try {
InputStream inputStream = accept.getInputStream();
// 转换成string类型
byte[] buf = new byte[1024];
int len = inputStream.read(buf);
String str = new String(buf, 0, len);
System.out.println("服务器接受客户端内容:" + str);
} catch (Exception e) {
// TODO: handle exception
}
}
});
}
} catch (Exception e) {
e.printStackTrace();
} finally {
serverSocket.close();
}
}
}
public class TcpClient {
public static void main(String[] args) throws UnknownHostException, IOException {
System.out.println("socket tcp 客户端启动....");
Socket socket = new Socket("127.0.0.1", 8080);
OutputStream outputStream = socket.getOutputStream();
outputStream.write("我是客户端".getBytes());
socket.close();
}
}
同步非阻塞IO:服务端有一个线程专门用来监视这些IO,当该线程监视到某个IO完成后服务端就会启动一个线程处理其实也就是做出响应,之所以说非阻塞是因为我只需要扫描(轮询)这个线程上的IO,而不关心该IO是否在处理是否改通道有请求来了等等,只有满足我的要求的IO到达或者完成我才启动一个线程来处理,所以对于服务端来说我一直在扫描(轮询)我并没有阻塞是吧????
如下图:在NIO中,当客户端有新的连接(通道)时,会将其注册到选择器上,只有选择器监听到有请求到达数据准备好后,服务端采用启动一个线程进行处理,而在准备过程中或者数据等待之前,服务端是不会等待的是吧,所以服务端是没有任何的阻塞问题的。。
//NIO服务端
public class NIOServer {
public static void main(String[] args) throws IOException {
System.out.println("服务器端已经被启动。。。。。。");
//1、创建服务通道
ServerSocketChannel socketChannel = ServerSocketChannel.open();
//2、切换异步非阻塞
socketChannel.configureBlocking(false);//jdk1.7以上
//3、绑定连接
socketChannel.bind(new InetSocketAddress(8080));
//4、获取选择器
Selector selector=Selector.open();
//5、将通道注册到选择器中 并且指定其监听接受事件
socketChannel.register(selector,SelectionKey.OP_ACCEPT);
//6、轮询获取“已经准备就绪的事件”
while(selector.select()>0){
//7、获取当前选择器中所有已经注册的选择键
Iterator
while(selectionKeys.hasNext()){
//8、获取准备就绪事件
SelectionKey selectionKey = selectionKeys.next();
//9、判断准备就绪的事件类型
if(selectionKey.isAcceptable()){
//10、如果是 接受就绪,则获取客户端连接
SocketChannel socketChannel1=socketChannel.accept();
//11、设置阻塞模式
socketChannel1.configureBlocking(false);
//12、注册通道
socketChannel1.register(selector,SelectionKey.OP_READ);
} else if(selectionKey.isReadable()){
//13、获取当前选择器“就绪”状态的通道
SocketChannel channel = (SocketChannel) selectionKey.channel();
//14、读取数据
ByteBuffer buffer = ByteBuffer.allocate(1024);
int len=0;
while((len=channel.read(buffer))>0){
buffer.flip();
System.out.println(new String(buffer.array(),0,len));
buffer.clear();
}
}
selectionKeys.remove();
}
}
}
}
public class NIOClient {
public static void main(String[] args) throws IOException {
System.out.println("客户端已经启动。。。");
//1、创建socket 通道
SocketChannel socketChannel = SocketChannel.open(new InetSocketAddress("127.0.0.1", 8080));
//2、设置异步非阻塞
socketChannel.configureBlocking(false);//jdk1.7以上方可
//指定缓冲区大小
ByteBuffer buffer = ByteBuffer.allocate(1024);
buffer.put(new Date().toString().getBytes());
//切换到读取模式
buffer.flip();
socketChannel.write(buffer);
buffer.clear();
socketChannel.close();
}
}
---------------------
作者:losemyfuture
来源:CSDN
原文:https://blog.csdn.net/qq_37049496/article/details/81267367
版权声明:本文为博主原创文章,转载请附上博文链接!