NIO相关(详解)

本文借鉴网络视频及网络资料汇总如下,如果您有更好的建议和分享,欢迎您在评论区留言与补充!

1.缓冲区(buffer):

在Java负责的数据的存取,缓冲区底层为数组,用于存储不同数据类型的数据,根据数据类型不同(boolean除外),提供相应类型的缓冲区:
ByteBuffer,CharBuffer,ShortBuffer,IntBuffer,LongBuffer,DoubleBuffer,FloatBuffer。
上述缓冲区管理方法基本一致,通过allocate()获取缓冲区。

2.缓冲区获取数据核心方法:

put():存入数据到缓冲区
get():获取缓冲区的数据

3.缓冲区四个核心属性:

capacity:容量,表示缓冲区中最大容量,声明之后不能改变。
limit:界限,表示缓冲区可以操作数据的大小(limit后的数据不能进行读写)。
position:位置,表示缓冲区正在操作数据的位置。
mark:标记,表示记录当前position的位置,通过reset()恢复到mark位置。
0<=mark<=position <= limit <= capacity

import org.junit.Test;
import java.nio.ByteBuffer;

public class NIO1 {
    @Test
    public void newIOTest1() {
        String str = "abcde";

        // 1.分配指定大小的缓冲区
        ByteBuffer buffer = ByteBuffer.allocate(1024);
        System.out.println(buffer.position()); // 0
        System.out.println(buffer.limit());  // 1024
        System.out.println(buffer.capacity()); // 1024

        // 2.put存入数据到缓冲区
        buffer.put(str.getBytes());
        System.out.println(buffer.position()); // 5
        System.out.println(buffer.limit()); // 1024
        System.out.println(buffer.capacity()); // 1024

        // 3.切换到读取数据
        buffer.flip();
        System.out.println(buffer.position()); // 0
        System.out.println(buffer.limit()); // 5
        System.out.println(buffer.capacity()); // 1024

        // 4.get从缓冲区读取数据
        byte[] bytes = new byte[buffer.limit()];
        buffer.get(bytes);
        System.out.println(new String(bytes, 0, bytes.length));
        System.out.println(buffer.position()); // 5
        System.out.println(buffer.limit()); // 5
        System.out.println(buffer.capacity()); // 1024

        // 5.rewind(可重复读取)
        buffer.rewind();
        System.out.println(buffer.position()); // 0
        System.out.println(buffer.limit()); // 5
        System.out.println(buffer.capacity()); // 1024

        // 6.clear,清空缓存区,缓存区中数据依然存在但是处于"被遗忘"状态
        buffer.clear();
        System.out.println(buffer.position()); // 0
        System.out.println(buffer.limit()); // 1024
        System.out.println(buffer.capacity()); // 1024
        System.out.println((char) buffer.get()); // a
    }

    @Test
    public void newIOTest2() {
        String str = "abcde";
        ByteBuffer buffer = ByteBuffer.allocate(1024);
        buffer.put(str.getBytes());
        buffer.flip();
        byte[] bytes = new byte[buffer.limit()];
        buffer.get(bytes, 0, 2);
        System.out.println(new String(bytes, 0, 2)); // ab
        System.out.println(buffer.position()); // 2
        buffer.mark();
        buffer.get(bytes, 2, 2);
        System.out.println(new String(bytes, 2, 2)); // cd
        System.out.println(buffer.position()); // 4
        buffer.reset();
        System.out.println(buffer.position()); // 2

        // 查看缓冲区中是否还有剩余数据
        if (buffer.hasRemaining()) {
            // 获取缓冲区可以操作的数量
            System.out.println(buffer.remaining());
        }
    }
}

4.直接缓冲区与非直接缓冲区:

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

在这里插入图片描述

直接缓冲区:通过allocateDirect()方法分配缓冲区,将缓冲区建立在操作系统的物理内存中。

在这里插入图片描述

