Java-NIO

NIO概述

  • Java NIO(New IO, Non-Blocking IO)是从 Java 1.4开始引入的全新的 IO. 特点是同步非阻塞, 面向缓冲区的
  • NIO是 Reactor模式, 当有事件触发时, 服务器端得到通知, 进行相应处理的

NIO与传统 IO的区别

IONIO
面向流(Stream Oriented)单向的面向缓冲区(Buffer Oriented)双向的
阻塞IO(Blocking IO)非阻塞IO(Non Blocking IO)
-选择器(Selectors)

*传统 IO操作是 DMA(Direct Memory Access, 直接存储器访问;特点是操作时需往 CPU请求获取权限)负责 IO接口与内存的交互. 而 NIO是通过 Channel与内存交互, 相比 DMA方式 Channel方式是独立的, 因此性能略高

通道(Channel)

  • NIO的 Channel类似于传统的“流”, 但它本身是不能直接访问数据, 而只负责传输(只提供从文件或网络读取数据的渠道), 数据的存取是使用了缓冲区(Buffer)
  • 与流的区别:
  1. 流只能读或者只能写, 也就是单向的
  2. 通道可以实现异步读写数据
  3. 通道可以从缓冲读或写数据到缓冲区, 也就是双向的
  • 通道的主要实现类
    (-) FileChannel: 读写文件的通道
    (-) SocketChannel: 通过 TCP连接, 读写网络数据的通道
    (-) ServerSocketChannel: 监听 TCP连接, 为每一个连接都会创建一个 SocketChannel
    (-) DatagramChannel: 通过 UDP连接, 读写网络数据的通道
  • 获取通道
  1. 支持通道的类, 可通过 getChannel()方法获取通道
    (1-1) 本地 IO:
    (-) FileInputStream/FileOutputStream
    (-) RandomAccessFile
    (1-2) 网络 IO:
    (-) Socket
    (-) ServerSocket
    (-) DatagramSocket
    * 其它: 可以通过 Files类的静态方法 newByteChannel()获取字节通道. 或可以通过通道的静态方法 open()打开并返回指定通道
  • 通道之间的数据传输
    (-) inFileChannel.transferTo(long position, long count, WritableByteChannel target): 从源(inFileChannel)传输到 target
    (-) outFileChannel.transferFrom(ReadableByteChannel src, long position, long count): 从(src)源传输到 outFileChannel
  • 分散(Scatter)& 聚集(Gather)
    (-) 分散读取(Scattering Reads): 将通道中的数据分散到多个缓冲区中. 注: 按照缓冲区的顺序依次填满 Buffer
    (-) 聚集写入(Gathering Writes): 将多个 Buffer中的数据“聚集”到一个 Channel. 注: 按照缓冲区的顺序写入, 从 position到 limit之间的数据到 Channel
  • 字符集(Charset)
    编码(CharsetEncoder): 字符串 -> 字节数组
    解码(CharsetDecoder): 字节数组 -> 字符串

在这里插入图片描述

  • FileChannel的常用方法:

int read(ByteBuffer dst)从 Channel中读取数据到 dst
long read(ByteBuffer[] dsts)将 Channel中的数据“分散”存到 dsts
int write(ByteBuffer src)将 ByteBuffer中的数据写入到 Channel
long write(ByteBuffer[] srcs)将 ByteBuffer[]中的数据“聚集”写入到 Channel
long position()返回此通道的文件位置
FileChannel position(long p)设置此通道的文件位置
long size()返回此通道的文件的当前大小
FileChannel truncate(long s)将通道中的文件截断为 s个字节
void force(boolean metaData)将通道中还未写入到磁盘的数据, 强制写完

  • 本地 Channel使用例子

# 利用通道完成文件的复制(非直接缓冲区)
public class TestChannel {
    public static void main(String[] args) throws IOException {
        FileInputStream fis = new FileInputStream("E:/1.ico");
        FileOutputStream fos = new FileOutputStream("E:/2.ico");
        FileChannel inChannel = fis.getChannel();
        FileChannel outChannel = fos.getChannel();
        // 分配指定大小的缓冲区
        ByteBuffer buf = ByteBuffer.allocate(1024);
        // 从 inChannel中读取数据到 buf中
        while(inChannel.read(buf) != -1) {
			// 切换读取模式
            buf.flip();
			// 将 buf中的数据写入到 outChannel中
            outChannel.write(buf);
			// 清空缓冲区
            buf.clear(); 
        }

        outChannel.close();
        inChannel.close();
        fos.close();
        fis.close();
    }
}

# 通过内存映射文件复制(直接缓冲区)
public class TestChannel {
    public static void main(String[] args) throws IOException {
		FileChannel inChannel = FileChannel.open(Paths.get("E:/1.ico"), StandardOpenOption.READ);
		FileChannel outChannel = FileChannel.open(Paths.get("E:/2.ico"), StandardOpenOption.WRITE, StandardOpenOption.READ, StandardOpenOption.CREATE);
		// 内存映射文件
		MappedByteBuffer inMappedBuf = inChannel.map(MapMode.READ_ONLY, 0, inChannel.size());
		MappedByteBuffer outMappedBuf = outChannel.map(MapMode.READ_WRITE, 0, inChannel.size());
		// 直接对缓冲区进行数据的读写操作
		byte[] dst = new byte[inMappedBuf.limit()];
		// 从 inMappedBuf中读取字节数据到 dst中
		inMappedBuf.get(dst);
		// 将 dst中的字节数据写入到 outMappedBuf中
		outMappedBuf.put(dst);

		inChannel.close();
		outChannel.close();
    }
}

