深入浅出NIO

Java NIO由以下核心部分组成

  • Buffer
  • Channel
  • Selector

Buffer

  • ByteBuffer
  • ShortBuffer
  • FloatBuffer
  • CharBuffer
  • IntBuffer
  • LongBuffer
  • DirectByteBuffer
  • HeapByteBuffer
Buffer属性以及相关操作
属性说明
capacity 容量Buffer所能够存放的最大容量
position 位置下一个被读或写的位置
limit 上界可供读写的最大位置,用于限制position position < limit
mark 标记标记位置,用于记录某次读写的位置,可以通过reset()方法回到这里
mark <= position <= limit <= capacity
读写ByteBuffer
// 读操作就是直接读取下一个index的数据
public byte get() {
    return hb[ix(nextGetIndex())];
}
public ByteBuffer get(byte[] dst, int offset, int length) {
    checkBounds(offset, length, dst.length);
    if (length > remaining())
        throw new BufferUnderflowException();
    System.arraycopy(hb, ix(position()), dst, offset, length);
    position(position() + length);
    return this;
}   
// offset就是偏移量,是一个数字的偏移量,这里字节数组所以偏移量为1,如果为intBuffer当然会是4了.
protected int ix(int i) {
    return i + offset;
}
// 然后会检查这里的读是否超过了读的界限.
final int nextGetIndex() {                       
        if (position >= limit)
            throw new BufferUnderflowException();
        return position++;
    }
// 这个方法直接找到下n个位置
final int nextGetIndex(int nb) {                   
    if (limit - position < nb)
        throw new BufferUnderflowException();
    int p = position;
    position += nb;
    return p;
}

// 当然了读操作也不能无限制的读啊,当然的是需要有一个界限了,这里就有了下面这个方法,能够检测是否还能继续读取.可以就返回true
public final boolean hasRemaining() {
        return position < limit;
    }


Channel

Channel分别为本地I/O设备和网络I/O提供了以下实现,并且和Java IO体系的类是一一对应的:

  1. 网络IO
    • DatagramChannel:读写UDP通信的数据,对应DatagramSocket类
    • SocketChannel:读写TCP通信的数据,对应Socket类
    • ServerSocketChannel:监听新的TCP连接,并且会创建一个可读写的SocketChannel,对应ServerSocket类
  2. 本地设备IO
    • FileChannel:读写本地文件的数据,不支持Selector控制,对应File类
ServerSocketChannel

让我们从最简单的 ServerSocketChanne来开始对socket通道类的讨论。以下是ServerSocketChanne的完整 API:

public abstract class ServerSocketChannel extends AbstractSelectableChannel {

    public static ServerSocketChannel open() throws IOException
    public abstract ServerSocket socket();
    public abstract SocketChannel accept() throws IOException;
    //支持的SelectionKey类型,返回OP_ACCEPT
    public final int validOps()
}

新建一个ServerSocketChannelImpl其本质是在底层操作系统创建了一个fd(即文件描述符),相当于建立了一个用于网络通信的通道,通过这个通道我们可以和外部网络进行通信;

SocketChannel

使用ServerSocketChannel可以实时获取到新建的TCP连接,从上面accpet()方法得出,其返回的是一个SocketChannelImpl对象,其继承的类的是SocketChannel,以下是SocketChannel的API:

public abstract class SocketChannel extends AbstractSelectableChannel implements ByteChannel, ScatteringByteChannel, GatheringByteChannel {
    // 这里仅列出部分API
    public static SocketChannel open() throws IOException
    public static SocketChannel open(InetSocketAddress remote) throws IOException
    public abstract Socket socket();
    public abstract boolean connect (SocketAddress remote) throws IOException;
    // 当前的连接channel是否有并发连接,非阻塞状态下才有可能返回true
    public abstract boolean isConnectionPending();
    //调用finishConnect()方法来完成连接过程,该方法任何时候都可以安全地进行调用。假如在一个非阻塞模式的SocketChannel对象上调用finishConnect()方法,将可能出现下列情形之一:
    /**
    * 1.connect()方法尚未被调用。那么将产生NoConnectionPendingException异常。
    * 2.连接建立过程正在进行,尚未完成。那么什么都不会发生,finishConnect()方法会立即返回false值。
    * 3.在非阻塞模式下调用connect()方法之后,SocketChannel又被切换回了阻塞模式。那么如果有必要的话,调用线程会阻塞直到连接建立完成,finishConnect()方法接着就会返回true值。
    * 4.在初次调用connect()或最后一次调用finishConnect()之后,连接建立过程已经完成。那么SocketChannel对象的内部状态将被更新到已连接状态,finishConnect()方法会返回true值,然后SocketChannel对象就可以被用来传输数据了。
    * 5.连接已经建立。那么什么都不会发生,finishConnect()方法会返回true值。
    */
    public abstract boolean finishConnect() throws IOException;
    // 是否连接成功
    public abstract boolean isConnected();
    
    // 支持的SelectionKey类型,返回OP_CONNECT,OP_READ,OP_WRITE
    public final int validOps();
    
    public abstract int read(ByteBuffer dst) throws IOException;
    public abstract int write(ByteBuffer src) throws IOException;
}


上文我们提到SocketChannel是用于读写TCP通信的数据,与Socket类一致,其封装的是点对点、有序的网络连接;一个SocketChannel的创建必然伴随着会创建一个同等的Socket对象(实际是SocketAdaptor),通过socket方法能获取;