@Test
public void newIOTest3() {
    // 获取直接缓冲区
    ByteBuffer buffer = ByteBuffer.allocateDirect(1024);
    // 判断是直接缓冲区还是非直接缓冲区,true为直接缓冲区,false反之
    System.out.println(buffer.isDirect());
}

5.通道:

演化过程:

在这里插入图片描述

在这里插入图片描述

在这里插入图片描述

通道(Channel):用于源节点与目标节点的连接,在Java NIO中负责缓冲区数据的传输,Channel本身不存储数据,需要配合缓冲区进行传输。

通道主要实现类:

java.nio.channels.Channel下的接口:

FileChannel:用于本地
SocketChannel:用于网络I/O(TCP)
ServerSocketChannel:用于网络I/O(TCP)
DatagramChannel:用于网络I/O(UDP)

获取通道的几种方式:

1.Java针对支持通道的类提供了getChannel()方法:
本地IO:
FileInputstream/FileOutputstream,RandomAccessFile,
网络IO:
Socket,ServerSocket,DatagramSockte
2.在JDK1.7中的NIO.2针对各个通道提供了静态方法open()
3.在JKD1.7中的NIO.2的Files工具类的newByteChannel()
4.通道之间的数据传输:
transferFrom(),transferTo()
5.分散(Scatter)与聚集(Gather):
分散读取(Scattering Reads):将通道中的数据分散到多个缓冲区
聚集写入(Gathering Writes):将多个缓冲区的数据聚集到通道中
6.字符集:Charset
编码:字符串→字符数组
解码:字符数组→字符串

(以下案例的顺序与上述获取通道方式的编号顺序相反)

import org.junit.Test;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.RandomAccessFile;
import java.nio.ByteBuffer;
import java.nio.CharBuffer;
import java.nio.MappedByteBuffer;
import java.nio.channels.FileChannel;
import java.nio.charset.CharacterCodingException;
import java.nio.charset.Charset;
import java.nio.charset.CharsetDecoder;
import java.nio.charset.CharsetEncoder;
import java.nio.file.Paths;
import java.nio.file.StandardOpenOption;
import java.util.Map;
import java.util.Map.*;
import java.util.Set;


public class ChannelTest {
    @Test
    public void channelTest6() {
        Charset cs = Charset.forName("GBK");
        // 获取编码器
        CharsetEncoder ce = cs.newEncoder();
        // 获取解码器
        CharsetDecoder cd = cs.newDecoder();
        CharBuffer charBuffer = CharBuffer.allocate(1024);
        charBuffer.put("文字 + 英文:Hello");
        // 编码
        ByteBuffer byteBuffer = null;
        try {
            charBuffer.flip();
            byteBuffer = ce.encode(charBuffer);
            for (int i = 0; i < byteBuffer.limit(); i++) {
                // 查看对应16个字节是否编码成功
                System.out.println(byteBuffer.get());
            }
            System.out.println(byteBuffer.position());
            // 解码
            byteBuffer.flip();
            CharBuffer cb = cd.decode(byteBuffer);
            System.out.println(cb.toString());
         // byteBuffer.flip();
         // System.out.println((cd.decode(byteBuffer)).toString());
            // 用GBK编码,UTF-8解码会导致乱码
            Charset charset = Charset.forName("UTF-8");
            byteBuffer.flip();
            System.out.println((charset.decode(byteBuffer)).toString());
        } catch (CharacterCodingException e) {
            e.printStackTrace();
        }
    }

    // 查看打印支持的字符集
    @Test
    public void channelTest5() {
        Map<String, Charset> map = Charset.availableCharsets();
        Set<Entry<String, Charset>> set = map.entrySet();
        for (Entry<String, Charset> entry : set) {
            System.out.println(entry.getKey() + "==" + entry.getValue());
        }
    }