# 通道之间的数据传输(直接缓冲区)
public class TestChannel {
    public static void main(String[] args) throws IOException {
        FileChannel inChannel = FileChannel.open(Paths.get("E:/1.ico"), StandardOpenOption.READ);
        FileChannel outChannel = FileChannel.open(Paths.get("E:/2.ico"), StandardOpenOption.WRITE, StandardOpenOption.READ, StandardOpenOption.CREATE);
		// 从源(inFileChannel)传输到 target
        inChannel.transferTo(0, inChannel.size(), outChannel);
		// 从(src)源传输到 outFileChannel
        outChannel.transferFrom(inChannel, 0, inChannel.size());

        inChannel.close();
        outChannel.close();
    }
}

# 分散和聚集
public class TestChannel {
    public static void main(String[] args) throws IOException {
        RandomAccessFile raf1 = new RandomAccessFile("E:/1.sql", "rw");
        // 获取通道
        FileChannel channel1 = raf1.getChannel();
        // 分配指定大小的缓冲区
        ByteBuffer buf1 = ByteBuffer.allocate(100);
        ByteBuffer buf2 = ByteBuffer.allocate(1024);
        // 分散读取
        ByteBuffer[] bufs = {buf1, buf2};
		// 将 channel1中的数据分散读取到 bufs中
        channel1.read(bufs);
        for (ByteBuffer byteBuffer: bufs) {
			// 切换读取模式
            byteBuffer.flip();
        }
        System.out.println("buf1: " + new String(bufs[0].array(), 0, bufs[0].limit()));
        System.out.println("buf2: " + new String(bufs[1].array(), 0, bufs[1].limit()));
        // 聚集写入
        RandomAccessFile raf2 = new RandomAccessFile("E:/2.sql", "rw");
        FileChannel channel2 = raf2.getChannel();
		// 将 bufs中的数据聚集写入到 channel2中
        channel2.write(bufs);

        channel1.close();
        channel2.close();
		raf1.close();
		raf2.close();
    }
}

# 字符集
public class TestChannel {
    public static void main(String[] args) throws IOException {
        // 指定编码
        Charset cs1 = Charset.forName("GBK");
        // 获取编码器
        CharsetEncoder encoder = cs1.newEncoder();
        // 获取解码器
        CharsetDecoder decoder = cs1.newDecoder();
        // 分配指定大小的非直接缓冲区
        CharBuffer buf1 = CharBuffer.allocate(1024);
        buf1.put("全abc12");
        // 切换读取数据模式
        buf1.flip();
        // 编码
        ByteBuffer buf2 = encoder.encode(buf1);
        for (int i = 0; i < 6; i++) {
            System.out.println(buf2.get());
        }
        // --> -56
        // --> -85
        // --> 97
        // --> 98
        // --> 99
        // --> 49
        // 切换读取数据模式, 并重新配置 position
        buf2.flip();
        // 解码
        CharBuffer buf3 = decoder.decode(buf2);
        System.out.println(buf3.toString()); // --> 全abc1

        System.out.println("-----");

        Charset cs2 = Charset.forName("GBK");
        // 切换读取数据模式, 并重新配置 position
        buf2.flip();
        CharBuffer buf4 = cs2.decode(buf2);
        System.out.println(buf4.toString());
    }
}

直接与非直接缓冲区

  • 直接缓冲区拷贝又称零拷贝: Java中, 常用的零拷贝有 MappedByteBuffer(内存映射, 简称 mmap)和 sendFile(transferTo& transferFrom). *提示: 从操作系统角度看, 零拷贝是没有 cpu拷贝

  • 网络传输过程(零拷贝): 起始到结尾各角色: [Hard drive] - [Kernel buffer] - [user buffer] - [socket buffer] - [protocol engine]
    (-) mmap内存映射方式是, 将文件映射到内核缓冲区, 由于, 用户空间可以共享内核空间的数据, 在进行网络传输时, 可以减少内核空间到用户空间的拷贝次数
    (-) Linux2.1版提供了 sendFile函数, 其原理是, 数据不经过用户态, 直接从内核缓冲区进入到 Socket buffer, 由于和用户态无关, 就减少了一次上下文切换
    () Linux在 2.4版中, 对 sendFile函数做了进一步优化, 避免了从内核缓冲区拷贝到 Socket buffer的操作, 而直接拷贝到 protocol engine协议栈, 从而再一次减少了数据拷贝(其实 Kernel buffer到 Socket buffer的拷贝依然存在的, 只不过不是文件主体, 而是文件的简单信息 如 length, offset等消耗低, 可以忽略不记)
    (
    ) sendFile是利用 DMA方式, 从内核拷贝到 Socket缓冲区, 而 mmap(内存映射)是利用 CPU 拷贝
    (*) 零拷贝的优点: 1. 更少的数据复制 2. 更少的上下文切换 3. 更少的 CPU缓存伪共享 4. 无 CPU校验和计算

  • 执行过程:
    (-) 非直接: 当程序从磁盘读取数据时, 会首先将数据复制到物理内存中, 再将数据复制到 JVM的内存中后便可取到数据
    (-) 直接: 在物理内存中建立一个缓冲区, 省略了复制到 JVM的步骤. 因此性能优于非直接缓冲区
    分配缓冲区:
    (-) 非直接: 通常是通过 allocate()方法来分配缓冲区, 将缓冲区建立在 JVM的内存中
    (-) 直接: 可以通过 allocateDirect()指定缓冲区, FileChannel.map()内存映射文件或 FileChannel.transferTo()和 FileChannel.transferFrom()通道之间的数据传输等, 将缓冲区建立在物理内存中
    通过 isDirect()方法来判断是否为直接缓冲区
    * 虽然直接缓冲区性能好, 不过使用时, 有一定安全隐患及不被 GC自动回收的问题. 因此建议只用于, 保存长时间持久的基础信息

