Loading

Java NIO学习笔记

用户空间和内核空间相关概念

针对于Linux来说一个32位操作系统,它的寻址空间(虚拟存储空间)为4G。操作系统的核心是内核,独立于普通的应用程序可以访问受保护的内存空间,也有访问底层设备的所有权限。为了保证内核的安全,操作系统将虚拟空间划分为2部分,一部分为内核空间供内核使用,一部分为用户空间供各个进程使用。每个进程可以通过系统调用进入内核,因此,Linux内核由系统内的所有进程共享。于是从具体进程来看,每个进程可以拥有4G字节的虚拟空间。

进程是无法直接操作I/O设备的,必须通过系统调用请求内核协助完成I/O动作,而内核会为每个I/O设备维护一个Buffer

graph LR A[Process];B[Kernel];C[IO Device] A --请求--> B --操作--> C C --从IO设备获取数据到Kernel's buffer-->B --将数据从Kernel's buffer拷贝到进程地址空间--> A

完整的请求过程:用户进程发起请求,内核接受请求,从I/O设备中获取数据到buffer中,再将buffer中的数据copy到用户进程的地址空间,该用户进程获取到数据后再响应客户端。

这个过程中I/O动作可以分为以下五种模式:

  • 阻塞I/O(Blocking I/O):系统内核等待数据(和IO设备交互)和从内核拷贝到用户内存时用户进程都是阻塞的。进程主动等待/询问。

  • 非阻塞I/O (Non-Blocking I/O):用户进程不会等待用户内存中的数据而是不断进行系统调用,系统返回是否准备完成。进程主动询问。

  • I/O复用 (I/O Multiplexing):使用一个或多个线程轮询所有的IO状态,并将准备完成的IO告知对应用户进程。

    • 文件描述符:Linux内核将所有外部设备都看做一个文件来读写,所以我们对外部设备的操作都可以看成是对文件的操作。我们对文件的调用都是由内核进行系统调用;内核返回一个文件描述符(filede scriptor),描述符就是一个数字,指向内核中的一个结构体(文件路径,数据区,等一些属性)。我们对应用程序的读写就通过对描述符的读写完成。进程被动通知。
    • select:监视writefds、readfds、exceptfds三类文件描述符,当有文件描述符就绪,select函数返回并轮询fdset找到就绪的描述符。
      • 缺点:1.select单个进程打开的FD有限制 2. 采用轮询,线性扫描效率低下 3. 需要维护一个存放大量fs的数据结构,使得用户空间和内核空间在传递该结构的时候复制开销大。
    • poll:原理和select一样,但采用链表实现FD大小没有限制。
      • 缺点:1. 大量的fd被复制在用户态和内核地址空间之间,而不管是否有意义 2. poll是水平触发的,若这一次没有被处理则下次仍然报告该fd。
    • epoll:epoll使用一个文件描述符管理多个文件描述符,将用户关系的文件描述符的事件存放到内核的一个事件表中,这样在用户空间和内核空间的copy只需要一次。
      • 优点:1. 没有最大并发链接限制 2. 不使用轮询的方式,只有“活跃”的连接才会被管理和连接总数无关,在实际网络连接中效率远高于select和epoll 3. 内存拷贝,利用mmap()文件映射内存加速与内核空间的消息 传递,减少了复制开销。
  • 信号驱动的I/O (Signal Driven I/O):系统为进程生成一个信号量通知数据准备完成(可以进行I/O操作)。进程被动通知。

  • 异步I/O (Asychronous I/O):通过告知内核启动操作并在整个操作完成时通知(I/O完成时通知),发起异步请求后会立即得到返回并将所有的任务都交由内核去完成,只需返回一个信号量告诉用户已经完成就可以了。

关于同步、异步、阻塞、非阻塞

  • 同步:发起调用后,没有得到结果的话调用就一直不返回。
  • 异步:发起调用后,调用直接返回。交由调用对象处理完成后再自发的通过状态,通知和回调来通知调用者。
  • 阻塞:发起调用后,当前线程会被挂起,直到有数据当前线程才会被激活。
  • 非阻塞:发起调用后,没有得到结果之前该函数(调用)也不会阻塞当前线程。

Java NIO

  • java1.4的java.nio.*包中引入了新的JAVA IO类库,其目的在于提高速度,实际上旧的IO已经使用nio重新实现过。

FileInputStream,FileOutputStream和RandomAccessFile可以产生FileChannel

  • nio的执行方式更接近与系统执行IO的方式(Zero-Copy):通道和缓冲器,客户端通过缓冲器,缓冲器通过通道和用户进程缓冲区进行交互。通过这种方式避免频繁的系统io调用。
  • Java NIO的三个核心部分,标准的IO面向流,而NIO是基于通道(Channel)和缓冲区(Buffer)进行操作,数据总是从通道读到缓存区或从缓存区写到通道。
    • Channel(通道):既可以用来读也可以用来写。
      • FileChannel : IO
      • SocketChannel /ServerSocketChannel: TCP
      • DatagramChannel : UDP
    • Buffers(缓存区):基本数据类型都有对应的缓存区。最基本的就是ByteBuffer
    • Selectors(选择器):选择器用于监听多个通道的事件。实现了单个线程对多个数据通道的监听。

ByteBuffer

  • Java NIO中的Buffer用于和NIO通道Channel进行交互,Buffer本质上是一块可以读写的内存(块),这块内存被包装成了NIO Buffer对象,并提供了一组方法用来访问该组内存。而唯一直接和Channel交互的Buffer就是ByteBuffer,但我们可以通过视图缓冲器对底层的ByteBuffer进行交互。
  • ByteBuffer被告知分配多少存储空间来创建一个ByteBuffer对象,并有一个方法选择集,用于以原始的字节形式或基本数据类型输出和读取数据。
  • 向Buffer中写数据
    • 从Channel写到Buffer:inChannel.read(buf)
    • 通过Buffer的put()方法写到Buffer中:buf.put(127)
  • 从Buffer中读数据
    • 从Buffer读到Channel:outChannel.write(buf)
    • 通过Buffer的get()方法获取数据:buf.get()
  • Buffer可以直接使用系统内存也就是不在堆上分配内存,不会被GC所管理。