    // 分散与聚集
    @Test
    public void channelTest4() {
        RandomAccessFile file = null;
        try {
            file = new RandomAccessFile("1.txt", "r");
            // 获取通道
            FileChannel channel1 = file.getChannel();
            // 分配多个指定大小的缓冲区
            ByteBuffer byteBuffer1 = ByteBuffer.allocate(100);
            ByteBuffer byteBuffer2 = ByteBuffer.allocate(1024);
            // 分散读取
            ByteBuffer[] byteBuffers = {byteBuffer1, byteBuffer2};
            channel1.read(byteBuffers);
            for (ByteBuffer bytes : byteBuffers) {
                bytes.flip();
            }
            System.out.println(new String(byteBuffers[0].array(), 0, byteBuffers[0].limit()));
            System.out.println(new String(byteBuffers[1].array(), 0, byteBuffers[1].limit()));

            // 聚集写入
            RandomAccessFile file1 = new RandomAccessFile("2.txt", "rw");
            FileChannel channel2 = file1.getChannel();
            channel2.write(byteBuffers);
        } catch (IOException e) {
            e.printStackTrace();
        }
    }

    // 通道之间的数据传输(直接缓冲区)
    @Test
    public void channelTest3() {
        FileChannel inChannel = null;
        FileChannel outChannel = null;
        try {
            inChannel = FileChannel.open(Paths.get("图片1"), StandardOpenOption.READ);
            outChannel = FileChannel.open(Paths.get("图片4"), StandardOpenOption.READ, StandardOpenOption.WRITE, StandardOpenOption.CREATE);
            inChannel.transferTo(0, inChannel.size(), outChannel);
            // outChannel.transferFrom(inChannel, 0, inChannel.size());
            inChannel.close();
            outChannel.close();
        } catch (IOException e) {
            e.printStackTrace();
        }
    }

    // 使用直接缓冲区完成文件的复制(内存映射文件,直接缓冲区)
    // 复制时间短但不一定稳定
    @Test
    public void channelTest2() {
        long start = System.currentTimeMillis();
        FileChannel inChannel = null;
        FileChannel outChannel = null;
        try {
            inChannel = FileChannel.open(Paths.get("图片1"), StandardOpenOption.READ);
            // StandardOpenOption.CREATE,如果文件不存在则创建文件,如果文件已存在则依旧覆盖存在文件
            // StandardOpenOption.CREATE_NEW,如果文件不存在则创建文件,如果文件已存在则报错
            outChannel = FileChannel.open(Paths.get("图片4"), StandardOpenOption.WRITE, StandardOpenOption.READ, StandardOpenOption.CREATE_NEW);
            // 内存映射文件
            MappedByteBuffer inMappedByteBuffer = inChannel.map(FileChannel.MapMode.READ_ONLY, 0, inChannel.size());
            MappedByteBuffer outMappedByteBuffer = outChannel.map(FileChannel.MapMode.READ_WRITE, 0, inChannel.size());
            // 直接对缓冲区进行数据的读写操作
            byte[] bytes = new byte[inMappedByteBuffer.limit()];
            inMappedByteBuffer.get(bytes);
            outMappedByteBuffer.put(bytes);
            // 关闭通道
            inChannel.close();
            outChannel.close();
        } catch (IOException e) {
            e.printStackTrace();
        }
    }

    // 利用通道完成文件复制(非直接缓冲区)
    // 复制时间长
    @Test
    public void channelTest1() {
        FileInputStream fis = null;
        FileOutputStream fos = null;
        // ①获取通道
        FileChannel channel1 = null;
        FileChannel channel2 = null;
        try {
            fis = new FileInputStream("图片1");
            fos = new FileOutputStream("图片3");

            channel1 = fis.getChannel();
            channel2 = fos.getChannel();
            // ②分配指定大小
            ByteBuffer byteBuffer = ByteBuffer.allocate(900000);

            // ③将通道数据存入缓冲区
            if (channel1.read(byteBuffer) != -1) {
                byteBuffer.flip(); // 切换读取模式
                // ④将缓冲区中数据写入通道
                channel2.write(byteBuffer);
                byteBuffer.clear(); // 清空缓冲区
            }
        } catch (IOException e) {
            e.printStackTrace();
        } finally {
            try {
                channel1.close();
                channel2.close();
                fos.close();
                fis.close();
            } catch (IOException e) {
                e.printStackTrace();
            }
        }
    }
}