在这里插入图片描述

在这里插入图片描述

  • 使用 transferTo零拷贝网络传输例子:

public class NewIOServer {
    // 服务器
    public static void main(String[] args) throws Exception {
        // 创建 ServerSocketChannel
        ServerSocketChannel serverSocketChannel = ServerSocketChannel.open();
        ServerSocket serverSocket = serverSocketChannel.socket();
        serverSocket.bind(new InetSocketAddress(7000));
        // 创建 buffer
        ByteBuffer byteBuffer = ByteBuffer.allocate(4096);
        while (true) {
            SocketChannel socketChannel = serverSocketChannel.accept();
            int readcount = 0;
            while (readcount != -1) {
                try {
                    readcount = socketChannel.read(byteBuffer);
                } catch (Exception e) {
                    break;
                }
                // 倒带 position = 0 mark 作废
                byteBuffer.rewind();
            }
        }
    }

}

public class NewIOClient {
    // 客户端(使用 transferTo零拷贝实现)
    public static void main(String[] args) throws Exception {
        // 创建 SocketChannel
        SocketChannel socketChannel = SocketChannel.open();
        // 连接服务器
        socketChannel.connect(new InetSocketAddress("localhost", 7000));
        // 打开文件通道 channel
        FileChannel fileChannel = new FileInputStream("e:\\SteamOnline_BZTG138_885150.exe").getChannel();
        // 起始时间
        long startTime = System.currentTimeMillis();
        // linux下, 一次 transferTo方法, 便可以完成传输
        // windows下, 调用 transferTo, 一次只能发送8m, 按分段传输文件(每段, 需指定 long position, long count)
        long transferCount = fileChannel.transferTo(0, fileChannel.size(), socketChannel);
        System.out.println("发送的总字节数=" + transferCount + ", 耗时=" + (System.currentTimeMillis() - startTime));
        fileChannel.close();
    }

}

通道& 缓冲区(Buffer)

  • NIO的 Buffer是用于与通道进行交互的, 从通道将数据读入到缓冲区, 从缓冲区写入通道的
  • 缓冲区本质上是一个可以读写数据的内存块, 可理解为容器对象(底层是一个数组)
  • 每个 Channel都会对应一个 Buffer
  • 数据的读取写入是通过 Buffer, 在 BIO中是单项处理输入或输出流, 不能双向, 但在 NIO的 Buffer是可以读也可以写, 需要 flip方法切换 Channel是双向的
  • 常用子类
    (-) ByteBuffer, CharBuffer, ShortBuffer, IntBuffer, LongBuffer, FloatBuffer, DoubleBuffer
  • 基本属性
    (-) capacity: Buffer的最大容量. capacity不能为负, 且创建后不能更改
    (-) limit: 第一个不应该读取或写入的数据索引. 即位于 limit后的数据不可读写. 且不能大于 capacity
    (-) position: 下一个要读取或写入的数据索引. position不能为负, 且不能大于 limit
    (-) mark& reset: mark是一个索引, 通过 Buffer的 mark()方法指定 Buffer中特定的 position, 之后可以通过调用 reset()方法, 将 position转到以前设置的 mark所在的位置
    (-) * mark, position, limit, capacity遵守以下不变式: 0 <= mark <= position <= limit <= capacity

在这里插入图片描述

  • Buffer的常用方法:

Buffer allocate(int capacity)分配非直接缓冲区
Buffer allocateDirect(int capacity)分配直接缓冲区
Buffer clear()清空缓冲区, 并返回对缓冲区的引用. 但清空后数据依然存在, 只不过数据状态是被遗忘的状态
Buffer flip()将缓冲区的界限设置为当前位置, 并将当前 position设置为0;切换为读模式
int capacity()返回 Buffer的 capacity大小
boolean hasRemaining()判断缓冲区中是否还有元素
int limit()返回 Buffer的 limit(界限)的位置
Buffer limit(int n)将设置缓冲区界限为 n, 并返回一个具有新 limit的缓冲区对象
int position()返回缓冲区的当前 position
Buffer position(int n)设置缓冲区的当前位置为 n, 并返回修改后的 Buffer对象
int remaining()返回 position和 limit之间的元素个数
Buffer mark()为指定 position设置标记
Buffer reset()将 position设置为 mark所在的位置;丢弃 mark位置之后的数据, 重新从 mark位置开始写入(注: mark必须为已设置)
Buffer rewind()将 position设为 0, 取消 mark设置; 可以重复读数据
public static ByteBuffer wrap(byte[] array) 把一个数组放到缓冲区中使用
public static ByteBuffer wrap(byte[] array, int offset, int length) 构造初始化位置 offset和上界 length的缓冲区

  • 缓冲区的数据操作:
  • 获取 Buffer中的数据:
    get(): 读取单个字节
    get(byte[] dst): 批量读取多个字节到 dst中
    get(int index): 读取指定索引位置的字节(不会移动 position)
  • 放入数据到 Buffer中:
    put(byte b): 将给定单个字节写入缓冲区的当前位置
    put(byte[] src): 将 src中的字节写入缓冲区的当前位置
    put(int index, byte b): 将指定字节写入缓冲区的索引位置(不会移动 position)
  • Buffer使用例子

