java中的nio详述_Java中NIO的简单介绍

NIO基本介绍

Java NIO(New IO) 也有人称之为Java non-blocking IO 是从Java1.4版本开始引入的一个新的IO API,可以代替标准的IO API。NIO与原来的IO有同样的作用和目的,但是使用的方式完全不同,NIO支持面向缓冲区的,基于通道的IO操作。NIO将以更加高效的方式进行文件的读写操作。NIO可以理解为非阻塞IO,传统的IO的read和write只能阻塞执行,线程在读写IO期间不能干其他事情,比如调用socket.read()时,如果服务器一直没有数据传输过来,线程就一直阻塞,而NIO中可以配置socket为非阻塞模式

NIO有三大核心部分:Channel(通道),Buffer(缓冲区),Selector(选择器)

NIO和BIO的比较

BIO以流的方式处理数据,而NIO以块的方式处理数据,块IO的效率比流IO高很多

BIO是阻塞的,NIO则是非阻塞的

BIO基于字节流和字符流进行操作,而NIO基于Channel(通道)和Buffer(缓冲区)进行操作,数据总是从通道读取到缓冲区中,或者从缓冲区写入到通道中。Selector(选择器)用于监听多个通道的事件(比如:连接请求,数据到达等),因此使用单个线程就可以监听多个客户端通道

NIO三大核心原理

Buffer缓冲区

缓冲区本质上是一块可以写入数据,然后可以从中读取数据的内存,这块内存被包装成NIO Buffer对象,并提供了一组方法,用来方便的访问该块内存,相比较直接对数组的操作,Buffer API更加容易操作和管理

Channel(通道)

Java NIO的通道类似流,但又有些不同 : 既可以从通道中读取数据,又可以写数据到通道。但流的(input或output)读写通常是单向的。通道可以非阻塞读取和写入通道,通道可以支持读取或写入缓冲区,也支持异步地读写

Selector选择器

Selector是一个Java NIO组件,可以能够检查一个或多个NIO通道,并确定哪些通道已经准备好进行读取或写入。这压根,一个单独的线程可以管理多个channel,从而管理多个网络连接

6fc8977b9c326bb4f1ed1f717006ce11.png

每个Channel都会对应一个Buffer

一个线程对应Selector,一个Selector对应多个Channel(连接)

程序切换到那个Channel是由事件决定的

Selector会根据不同的事件,在各个通道上切换

Buffer就是一个内存块,底层是一个数组

数据的读取写入是通过Buffer完成的,BIO中要么是输入流,或者是输出流,不能双向,但是NIO的Buffer时可以读也可以写。

Channel负责传输,Buffer负责存取数据

缓冲区Buffer

一个用于特定基本数据类型的容器。由 Java。nio包定义的,所有缓冲区都是Buffer抽象类的子类。Java NIO中的Buffer主要用于与NIO通道进行交互,数据是从通道读入缓冲区,从缓冲区写入通道中的

48d43d2e04b4d2c0e1ce277eaa3ba88a.png

Buffer类及其子类

Buffer就像一个数组,可以保存多个相同类型的数据。根据数据类型不同,有以下Buffer常用子类:

ByteBuffer

CharBuffer

ShortBuffer

xxxBuffer(xxx代表八种基本数据类型)

上述Buffer类 他们都采用相似的方法进行管理数据,只是各自管理的数据类型不同而已。都是通过如下方法获取一个Buffer对象:

//创建一个容量为capacity的xxxBuffer对象

static xxxBuffer allocate(int capacity);

|

|

|

IntBuffer buffer = IntBuffer.allocate(10);

Buffer中的重要概念

容量(capacity):创建后不能更改,且容量不能为负

限制(limit):表示缓冲区中可以操作数据的大小.缓冲区的限制不能为负,并且不能大于其容量.

写入模式,限制等于buffer的容量.读取模式下,limit等于写入的数据量

位置(position):下一个要读取或写入的数据的索引.缓冲区的位置不能为负,并且不能大于其限制

标记(mark)与重置(reset):标记是一个索引,通过Buffer中的mark()方法 指定 Buffer中一个特定的position,之后可以通过调用reset()方法恢复到这个position.

常用API测试

/**

* @PROJECT_NAME: JAVA_Test

* @DESCRIPTION:

* @USER: 罗龙达

* @DATE: 2021/2/10 17:34

*/