先写一个用ByteBuffer读取文本数据并输出到控制台的小程序:

public void readFileDemo(String fileName){
    try (
            RandomAccessFile accessFile = new RandomAccessFile(fileName, FILE_MODE);
            FileChannel fileChannel = accessFile.getChannel();
            ){
        ByteBuffer buffer = ByteBuffer.allocate(BUFFER_SIZE);
        int bytesRead;

        while ((bytesRead = fileChannel.read(buffer)) != -1){
            byte[] buf = new byte[bytesRead];
            buffer.flip();
            buffer.get(buf, 0, bytesRead);
            System.out.print(new String(buf, 0, bytesRead));
            buffer.clear();
        }
    }catch (Exception e){
        e.printStackTrace();
    }
}

可以看出Buffer实际上就是一个容器,一个连续数组

graph LR client[Client];channelClient[Channel];bufferClient[Buffer]; server[Server];channelServer[Channel];bufferServer[Buffer]; client --> |data| bufferClient --> |data| channelClient --> |data| channelServer --> |data| bufferServer --> |data| server

Byte类型有四个索引参数分别表示Byte容器当前的属性:

大小关系:mark(标记) <= position(当前读写位置) <= limit(可读可写上界) <= capacity(缓冲器尺寸)

索引 说明
capacity 缓冲数组的总长度,Buffer容量大小
position 下一个要操作的元素位置
limit 初始值为capacity即可以写入capacity个数据,调用flip()切换为读之后,值为之前的position
mark 用于记录position 的前一个位置或为-1

Buffer中的实例域:

private int mark = -1; //标记位置默认为1
private int position = 0; //当前位置默认为数组起始位
private int limit; //读写上界
private int capacity; //尺寸

Buffer的一些方法:

构造方法:

public static ByteBuffer allocate(int capacity) { //堆上的缓存器由JVM管理
    if (capacity < 0)
        throw new IllegalArgumentException();
    return new HeapByteBuffer(capacity, capacity);
}
//使用堆外内存创建缓存器,分配的内存是系统内存,并不在JVM的管理范围之内。
public static ByteBuffer allocateDirect(int capacity) { 
    return new DirectByteBuffer(capacity);
}

关于DirectByteBuffer的详细信息可以参考博客

clear()

清除Buffer中的数据,其实就是把索引初始化将读记录遗忘,为写操作让出空间。

public final Buffer clear() {
    position = 0; //遗忘读记录
    limit = capacity; //limit重新设置为容量大小
    mark = -1; //标记位置初始化
    return this;
}

flip()

从写切换到读模式

public final Buffer flip() {
    limit = position; //读模式的上界就是之前写的多少
    position = 0; //读位置初始化
    mark = -1;
    return this;
}

remaining()

返回剩余可读/写大小

public final int remaining() {
    int rem = limit - position; //上界减去当前position就是剩余大小
    return rem > 0 ? rem : 0;
}

hasRemaining()

public final boolean hasRemaining() { //是否还有元素可读/写
    return position < limit; //返回当前位置是否到达上界
}

mark()

标记当前位置

public final Buffer mark() {
    mark = position; //标记位置
    return this;
}

reset()

如果有标记可用,则返回标记位置

public final Buffer reset() {
    int m = mark;
    if (m < 0)
        throw new InvalidMarkException();
    position = m; //调整position为mark标记位
    return this;
}

rewind()

position重置为0,表示重新读缓存中的数据

public final Buffer rewind() {
    position = 0; //重置为0
    mark = -1; //标记位也重置
    return this;
}

ByteBuffer的视图缓冲器

  • 我们平时除了处理基本的字节类型数据以外,还会对其他基本类型数据进行处理(写入或读取),这时我们可以使用ByteBuffer的视图缓冲器(view buffer)。

  • 视图缓冲器可以让我们通过某个特定的基本类型数据类型查看其底层的ByteBuffer。这时ByteBuffer仍然是存储数据的地方,但我们对视图进行的操作都会绑定到ByteBuffer中使得操作反应到ByteBuffer上。

  • 视图允许我们单个单个的或者成批的读取基本类型值。

  • 向ByteBuffer插入基本类型数据的简单方式就是:先将ByteBuffer转换为该缓冲器之上的视图,然后使用该视图的put()方法存入对应视图的基本类型数据。(只有Short类型在存值时会有类型转换)

例如:

利用Charset对字节缓冲进行编码返回字符缓冲视图。

public void readByViewBuffer(String fileName){
    try (
            FileChannel channel = new FileInputStream(fileName).getChannel()
    ){
        ByteBuffer buffer = ByteBuffer.allocate(BUFFER_SIZE);
        while (channel.read(buffer) != -1){
            buffer.flip();
            CharBuffer decode = Charset.forName(ENCODING).decode(buffer);
            System.out.print(decode);
            buffer.clear();
        }
    }catch (Exception e){
        e.printStackTrace();
    }
}

Charset.forName(ENCODING).decode(buffer)返回特定编码解码的CharBuffer类型,直接调用toString()方法返回包含所有字符的字符串。