public class TestBuffer {
    public static void main(String[] args) {
        String str = "abcde";
        // 1. 分配指定大小的非直接缓冲区
        ByteBuffer buf = ByteBuffer.allocate(1024);
        System.out.println(buf.capacity()); // --> 1024
        System.out.println("----- allocate() -----");
        System.out.println(buf.position()); // --> 0
        System.out.println(buf.limit()); // --> 1024
        // 2. 存入数据到缓冲区中
        buf.put(str.getBytes());
        System.out.println("----- put() -----");
        System.out.println(buf.position()); // --> 5
        System.out.println(buf.limit()); // --> 1024
        // 3. 切换读取数据模式, 并重新配置 position
        buf.flip();
        System.out.println("----- flip() -----");
        System.out.println(buf.position()); // --> 0
        System.out.println(buf.limit()); // --> 5
        // 4. 读取缓冲区中的数据
        byte[] dst = new byte[buf.limit()];
        // 4.1 将 buf中的数据输入到 dst, 并重新配置 position
        buf.get(dst);
        System.out.println(new String(dst, 0, dst.length)); // --> abcde
        System.out.println("----- get() -----");
        System.out.println(buf.position()); // --> 5
        System.out.println(buf.limit()); // --> 5
        // 5. 可重复读, 将 position设为 0, 并取消 mark设置
        buf.rewind();
        System.out.println("----- rewind() -----");
        System.out.println(buf.position()); // --> 0
        System.out.println(buf.limit()); // --> 5
        // 6. 清空缓冲区. 但是缓冲区中的数据依然存在,但是处于“被遗忘”状态
        buf.clear();
        System.out.println("----- clear() -----");
        System.out.println(buf.position()); // --> 0
        System.out.println(buf.limit()); // --> 1024
        // 6.1 数据状态为被遗忘, 但依然可以输出
        System.out.println((char) buf.get()); // --> a

        // 是否为直接缓冲区
        System.out.println("----- isDirect() -----");
        System.out.println(buf.isDirect()); // --> false

		// 创建一个 Buffer, 大小为5, 即可以存放5个int
        IntBuffer intBuffer = IntBuffer.allocate(5);
        intBuffer.put(10);
        intBuffer.put(11);
        intBuffer.put(12);
        intBuffer.put(13);
        intBuffer.put(14);
//        for (int i = 0; i < intBuffer.capacity(); i++) {
//            intBuffer.put(i * 2);
//        }
        /* 读写切换
        public final Buffer flip() {
            limit = position; //读数据不能超过5
            position = 0;
            mark = -1;
            return this;
        }*/
        intBuffer.flip(); // 读写切换
        intBuffer.position(1);// 索引1开始, 从 11的位置开始
        System.out.println(intBuffer.get());
        intBuffer.limit(3); // limit到 13的索引位
        while (intBuffer.hasRemaining()) {
            System.out.println(intBuffer.get()); // 输出11和12
        }
    }
}

public class TestBuffer {
    public static void main(String[] args) {
        String str = "abcde";
        // 分配指定大小的非直接缓冲区
        ByteBuffer buf = ByteBuffer.allocate(5);
        // 存入数据到缓冲区中
        buf.put(str.getBytes());
        // 切换读取数据模式, 并重新配置 position
        buf.flip();
        byte[] dst2 = new byte[buf.limit()];
        buf.get(dst2, 0, 2);
        System.out.println("----- flip() -----");
        System.out.println(new String(dst2, 0, 2)); // --> ab
        System.out.println(buf.position()); // --> 2
        // 标记
        buf.mark(); // 标记 position = 2
        buf.get(dst2, 2, 3);
        System.out.println(new String(dst2, 2, 2)); // --> cd
        System.out.println(buf.position()); // --> 5
        System.out.println("----- hasRemaining() -----");
        System.out.println(buf.hasRemaining()); // --> false
        // 恢复到 mark的位置
        buf.reset();
        System.out.println("----- reset() -----");
        System.out.println(buf.position()); // --> 2
        // 判断缓冲区中是否还有剩余数据
        if(buf.hasRemaining()){
            // 获取缓冲区中可以操作的数量
            System.out.println("----- remaining() -----");
            System.out.println(buf.remaining()); // --> 3
        }
    }
}

NIO非阻塞式网络通信

  • 阻塞与非阻塞
  • 阻塞式(IO): 通过传统 IO处理流时, 指定线程会被阻塞. 也就是说, 当使用传统的 IO构建网络通信时, 服务器端会为每个客户端, 开辟独立的线程来维持连接, 由此引起, 性能急剧下降
  • 非阻塞式(NIO): 在通道连接的状态下, 若没有数据可用时, 该线程可以进行其它任务. 如处理其它通道上的 IO, 也就是说, 一个线程可以管理多个输入输出通道. 因此, 区别于阻塞式, 通过非阻塞式实现的网络通信服务器, 可以将有限的线程(性能)更有效的利用来降低性能上的成本, 由此, 同步更多的客户端

阻塞式例子

  • 通过 Channel& Buffer, 传输文件