5.阻塞式与非阻塞式:

阻塞式:

在这里插入图片描述

传统的 IO 流都是阻塞式的。当客户端发送读写请求给服务端时,若服务端不能确定该线程的数据是否真实有效,该线程会一直处于阻塞状态,服务端会等待客户端,该线程什么时候有数据,才可以进行工作,但是在等待期间,服务端的该线程做不了任何事情。服务端判断内核地址空间中有没有数据,如果没有数据服务端继续等待,该线程依旧处于阻塞状态,什么时候有数据了在把数据复制到用户地址空间,读到我们的程序中。
因此,在完成网络通信进行 IO 操作时,由于线程会阻塞,所以服务器端必须为每个客户端都提供一个独立的线程进行处理,当服务器端需要处理大量客户端时,性能急剧下降。

非阻塞式:

在这里插入图片描述

Java NIO 是非阻塞模式的。正因为客户端与服务端之间建立了通道,选择器会把客户端中每一个用于传输数据的通道注册到选择器上,该选择器的作用是用来监控每一个通道的IO状态。当某一条通道上某一个请求的事件完全准备就绪时,那么服务器才会把该任务分配到服务端的一个或多个线程上,再去运行。如果客户端的请求没有完全准备就绪时,服务端的线程依旧正常工作,不受阻碍。

使用NIO完成网络通信的三个核心:

1.通道(Channel):负责连接
java.nio.channels.Channel 接口:
SelectableChannel,
SocketChannel,
ServerSocketChannel,
DatagramChannel,

Pipe.SinkChannel,
Pipe.SourceChannel
2. 缓冲区(Buffer):负责数据的存取
3. 选择器(Selector):是 SelectableChannel 的多路复用器。用于监控 SelectableChannel 的 IO 状况

NIO_阻塞式:

import org.junit.Test;
import java.io.IOException;
import java.net.InetSocketAddress;
import java.nio.ByteBuffer;
import java.nio.channels.FileChannel;
import java.nio.channels.ServerSocketChannel;
import java.nio.channels.SocketChannel;
import java.nio.file.Paths;
import java.nio.file.StandardOpenOption;

// 阻塞式
public class BlockingNIOTest1 {
    // 客户端
    @Test
    public void client() {
        // 获取通道
        SocketChannel schannel = null;
        try {
            schannel = SocketChannel.open(new InetSocketAddress("127.0.0.1", 6666));
            FileChannel fchannel = FileChannel.open(Paths.get("图片1"), StandardOpenOption.READ);
            // 分配指定大小的缓冲区
            ByteBuffer byteBuffer = ByteBuffer.allocate(1024);
            // 读取本地文件,并发送到服务器
            while (fchannel.read(byteBuffer) != -1) {
                byteBuffer.flip();
                schannel.write(byteBuffer);
                byteBuffer.clear();
            }
            // 单项关闭客户端即可避免一直等待
            schannel.shutdownOutput();
            // 接收服务端的反馈
            int len = 0;
            while ((len = schannel.read(byteBuffer)) != -1) {
                byteBuffer.flip();
                System.out.println(new String(byteBuffer.array(), 0, len));
                byteBuffer.clear();
            }
            // 关闭通道
            schannel.close();
            fchannel.close();
        } catch (IOException e) {
            e.printStackTrace();
        }
    }