public class apiTest {

public void print(Buffer buffer){

System.out.println("pos = " + buffer.position());

System.out.println("lim = " + buffer.limit());

System.out.println("cap = " + buffer.capacity());

}

@Test

public void test001(){

//1. 分配一个缓冲区,容量设置为10

ByteBuffer buffer = ByteBuffer.allocate(10);

print(buffer);

//2. put往缓冲区中添加数据

System.out.println("--------缓冲区添加数据--------");

buffer.put("LongDa66".getBytes());

print(buffer);

//3. Buffer flip() --> 将缓冲区的界限设置为当前位置,并将当前位置设置为0 可读模式

System.out.println("--------调用flip()方法--------");

buffer.flip();

print(buffer);

//4. get数据的读取

System.out.println("--------缓冲区中读取数据--------");

byte b = buffer.get();

System.out.println("从缓冲区中读取 " + (char)b);

print(buffer);

}

@Test

public void test002(){

//1. 分配一个缓冲区,容量设置为10

ByteBuffer buffer = ByteBuffer.allocate(10);

print(buffer);

//2. put往缓冲区中添加数据

System.out.println("--------缓冲区添加数据--------");

buffer.put("LongDa66".getBytes());

print(buffer);

//2. 清除缓冲区中的数据,调用clear方法后只是将pos移到了0.

System.out.println("-------调用clear()方法后--------");

buffer.clear();

byte b = buffer.get();

System.out.println("从缓冲区中读取 " + (char)b);

print(buffer);

}

@Test

public void test003(){

//1. 分配一个缓冲区,容量设置为10

ByteBuffer buffer = ByteBuffer.allocate(10);

print(buffer);

//2. put往缓冲区中添加数据

System.out.println("--------缓冲区添加数据--------");

System.out.println("向缓冲区添加 : LongDa66");

buffer.put("LongDa66".getBytes());

print(buffer);

buffer.flip();

//3. 从缓冲区中读取前4位

System.out.println("--------缓冲区读取前4位数据--------");

byte[] bytes = new byte[4];

buffer.get(bytes);

String s = new String(bytes);

System.out.println(s);

print(buffer);

System.out.println("-----接着用mark()标记后,读取的数据-----");

buffer.mark();

byte[] bytes2 = new byte[4];

buffer.get(bytes2);

String s2 = new String(bytes2);

System.out.println(s2);

print(buffer);

System.out.println("-----调用reset()回到标记位置-----");

buffer.reset();

print(buffer);

System.out.println("-----调用remaining,看看position和limit之间剩余元素个数-----");

print(buffer);

System.out.println("缓冲区剩余元素个数" + buffer.remaining());

}

}

直接缓冲区与非直接缓冲区

非直接缓冲区 : 通过allocate() 方法分配缓冲区,将缓冲区建立在JVM的内存中。

8c034b076668ada9d5abdd04b81dc372.png

直接缓冲区 : 通过 allocateDirect() 方法分配直接缓冲区,将缓冲区建立在物理内存中,可以提高效率。

31085109e7208c2991207c6feed091e4.png

通道Channel

通道(Channel):表示IO源与目标打开的连接。Channel类似于传统的“流”.只不过Channel本身不能直接访问数据,Channel只能与Buffer进行交互

Channel与流的区别

Channel可以同时进行读写,而流只能读或者写

Channel可以实现异步读写数据

Channel可以从缓冲读数据,也可以写数据到缓冲

Channel在NIO中是一个接口

Channel常用实现类

FileChannel : 用于读取/写入/映射和操作文件的通道

DatagramChannel : 通过UDP读写网络中的数据通道

SocketChannel : 通过TCP读写网络中的数据

ServerSocketChannel : 可以监听新进来的TCP连接,对每一个新进来的连接都会创建一个SocketChannel

FileChannel类

常用方法测试

写入文件

@Test

public void test004() {

try {

//1. 字节输出流通向目标文件

FileOutputStream fos = new FileOutputStream("D:\\data2.txt");

//2. 得到字节输出流对应的通道

FileChannel channel = fos.getChannel();

//3. 分配缓冲区

ByteBuffer bufer = ByteBuffer.allocate(1024);

bufer.put("hello,world".getBytes());

//4. 切换成写模式

bufer.flip();

channel.write(bufer);

//5. 关闭通道

channel.close();

} catch (Exception e) {

e.printStackTrace();

}

}