public class NIOBlockingSocketServer {
    public static void main(String[] args) throws IOException {
        // 1. 获取通道
        ServerSocketChannel ssChannel = ServerSocketChannel.open();
        // 2. 绑定连接
        ssChannel.bind(new InetSocketAddress(9898));
        // 3. 获取本地文件通道, 写入模式(文件不存在, 则创建)
        FileChannel outChannel = FileChannel.open(Paths.get("C:\\Users\\Shawn Jeon\\Pictures\\2.jpg"), StandardOpenOption.WRITE, StandardOpenOption.CREATE);
        // 4. 获取客户端连接的通道
        SocketChannel sChannel = ssChannel.accept();
        // 5. 分配指定大小的缓冲区
        ByteBuffer buf = ByteBuffer.allocate(1024);
        // 6. 从 sChannel(客户端通道)接收数据, 并保存到 buf
        while (sChannel.read(buf) != -1) {
            // 切换读取模式
            buf.flip();
            // 往 outChannel通道(本地文件), 写入数据
            outChannel.write(buf);
            // 清空缓冲区
            buf.clear();
        }

        // 反馈信息到客户端
        buf.put("服务端接收数据成功".getBytes());
        // 切换读取模式
        buf.flip();
        // 往 sChannel通道(客户端), 发送数据
        sChannel.write(buf);

        // 7. 关闭通道
        sChannel.close();
        outChannel.close();
        ssChannel.close();
    }
}

public class NIOBlockingSocketClient {
    public static void main(String[] args) throws IOException {
        // 1. 获取通道
        SocketChannel sChannel = SocketChannel.open(new InetSocketAddress("127.0.0.1", 9898));
        // 2. 获取本地文件通道, 读取模式
        FileChannel inChannel = FileChannel.open(Paths.get("C:\\Users\\Shawn Jeon\\Pictures\\1.jpg"), StandardOpenOption.READ);
        // 3. 分配指定大小的缓冲区
        ByteBuffer buf = ByteBuffer.allocate(1024);
        // 4. 从 inChannel读取数据, 并保存到 buf
        while (inChannel.read(buf) != -1) {
            // 切换读取模式
            buf.flip();
            // 往 sChannel通道(服务器端), 写入数据
            sChannel.write(buf);
            // 清空缓冲区
            buf.clear();
        }

        // 数据发送完毕, 不再发送更多数据
        sChannel.shutdownOutput();

        // 接收服务器端的反馈
        int len = 0;
        // 从 sChannel(服务器端通道)接收数据, 并保存到 buf
        while ((len = sChannel.read(buf)) != -1) {
            // 切换读取模式
            buf.flip();
            // 打印反馈内容
            System.out.println(new String(buf.array(), 0, len));
            // 清空缓冲区
            buf.clear();
        }

        // 5. 关闭通道
        inChannel.close();
        sChannel.close();
    }
}

非阻塞式

  • Selector(选择器)
  1. Selector对应一个线程, 一个线程对应多个 Channel(连接), 每个 Channel都会对应一个 Buffer
  2. 程序切换到哪个 Channel是由事件决定的
  3. Selector会根据不同的事件, 在各个通道上切换
  • Selector是 Channel的多路复用器, NIO将所有的 Channel都注册到 Selector上, 来监控各个 Channel的 IO状态, 使一个线程管理多个 Channel
  • 创建& 注册通道
  1. 创建选择器 Selector ServerSocketChannel::open()
  2. 向选择器注册通道 SelectionKey(表示 Selector和网络通道的注册关系), SelectableChannel.register(Selector sel, int ops); 注册通道时, 可以通过参数 ops指定事件
    (-) 读 SelectionKey.OP_READ = 1 << 0 = 1
    (-) 写 SelectionKey.OP_WRITE = 1 << 2 = 4
    (-) 连接 SelectionKey.OP_CONNECT = 1 << 3 = 8
    (-) 接收 SelectionKey.OP_ACCEPT = 1 << 4 = 16
    * 若监听多个事件, 则可以使用"位或"操作符 例 int interestSet = SelectionKey.OP_READ| SelectionKey.OP_WRITE;
  • SelectionKey常用方法描述:
    int interestOps() 获取选择器中, 指定选择器键的事件集
    int readyOps() 获取已就绪的操作集
    SelectableChannel channel() 获取已注册的通道
    Selector selector() 返回创建此键的选择器
    boolean isReadable() 判断指定选择器键的 Channal的读事件是否就绪
    boolean isWritable() 判断指定选择器键的 Channal的写事件是否就绪
    boolean isConnectable() 判断指定选择器键的 Channal的连接事件是否就绪
    boolean isAcceptable() 判断指定选择器键的 Channal的接收事件是否就绪
  • Selector方法描述:
    Selector open() 打开一个选择器
    boolean isOpen() 判断当前选择器是否已开启
    Set keys() 返回当前选择器关联的已注册的 SelectionKey集合
    Set selectedKeys() 返回当前选择器中已被选定(就绪)的 SelectionKey集合, 每个键都关联一个已就绪的(至少含一种操作的通道)
    int select() 返回当前选择器中的就绪通道数量, 如选择器中没有"就绪通道", 则阻塞线程, 直到有就绪的通道
    int select(long timeout) 可以设置阻塞的超时时间(毫秒)
    int selectNow() 返回当前选择器中的就绪通道数量, 如选择器中没有"就绪通道", 将立即返回0, 而不阻塞线程
    Selector wakeup() 使阻塞中的 select()方法立即返回
    void close() 关闭当前选择器
  • TCP非阻塞式例子