    // 服务端
    @Test
    public void server() {
        // 获取通道
        ServerSocketChannel sschannel = null;
        try {
            sschannel = ServerSocketChannel.open();
            FileChannel fchannel = FileChannel.open(Paths.get("图片2"), StandardOpenOption.WRITE, StandardOpenOption.CREATE);
            // 绑定连接
            sschannel.bind(new InetSocketAddress(6666));
            // 获取客户连接的通道
            SocketChannel socketChannel = sschannel.accept();
            // 分配指定大小的缓冲区
            ByteBuffer byteBuffer = ByteBuffer.allocate(1024);
            // 接收客户端的数据,并保存到本地
            while (socketChannel.read(byteBuffer) != -1) {
                byteBuffer.flip();
                fchannel.write(byteBuffer);
                byteBuffer.clear();
            }

            // 发送反馈给客户端
            byteBuffer.put("服务端已成功接收".getBytes());
            byteBuffer.flip();
            socketChannel.write(byteBuffer);

            // 关闭通道
            sschannel.close();
            fchannel.close();
            socketChannel.close();
        } catch (IOException e) {
            e.printStackTrace();
        }
    }
}

NIO_非阻塞式:

import org.junit.Test;
import java.io.IOException;
import java.net.InetSocketAddress;
import java.nio.ByteBuffer;
import java.nio.channels.SelectionKey;
import java.nio.channels.Selector;
import java.nio.channels.ServerSocketChannel;
import java.nio.channels.SocketChannel;
import java.time.LocalDateTime;
import java.util.Iterator;
import java.util.Scanner;

public class BlockingNIOTest2 {
    // 客户端
    @Test
    public void client() {
        // 1.获取通道
        SocketChannel sChannel = null;
        try {
            sChannel = SocketChannel.open(new InetSocketAddress("127.0.0.1", 6666));
            // 2.切换非阻塞模式
            sChannel.configureBlocking(false);
            // 3.分配指定大小的缓冲区
            ByteBuffer byteBuffer = ByteBuffer.allocate(1024);
            // 4.发送数据给服务端
            Scanner scanner = new Scanner(System.in);
            while (scanner.hasNext()) {
                String str = scanner.next();
                byteBuffer.put((LocalDateTime.now().toString() + "\n" + str).getBytes());
                byteBuffer.flip();
                sChannel.write(byteBuffer);
                byteBuffer.clear();
            }
            // 5.关闭通道
            sChannel.close();
        } catch (IOException e) {
            e.printStackTrace();
        }
    }

    // 服务端
    @Test
    public void server() {
        try {
            // 1.获取通道
            ServerSocketChannel ssChannel = ServerSocketChannel.open();
            // 2.切换成非阻塞模式
            ssChannel.configureBlocking(false);
            // 3.绑定连接
            ssChannel.bind(new InetSocketAddress(6666));
            // 4.获取选择器
            Selector selector = Selector.open();
            // 5.将通道注册到选择器上,并指定"监听接收事件"
            ssChannel.register(selector, SelectionKey.OP_ACCEPT);
            // 6.轮询式的获取选择器上已经"准备就绪"的事件
            while (selector.select() > 0) {
                // 7.获取当前选择器中所有注册的"选择键(已就绪的监听事件)"
                Iterator<SelectionKey> it = selector.selectedKeys().iterator();
                while (it.hasNext()) {
                    // 8.获取准备"就绪"的事件
                    SelectionKey sKey = it.next();
                    // 9.判断具体是什么事件准备就绪
                    if (sKey.isAcceptable()) {
                        // 10.若"接收就绪",获取客户端链接
                        SocketChannel sChannel = ssChannel.accept();
                        // 11.切换非阻塞模式
                        sChannel.configureBlocking(false);
                        // 12.将该通道注册到选择器上
                        sChannel.register(selector, SelectionKey.OP_READ);
                    } else if (sKey.isReadable()) {
                        // 13.获取当前选择器上"读就绪"的通道
                        SocketChannel sChannel = (SocketChannel) sKey.channel();
                        // 14.读取数据
                        ByteBuffer byteBuffer = ByteBuffer.allocate(1024);
                        int len = 0;
                        while ((len = sChannel.read(byteBuffer)) > 0) {
                            byteBuffer.flip();
                            System.out.println(new String(byteBuffer.array(), 0, len));
                            byteBuffer.clear();
                        }
                    }
                    // 15.取消选择键 SelectionKey
                    it.remove();
                }
            }
        } catch (IOException e) {
            e.printStackTrace();
        }
    }
}
这里加入了Scanner,本人用的是IDEA,但是用@Test注解测试的时候导致控制台无法输入,所以以下是解决方案(嫌麻烦的小伙伴可以将@Test里面的东西放入main函数,也可以解决控制台不能输入的问题):