读取文件

@Test

public void test005(){

try {

//定义一个文件字节输入流与源文件连通

FileInputStream fis = new FileInputStream("D:\\data.txt");

//得到文件字节输入流的文件通道

FileChannel channel = fis.getChannel();

//定义一个缓冲区

ByteBuffer buffer = ByteBuffer.allocate(1024);

//读取数据到缓冲区

channel.read(buffer);

buffer.flip();

//读取缓冲区中的数据

String s = new String(buffer.array());

System.out.println(s);

} catch (Exception e) {

e.printStackTrace();

}

}

文件的复制测试

@Test

public void test006(){

File file = new File("D:\\data.txt");

try {

//得到字节输入/输出流

FileInputStream fis = new FileInputStream(file);

FileOutputStream fos = new FileOutputStream("D:\\data3.txt");

//得到输入输出流的通道

FileChannel fisChannel = fis.getChannel();

FileChannel fosChannel = fos.getChannel();

//分配缓冲区

ByteBuffer buffer = ByteBuffer.allocate(1024);

while(true){

//清空缓冲区再写入数据

buffer.clear();

//判断文件是否结束

int flag = fisChannel.read(buffer);

if(flag == -1){

break;

}

//切换写模式,写入数据

buffer.flip();

fosChannel.write(buffer);

fisChannel.close();

fosChannel.close();

}

} catch (Exception e) {

e.printStackTrace();

}

}

分散读取和聚集操作数据

@Test

public void test007(){

File file = new File("D:\\data.txt");

File file2 = new File("D:\\data3.txt");

try {

//字节输入输出流

FileInputStream fis = new FileInputStream(file);

FileOutputStream fos = new FileOutputStream(file2);

//定义多个缓冲区 --> 数据分散

ByteBuffer buffer1 = ByteBuffer.allocate(4);

ByteBuffer buffer2 = ByteBuffer.allocate(400);

ByteBuffer[] buffers = {buffer1,buffer2};

//从通道中读取数据分散到各个缓冲区

FileChannel fisChannel = fis.getChannel();

FileChannel fosChannel = fos.getChannel();

//从通道中读取数据分散到各个缓冲区

fisChannel.read(buffers);

//从每个缓冲区中查询是否有数据读取到了

for (ByteBuffer buffer : buffers) {

buffer.flip();

System.out.println(new String(buffer.array(),0,buffer.remaining()));

}

//聚集操作缓冲区

fosChannel.write(buffers);

fisChannel.close();

fosChannel.close();

} catch (Exception e) {

e.printStackTrace();

}

}

TransferFrom() & TransferTo()方法

@Test

public void test008(){

File file = new File("D:\\data.txt");

File file2 = new File("D:\\data3.txt");

try {

//字节输入输出流

FileInputStream fis = new FileInputStream(file);

FileOutputStream fos = new FileOutputStream(file2);

//从通道中读取数据分散到各个缓冲区

FileChannel fisChannel = fis.getChannel();

FileChannel fosChannel = fos.getChannel();

//复制数据

//从目标通道中复制原通道数据

// fosChannel.transferFrom(fisChannel,fisChannel.position(),fisChannel.size());

//把原通道数据复制到目标通道数据

fisChannel.transferTo(fisChannel.position(),fisChannel.position(),fosChannel);

fisChannel.close();

fosChannel.close();

} catch (Exception e) {

e.printStackTrace();

}

}

NIO非阻塞式网络通信原理分析

Selector可以实现 : 一个I/O线程可以并发处理N个客户端连接和读写操作,这从根本上解决了传统同步阻塞I/O已连接一线程模型,架构的性能,弹性伸缩能力和可靠性都得到了极大的提升.

2cdf9154fb8397ac40ec0a2ea38c708d.png

入门案例

服务器端

/**

* @PROJECT_NAME: JAVA_Test

* @DESCRIPTION: 目标 : NIO非阻塞通信下的入门案例 : 服务器端

* @USER: 罗龙达

* @DATE: 2021/2/11 0:33

*/