例子 1:
public class NIONonBlockingSocketServer {
    public static void main(String[] args) throws IOException {
        // 1. 获取通道
        ServerSocketChannel ssChannel = ServerSocketChannel.open();
        // 2. 切换非阻塞模式
        ssChannel.configureBlocking(false);
        // 3. 绑定连接
        ssChannel.bind(new InetSocketAddress(9898));
        // 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 sk = it.next();
                // 9. 判断具体是什么事件准备就绪
                if (sk.isAcceptable()) {
                    // 10. 若“接收就绪”, 获取客户端连接
                    SocketChannel sChannel = ssChannel.accept();
                    // 11. 切换非阻塞模式
                    sChannel.configureBlocking(false);
                    // 12. 将该通道注册到选择器上
                    sChannel.register(selector, SelectionKey.OP_READ);
                } else if (sk.isReadable()) { // 判断选定的通道是否准备“就绪”, 用于读取“接收到的数据“
                    // 13. 获取当前选择器上“读就绪”状态的通道
                    SocketChannel sChannel = (SocketChannel) sk.channel();
                    // 14. 分配指定大小的缓冲区
                    ByteBuffer buf = ByteBuffer.allocate(1024);
                    // 15. 从 sChannel(客户端通道)接收数据, 并保存到 buf
                    int len = 0;
                    while ((len = sChannel.read(buf)) > 0 ) {
                        // 切换读取模式
                        buf.flip();
                        // 打印内容
                        System.out.println(new String(buf.array(), 0, len));
                        // 清空缓冲区
                        buf.clear();
                    }
                }
                // 16. 取消选择键 SelectionKey
                it.remove();
            }
        }
    }
}

public class NIONonBlockingSocketClient {
    public static void main(String[] args) throws IOException {
        // 1. 获取通道
        SocketChannel sChannel = SocketChannel.open(new InetSocketAddress("127.0.0.1", 9898));
        // 2. 切换非阻塞模式
        sChannel.configureBlocking(false);
        // 3. 分配指定大小的缓冲区
        ByteBuffer buf = ByteBuffer.allocate(1024);
        // 4. 写入数据到服务端
        Scanner scan = new Scanner(System.in);
        while (scan.hasNext()) {
            String str = scan.next();
            buf.put((str + " " + new Date().toString()).getBytes());
            // 切换读取模式
            buf.flip();
            // 往 sChannel通道(服务器端), 写入数据
            sChannel.write(buf);
            // 清空缓冲区
            buf.clear();
        }
        // 5. 关闭通道
        sChannel.close();
    }
}

例子 2:
public class NIOServer {
    // NIO非阻塞 Server
    public static void main(String[] args) throws Exception {
        // 创建 ServerSocketChannel
        ServerSocketChannel serverSocketChannel = ServerSocketChannel.open();
        // 打开一个选择器
        Selector selector = Selector.open();
        // 监听端口6666
        serverSocketChannel.socket().bind(new InetSocketAddress(6666));
        // 设置为非阻塞
        serverSocketChannel.configureBlocking(false);
        // 将 serverSocketChannel注册到 selector, 关注 OP_ACCEPT事件
        serverSocketChannel.register(selector, SelectionKey.OP_ACCEPT);
        System.out.println("当前注册的 selectionkey总数量=" + selector.keys().size()); // 1
        // 循环等待客户端连接
        while (true) {
            // 阻塞等待客户端连接(设置了过期时间1秒, 也就是等满1秒仍然未发生事件, 则 continue)
            if (selector.select(1000) == 0) {
                System.out.println("服务器等待了1秒, 无连接");
                continue;
            }
            // 1. selector发现有事件, 将获取当前所有有事件的 keys集合
            // 2. 通过 selectionKeys反向获取通道
            Set<SelectionKey> selectionKeys = selector.selectedKeys();
            System.out.println("selectionKeys总数量 = " + selectionKeys.size());
            Iterator<SelectionKey> keyIterator = selectionKeys.iterator();
            while (keyIterator.hasNext()) {
                SelectionKey key = keyIterator.next();
                // 根据 key对应的通道发生的事件, 做相应处理
                // 如果有新的客户端连接(OP_ACCEPT)
                if (key.isAcceptable()) {
                    // 获取该客户端 SocketChannel
                    SocketChannel socketChannel = serverSocketChannel.accept();
                    System.out.println("客户端连接成功=" + socketChannel.hashCode());
                    // 将该 SocketChannel设置为非阻塞
                    socketChannel.configureBlocking(false);
                    // 将该 socketChannel注册到 selector, 同时关注 OP_READ事件, 并关联一个 Buffer
                    socketChannel.register(selector, SelectionKey.OP_READ, ByteBuffer.allocate(1024));
                    System.out.println("该客户端连接后, 注册的 selectionkey总数量=" + selector.keys().size()); // 2,3,4..
                }
                // 如果有发生 OP_READ事件
                if (key.isReadable()) {
                    // 通过 key, 反向获取到对应 channel
                    SocketChannel channel = (SocketChannel) key.channel();
                    // 获取到该 channel关联的 buffer
                    ByteBuffer buffer = (ByteBuffer) key.attachment();
                    // 将客户端发送的数据读入到该 buffer
                    channel.read(buffer);
                    System.out.println("从客户端=" + channel.hashCode()  + ", 收到信息=" + new String(buffer.array()));
                }
                // 移除此次处理完的 selectionKey(此次发生事件的key), 防止重复操作
                keyIterator.remove();
            }
        }
    }

}