在这里插入图片描述

在这里插入图片描述

如图:

在这里插入图片描述

在最下方加入:

-Deditable.java.test.console=true

之后重启IDEA即可在控制台写入。

6.NIO_DatagramChannel:

Java NIO中的DatagramChannel是一个能收发UDP包的通道。

import org.junit.Test;
import java.io.IOException;
import java.net.InetSocketAddress;
import java.nio.ByteBuffer;
import java.nio.channels.DatagramChannel;
import java.nio.channels.SelectionKey;
import java.nio.channels.Selector;
import java.time.LocalDateTime;
import java.util.Iterator;
import java.util.Scanner;

public class BlockingNIOTest3 {
    @Test
    public void send() {
        DatagramChannel dgChannel = null;
        try {
            dgChannel = DatagramChannel.open();
            dgChannel.configureBlocking(false);
            ByteBuffer byteBuffer = ByteBuffer.allocate(1024);
            Scanner scanner = new Scanner(System.in);
            while (scanner.hasNext()) {
                String str = scanner.next();
                byteBuffer.put((LocalDateTime.now().toString() + "\n" + str).getBytes());
                byteBuffer.flip();
                dgChannel.send(byteBuffer, new InetSocketAddress("127.0.0.1", 6666));
                byteBuffer.clear();
            }
            dgChannel.close();
        } catch (IOException e) {
            e.printStackTrace();
        }
    }

    @Test
    public void receive() {
        DatagramChannel dgChannel = null;
        try {
            dgChannel = DatagramChannel.open();
            dgChannel.configureBlocking(false);
            dgChannel.bind(new InetSocketAddress(6666));
            Selector selector = Selector.open();
            dgChannel.register(selector, SelectionKey.OP_READ);
            while (selector.select() > 0) {
                Iterator<SelectionKey> it = selector.selectedKeys().iterator();
                while (it.hasNext()) {
                    SelectionKey sKey = it.next();
                    if (sKey.isReadable()) {
                        ByteBuffer byteBuffer = ByteBuffer.allocate(1024);
                        dgChannel.receive(byteBuffer);
                        byteBuffer.flip();
                        System.out.println(new String(byteBuffer.array(), 0, byteBuffer.limit()));
                        byteBuffer.clear();
                    }
                }
                it.remove();
            }
        } catch (IOException e) {
            e.printStackTrace();
        }
    }
}

7.NIO_PiPe:

Java NIO管道是两个线程之间的单项数据连接。PiPe有一个source通道和一个sink通道,数据会被写入到sink通道,从source通道读取。

import org.junit.Test;
import java.io.IOException;
import java.nio.ByteBuffer;
import java.nio.channels.Pipe;

public class PiPeTest {
    @Test
    public void test() {
        // 1.获取管道
        Pipe pipe = null;
        try {
            pipe = Pipe.open();
            // 2.将缓冲区中的数据写入管道
            ByteBuffer byteBuffer = ByteBuffer.allocate(1024);

            Pipe.SinkChannel sinkChannel = pipe.sink();
            byteBuffer.put("这是一条数据".getBytes());
            byteBuffer.flip();
            sinkChannel.write(byteBuffer);
            
            // 3.读取缓冲区中的数据
            Pipe.SourceChannel sourceChannel = pipe.source();
            byteBuffer.flip();
            int len = sourceChannel.read(byteBuffer);
            System.out.println(new String(byteBuffer.array(), 0, len));
        } catch (IOException e) {
            e.printStackTrace();
        }
    }
}
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值