NIO的使用

NIO

所在包:java.nio

一、概述

BIO与NIO

BIO:即Blocking IO 阻塞IO,顾名思义就是会被阻塞的IO。在数据读取(或者写数据)的时候,只有当读取到(或写完)数据之后才能做其他的事情,而在这期间一直处于阻塞状态(等待操作的完成)。

NIO:Non-Blocking IO 非阻塞IO,顾名思义就是不会被阻塞的IO。在读取(或写)数据的时候,只将请求发送给底层操作而自己就可以继续做别的事情,当读取(或写)完成自己后,会有Selector通知该线程已经完成,则该线程在回来操作数据。

比较:

BIONIO
面向流面向缓冲区
阻塞io非阻塞io
有选择器

NIO的核心组件

  • Channel:与Stream同等级,Stream(流)是单向的通道,而Channel(通道)是双向的,可读也可写;
  • Buffer:缓冲区;
  • Selector:选择器,用于监视Channel。

二、Channel

Channel(通道)可以进行对数据的读或写。数据可以是文件,也可以是网络的socket。

与Stream不同的是,stream是单向的;而Channel是双向的,可同时读写(全双工通信)。

Channel必须与缓冲区配合使用。

Channel接口定义

public interface Channel extends Closeable {
	// 是否开启
    public boolean isOpen();
	// 关闭
    public void close() throws IOException;

}

主要实现类如下:

在这里插入图片描述

  • FileChannel:从文件中读写数据;
  • DatagramChannel:通过UDP读写网络中的数据;
  • SocketChannel:通过TCP读写网络中的数据;
  • ServerSocketChannel:监听新进来的TCP连接,就像服务器一样——对于每一个新的连接都会创建一个ScoketChannel。

FileChannel

主要方法:

方法描述
int read(ByteBuffer dst)`从该通道读取到给定缓冲区的字节序列。
long read(ByteBuffer[] dsts)从该通道读取到给定缓冲区的字节序列。
int write(ByteBuffer src)从给定的缓冲区向该通道写入一个字节序列。
long write(ByteBuffer[] srcs)从给定的缓冲区向该通道写入一系列字节。

实例

前提:

  1. 在使用通道的时候需要先打开通道;方式如下:
    • 通过创建输入流/输出流获取;
    • 通过RandomAccessFile获取(下面的示例使用的就是这种方法);
  2. 通道必须配合缓冲区使用(缓冲区的使用见缓冲区的描述章节)。

读取数据:

步骤:

  1. 打开channel;
  2. 创建缓冲区;
  3. 读取数据到缓冲区;
  4. 数据处理;
  5. 关闭通道;
/**
 * 从文件中读取数据到buffer
 */
public class FielChannelDemo01 {
    public static void main(String[] args) throws Exception {
        // 1. 打开FileChannel
        RandomAccessFile raf = new RandomAccessFile("C:\\file.txt", "rw");
        FileChannel channel = raf.getChannel();
        // 2. 创建buffer
        ByteBuffer buffer = ByteBuffer.allocate(32);
        // 3.读取数据
        int read = channel.read(buffer);
        // 这里的操作和流一样,-1表示没有读取到数据
        while(read != -1){
            // 4.输出缓存中的数据
            buffer.flip();
            while(buffer.hasRemaining()){
                System.out.println((char)buffer.get());
            }
            buffer.clear();
            read = channel.read(buffer);
        }
        // 关闭通道
        channel.close();
    }
}

写数据:

/**
 * 将数据写入文件
 */
public class FileChannelDemo02 {
    public static void main(String[] args) throws Exception {
        // 1.开启文件通道
        RandomAccessFile rw = new RandomAccessFile("C:\\file02.txt", "rw");
        FileChannel channel = rw.getChannel();
        // 2.创建缓冲区
        ByteBuffer buffer = ByteBuffer.allocate(1024);
        String data = "hello world";
        buffer.clear();
        // 3.缓冲区中添加数据
        buffer.put(data.getBytes());
        buffer.flip();
        // 4.通过通道写数据
        while(buffer.hasRemaining()){
            channel.write(buffer);
        }
        // 5.关闭通道
        channel.close();
    }
}

看到在write的过程中需要在循环中使用,因为不能保证一就能写完。

其他方法介绍

  • channel.size():

    方法法会返回channel关联的文件大小(字节)

  • channel.truncate( int len ):

    该方法会截取文件的前len的长度,其余的将会被删除(会修改文件)

  • channel.position():

    无参表示返回当前光标在文件中的位置

    有参表示设置光标的位置

  • channel.force():

    将通道里的数据强制写到硬盘

  • transferFrom(ReadableByteChannel src, long position, long count) :

    将src中的数据传输到该通道中

  • transferTo(long position, long count, WritableByteChannel target) :

    将该通道中的数据写入到target中

示例:

/**
 * 通道间传输
 */
public class FielChannelDemo03 {
    public static void main(String[] args) throws Exception {
        // 打开通道1
        RandomAccessFile file1 = new RandomAccessFile("C:\\file.txt", "rw");
        FileChannel fromchannel = file1.getChannel();
        // 打开通道二
        RandomAccessFile file2 = new RandomAccessFile("C:\\file02.txt", "rw");
        FileChannel tochannel = file2.getChannel();
        // 起始位置
        int position = 0;
        // 传输的长度
        long len = fromchannel.size();
        // 将from通道的数据传输到to中
        tochannel.transferFrom(fromchannel, position, len);
        // 关闭文件
        file1.close();
        file2.close();

    }
}

通过测试发现:通道间的传输是直接将目标文件的数据覆盖。

ServerSocketChannel

SocketChannel有三个实现类

  • DatagramChannel
  • SocketChannel
  • ServerScoketChannel

三者之间有以下的异同:

  1. 三者都继承自AbstractSelectableChannel类,即都可以使用Selector对象来执行Socket的就绪选择;
  2. ServerScoketChannel没有实现具有读、写功能的接口,即该类不具备数据传输的功能;
  3. 这些通道都可以被重复使用,且这些类的实例化都会创建对等的socket;
  4. socket通道可以设置阻塞和非阻塞模式:通过调用configureBlocking()方法实现。

示例

服务端:

步骤:

  1. 打开ServerSocketChannel;
  2. 绑定端口(该端口接收链接);
  3. 创建缓冲区(发送数据);
  4. 等待连接(有阻塞和非阻塞之分);
  5. 写数据;
  6. 关闭流。
/**
 * 服务端
 */
public class ServerSocketChannelDemo01 {
    public static void main(String[] args) throws Exception {
        // 1.打开ServerSocketChannel
        ServerSocketChannel channel = ServerSocketChannel.open();
        // 2.绑定端口
        channel.socket().bind(new InetSocketAddress(8080));
        // 3.创建缓冲区
        ByteBuffer buffer = ByteBuffer.allocate(1024);
        String str = "hello";
        buffer.put(str.getBytes());
        
        // 设置为非阻塞式的
        // channel.configureBlocking(false);
        // 4.等待连接
        while(true){
            System.out.println("waiting for connection...");
            // 监听,这里是阻塞式的,所以在没有连接的时候会一直等待
            SocketChannel sc = channel.accept();
            // 当有连接进来后
            if(sc != null){
                // 5.输出client的ip
                System.out.println(sc.getRemoteAddress() + "is connecting");
                // buffer的光标指向0
                buffer.rewind();
                // 6.向客户端发送数据
                sc.write(buffer);
                // 7.关闭当前连接
                sc.close();
            }
        }
    }
}
/**
 *输出:
 * waiting for connection...
 * /127.0.0.1:52493is connecting
 * waiting for connection...
 * /127.0.0.1:52500is connecting
 * waiting for connection...
 * /127.0.0.1:52511is connecting
 * waiting for connection...
 */

客户端:

/**
 * 客户端
 */
public class ClientDemo {
    public static void main(String[] args) throws Exception {
        // 建立连接
        Socket socket = new Socket("127.0.0.1", 8080);
        // 获取输入流
        InputStream is = socket.getInputStream();
        byte[] bytes = new byte[1024];
        is.read(bytes,0,1024);
        // 输出信息
        System.out.println(new String(bytes));
    }
}
/*
* 输出:
* hello
*/

SocketChannel

  • 用来连接Socket套接字;
  • 处理网络I/O请求;
  • 基于TCP连接传输;
  • 可以选择通道,可以被多路复用。

示例

步骤:

  1. 打开SocketChannel(并绑定连接的地址);
  2. 创建缓冲区;
  3. 读取数据;
  4. 数据处理;
  5. 关闭通道。
/**
 * SocketChannel示例
 */
public class SocketChannelDemo01 {
    public static void main(String[] args) throws Exception {
        // 1.打开SocketChannel
        // 方式一:这里连接的是ServerSocketChannel(见一个示例)
        SocketChannel channel = SocketChannel.open(new InetSocketAddress("127.0.0.1", 8080));
        // 方式二:
        // SocketChannel channel1 = SocketChannel.open();
        // channel1.connect(new InetSocketAddress("127.0.0.1",8080));
        
        // 2.创建缓冲区
        ByteBuffer buffer = ByteBuffer.allocate(1024);
        // 设置为非阻塞
        // channel.configureBlocking(false);
        // 3.读取数据
        channel.read(buffer);
        // 4.关闭通道
        channel.close();
        // 5.输出读取到的数据
        byte[] bytes = buffer.array();
        System.out.println(new String(bytes));
    }
}

个别方法介绍:

// 是否已连接
boolean connected = channel.isConnected();
// 是否正在连接
boolean connectionPending = channel.isConnectionPending();
// 是否正在阻塞
boolean blocking = channel.isBlocking();
// 设置连接的参数
channel.setOption(StandardSocketOptions.SO_KEEPALIVE, Boolean.TRUE);

DatagramChannel

对应的是UDP的协议,可以发送数据到其他地址,也可以接收任意地址的数据。

示例

接收端:

/**
 * 接收消息
 */
public class DatagramDemo01 {
    public static void main(String[] args) throws Exception {
        // 1.打开DatagramChannel通道
        DatagramChannel receiveChannel = DatagramChannel.open();
        // 2.绑定端口
        receiveChannel.bind(new InetSocketAddress(8080));
        // 3.创建缓冲区
        ByteBuffer receiveBuffer = ByteBuffer.allocate(1024);
        // 循环等待接收
        while(true){
            System.out.println("waiting...");
            receiveBuffer.clear();
            // 4.接收消息,默认是阻塞的
            receiveChannel.receive(receiveBuffer);
            // 5.打印消息
            receiveBuffer.flip();
            System.out.println(new String(receiveBuffer.array()));
        }
    }
}

发送端:

/**
 * 发送端
 */
public class DatagramDemo02 {
    public static void main(String[] args) throws Exception {
        // 1.打开DatagramChannel通道
        DatagramChannel sendChannel = DatagramChannel.open();
        // 2.绑定发送的目标地址
        InetSocketAddress sendAddr = new InetSocketAddress("127.0.0.1", 8080);
        // 3.创建缓冲区
        ByteBuffer sendBuffer = ByteBuffer.wrap("hello".getBytes(StandardCharsets.UTF_8));
        // 4.循环发送
        while(true){
            // 4.1发送数据
            sendChannel.send(sendBuffer, sendAddr);
            // 4.2发送完成,等待1s
            System.out.println("send ok");
            TimeUnit.SECONDS.sleep(1);
        }
    }
}

Scatter AND gather

Scatter:分散,即创建一个buffer数组,里面包含有多个buffer,read()的时候,将读取的数据按顺序读到buffer数组中buffer中;当第一个buffer放满之后,放入第二个buffer中,以此类推。

Gather:聚集,即调用write()方法的时候,将buffer数组中的buffer数据按顺序写到通道中取。注意如果buffer中的实际数据小于buffer大小,则只写实际的大小。

示例

// 通道1
RandomAccessFile raf = new RandomAccessFile("C\\file02.txt", "rw");
FileChannel channel1 = raf.getChannel();
// 通道2
RandomAccessFile raf2 = new RandomAccessFile("C:\\file.txt", "rw");
FileChannel channel2 = raf2.getChannel();
// 缓冲区数组创建
ByteBuffer buffer1 = ByteBuffer.allocate(1024);
ByteBuffer buffer2 = ByteBuffer.allocate(2048);
ByteBuffer[] bufferArray = {buffer1, buffer2};
// channel1读取数据到缓冲区(Scatter)
channel1.read(bufferArray);
// channel2写数据(Gather)
buffer1.flip();
buffer2.flip();
channel2.write(bufferArray);
// 关闭流
raf.close();;
raf2.close();

三、Buffer

在NIO中buffer用于与通道之间的交互。从通道中读取数据存到缓冲区,再从缓冲区中将数据写到通道。

Buffer本质上就是是一个数组,也就是一块内存空间,通过将其封装成一个对象并提供了一系列方法便于操作。

Buffer接口及其实现类图

在这里插入图片描述

Buffer使用步骤

  1. 写入数据到Buffer;
  2. 调用flip()方法;
  3. 从Buffer中读取数据;
  4. diaoyongclear()或者compact()方法。

解释:首先通过通道将读取数据到的数据写到buffer中(此时buffer的模式为写模式),然后调用flip()方法进行模式切换(切换为读模式);然后将buffer中的数据从buffer中读取出来;最后调用clear()或compact()将缓冲区清空。(clear会清空全部数据,compact只会清空被读取的数据)。

主要的属性

  • capacity:

    表示缓冲区的大小,当缓冲区满了之后只有清空缓冲区才能继续写数据。

  • position:

    写模式:表示当前写入数据的位置,当写完之后position移动到下一个位置;position的起始位置为0,最大位置为capacity-1。

    读模式:表示当前以读position-1个单位的数据;当读取完position位置的数据之后,position的位置会向后移动一个位置;调用flip()方法会将position的位置置为0。

  • limit:

    写模式:表示最大可写的数据数量,即capacity。

    读模式:表示缓冲区中有多少数据可以读(非空数据),也就是前面写如的数据的数量。

主要方法

方法描述
int capacity()返回此缓冲区的容量
Buffer clear()清除此缓冲区
Buffer flip()翻转这个缓冲区
boolean hasRemaining()告诉当前位置和极限之间是否存在任何元素
Buffer reset()将此缓冲区的位置重置为先前标记的位置
Buffer rewind()倒带这个缓冲区(将position置为0)

一些解释:

  1. 为buffer分配空间使用的是allocate()方法;
  2. 往buffer中写入数据有两种方式:(1)通过通道的read(buf)方法;(2)通过buffer的put()方法;
  3. flip()方法:该方法会将buffer从写模式转换为读模式。转换的时候将position设置为0,并将limit设置为原来的position。这样就将最大的刻度数据设置为limit的数量。
  4. 从buffer中读数据有两中方式:(1)通过通道的write(buf)方法;(2)通过buffer的get()方法;

读写示例

1.读取缓冲区中的数据

RandomAccessFile raf = new RandomAccessFile("D:\\TestFile\\file1.txt", "rw");
FileChannel channel = raf.getChannel();
// 1.申请缓冲区空间,1024字节
ByteBuffer buffer = ByteBuffer.allocate(1024);
// 2.将通道中的数据写到缓冲区中
int read = channel.read(buffer);
while (read != -1){
    // 3. 模式转换
    buffer.flip();
    // 4.当缓冲区中还有数据则继续读
    while (buffer.hasRemaining()){
        // 5.获取缓冲区中的数据并输出
        System.out.println((char)buffer.get());
    }
    buffer.clear();
    read = channel.read(buffer);
}
channel.close();

2.往缓存区中写数据

// 1.创建一个IntBuffer,大小为1024个int
IntBuffer buffer = IntBuffer.allocate(1024);
// 2.写入数据
for(int i=0; i<100; i++){
    int num = i*2;
    buffer.put(num);
}
// 3.模式翻转
buffer.flip();
// 4.输出数据
while(buffer.hasRemaining()){
    System.out.println(buffer.get());
}

缓冲区分片

缓冲区分片就是创建一个缓冲区之后,设置该缓冲区的position和limit,然后调用分片方法从缓冲区中划出一块区域作为其子缓冲区,二者共享内存数据

// 创建一个intbuffer
IntBuffer buffer = IntBuffer.allocate(10);
// 填充数据
for(int i=0; i<buffer.capacity(); i++){
    buffer.put(i+1);
}
// 设置position和limit
buffer.position(5);
buffer.limit(8);
// 根据position和limit创建分片
IntBuffer slice = buffer.slice();
// 更改分片中的数据
for(int j=0; j<slice.capacity(); j++){
    int num  = slice.get(j) * 10;
    slice.put(j,num);
}
// 重置position和limit
buffer.position(0);
buffer.limit(buffer.capacity());
// 打印buffer中的数据
while(buffer.hasRemaining()){
    System.out.println(buffer.get());
}
/**
 * 1
 * 2
 * 3
 * 4
 * 5
 * 60
 * 70
 * 80
 * 9
 * 10
 */

通过打印出来的数据可以看到,创建分片和重新赋值之后原buffer中的数据也发生了变化,所以二者是共享数据的。

只读缓冲区

只读缓存区表示该缓冲区只能进行读操作,不能进行写操作。通过asReadOnlyBuffer()方法返回一个只读buffer,该buffer与原buffer共享数据区域,即原buffer修改数据之后,只读buffer中的数据也会修改。

// 1.创建buffer
ByteBuffer buffer = ByteBuffer.allocate(10);
// buffer填充数据
for(int i=0; i<buffer.capacity(); i++){
    buffer.put((byte)i);
}
// 2.创建只读buffer
ByteBuffer readOnlyBuffer = buffer.asReadOnlyBuffer();
// 3.将只读的buffer的position置为0,并遍历输出
readOnlyBuffer.position(0);
while(readOnlyBuffer.remaining() > 0){
    System.out.println(readOnlyBuffer.get());
}
// 4.修改buffer中的数据
for(int i=0; i<buffer.capacity(); i++){
    byte b = buffer.get(i);
    buffer.put((byte)(b*2));
}
// 将只读buffer的position置为0,并再次遍历
readOnlyBuffer.position(0);
while(readOnlyBuffer.remaining() > 0){
    System.out.println(readOnlyBuffer.get());
}

从上面的示例中可以看到,原buffer修改前后,readOnlyBuffer的输出是不同的。

直接缓冲区

直接缓冲区是为了加快I/O速度而使用的一种特殊的方式为其分配内存的缓冲区。有虚拟机直接控制。

 // 使用输入流创建读取的通道
 FileInputStream fis = new FileInputStream("D:\\TestFile\\file1.txt");
 FileChannel inChannel = fis.getChannel();
 // 使用输出流创建写通道
 FileOutputStream fos = new FileOutputStream("D:\\TestFile\\file2.txt");
 FileChannel outChannel = fos.getChannel();
 // 创建直接缓冲区,注意allocateDirect()方法
 ByteBuffer buffer = ByteBuffer.allocateDirect(32);
 // 循环读取和写入
 while(true){
     int read = inChannel.read(buffer);
     if(read == -1){
         break;
     }
     buffer.flip();
     outChannel.write(buffer);
     buffer.clear();
 }
 // 关闭流
 fis.close();
 fos.close();
 inChannel.close();
outChannel.close(); 

内存映射文件I/O

将实际需要读写的内容映射到内存中,再进行读写操作,所以速度很快。使用map()方法。

        // 创建通道
        RandomAccessFile raf = new RandomAccessFile("D:\\TestFile\\file1.txt", "rw");
        FileChannel channel = raf.getChannel();
        // 创建内存映射缓存
        MappedByteBuffer map = channel.map(FileChannel.MapMode.READ_WRITE, 0, 1024);
        // 写入数据
        map.put(0,(byte)1);
        map.put(1023,(byte)2);
        map.flip();
        // 将数据写入文件
        channel.write(map);
        raf.close();

四、Selector

Selector(选择器),又作多路复用器。用于检查一个或多个channel是否处于可读、可写状态。以此实现单线程管理多个通道(网络连接)。

使用较少的线程管理通道,以减少上下文切换的开销。

只有继承了SelectableChannel抽象类的通道才能被selector复用。比如FileChannel就没有继承该类,所以不能selector被使用。实现了该接口的通道就可以注册到Selector中,一个通道可以注册到多个选择器中,但是只能注册到同一个selector中一次。

注册:所谓注册就是将通道与选择器绑定并被选择器监视,同时需要告知选择器关注哪些状态(selector感兴趣的事),当channel处于这种状态之下,selector就会触发。

调用如下方法即可实现注册:

channel.register(Selector s, int ops)
  • 第一个参数为注册到的目标选择器;

  • 第二个参数是感兴趣的选项:

    参数类型参数及描述
    static intOP_ACCEPT 操作集位用于插座接受操作。
    static intOP_CONNECT 用于套接字连接操作的操作集位。
    static intOP_READ 读操作的操作位。
    static intOP_WRITE 写操作的操作位。

    当有多个同时感兴趣的时候,使用"|"符号来连接(位或)。

SelectionKey:选择键。

  • 使用selector的select()方法可以查询某个感兴趣的通道就绪状态(上表格种的四种);
  • 如果查询到某个channel的就绪状态是selector感兴趣的状态,就会被selector选中放入选择键集合中。
  • 在开发过程中,选择键的逻辑是实现是关键,通过选择键的逻辑来实现各种场景的业务;
  • 比如:Selector中注册了ChannelA(读就绪感兴趣) ,ChannelB(写就绪感兴趣);那么选择器就会不断的对这两个通道进行状态查询select();当查询到ChannelA的就绪状态是读,那么就会对ChannelA做后续的操作;ChannelB也是同理。

Selector的使用

  1. 创建选择器:Selector.open();
  2. 查询当前通道可以注册的选项:channel1.validOps();
  3. 注册前必须将通道设置为非阻塞,否则报错;
  4. 注册通道: channel1.register(selector, SelectionKey.OP_ACCEPT);
  5. 轮询所有的通道并查询其状态:selector.selectedKeys();
  6. 根据每个通道的状态进行相应的操作;
  7. 停止选择:wakeup(),该方法会是select()方法直接返回,并唤醒阻塞的线程;
  8. 关闭选择器:close(),该方法会关闭选择器,同时注销其中的通道。
public class SelectorDemo01 {
    public static void main(String[] args) throws Exception {
        // 1.创建选择器
        Selector selector = Selector.open();
        // 2.打开通道
        ServerSocketChannel channel1 = ServerSocketChannel.open();
        channel1.bind(new InetSocketAddress(8000));

        // 获取该通道可以注册的状态
        System.out.println(channel1.validOps());

        // 3.设置为非阻塞,否则.IllegalBlockingModeException
        channel1.configureBlocking(false);

        // 4.注册
        channel1.register(selector, SelectionKey.OP_ACCEPT);

        // 5.轮询选择器中所有通道的就绪状态
        Set<SelectionKey> keys = selector.selectedKeys();
        Iterator<SelectionKey> iterator = keys.iterator();
        while (iterator.hasNext()){
            SelectionKey key = iterator.next();
            // 6.根据对应的状态进行操作
            if (key.isAcceptable()){
                // do....
            }else if (key.isConnectable()){
                // do....
            }else if (key.isReadable()){
                // do....
            }else if (key.isWritable()){
                // do....
            }
            // 该选择键移除
            iterator.remove();
        }
        // 停止选择:当调用该方法会唤醒所有因select()方法查询状态而阻塞的线程
        // selector.wakeup();
        // 关闭selector:该线程会将selector关闭并注销其中所有的通道(只是删除key,通道本身还是开启的)
        // selector.close();
    }
}

使用模板

服务端模板:

在实际使用的时候,主要修改的就是迭代器迭代过程中的逻辑代码以实现对不同感兴趣的通道的操作。

步骤:

  1. 创建ServerSocketChannel通道,绑定监听端口号
  2. 设置通道非阻塞模式
  3. 创建Selector选择器
  4. 把Channel注册到选择器上,监听事件
  5. 调用Selector的select()方法,监听通道是否就绪
  6. 调用selectionKeys()方法,获取就绪的channel集合
  7. 遍历就绪的channel集合,判断就绪事件类型,实现具体操作
  8. 根据业务决定是否需要再次注册监听事件,重复执行5-8.
// 1.创建服务端通道
ServerSocketChannel serverChannel = ServerSocketChannel.open();
serverChannel.configureBlocking(false);
// 2.绑定地址
serverChannel.socket().bind(new InetSocketAddress(5000));

// 3.创建选择器
Selector selector = Selector.open();
// 4.注册
serverChannel.register(selector, SelectionKey.OP_ACCEPT);

// 5.进入循环,将会一直进行获取SelectionKey,并进行相应的操作
while(true){
    // 5.1判断:如果select的值为0(表示没有感兴趣的key),则跳过此次循环
    if (selector.select() == 0) {
        System.out.println("continue");
        continue;
    }
    // 以下是轮询key的操作:
    // 5.2获取所有的key集,并获得其迭代器进行一一判断
    Set<SelectionKey> selectionKeys = selector.selectedKeys();
    Iterator<SelectionKey> iterator = selectionKeys.iterator();
    // 5.3迭代每一个key
    while(iterator.hasNext()){
        SelectionKey next = iterator.next();
        // 从迭代器中移除该key
        iterator.remove();
        // 如果处于接受就绪状态
        if(next.isAcceptable()){
            System.out.println(" is accept");
            // 获得连接并返回这个新的连接通道
            SocketChannel accept = serverChannel.accept();
            // 注册该新的通道,并只对读感兴趣
            accept.configureBlocking(false);
            accept.register(selector, SelectionKey.OP_READ);
        }
        // 如果处于读就绪状态
        else if (next.isReadable()){
            // 获取对读感兴趣的通道
            SocketChannel channel = (SocketChannel)next.channel();
            ByteBuffer buffer = ByteBuffer.allocate(1024);
            // 读取数据
            while(channel.read(buffer) > 0){
                buffer.flip();
                System.out.println();
                System.out.println("get info  " + new String(buffer.array()));
                buffer.clear();
            }
        }
    }
}

客户端模板:

直接建立连接发送数据即可。

// 1.打开通道
SocketChannel clientChannel = SocketChannel.open();
// 2.连接的地址
clientChannel.connect(new InetSocketAddress("127.0.0.1", 5000));
clientChannel.configureBlocking(false);

// 3.创建缓冲区
ByteBuffer buffer = ByteBuffer.allocate(1024);
// 4.写入数据
buffer.put("hello".getBytes());
buffer.flip();
// 5.发送数据
clientChannel.write(buffer);
// 6.关闭通道
clientChannel.close();

五、Pipe AND FileLock

Pipe

在NIO中,管道的作用是对于两个线程之间进行单向的数据连接。

Pipe中有两个通道:

  • Sink Channel
  • Source Channel

数据会从ThreadA 写入到 Sink通道中,然后ThreadB 从Source通道中读取数据。

在这里插入图片描述

示例:

// 1.获取管道
Pipe pipe = Pipe.open();
// 2. 获取sink通道
Pipe.SinkChannel sink = pipe.sink();
// 3.创建缓冲区
ByteBuffer buf = ByteBuffer.allocate(1024);
buf.put("hello pipe".getBytes());  // buf写模式
// 4.写入数据
buf.flip();
sink.write(buf);     // buf读模式
// 5.获取source通道
Pipe.SourceChannel source = pipe.source();
// 6.读取数据
buf.flip();
int read = source.read(buf);    // buf写模式
System.out.println(new String(buf.array()));
 // 7.关闭通道
sink.close();
source.close();

FileLock

文件锁是为了保证进程间访问文件的同步锁。不同进程之间同时只能有一个线程进行读或写。

但是同一个进程间的线程可以同时访问。进程对文件不能重复加锁。

文件所分类:

  • 独占锁:只有一个进程能读、写该文件,其他进程不能读写直到该进程释放锁;
  • 共享锁:一个进程对某文件上共享锁,其他进程也可以访问,但是这些进程只能读,不能写,直到没有线程在读位置。

常用方法:

  • channel.lock():上锁(阻塞);
  • channel.lock(long position, long size, boolean shared):共享锁;
  • channel.trylock():上锁(非阻塞);

示例:

// 创建buf
ByteBuffer buffer = ByteBuffer.wrap("hello filelock".getBytes(StandardCharsets.UTF_8));
Path path = Paths.get("D:\\TestFile\\file3.txt");
// 创建文件通道(参数1:文件路径;参数2:模式;参数三:写入的方式)
FileChannel channel = FileChannel.open(path, StandardOpenOption.WRITE, StandardOpenOption.APPEND);
// 上锁
FileLock lock = channel.lock();
// 非阻塞锁
// channel.tryLock();
// 判断是否是共享锁
System.out.println("是否是共享锁:" + lock.isShared());
channel.write(buffer);
channel.close();

// 读文件
// 创建Reader字符流
FileReader reader = new FileReader("D:\\TestFile\\file3.txt");
BufferedReader br = new BufferedReader(reader);
// 读取文件
String s;
while ((s= br.readLine()) != null){
    System.out.println(s);
}
// 关闭流
br.close();

六、Path AND Files AND AsynchronousFileChannel

Path

Path表示文件系统中的路径,可以指向文件或者目录。可以是绝对路径也可以是相对路径。

全限定名:java.nio.file.Path

常用方法:

// 创建绝对路径
Path path1 = Paths.get("D:\\TestFile");

// 创建相对路径
// 如下表示:D:\TestFile\demo1\file.txt
Path path = Paths.get("D:\\TestFile", "demo1\\file.txt");

Files

Files类中提供了对文件系统中文件的操作方法。常与Path一起使用。

常用方法:

        // 1.创建文件夹
        Path path = Paths.get("D:\\TestFile\\Demo02");
        try {
            Path directory = Files.createDirectory(path);
        } catch (IOException e) {
            // 当文件夹已存在会报错
            // 当父目录不存在会报错
            e.printStackTrace();
        }
        // 2.创建文件
        Path path2 = Paths.get("D:\\TestFile\\Demo01\\file.txt");
        try {
            // 文件已创建会报错
            // 当父目录不存在会报错
            Path file = Files.createFile(path2);
        } catch (IOException e) {
            e.printStackTrace();
        }

        // 文件复制
        Path path3 = Paths.get("D:\\TestFile\\Demo01\\file.txt");
        Path path4 = Paths.get("D:\\TestFile\\Demo02\\file.txt");
        try {
            // 将path3的文件复制到path4中
            Path copy = Files.copy(path3, path4);
        } catch (IOException e) {
            e.printStackTrace();
        }

        // move 移动或重命名
        Path path5 = Paths.get("D:\\TestFile\\Demo01\\file.txt");
        Path path6 = Paths.get("D:\\TestFile\\Demo01\\newFile.txt");
        try {
            Files.move(path5, path6, StandardCopyOption.REPLACE_EXISTING);
        } catch (IOException e) {
            e.printStackTrace();
        }

        // 删除文件
        Path path7 = Paths.get("D:\\\\TestFile\\\\Demo01\\\\newFile.txt");
        try {
            Files.delete(path7);
        } catch (IOException e) {
            e.printStackTrace();
        }

AsynchronousFileChannel

该通道支持异步读取数据,即当读取数据完成的时候在回来处理数据即可。

1.通过Future读取数据:

// 1.创建异步文件通道
Path path = Paths.get("D:\\TestFile\\Demo02\\file.txt");
AsynchronousFileChannel channel = AsynchronousFileChannel.open(path, StandardOpenOption.READ);
// 2.创建buf
ByteBuffer buffer = ByteBuffer.allocate(1024);
// 3.读取文件
Future<Integer> read = channel.read(buffer, 0);
// 4.等待读取完成
while(!read.isDone());
// 5.读取buffer
buffer.flip();
System.out.println(new String(buffer.array()));
// 6.关闭通道
channel.close();

2.通过Future读取数据:

// 1.创建异步文件通道
Path path = Paths.get("D:\\TestFile\\Demo02\\file.txt");
AsynchronousFileChannel channel = AsynchronousFileChannel.open(path, StandardOpenOption.WRITE);
// 2.创建buf
ByteBuffer buffer = ByteBuffer.allocate(1024);
buffer.put("hello".getBytes());
// 3.写数据到文件
buffer.flip();
Future<Integer> write = channel.write(buffer, 0);
// 4.等待写完成
while(!write.isDone());
// 5.关闭通道
channel.close();
System.out.println("写完成");

3.通过CompletionHandler读取数据

Path path = Paths.get("D:\\TestFile\\Demo02\\file.txt");
AsynchronousFileChannel channel = AsynchronousFileChannel.open(path, StandardOpenOption.READ);
ByteBuffer buffer = ByteBuffer.allocate(1024);
// 区别所在
/**
 * 参数1:要传输字节的缓冲区
 * 参数2:开始传输的文件位置;必须是非负数
 * 参数3:附加到 I/O 操作的对象;可以为空
 * 参数4:使用结果的处理程序
 */
channel.read(buffer, 0, buffer, new CompletionHandler<Integer, ByteBuffer>() {
    // 读取完成调用的方法
    @Override
    public void completed(Integer result, ByteBuffer attachment) {
        System.out.println(result);
        attachment.flip();
        byte[] data = new byte[attachment.limit()];
        attachment.get(data);
        System.out.println(new String(data));
        attachment.clear();
    }
    // 读取失败调用的方法
    @Override
    public void failed(Throwable exc, ByteBuffer attachment) {
        System.out.println("read failed");
    }
});
// 6.关闭通道
channel.close();

4.通过CompletionHandler写数据

Path path = Paths.get("D:\\TestFile\\Demo02\\file.txt");
AsynchronousFileChannel channel = AsynchronousFileChannel.open(path, StandardOpenOption.WRITE);
ByteBuffer buffer = ByteBuffer.allocate(1024);
buffer.put("hello CompletionHandler".getBytes());
buffer.flip();
// 区别所在
/**
 * 参数1:要传输字节的缓冲区
 * 参数2:开始传输的文件位置;必须是非负数
 * 参数3:附加到 I/O 操作的对象;可以为空
 * 参数4:使用结果的处理程序
 */
channel.write(buffer, 0, buffer, new CompletionHandler<Integer, ByteBuffer>() {
    // 读取完成调用的方法
    @Override
    public void completed(Integer result, ByteBuffer attachment) {
        System.out.println(result);
        System.out.println("write over");
    }
    // 读取失败调用的方法
    @Override
    public void failed(Throwable exc, ByteBuffer attachment) {
        System.out.println("write failed");
    }
});
// 6.关闭通道
channel.close();

Charset

// 设置Charset的编码集
Charset charset = Charset.forName("Utf-8");
// 创建buffer
CharBuffer buffer = CharBuffer.allocate(1024);
buffer.put("你好 charset");
buffer.flip();
System.out.println("编码前: " + buffer.toString());
/**
 * 输出:
 * 编码前: 你好 charset
 */
// 创建编码器
CharsetEncoder charsetEncoder = charset.newEncoder();
// 编码
ByteBuffer encode = charsetEncoder.encode(buffer);

System.out.println("编码后: ");
while (encode.hasRemaining()){
    System.out.print(encode.get() + " ");
}
System.out.println();
encode.flip();
/**
 * 输出:
 * 编码后:
 *-28 -67 -96 -27 -91 -67 32 99 104 97 114 115 101 116 
 */
// 创建解码器
CharsetDecoder charsetDecoder = charset.newDecoder();
// 解码
CharBuffer decode = charsetDecoder.decode(encode);
System.out.println("解码后: " + decode.toString());
/**
 * 输出:
 * 解码后: 你好 charset
 */

// 使用GBK解码器解码UTF-8编码的字符集会出现乱码
Charset gbk = Charset.forName("GBK");
CharsetDecoder gbkDecoder = gbk.newDecoder();
encode.flip();
CharBuffer decode1 = gbkDecoder.decode(encode);
System.out.println("gbk节码: " + decode1.toString());
/**
 * 输出:
 * gbk节码: 浣犲ソ charset
 */

七、综合案例

实现一个多人聊天室(群聊):其他用户会收到某用户发送到服务器返回的消息。

在这里插入图片描述

ChatServer(服务端)

  • 使用serverSocketChannel作为服务端通道,接收客户端连接
  • 使用Selector对就绪的通道进行操作
  • 使用Charset对写入\读取的数据进行 编码或解码
  • 接收到消息后广播给其他客户端
public class ChatServer {
    // 主函数入口
    public static void main(String[] args) {
        // 开启服务
        startServer();
    }

    /**
     * 启动服务方法
     * 主要功能:
     * 实现serverSocketChannel的创建并注册到selector中;检查selector中的通道就绪状态,根据状态执行相应的操作
     */
    private static void startServer(){
        Selector selector;
        ServerSocketChannel serverSocketChannel;
        try{
            // 1.创建Selector选择器
            selector = Selector.open();
            // 2.创建ServerSocketChannel
            serverSocketChannel = ServerSocketChannel.open();
            // 3.绑定端口并注册
            serverSocketChannel.configureBlocking(false);
            serverSocketChannel.socket().bind(new InetSocketAddress(8000));
            serverSocketChannel.register(selector, SelectionKey.OP_ACCEPT);
            System.out.println("服务器启动...");
            // 4.循环等待新连接的进入
            while(true){
                // 判断是否有就绪状态key
                if (selector.select() == 0){
                    continue;
                }
                // 5.获取所有的就绪的key集合
                Set<SelectionKey> selectionKeys = selector.selectedKeys();
                Iterator<SelectionKey> keyIterator = selectionKeys.iterator();
                // 6.迭代集合并对各个就绪状态的通道进行操作
                while(keyIterator.hasNext()){
                    // 6.1 获取当前的key
                    SelectionKey key = keyIterator.next();
                    // 6.2 移除当前key
                    keyIterator.remove();;
                    // 6.3 accept状态
                    if (key.isAcceptable()){
                        acceptOperation(serverSocketChannel,selector,key);
                    }
                    // 6.4 read状态
                    else if (key.isReadable()){
                        readOperation(selector, key);
                    }
                }
            }
        }catch (Exception e){

        }
    }

    /**
     * 读取客户端发送来的消息的方法
     * 主要功能:
     * 从key中获取该就绪的通道,将数据读取出来并将其在注册回selector;最后需对数据进行广播
     * @param selector 选择器
     * @param key   各个通道的key
     */
    private static void readOperation(Selector selector, SelectionKey key) {
        try{
            // 1.获取读就绪的channel
            SocketChannel readChannel = (SocketChannel)key.channel();
            SocketAddress remoteAddress = readChannel.getRemoteAddress();
            // 2.创建buffer
            ByteBuffer readBuf = ByteBuffer.allocate(1024);
            StringBuilder str = new StringBuilder(": ");
            // 3.读取客户端发送的消息
            while(readChannel.read(readBuf) > 0){
                readBuf.flip();
                // 3.1将数据拼接起来
                str.append(Charset.forName("UTF-8").decode(readBuf));
                readBuf.clear();
            }
            // 4.再将该channel注册到selector中,为可读的状态
            readChannel.register(selector, SelectionKey.OP_READ);
            // 5.将消息广播至所有客户端
            String msg = remoteAddress + str.toString();
            System.out.println(msg);
            if(msg.length() > 0){
                // 调用广播方法
                broadcast(msg, selector, readChannel);
            }
        }catch (Exception e){
            e.printStackTrace();
        }
    }

    /**
     * 广播消息的方法
     * 主要功能:
     *  对非发送至进行消息广播
     * @param msg   收到的消息
     * @param selector   选择器
     * @param readChannel   发送者通道
     */
    private static void broadcast(String msg, Selector selector, SocketChannel readChannel) {
        // 1.获取所有接入的channel
        Set<SelectionKey> keys = selector.keys();
        // 2.向所有通道广播
        for (SelectionKey key : keys){
            // 2.1通过key获取对应的通道
            Channel channel = key.channel();
            // 2.2排除消息的发送者
            if(channel instanceof SocketChannel && channel != readChannel){
                try {
                    // 2.3一次向这些通道写入消息
                    ((SocketChannel)channel).write(Charset.forName("UTF-8").encode(msg));
                } catch (IOException e) {
                    e.printStackTrace();
                }
            }
        }
    }

    /**
     * 接收连接方法
     * 主要功能:
     * 对请求连接的客户端进行连接,获取新的连接通道并将其注册到选择器中
     * @param channel   服务器通道
     * @param selector  选择器
     * @param key
     */
    public static void acceptOperation(ServerSocketChannel channel, Selector selector, SelectionKey key){
        try{
            System.out.println("接收连接...");
            // 1.接收连接并获取新的通道
            SocketChannel acceptChannel = channel.accept();
            // 2.设置非阻塞通道
            acceptChannel.configureBlocking(false);
            // 3.注册通道
            acceptChannel.register(selector, SelectionKey.OP_READ);
            // 4.返回消息给客户端
            acceptChannel.write(Charset.forName("UTF-8").
                            encode("欢迎进入xx聊天室!"));
        }catch (Exception e){
            e.printStackTrace();
        }
    }
}

ChatClient(客户端)

  • 使用SocketChannel作为client端通道
  • 实现向服务器发送消息
  • 创建一个线程,在线程中使用selector对通道监视,并对读就绪的通道进行读操作
public class ChatClient {
    public static void main(String[] args) {
        startClient();
    }

    /**
     * 启动客户端方法
     */
    private static  void startClient(){
        try{
            // 1.连接服务端
            SocketChannel clientChannel = SocketChannel.open(new InetSocketAddress("127.0.0.1", 8000));
            clientChannel.configureBlocking(false);
            // 2.接收服务端返回的消息
            Selector selector = Selector.open();
            clientChannel.register(selector, SelectionKey.OP_READ);
            // 创建线程并启动
            ClientReadThead readThead = new ClientReadThead(selector);
            new Thread(readThead).start();

            // 3.向服务端法消息
            Scanner sc = new Scanner(System.in);
            while(sc.hasNextLine()){
                String s = sc.nextLine();
                clientChannel.write(Charset.forName("UTF-8").encode(s));
            }

        }catch (Exception e){
            e.printStackTrace();
        }
    }
}

ClientReadThead(读取服务器返回的消息线程)

  • 与服务器端读取的逻辑类似
public class ClientReadThead implements Runnable {
    // 选择器
    private Selector selector;
    public ClientReadThead(Selector selector) {
        this.selector = selector;
    }

    @Override
    public void run() {

        // 循环遍历选择器,对key进行监视
        while (true){
           try{
               if (selector.select() == 0){
                   continue;
               }
               // 获取key集合
               Set<SelectionKey> selectionKeys = selector.selectedKeys();
               Iterator<SelectionKey> keyIterator = selectionKeys.iterator();
               // 迭代key并根据就绪状态进行操作
               while(keyIterator.hasNext()){
                   SelectionKey key = keyIterator.next();
                   keyIterator.remove();
                   // 如果有都就绪通道则进行读操作
                   if(key.isReadable()){
                       SocketChannel channel = (SocketChannel)key.channel();
                       ByteBuffer buffer = ByteBuffer.allocate(1024);
                       StringBuilder msg = new StringBuilder();
                       // 一直读并拼接到一起
                       while (channel.read(buffer) > 0){
                           buffer.flip();
                           msg.append(Charset.forName("UTF-8").decode(buffer));
                           buffer.clear();
                       }
                       // 输出收到的消息
                       System.out.println(msg.toString());
                       channel.register(selector, SelectionKey.OP_READ);
                   }
               }
           }catch (Exception e){
               e.printStackTrace();
           }
        }
    }
}

结果截图

  • 服务端:

    在这里插入图片描述

  • 客户端1:

    在这里插入图片描述

  • 客户端2:

    在这里插入图片描述

  • 客户端3:

    在这里插入图片描述

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值