public class NIOClient {
    // NIO非阻塞 Client
    public static void main(String[] args) throws Exception {
        // 开启一个网络通道
        SocketChannel socketChannel = SocketChannel.open();
        // 设置为非阻塞
        socketChannel.configureBlocking(false);
        // 连接服务器
        if (!socketChannel.connect(new InetSocketAddress("127.0.0.1", 6666))) {
            // 连接完成, 则返回 true
            while (!socketChannel.finishConnect()) {}
        }
        // 如果连接成功, 就会发送数据
        String str = "hello, world!!";
        // 将发送的数据转为字节数组, 再放入 Buffer
        ByteBuffer buffer = ByteBuffer.wrap(str.getBytes());
        // 发送数据, 将 Buffer里的数据写入 channel
        socketChannel.write(buffer);
        System.in.read();
    }

}

例子 3(群聊):
public class GroupChatServer {
    private ServerSocketChannel listenChannel;
    private Selector selector;

    // 初始化
    public GroupChatServer() {
        try {
            // 创建 ServerSocketChannel
            listenChannel = ServerSocketChannel.open();
            // 打开一个选择器
            selector = Selector.open();
            // 监听端口6666
            listenChannel.socket().bind(new InetSocketAddress(6666));
            // 设置非阻塞模式
            listenChannel.configureBlocking(false);
            // 将 serverSocketChannel注册到 selector, 关注 OP_ACCEPT事件
            listenChannel.register(selector, SelectionKey.OP_ACCEPT);
        } catch (IOException e) {
            e.printStackTrace();
        }
    }

    // 监听
    public void listen() {
        System.out.println("监听线程: " + Thread.currentThread().getName());
        try {
            while (true) {
                int count = selector.select(); // 阻塞等待, 直到有事件 keys, 便返回事件数量
                if (count > 0) {
                    Iterator<SelectionKey> iterator = selector.selectedKeys().iterator();
                    while (iterator.hasNext()) {
                        SelectionKey key = iterator.next();
                        if (key.isAcceptable()) {
                            SocketChannel sc = listenChannel.accept();
                            sc.configureBlocking(false);
                            // 将该 channel注册到 seletor
                            sc.register(selector, SelectionKey.OP_READ);
                            System.out.println("客户端: " + sc.getRemoteAddress() + " 上线!");
                        }
                        if (key.isReadable()) {
                            // 处理读
                            readData(key);
                        }
                        // 删除当前 key, 防止重复处理
                        iterator.remove();
                    }
                } else {
                    System.out.println("等待...");
                }
            }
        } catch (Exception e) {
            e.printStackTrace();
        } finally {
            // 发生异常处理...
        }
    }

    // 读取客户端发的消息
    private void readData(SelectionKey key) {
        SocketChannel channel = null;
        try {
            // 得到 channel
            channel = (SocketChannel) key.channel();
            ByteBuffer buffer = ByteBuffer.allocate(1024);
            // 将客户端发送的数据读入到该 buffer
            int count = channel.read(buffer);
            if (count > 0) {
                // 把缓存区的数据转成字符串
                String msg = new String(buffer.array());
                // 输出该消息
                System.out.println("来自客户端=" + msg);
                //向其它的客户端转发消息(去掉自己), 专门写一个方法来处理
                sendInfoToOtherClients(msg, channel);
            }
        } catch (IOException e) {
            try {
                System.out.println(channel.getRemoteAddress() + " 离线了..");
                //取消注册
                key.cancel();
                //关闭通道
                channel.close();
            } catch (IOException e2) {
                e2.printStackTrace();
                ;
            }
        }
    }

    // 转发消息给其它客户(通道)
    private void sendInfoToOtherClients(String msg, SocketChannel self) throws IOException {
        // 遍历所有注册到 selector上的 SocketChannel, 并排除 self(发消息的用户)
        for (SelectionKey key : selector.keys()) {
            // 获取对应的 SocketChannel
            Channel targetChannel = key.channel();
            // 并排除 self
            if (targetChannel instanceof SocketChannel && targetChannel != self) {
                SocketChannel dest = (SocketChannel) targetChannel;
                // 将 msg存储到 buffer
                ByteBuffer buffer = ByteBuffer.wrap(msg.getBytes());
                // 将 buffer的数据写入通道
                dest.write(buffer);
                System.out.println("往客户端=" + dest.getRemoteAddress() + ", 转发消息成功!");
            }
        }
    }

    public static void main(String[] args) {
        // 创建服务器对象
        GroupChatServer groupChatServer = new GroupChatServer();
        // 服务器开始监听事件
        groupChatServer.listen();
    }

}

public class GroupChatClient {
    private Selector selector;
    private SocketChannel socketChannel;
    private String username;

    // 初始化
    public GroupChatClient() throws IOException {
        // 打开一个选择器
        selector = Selector.open();
        // 连接服务器
        socketChannel = socketChannel.open(new InetSocketAddress("127.0.0.1", 6666));
        // 设置非阻塞
        socketChannel.configureBlocking(false);
        // 将 SocketChannel注册到 selector, 关注 OP_READ事件
        socketChannel.register(selector, SelectionKey.OP_READ);
        // 得到 username
        username = socketChannel.getLocalAddress().toString().substring(1);
        System.out.println(username + " is ok...");
    }