public class Server {

public static void main(String[] args) throws IOException {

System.out.println("---------服务端启动-----------");

//获取通道 --> 接收客户端的连接请求

ServerSocketChannel ssChannel = ServerSocketChannel.open();

//切换为非阻塞模式

ssChannel.configureBlocking(false);

//绑定连接的端口

ssChannel.bind(new InetSocketAddress(9999));

//获取选择器 Selector

Selector selector = Selector.open();

//将通道都注册到选择器上,并且开始指定监听接收事件

ssChannel.register(selector, SelectionKey.OP_ACCEPT);

//使用Selector轮询已经准备就绪的事件

while (selector.select() > 0){

//获取选择器中的所有注册的通道中已经准备就绪的事件

Iterator it = selector.selectedKeys().iterator();

//遍历已经准备好的时间

while (it.hasNext()){

System.out.println("开始一轮事件处理");

//提取当前事件

SelectionKey sk = it.next();

//判断这个事件具体是什么

if(sk.isAcceptable()){

//接收事件准备就绪,直接获取当前接入的客户端通道

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();

}

}

//处理完毕移除当前事件 防止重复监听

it.remove();

}

}

}

}

客户端

/**

* @PROJECT_NAME: JAVA_Test

* @DESCRIPTION: 目标: 客户端案例实现 - 基于NIO非阻塞通信

* @USER: 罗龙达

* @DATE: 2021/2/11 0:56

*/

public class Client {

public static void main(String[] args) throws IOException {

//获取通道

SocketChannel socketChannel = SocketChannel.open(new InetSocketAddress("127.0.0.1",9999));

//切换成非阻塞模式

socketChannel.configureBlocking(false);

//指定缓冲区大小

ByteBuffer buffer = ByteBuffer.allocate(1024);

//发送数据给服务端

Scanner scanner = new Scanner(System.in);

while(true){

System.out.println("请说:");

String s = scanner.nextLine();

LocalDateTime timeNow = LocalDateTime.now();

buffer.put((timeNow + " 波妞 : " + s).getBytes());

buffer.flip();

socketChannel.write(buffer);

buffer.clear();

}

}

}

群聊案例

服务器端

/**

* @PROJECT_NAME: JAVA_Test

* @DESCRIPTION:

* @USER: 罗龙达

* @DATE: 2021/2/11 1:47

*/

public class Server {

//定义选择器,服务端通道,端口

private Selector selector;

private ServerSocketChannel ssChannel;

private static final int PORT = 9999;

//初始化

public Server(){

try {

//创建选择器

selector = Selector.open();

//获取通道

ssChannel = ServerSocketChannel.open();

//绑定客户端连接的端口

ssChannel.bind(new InetSocketAddress(PORT));

//设置非阻塞通信模式

ssChannel.configureBlocking(false);

//八通道注册到选择器上,并且开始指定接收事件

ssChannel.register(selector, SelectionKey.OP_ACCEPT);

} catch (IOException e) {

e.printStackTrace();

}

}

/**

* 监听事件

*/

private void listen(){

try {

while(selector.select() > 0){

//获取选择器中所有注册通道的就绪事件

Iterator iterator = selector.selectedKeys().iterator();

//开始遍历

while (iterator.hasNext()){

SelectionKey sk = iterator.next();

//判断事件的类型

if(sk.isAcceptable()){

//客户端接入请求

//获取当前客户端通道

SocketChannel socketChannel = ssChannel.accept();

socketChannel.configureBlocking(false);

socketChannel.register(selector,SelectionKey.OP_READ);

}

else if(sk.isReadable()){

//处理这个客户端的消息,接收它然后实现转发逻辑

readClientData(sk);

}

iterator.remove();//处理完毕,移除当前事件

}

}

} catch (IOException e) {

e.printStackTrace();

}

}

/**

* 接受当前客户端通道的信息,转发给其他全部客户端通道

* @param sk

*/

private void readClientData(SelectionKey sk) {

SocketChannel socketChannel = null;

try{

//获取当前客户端通道

socketChannel = (SocketChannel) sk.channel();

//创建缓冲区对象开始接受客户端通道的数据

ByteBuffer buffer = ByteBuffer.allocate(1024);

int count = socketChannel.read(buffer);

if (count > 0){

buffer.flip();

//提取读取到的信息

String msg = new String(buffer.array(), 0, buffer.remaining());

System.out.println("接收到客户端消息 : " + msg);

sendMsgToAllClient(msg,socketChannel);

}

}catch (Exception e){

try {

System.out.println("有人离线了 : " + socketChannel.getRemoteAddress());

//当前客户端离线

sk.cancel();

socketChannel.close();

} catch (IOException ioException) {

}

}

}

/**

* 把当前客户端的消息数据都推送给当前全部在线注册的channel

* @param msg

* @param socketChannel

*/

private void sendMsgToAllClient(String msg, SocketChannel socketChannel) {

System.out.println("服务端开始转发消息, 当前处理的线程 : " + Thread.currentThread().getName());

for (SelectionKey key : selector.keys()) {

Channel channel =key.channel();

if(channel instanceof SocketChannel && socketChannel != channel){

ByteBuffer buffer = ByteBuffer.wrap(msg.getBytes());

try {

((SocketChannel)channel).write(buffer);

} catch (IOException e) {

e.printStackTrace();

}

}

}

}

public static void main(String[] args) {

//创建服务端对象

Server server = new Server();

//开始监听客户端的各种消息事件

server.listen();

}

}