Channel

  • Java NIO中的通道,这些通道表示着一个可以进行I/O操作的实体(如文件FileChannel和套接字SocketChannel)。
  • 通道表示到实体(如硬件设备、文件、网络套接字、或者可以执行一个或多个诸如读取或写入之类的不同I/O操作的程序组件)的连接。通道可以处于打开或者关闭的状态,并且他们是可异步关闭的,又是可中断的。
    • 将Java NIO方式和操作系统进行类比(比如我们需要一个本地文件,获得文件不是直接从本地获取而是向系统内核请求获取,系统内核请求本地文件数据放到自身的缓冲区(内核缓冲区)而用户进程通过通道(Channel)获取内核缓冲区(ServerBuffer)中的数据,数据被传输到用户进程缓存(ClientBuffer)中,在这个过程中内核将内核缓存中的数据写入通道(Channel.write(Kernel Buffer))用户进程从通道中读取传输过来的数据到用户缓存中(Clannel.read(User Buffer))。这样我们就没有直接和I/O实体进行交互,而是和通道加中间缓冲进行数据的发送和接收。
    • Channel实际上就是一个可以进行I/O操作的连接,有着各种进行I/O操作的接口方法。而我们无法直接调用这些方法实现I/O操作,而是需要通过Buffer(缓冲器)来进行交互。数据总是从Channel读取到Bufffer或从Buffer写入到Channel中。
    • Channel主要用于传输数据,从缓冲区的一侧传到另一侧的实体(如套接字,文件)。使用通道我们可以以最小的开销访问操作系统的I/O服务。
    • 通道不能被重复使用,因为一个通道就代表着与一个特定I/O服务进行连接并封装了该连接的状态,通道一旦关闭该连接就会断开。
  • Java中有许多代表着和不同I/O实体连接的类:
    • FileChannel:用于表示文件实体。
    • SocketChannel:TCP连接中套接字通道,能通过TCP连接读写网络中的数据。
    • ServerSocketChannel:监听TCP连接,为每一个连接创建一个SocketChannel。
    • DatagramChannel:UDP连接通道,能通过UDP连接读写网络中的数据。
  • Channel的工作模式
    • 阻塞和非阻塞:阻塞和非阻塞是针对于进程在访问数据的时候根据I/O操作的就绪状态采用不同方式。是一种读取或者写入操作方法的实现方式。

      • 阻塞:调用的线程会挂起(休眠)直到调用返回。也就是调用的读写函数会一直等待。
      • 非阻塞:调用的线程不会休眠,调用会立即返回结果。读取或者写入函数会立即返回一个状态值。(FileChannel不能运行在此模式下)
    • 同步和异步:同步和异步是针对的应用程序(用户进程)和内核(kernel)的交互而言的

      • 同步:用户进程触发I/O操作并等待或者轮询的去查看I/O操作是否就绪。
      • 异步:用户进程触发I/O操作以后开始自己做自己的事情,当I/O操作就绪时会得到完成操作的通知。

FileChannel

  • FileChannel文件通道总是阻塞的(只能以阻塞模式开启文件通道)。

  • 获取FileChannel有从流中获取或者使用Channels工具类和使用FileChannel的静态方法三种方式。

    • 旧Java I/O库中有三个类被修改用于产生FileChannel:FileInputStream,FileOutputStream和RandomAccessFile。

    从IO类中获取通道:

public void getFileChannelAndAppendMsg(String fileName, String msg) throws Exception{
    //get FileChannel By FileOutputStream
    FileChannel fileChannel = new FileOutputStream(fileName).getChannel();
    fileChannel.write(ByteBuffer.wrap(msg.getBytes()));
    
    //get FileChannel By RandomAccessFile
    fileChannel = new RandomAccessFile(fileName, "rw").getChannel();
    fileChannel.position(fileChannel.size()); //追加写入文件
    fileChannel.write(ByteBuffer.wrap(msg.getBytes()));
    
    //get FileChannel By FileInputStream
    fileChannel = new FileInputStream(fileName).getChannel();
    ByteBuffer buf = ByteBuffer.allocate(BUFFER_SIZE);
    int bytesRead;
    while ((bytesRead = fileChannel.read(buf)) != -1){
        buf.flip();
        while (buf.hasRemaining()){
            System.out.print((char)buf.get());
        }
        buf.clear();
    }
}

ByteBuffer可以将已存在的字节数组进行包装。这样就可以不用复制底层数组,而是把它作为产生ByteBuffer的存储器,称之为数组支持的ByteBuffer。

对于只读访问我们必须显示的为ByteBuffer分配大小。nio的目标就是快速移动大量的数据,因此ByteBuffer的大小分配就显得尤为重要。

我们分别用三种方式对文件进行复制:

分别复制100次14.3 MB的文件求平均值。

平均26.72ms

public void copyFileWithStream(String source, String target){ //使用文件流进行复制
    try (
            FileInputStream fileInputStream = new FileInputStream(source);
            FileOutputStream fileOutputStream = new FileOutputStream(target);
            ){
        byte[] buf = new byte[BUFFER_SIZE];
        int len = 0;
        while ((len = fileInputStream.read(buf)) != -1){
            fileOutputStream.write(buf, 0, len);
        }
    }catch (Exception e){
        e.printStackTrace();
    }
}

平均27.76ms

public void copyFileWithChannel(String source, String target){ //手动用ByteBuffer复制
    try (
            FileChannel inputChannel = new FileInputStream(source).getChannel();
            FileChannel outputChannel = new FileOutputStream(target).getChannel();
            ){
        ByteBuffer buf = ByteBuffer.allocate(BUFFER_SIZE);
        while (inputChannel.read(buf) != -1){
            buf.flip();
            outputChannel.write(buf);
            buf.clear();
        }
    }catch (Exception e){
        e.printStackTrace();
    }
}

平均13.04ms

public void copyFileWithChannelTransfer(String source, String target){ //使用transferFrom()方法
    try (
            FileChannel inputChannel = new FileInputStream(source).getChannel();
            FileChannel outputChannel = new FileOutputStream(target).getChannel();
    ){
        outputChannel.transferFrom(inputChannel, 0, inputChannel.size());
    }catch (Exception e){
        e.printStackTrace();
    }
}
  • 使用FileChannel.open()从Path打开文件通道

使用静态方法,使用Path参数打开文件通道。

public static FileChannel open(Path path, OpenOption... options)
    throws IOException
{
    Set<OpenOption> set = new HashSet<OpenOption>(options.length);
    Collections.addAll(set, options);
    return open(path, set, NO_ATTRIBUTES);
}

例如:

public void getChannelByPath(String fileName){
    try (
            FileChannel channel = FileChannel.open(Paths.get(fileName), StandardOpenOption.WRITE, StandardOpenOption.CREATE);
            ){
        channel.write(ByteBuffer.wrap("test file".getBytes()));
    }catch (Exception e){
        e.printStackTrace();
    }
}
  • FileChannel的一些方法:

从通道读取到缓冲器中。

public abstract int read(ByteBuffer dst) throws IOException;

从通道读取到多个缓冲器中,依次填满。

public abstract long read(ByteBuffer[] dsts, int offset, int length) throws IOException;

从缓冲器写到通道中。

public abstract int write(ByteBuffer src) throws IOException;

从多个缓冲器写到通道中。

public abstract long write(ByteBuffer[] srcs, int offset, int length) throws IOException;

返回此通道文件的当前大小。

public abstract long size() throws IOException;

将这个文件通道中的可读字节传送到可写目标通道文件中。

public abstract long transferTo(long position, long count, WritableByteChannel target) throws IOException;

将可读通道中的可读字节传送到这个通道的文件中。

public abstract long transferFrom(ReadableByteChannel src, long position, long count) throws IOException;

从指定缓冲器位置开始,从文件通道读到缓冲器中。

public abstract int read(ByteBuffer dst, long position) throws IOException;

从缓冲器指定位置开始,从缓冲器写到文件通道中。

public abstract int write(ByteBuffer src, long position) throws IOException;

Socket

  • TCP用主机的IP地址加上主机的端口号作为TCP连接的端点,这种端点就叫做套接字(Socket)或插口。

    • TCP:(Transmission Control Protocol,传输控制协议)是一种面向连接的可靠的,基于字节流的传输层通讯协议。
  • 它是网络通讯中端点的抽象表示,包含进行网络通讯必须的五种信息:

    • 连接使用的协议
    • 本地主机的IP地址
    • 本地进程的协议端口
    • 远程主机的IP地址
    • 远程主机的进程端口
  • 套接字(Socket):使用TCP提供了两台计算机之间的通信机制。客户端程序创建一个套接字,并尝试连接服务器套接字。

    • 当简历连接时服务器会创建一个套接字,并且ServerSocket为服务器提供了一种来监听客户端,并与他们建立连接的机制。
  • 两台计算机之间使用套接字建立TCP连接时会出现

    • 服务器实例化一个ServerSocket对象,表示通过服务器上的端口通信。new ServerSocket(port)
    • 服务器调用ServerSocket类的accept()方法,该方法将一直等待,直到客户端连接到服务器上给定的端口。serverSocket.accept()。(先有服务器监听,否则客户端连接超时返回Connection refused: connect
    • 服务器等待时,一个客户端实例化一个Socket(套接字)对象,指定服务器名称和端口号来连接。new Socket(host, port),(通过地址和端口建立一个套接字对象对服务器进行连接)
    • Socket类的构造器函数试图将客户端连接到指定的复位群殴和端口号。如果通信被建立,则在客户端创建一个Socket对象能够与服务器进行连接。(serverSocket.accept()返回一个套接字Socket对象表示与客户端建立的可靠连接,可以通过这个套接字对象与客户端进行信息的传输
  • 所有的Socket通道类在被实例化时都会创建一个对等的socket对象用以复用socket中实现的协议API。

SocketChannel

  • Socket和SocketChannel都是封装点对点的,有序的网络连接。在NIO中真正处理和响应是SocketChannel套接字通道。
  • SocketChannel的打开方式:
    • SocketChannel.open(); socketChannel.connect(new InetSocketAddress(HOST, PORT));开启一个套接字通道
    • serverSocketChannel.accept();从服务器套接字通道中获取
  • SocketChannel的单独使用和Socket区别不大我们可以将Socket注册到Selector中

尝试进行一个简单的SocketChannel通讯:

服务端:

public class Server {
    private final int BUFFER_SIZE = 1024;
    private ServerSocketChannel serverChannel = null;
    private final int PORT;

    private Server(int port){
        this.PORT = port;
    }

    public static Server getInstance(int port){
        return new Server(port);
    }

    public void run(){
        try {
            serverChannel = ServerSocketChannel.open();
            serverChannel.bind(new InetSocketAddress(PORT));
            SocketChannel clientChannel = serverChannel.accept();

            //若和服务器连接成功,接收服务器传来的消息
            if (clientChannel.isConnected()){
                System.out.println("Message from: " + clientChannel.getRemoteAddress());
                ByteBuffer buf = ByteBuffer.allocate(BUFFER_SIZE);
                while (clientChannel.read(buf) != -1){
                    clientChannel.read(buf);
                    buf.flip();
                    while (buf.hasRemaining()){
                        System.out.print((char) buf.get());
                    }
                    buf.clear();
                }
            }

        }catch (Exception e){
            e.printStackTrace();
        }finally {
            if (serverChannel != null){
                try {
                    serverChannel.close();
                } catch (IOException e) {
                    e.printStackTrace();
                }
            }
        }
    }
}

客户端:

public class Client {
    private SocketChannel socketChannel = null;
    private final String HOST;
    private String msg = "default message";
    private final int PORT;

    private Client(String host, int port){
        this.HOST = host;
        this.PORT = port;
    }

    public static Client getInstance(String host, int port){
        return new Client(host, port);
    }

    public void run(){
        try {
            socketChannel = SocketChannel.open();
            socketChannel.configureBlocking(false);
            socketChannel.connect(new InetSocketAddress(HOST, PORT));
            while (!socketChannel.finishConnect()){
                TimeUnit.MILLISECONDS.sleep(200);
                System.out.println("正在连接。。。 time: " + System.currentTimeMillis());
            }
            
            //TCP连接目标地址和端口成功,就会发送消息过去
            if (socketChannel.isConnected()){
                ByteBuffer buf = ByteBuffer.wrap(msg.getBytes());
                while (buf.hasRemaining()){
                    socketChannel.write(buf);
                }
            }
        }catch (Exception e){
            System.err.println("连接失败");
            e.printStackTrace();
        }finally {
            if (socketChannel != null){
                try {
                    socketChannel.close();
                } catch (IOException e) {
                    e.printStackTrace();
                }
            }
        }
    }

    public Client setMsg(String msg){
        this.msg = msg;
        return this;
    }
}

从上面可以看出几个SocketChannel的使用方法。

SocketChannel.open()

public static SocketChannel open() throws IOException { //通过静态方法返回SocketChannel实例
    return SelectorProvider.provider().openSocketChannel(); 
}

connect()

public abstract boolean connect(SocketAddress remote) throws IOException; //连接这个通道的插座。

isConnected()

socketChannel.isConnected() //套接字是否连接

finishConnect()

socketChannel.finishConnect(); //表示是否连接成功。

close()

socketChannel.close(); //关闭套接字

configureBlocking()

public final SelectableChannel configureBlocking(boolean block) //传入false注册为非阻塞模式
    throws IOException
{
    synchronized (regLock) {
        if (!isOpen())
            throw new ClosedChannelException();
        if (blocking == block)
            return this;
        if (block && haveValidKeys())
            throw new IllegalBlockingModeException();
        implConfigureBlocking(block);
        blocking = block;
    }
    return this;
}

write()

public abstract int write(ByteBuffer src) throws IOException; //从缓冲器写入通道(对缓冲器来说是被读)

read()

public abstract int read(ByteBuffer dst) throws IOException; //从通道读取到缓冲器(对缓冲器来说是被写)

isConnectionPending()

public abstract boolean isConnectionPending();

ServerSocketChannel

  • 用于监听TCP连接,并为每一个连接创建一个SokcetChannel。而ServerSocketChannel自身不进行数据传输。

  • 打开方式:通过ServerSocketChannel.open()获得ServerSocketChannel对象。

  • 监听端口:

    • 绑定端口:serverChannel.bind(new InetSocketAddress(PORT));

    • 进行端口监听:SocketChannel clientChannel = serverChannel.accept();

      • 默认情况下SocketChannel是以阻塞的方式进行端口监听即上面SocketChannel示例中那样。serverChannel.accept()会阻塞,直到有新的连接接入服务器的该端口(accept()就会返回表示连接的套接字)。

      • 若我们需要以非阻塞方式进行端口监听那么只需要调用configureBlocking(false);方法,这个服务端套接字通道就会被设置成非阻塞方式。(我们调用的I/O操作会立即返回,即accept()方法立即返回,但如果此次调用没有监听到新的端口那么accept()方法就会返回null)

      • 尝试一个非阻塞方式的服务端:

      • public class Server {
            private final int BUFFER_SIZE = 1024;
            private ServerSocketChannel serverChannel = null;
            private final int PORT;
        
            private Server(int port){
                this.PORT = port;
            }
        
            public static Server getInstance(int port){
                return new Server(port);
            }
        
            public void run(){
                try {
                    serverChannel = ServerSocketChannel.open();
                    serverChannel.configureBlocking(false);
                    serverChannel.bind(new InetSocketAddress(PORT));
        
                    SocketChannel clientChannel = null;
                    while ((clientChannel = serverChannel.accept()) == null){ //当没有新连接时非阻塞方式持续监听,accept返回 null,若返回非null表示有连接连接成功
                        TimeUnit.MILLISECONDS.sleep(200); //200ms监听一次
                        System.out.println("正在监听... time:" + System.currentTimeMillis());
                    }
        
                    //若和服务器连接成功,接收服务器传来的消息
                    if (clientChannel.isConnected()){
                        System.out.println("Message from: " + clientChannel.getRemoteAddress());
                        ByteBuffer buf = ByteBuffer.allocate(BUFFER_SIZE);
                        while (clientChannel.read(buf) != -1){
                            clientChannel.read(buf);
                            buf.flip();
                            while (buf.hasRemaining()){
                                System.out.print((char) buf.get());
                            }
                            buf.clear();
                        }
                        clientChannel.finishConnect();
                    }
        
                }catch (Exception e){
                    e.printStackTrace();
                }finally {
                    if (serverChannel != null){
                        try {
                            serverChannel.close();
                        } catch (IOException e) {
                            e.printStackTrace();
                        }
                    }
                }
            }
        }
        
      • 其实这个和上面的区别只有

      1. 设置为非阻塞模式
      serverChannel.configureBlocking(false);
      
      1. 修改accept()的使用模式,从阻塞监听变为循环中判断是否有链接的非阻塞监听
      SocketChannel clientChannel = null;
      while ((clientChannel = serverChannel.accept()) == null){ //当没有新连接时非阻塞方式持续监听,accept返回 null,若返回非null表示有连接连接成功
          TimeUnit.MILLISECONDS.sleep(200); //200ms监听一次
          System.out.println("正在监听... time:" + System.currentTimeMillis());
      }
      

ServerSocketChannel方法

open()

public static ServerSocketChannel open() throws IOException { //打开服务器套接字通道。 
    return SelectorProvider.provider().openServerSocketChannel();
}

configureBlocking()

public final SelectableChannel configureBlocking(boolean block) //注册为费阻塞模式
    throws IOException
{
    synchronized (regLock) {
        if (!isOpen())
            throw new ClosedChannelException();
        if (blocking == block)
            return this;
        if (block && haveValidKeys())
            throw new IllegalBlockingModeException();
        implConfigureBlocking(block);
        blocking = block;
    }
    return this;
}

bind()

public final ServerSocketChannel bind(SocketAddress local) //将通道套接字绑到本地地址并进行侦听
    throws IOException
{
    return bind(local, 0);
}

bind()

public abstract ServerSocketChannel bind(SocketAddress local, int backlog) //将通道套接字绑到本地地址并进行侦听
    throws IOException;

accept()

//当处于非阻塞模式,且不存在挂起的连接时accept方法将返回null(函数调用直接返回)。
//当处于阻塞模式,在有新连接可用(函数调用不返回直到操作完成)之前将一直阻塞
public abstract SocketChannel accept() throws IOException; //接收此通道套接字的连接

多路IO复用(IO multiplexing)

  • 上个示例中我们将serverChannel.accept()) == null作为一个判断条件不断的轮询(非阻塞忙轮询)是否有新连接接入,但如果没有新的连接接入(I/O事件触发)我们也会一直向kernel查询事件是否准备就绪,那么cpu就会一直空转白白浪费系统资源。为此我们引入了一个中间层(Selector),用来管理这些线程。
  • 引入的中间层(Selector)会调用系统函数来监听我们关心的I/O事件(Channel的读就绪,写就绪,接收就绪,连接就绪)的触发状态,当系统完成I/O操作或准备就会将消息返回,这时中间层就能知道有新事件可以处理。(在这个过程中每一个I/O操作实际上是阻塞的,但是相比于传统的阻塞IO我们引入中间层之后调用系统函数监听I/O的完成状态,这样线程不需要一直等待函数调用返回而是当完成时被通知,也不需要一直进行系统调用查询I/O事件状态)
    • 阻塞:调用I/O操作,I/O操作没有准备好调用函数不会返回,线程挂起。(当我们为每一个阻塞Socket单独创建一个线程来处理阻塞线程那么可能出现大量线程阻塞的情况)
    • 非阻塞:调用I/O操作,I/O操作准备好了就返回,没有准备好就返回null。(当非阻塞式启动一个套接字那么就需要不断地忙轮询I/O的状态)
  • 整体流程为:我们将一个继承了SelectableChannel类的可以进行注册的Socket注册到一个Selector中,并绑定相应的I/O事件,Selector会对这些Socket的相应事件进行监听,当有事件触发时就会返回I/O操作准备好的键集。我们就可以通过键提取出Socket对Socket进行相应操作。

Selector

  • 选择器,用于检查一个或多个NIO Channel通道的状态是否可读、可写。相比于传统的为每一个网络连接创建一个套接字线程,使用一个Selector线程管理多个Channel(网络连接)减少了处理通道的线程。

  • Selector管理被注册的通道的集合信息和其就绪状态,同时也更新通道的就绪状态。并且一个通道可以被注册到多个选择器上,而对于同一个选择器只能被注册一次。

  • Selector的获得方法

    • NIO中前面提到的(FileChannel,SocketChannel,ServerSocketChannel)一样,不能直接获得,需要通过Selector.open()获得

    • public static Selector open() throws IOException {
          return SelectorProvider.provider().openSelector();
      }
      
  • 将Channel注册到Selector中,使得Selector管理这些Channel

    • public final SelectionKey register(Selector sel, int ops)
          throws ClosedChannelException
      {
          return register(sel, ops, null);
      }
      
      • int ops是一个interest集合表示Selector对Channel发生什么事件感兴趣,有以下几种注册方式。(通道触发事件就说明通道读的该事件已经就绪)

      • public static final int OP_READ = 1 << 0; //读就绪注册,Channel可以读取数据
        public static final int OP_WRITE = 1 << 2; //写就绪注册,Channel可以写入数据
        public static final int OP_CONNECT = 1 << 3; //连接就绪,成功连接到另一个服务器
        public static final int OP_ACCEPT = 1 << 4; //接收就绪,准备好接收新进入的连接
        
      • register()方法返回一个SelectionKey对象用于跟踪这些被注册事件的句柄,SelectionKey键表示一个特定的Channel对象和一个特定的Selector对象之间的注册关系。注册完成后Selector就会监控这些事件是否发生。

      • 我们对SelectionKey对象可以进行如下操作:

      • public abstract SelectableChannel channel(); //获取用于注册的通道对象
        public abstract Selector selector(); //获取用于注册的选择器
        //当key被调用cancel或者通道被关闭,或者selector被关闭都将导致key无效
        public abstract boolean isValid(); //这个键是否失效(当调用取消时,通道在选择器中的注册关系不会立即被取消但键会立即失效)
        public abstract void cancel(); //将这个键加入canceledKey集合中,用于下次selec释放资源并从key中移出
        public abstract int interestOps(); //检索此键的兴趣集。 
        public abstract SelectionKey interestOps(int ops);  //将此键的兴趣集设置为给定值。 
        public abstract int readyOps(); //检索此键的就绪操作集。 
        
        public final boolean isReadable() { //可读是否准备就绪
           return (readyOps() & OP_READ) != 0;
        }
        
        public final boolean isWritable() { //可写是否准备就绪
            return (readyOps() & OP_WRITE) != 0;
        }
        
        public final boolean isConnectable() { //是否连接成功
            return (readyOps() & OP_CONNECT) != 0;
        }
        
        public final boolean isAcceptable() { //是否准备好接收对象
            return (readyOps() & OP_ACCEPT) != 0;
        }
        
        public final Object attach(Object ob) { //将给定的对象连接到这个键。 (添加附件)
            return attachmentUpdater.getAndSet(this, ob);
        }
        
        public final Object attachment() { //检索当前附件。(附件也可以是注册Channel的时候指定)
            return attachment;
        }
        
    • 尝试写一个简单的SelectorDemo:客户端向服务器发送消息,服务器接收消息的简单示例

      • Server

      • public class Server {
            private Selector selector = null;
            private ServerSocketChannel serverChannel = null;
            private final int BUFFER_SIZE = 1024;
            private final int PORT;
        
            public static Server newInstance(int port){
                return new Server(port);
            }
        
            private Server(int port){
                this.PORT = port;
            }
        
            public void run(){
                try {
                    selector = Selector.open();
                    serverChannel = ServerSocketChannel.open();
                    serverChannel.bind(new InetSocketAddress(PORT));
                    serverChannel.configureBlocking(false);
                    serverChannel.register(selector, SelectionKey.OP_ACCEPT); //注册接收就绪时事件
        
                    while (true){
                        int readyChannels = selector.select(); //返回查询就绪事件数量
                        if (readyChannels == 0){
                            continue;
                        }
        
                        Iterator<SelectionKey> iterator = selector.selectedKeys().iterator(); //获取触发就绪事件的Channel所对应的令牌合集
                        while (iterator.hasNext()){
                            SelectionKey key = iterator.next();
                            if (key.isAcceptable() && key.isValid()){ //若为接收就绪事件
                                logClient(key);
                            }else if (key.isReadable() && key.isValid()){ //若为读就绪事件
                                readMsg(key);
                            }
                            iterator.remove(); //将这个就绪事件处理完后移出就绪队列
                        }
                    }
        
                }catch (Exception e){
                    System.err.println("服务器掉线");
                    e.printStackTrace();
                }finally {
                    if (serverChannel != null){
                        try {
                            serverChannel.close();
                        } catch (IOException e) {
                            e.printStackTrace();
                        }
                    }
                    if (selector != null){
                        try {
                            selector.close();
                        } catch (IOException e) {
                            e.printStackTrace();
                        }
                    }
                }
            }
        
            public void logClient(SelectionKey key){
                try {
                    ServerSocketChannel channel = (ServerSocketChannel)key.channel();
                    SocketChannel clientChannel = channel.accept();
                    clientChannel.configureBlocking(false);
                    clientChannel.register(selector, SelectionKey.OP_READ);
                    System.out.println(clientChannel.getRemoteAddress() + " connected on " + System.currentTimeMillis());
                    //clientChannel.write(ByteBuffer.wrap("connect success!".getBytes()));
                }catch (Exception e){
                    System.err.println("监听添加失败");
                    e.printStackTrace();
                }
            }
        
            public void readMsg(SelectionKey key){
                try {
                    SocketChannel clientChannel = (SocketChannel)key.channel();
                    ByteBuffer buffer = ByteBuffer.allocate(BUFFER_SIZE);
        
                    System.out.println("message from " + clientChannel.getRemoteAddress());
                    while (clientChannel.read(buffer) != -1){
                        buffer.flip();
                        while (buffer.hasRemaining()){
                            System.out.print((char)buffer.get()); //将客户端发来的消息打印在控制台上
                        }
                        buffer.clear();
                    }
                    System.out.println("message end");
                }catch (Exception e){
                    System.err.println("读取通道消息失败");
                    e.printStackTrace();
                }finally {
                    if (key != null){
                        try {
                            key.channel().close();
                            key.cancel();
                        }catch (Exception e){
                            e.printStackTrace();
                        }
                    }
                }
            }
        }
        
        
      • Client

      • public class Client {
            private SocketChannel socketChannel = null;
            private final String HOST;
            private String msg = "default message";
            private final int PORT;
        
            private Client(String host, int port){
                this.HOST = host;
                this.PORT = port;
            }
        
            public static Client getInstance(String host, int port){
                return new Client(host, port);
            }
        
            public void run(){
                try {
                    socketChannel = SocketChannel.open();
                    socketChannel.configureBlocking(false);
                    socketChannel.connect(new InetSocketAddress(HOST, PORT));
                    while (!socketChannel.finishConnect()){
                        TimeUnit.MILLISECONDS.sleep(200);
                        System.out.println("正在连接。。。 time: " + System.currentTimeMillis());
                    }
        
                    if (socketChannel.isConnected()){
                        for (int i = 0; i < 5; i++) {
                            //TCP连接目标地址和端口成功,就会发送消息过去
                            ByteBuffer buf = ByteBuffer.wrap(msg.getBytes());
                            while (buf.hasRemaining()){
                                socketChannel.write(buf);
                            }
                            TimeUnit.SECONDS.sleep(1);
                        }
                    }
                }catch (Exception e){
                    System.err.println("连接失败");
                    e.printStackTrace();
                }finally {
                    if (socketChannel != null){
                        try {
                            socketChannel.close();
                        } catch (IOException e) {
                            e.printStackTrace();
                        }
                    }
                }
            }
        
            public Client setMsg(String msg){
                this.msg = msg;
                return this;
            }
        }
        

上面示例中看出Selector的一些用法:

除去上面介绍过的open()和register()

select()

通过select()返回就绪事件的数量,如果没有新的就绪事件返回那么就会阻塞一定时间或者以非阻塞方式轮询。

所谓新的就绪事件就是自上次select调用和这次select调用之间新增的就绪事件。

调用过程:

  1. 处理cancel的key
  2. 设置中断
  3. 调用系统底层进行查询
  4. 解除中断
/*Selects a set of keys whose corresponding channels are ready for I/O operations.*/
public abstract int select() throws IOException; //返回新就绪事件数量实际上用的是select(0L)无限期阻塞直到有就绪事件返回
public abstract int select(long timeout) throws IOException; //固定一个等待就绪通道的时间(阻塞时间),若设置为0则无限期阻塞
public abstract int selectNow() throws IOException; //以非阻塞方式进行查询,若没有通道就绪则返回0(就是轮询检测I/O操作是否就绪)
  • select()的三种重载方式底层实现几乎一样

    public int select(long var1) throws IOException { //select()和select(long time)调用的
        if (var1 < 0L) {
            throw new IllegalArgumentException("Negative timeout");
        } else {
            return this.lockAndDoSelect(var1 == 0L ? -1L : var1); //无限期阻塞传入0L但在这里传入的是-1L
        }
    }
    public int selectNow() throws IOException {
        return this.lockAndDoSelect(0L); //非阻塞传入的是0L
    }
    

    所以lockAndDoSelect()传入的数字,可以解为调用select的等待时间0就是不等待,-1就是一直等待,>0就是指定等待时间

    lockAndDoSelect与doSelect

    最后实现doSelect是依赖不同平台实现不同

    protected abstract int doSelect(long var1) throws IOException;
    private int lockAndDoSelect(long var1) throws IOException {
        synchronized(this) {
            if (!this.isOpen()) {
                throw new ClosedSelectorException();
            } else {
                int var10000;
                synchronized(this.publicKeys) {
                    synchronized(this.publicSelectedKeys) {
                        var10000 = this.doSelect(var1);
                    }
                }
                return var10000;
            }
        }
    
    

keys()

返回这个选择器的键集

public abstract Set<SelectionKey> keys();
  • SelectorImpl中可以看到选择器的键集类型为UnmodifiableSet<>类型

    this.publicKeys = Collections.unmodifiableSet(this.keys);
    

    Collections.unmodifiableSet()

    UnmodifiableSet是一个只读的集合是不可以被改变的,所以keys()返回的集合是一个只读的键集

    public static <T> Set<T> unmodifiableSet(Set<? extends T> s) {
        return new UnmodifiableSet<>(s);
    }
    
    static class UnmodifiableSet<E> extends UnmodifiableCollection<E>
                                 implements Set<E>, Serializable {
        private static final long serialVersionUID = -9215047833775013803L;
        UnmodifiableSet(Set<? extends E> s)     {super(s);}
        public boolean equals(Object o) {return o == this || c.equals(o);}
        public int hashCode()           {return c.hashCode();}
    }
    

selectedKeys()

令牌集合可以判断事件取消状态(isValid),返回就绪类型(isXXX),获取和令牌绑定的通道(channel),获取兴趣事件集(interestOps为int类型,因为四个可选兴趣分别是用低4位的开关状态来选择的0001B就是“准备就绪事件”),就绪事件集(readyOps)

public abstract Set<SelectionKey> selectedKeys(); //返回就绪事件合集对应的令牌合集
  • SelectorImpl中就绪事件的令牌集合为ungrowableSet

    我们可以对其进行迭代和移出,但无法对其进行添加操作。

    this.publicSelectedKeys = Util.ungrowableSet(this.selectedKeys);
    
    static <E> Set<E> ungrowableSet(final Set<E> var0) {
        return new Set<E>() {
            public int size() {
                return var0.size();
            }
            public boolean isEmpty() {
                return var0.isEmpty();
            }
            public boolean contains(Object var1) {
                return var0.contains(var1);
            }
            public Object[] toArray() {
                return var0.toArray();
            }
            public <T> T[] toArray(T[] var1) {
                return var0.toArray(var1);
            }
            public String toString() {
                return var0.toString();
            }
            public Iterator<E> iterator() {
                return var0.iterator();
            }
            public boolean equals(Object var1) {
                return var0.equals(var1);
            }
            public int hashCode() {
                return var0.hashCode();
            }
            public void clear() {
                var0.clear();
            }
            public boolean remove(Object var1) {
                return var0.remove(var1);
            }
            public boolean containsAll(Collection<?> var1) {
                return var0.containsAll(var1);
            }
            public boolean removeAll(Collection<?> var1) {
                return var0.removeAll(var1);
            }
            public boolean retainAll(Collection<?> var1) {
                return var0.retainAll(var1);
            }
            public boolean add(E var1) { //无法添加
                throw new UnsupportedOperationException();
            }
            public boolean addAll(Collection<? extends E> var1) { //无法添加
                throw new UnsupportedOperationException();
            }
        };
    }
    

wakeup()

使阻塞的选择操作立即返回

public abstract Selector wakeup();

close()

public abstract void close() throws IOException; //关闭选择器
  • close()的底层实现

    public void implCloseSelector() throws IOException {
        this.wakeup(); //1. 唤醒正在阻塞的选择操作
        synchronized(this) {
            synchronized(this.publicKeys) {
                synchronized(this.publicSelectedKeys) {
                    this.implClose();
                }
            }
        }
    }
    protected abstract void implClose() throws IOException; //依据不同平台不同实现
    

    关闭流程:

    1. 唤醒正在阻塞的操作

      this.wakeup();
      
    2. 关闭selector底层关联的pipe连接信息

      this.wakeupPipe.sink().close();
      this.wakeupPipe.source().close();
      
    3. 对所有channel都删除当前的selector引用,即取消注册。如果当前channel已经关闭并且取消注册则直接销毁资源

      this.deregister(this.channelArray[var2]);
      
      if (!var3.isOpen() && !var3.isRegistered()) {
          ((SelChImpl)var3).kill();
      }
      
    4. 退出所有select线程池

isOpen()

public abstract boolean isOpen(); //判断选择器是否打开

provider()

public abstract SelectorProvider provider(); //返回创建此通道的提供器

关于SelectionKey的cancel()方法

调用SelectionKey的cancel方法,将这个键加入cancelledKeys集合中(这个键变为已取消键),下次select时将会触发这个键的deregister操作取消注册,释放资源并从key中移除。(所以调用cancel后并不会立即从令牌集合中移除而是需要等到下一次调用select方法才会取消注册并移出集合释放资源

public final void cancel() {
    // Synchronizing "this" to prevent this key from getting canceled
    // multiple times by different threads, which might cause race
    // condition between selector's select() and channel's close().
    synchronized (this) {
        if (valid) {
            valid = false;
            ((AbstractSelector)selector()).cancel(this);
        }
    }
}
private final Set<SelectionKey> cancelledKeys = new HashSet<SelectionKey>();
void cancel(SelectionKey k) {                       // package-private
    synchronized (cancelledKeys) {
        cancelledKeys.add(k);
    }
}
posted @ 2022-04-19 19:11  AxiaNibiru  阅读(71)  评论(0)    收藏  举报