    // 向服务器发送消息
    public void sendInfo(String info) {
        info = username + " 说: " + info;
        try {
            socketChannel.write(ByteBuffer.wrap(info.getBytes()));
        } catch (IOException e) {
            e.printStackTrace();
        }
    }

    // 读取从服务器端回复的消息
    public void readInfo() {
        try {
            // 阻塞等待直到, 存在有事件的通道
            int readChannels = selector.select();
            if (readChannels > 0) {
                Iterator<SelectionKey> iterator = selector.selectedKeys().iterator();
                while (iterator.hasNext()) {
                    SelectionKey key = iterator.next();
                    if (key.isReadable()) {
                        // 得到相关的通道
                        SocketChannel channel = (SocketChannel) key.channel();
                        ByteBuffer buffer = ByteBuffer.allocate(1024);
                        // 读取其它客户端或服务器发送的数据到 buffer
                        int count = channel.read(buffer);
                        if (count > 0) {
                            // 把缓存区的数据转成字符串
                            String msg = new String(buffer.array());
                            System.out.println("来自=" + msg.trim());
                        }
                    }
                }
                iterator.remove();
            }
        } catch (Exception e) {
            e.printStackTrace();
        }
    }

    public static void main(String[] args) throws Exception {
        // 启动客户端
        GroupChatClient chatClient = new GroupChatClient();
        // 启动一个线程, 每个8秒, 读取从服务器发送数据
        new Thread() {
            public void run() {
                while (true) {
                    chatClient.readInfo();
                    try {
                        Thread.currentThread().sleep(8000);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                }
            }
        }.start();
        // 用户发送数据给服务器端
        Scanner scanner = new Scanner(System.in);
        while (scanner.hasNextLine()) {
            String s = scanner.nextLine();
            chatClient.sendInfo(s);
        }
    }

}

  • UDP非阻塞式例子

public class NonBlockingNIODatagramReceive {
    public static void main(String[] args) throws IOException {
        // 1. 获取通道
        DatagramChannel dc = DatagramChannel.open();
        // 2. 切换非阻塞模式
        dc.configureBlocking(false);
        // 3. 给当前通道绑定端口
        dc.bind(new InetSocketAddress(9898));
        // 4. 获取选择器
        Selector selector = Selector.open();
        // 5. 将该通道注册到选择器上
        dc.register(selector, SelectionKey.OP_READ);
        // 6. 轮询式的获取选择器上已经“准备就绪”的事件
        while (selector.select() > 0) {
            // 7. 获取当前选择器中所有注册的“选择键(已就绪的监听事件)”
            Iterator<SelectionKey> it = selector.selectedKeys().iterator();
            while (it.hasNext()) {
                // 8. 获取准备“就绪”的是事件
                SelectionKey sk = it.next();
                // 判断选定的通道是否准备“就绪”, 用于读取“接收到的数据“
                if (sk.isReadable()) {
                    ByteBuffer buf = ByteBuffer.allocate(1024);
                    // 从 DatagramChannel接收数据, 并保存到 buf
                    dc.receive(buf);
                    // 切换读取模式
                    buf.flip();
                    // 打印内容
                    System.out.println(new String(buf.array(), 0, buf.limit()));
                    // 清空缓冲区
                    buf.clear();
                }
            }
            // 9. 取消选择键 SelectionKey
            it.remove();
        }
    }
}

public class NonBlockingNIODatagramSend {
    public static void main(String[] args) throws IOException {
        // 1. 获取通道
        DatagramChannel dc = DatagramChannel.open();
        // 2. 切换非阻塞模式
        dc.configureBlocking(false);
        // 3. 分配指定大小的缓冲区
        ByteBuffer buf = ByteBuffer.allocate(1024);
        // 4. 写入数据到服务端
        Scanner scan = new Scanner(System.in);
        while (scan.hasNext()) {
            String str = scan.next();
            buf.put((str + " " + new Date().toString()).getBytes());
            // 切换读取模式
            buf.flip();
            // 往指定 UDP接收端的地址, 发送数据
            dc.send(buf, new InetSocketAddress("127.0.0.1", 9898));
            // 清空缓冲区
            buf.clear();
        }
        // 5. 关闭通道
        dc.close();
    }
}

管道(Pipe)

  • Java NIO在 JVM内不同线程之间通过管道单向传输数据. 通过 pipe.source()通道读取数据, 再通过 pipe.sink()通道写入并传输数据

在这里插入图片描述


public class PipeTest {
    public static void main(String[] args) throws IOException {
        // 获取管道
        Pipe pipe = Pipe.open();
        // 分配指定大小的缓冲区
        ByteBuffer buf = ByteBuffer.allocate(1024);

        // 获取 sink管道,用来传送数据
        Pipe.SinkChannel sinkChannel = pipe.sink();
        buf.put("通过管道单向发送数据".getBytes());
        buf.flip();
        // 传送数据
        sinkChannel.write(buf);

        // 获取 source管道, 用来接收管道数据
        Pipe.SourceChannel sourceChannel = pipe.source();
        buf.flip();
        // 读取数据
        int len = sourceChannel.read(buf);
        // 打印内容
        System.out.println(new String(buf.array(), 0, len));

        // 关闭通道
        sourceChannel.close();
        sinkChannel.close();
    }
}

如果您觉得有帮助,欢迎点赞哦 ~ 谢谢!!

  • 2
    点赞
  • 11
    收藏
    觉得还不错? 一键收藏
  • 12
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值