客户端

/**

* @PROJECT_NAME: JAVA_Test

* @DESCRIPTION: 客户端代码逻辑实现

* @USER: 罗龙达

* @DATE: 2021/2/11 17:33

*/

public class Client {

private Selector selector;

private static int PORT = 9999;

private static SocketChannel socketChannel;

public Client(){

try {

//创建选择器

selector = Selector.open();

//连接服务端

socketChannel = SocketChannel.open(new InetSocketAddress("127.0.0.1",PORT));

//设置非阻塞通信模式

socketChannel.configureBlocking(false);

//八通道注册到选择器上,并且开始指定接收事件

socketChannel.register(selector, SelectionKey.OP_READ);

System.out.println("当前客户端准备完成");

} catch (IOException e) {

e.printStackTrace();

}

}

public static void main(String[] args) {

Client client = new Client();

//定义一个线程专门负责监听服务端发送过来的读消息事件

new Thread(client::readInfo).start();

Scanner sc = new Scanner(System.in);

while (sc.hasNextLine()){

String s = sc.nextLine();

Client.sendMsg(s);

}

}

private static void sendMsg(String s) {

try {

socketChannel.write(ByteBuffer.wrap(("波仔说:" + s).getBytes()));

} catch (IOException e) {

e.printStackTrace();

}

}

private void readInfo() {

try{

while(selector.select() > 0){

Iterator iterator = selector.selectedKeys().iterator();

while(iterator.hasNext()){

SelectionKey key = iterator.next();

if (key.isReadable()){

SocketChannel sc = (SocketChannel) key.channel();

ByteBuffer buffer = ByteBuffer.allocate(1024);

sc.read(buffer);

System.out.println(new String(buffer.array()).trim());

System.out.println("--------分割线-----------");

}

iterator.remove();

}

}

}catch (Exception e){

e.printStackTrace();

}

}

}

AIO异步非阻塞IO

Java AIO : 异步非阻塞,服务器实现模式为一个有效请求一个线程,客户端的I/O请求都是由OS先完成了再通知服务器应用去启动线程进行处理

与NIO不同,当进行读写操作时,只须直接调用API的read或write方法即可,这两种方法均为异步的,对于读操作而言,当有流可读取时,操作系统会将可读的流传入read方法的缓冲区,对于写操作而言,当操作系统将write方法传递的流写入完毕时,操作系统会主动通知应用程序

可以理解为,read/write方法都是异步的,完成后会主动调用回调函数,在JDK1.7中,这部分内容被称作NIO 2

BIO,NIO,AIO三者比较

Java BIO :

同步并阻塞,服务器实现模式为一个链接一个线程,即客户端有连接请求时服务器端就需要启动一个线程进行处理,如果这个链接不做任何事情会造成不必要的线程开销,可以通过线程池机制改善.

Java NIO :

同步非阻塞,服务器实现模式为一个请求一个线程,即客户端发送的连接请求都会注册到多路复用器上,多路复用轮询到连接有I/O请求时才启动一个线程进行处理

Java AIO :

异步非阻塞,服务器实现模式为一个有效请求一个线程,客户端的I/O请求都是由OS先完成了再通知服务器应用去启动线程进行处理

使用场景分析

BIO适用于连接数目较小且固定的架构,这种方式对服务器资源要求比较高,并发局限于应用中,但程序直观简单易理解

NIO方式适用于连接数目多且连接比较短的架构,比如聊天服务器,并发局限于应用中,编程比较复杂

AIO方式适用于连接数目多且连接比较长的架构,比如相册服务器,充分调用OS参与并发操作,编程比较复杂

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值