DatagramChannel

DatagramChannel是NIO中面向Datagram(数据报)的套接字通道.

一般我们在实际编程中用到这个Channel的情况很少,所以我在这里就不详细说明了。

FileChannel

FileChannel是线程安全的,只能通过FileInputStream,FileOutputStream,RandomAccessFile的getChannel方法获取FileChannel通道,原理是获取到底层操作系统生成的fd(file descriptor)

Selector

想想一个场景:在一个养鸡场,有这么一个人,每天的工作就是不停检查几个特殊的鸡笼,如果有鸡进来,有鸡出去,有鸡生蛋,有鸡生病等等,就把相应的情况记录下来,如果鸡场的负责人想知道情况,只需要询问那个人即可。
在这里,这个人就相当Selector,每个鸡笼相当于一个SocketChannel,每个线程通过一个Selector可以管理多个SocketChannel。

为了实现Selector管理多个SocketChannel,必须将具体的SocketChannel对象注册到Selector,并声明需要监听的事件(这样Selector才知道需要记录什么数据),一共有4种事件:

  1. connect:客户端连接服务端事件,对应值为SelectionKey.OP_CONNECT(8)
  2. accept:服务端接收客户端连接事件,对应值为SelectionKey.OP_ACCEPT(16)
  3. read:读事件,对应值为SelectionKey.OP_READ(1)
  4. write:写事件,对应值为SelectionKey.OP_WRITE(4)

这个很好理解,每次请求到达服务器,都是从connect开始,connect成功后,服务端开始准备accept,准备就绪,开始读数据,并处理,最后写回数据返回。

所以,当SocketChannel有对应的事件发生时,Selector都可以观察到,并进行相应的处理 。

服务端操作过程
  1. 创建ServerSocketChannel实例,并绑定指定端口;
  2. 创建Selector实例;
  3. 将serverSocketChannel注册到selector,并指定事件OP_ACCEPT,最底层的socket通过channel和selector建立关联;
  4. 如果没有准备好的socket,select方法会被阻塞一段时间并返回0;
  5. 如果底层有socket已经准备好,selector的select方法会返回socket的个数,而且selectedKeys方法会返回socket对应的事件(connect、accept、read or write);
  6. 根据事件类型,进行不同的处理逻辑;

在步骤3中,selector只注册了serverSocketChannel的OP_ACCEPT事件

  1. 如果有客户端A连接服务,执行select方法时,可以通过serverSocketChannel获取客户端A的socketChannel,并在selector上注册socketChannel的OP_READ事件
  2. 如果客户端A发送数据,会触发read事件,这样下次轮询调用select方法时,就能通过socketChannel读取数据,同时在selector上注册该socketChannel的OP_WRITE事件,实现服务器往客户端写数据。
服务端代码示例:
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.nio.charset.StandardCharsets;
import java.util.Iterator;

public class NIOServer {

    public static void main(String[] args) throws Exception {
        ServerSocketChannel serverSocketChannel = ServerSocketChannel.open();
        serverSocketChannel.configureBlocking(false);
        serverSocketChannel.bind(new InetSocketAddress(9990));
        Selector selector = Selector.open();
        serverSocketChannel.register(selector, SelectionKey.OP_ACCEPT);
        while (selector.select() > 0) {
            Iterator<SelectionKey> iterator = selector.selectedKeys().iterator();
            while (iterator.hasNext()) {
                SelectionKey next = iterator.next();
                if (next.isAcceptable()) {
                    SocketChannel socketChannel = serverSocketChannel.accept();
                    socketChannel.configureBlocking(false);
                    socketChannel.register(selector, SelectionKey.OP_READ);
                } else if (next.isReadable()) {
                    SocketChannel channel = (SocketChannel) next.channel();
                    ByteBuffer byteBuffer = ByteBuffer.allocate(1024);
                    int len = 0;
                    while ((len = channel.read(byteBuffer)) > 0) {
                        byteBuffer.flip();
                        System.out.println(new String(byteBuffer.array(), 0, len));
                        byteBuffer.clear();
                        ByteBuffer out = ByteBuffer.allocate(1024);
                        out.put("Sure".getBytes(StandardCharsets.UTF_8));
                        out.flip();
                        channel.write(out);
                        out.clear();
                    }
                }
            }
            iterator.remove();
        }
    }

}
客户端代码示例:
import java.net.InetSocketAddress;
import java.nio.ByteBuffer;
import java.nio.channels.SocketChannel;
import java.nio.charset.StandardCharsets;
import java.util.Scanner;

public class NIOClient {

    public static void main(String[] args) throws Exception {
        SocketChannel channel = SocketChannel.open(new InetSocketAddress("127.0.0.1", 9990));
        ByteBuffer buffer = ByteBuffer.allocate(1024);
        ByteBuffer reve = ByteBuffer.allocate(1024);
        Scanner scanner = new Scanner(System.in);

        while (scanner.hasNext()) {
            String in = scanner.next();
            buffer.put(in.getBytes(StandardCharsets.UTF_8));
            buffer.flip();
            channel.write(buffer);
            buffer.clear();
            channel.read(reve);
            reve.flip();
            System.out.println(new String(reve.array(), 0, 1024));
        }
        channel.close();
    }

}
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值